First attempt at login/register
This commit is contained in:
parent
6028779b45
commit
76f151a64c
26
index.html
26
index.html
@ -115,6 +115,32 @@
|
|||||||
layersTitle: 'Layers',
|
layersTitle: 'Layers',
|
||||||
copyToClipboardSuccess: 'Copied to clipboard',
|
copyToClipboardSuccess: 'Copied to clipboard',
|
||||||
copyToClipboardError: 'Unable to copy to clipboard',
|
copyToClipboardError: 'Unable to copy to clipboard',
|
||||||
|
|
||||||
|
loginTitle: 'Login/Register',
|
||||||
|
loginHeading: 'Existing User',
|
||||||
|
loginUsernameLabel: 'Username',
|
||||||
|
loginPasswordLabel: 'Password',
|
||||||
|
loginSubmit: 'Login',
|
||||||
|
loginErrorUnknown: 'Unexpected error while logging in',
|
||||||
|
loginErrorDisabled: 'Logging in is disabled on this server',
|
||||||
|
loginErrorIncorrect: 'Incorrect username or password',
|
||||||
|
loginSuccess: 'Logged in successfully',
|
||||||
|
|
||||||
|
registerHeading: 'New User',
|
||||||
|
registerDescription: `Enter your username and password, along with your registration code.
|
||||||
|
|
||||||
|
You can get a registration code by running /dynmap webregister in-game.`,
|
||||||
|
registerConfirmPasswordLabel: 'Confirm Password',
|
||||||
|
registerCodeLabel: 'Registration Code',
|
||||||
|
registerSubmit: 'Register',
|
||||||
|
registerErrorUnknown: 'Unexpected error during registration',
|
||||||
|
registerErrorDisabled: 'Registration is disabled on this server',
|
||||||
|
registerErrorVerifyFailed: 'The entered passwords do not match',
|
||||||
|
registerErrorIncorrect: 'Registration failed, please check the entered details are correct',
|
||||||
|
|
||||||
|
logoutTitle: 'Logout',
|
||||||
|
logoutErrorUnknown: 'Unexpected error while logging out',
|
||||||
|
logoutSuccess: 'Logged out successfully',
|
||||||
},
|
},
|
||||||
|
|
||||||
ui: {
|
ui: {
|
||||||
|
22
src/App.vue
22
src/App.vue
@ -17,6 +17,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Map></Map>
|
<Map></Map>
|
||||||
<ChatBox v-if="chatBoxEnabled" v-show="chatBoxEnabled && chatVisible"></ChatBox>
|
<ChatBox v-if="chatBoxEnabled" v-show="chatBoxEnabled && chatVisible"></ChatBox>
|
||||||
|
<LoginModal v-if="loginEnabled"></LoginModal>
|
||||||
<Sidebar></Sidebar>
|
<Sidebar></Sidebar>
|
||||||
<notifications position="bottom center" :speed="250" :max="3" :ignoreDuplicates="true" classes="notification" />
|
<notifications position="bottom center" :speed="250" :max="3" :ignoreDuplicates="true" classes="notification" />
|
||||||
</template>
|
</template>
|
||||||
@ -32,13 +33,16 @@ import {parseUrl} from '@/util';
|
|||||||
import {hideSplash, showSplash, showSplashError} from '@/util/splash';
|
import {hideSplash, showSplash, showSplashError} from '@/util/splash';
|
||||||
import {MutationTypes} from "@/store/mutation-types";
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
import {LiveAtlasServerDefinition, LiveAtlasUIElement} from "@/index";
|
import {LiveAtlasServerDefinition, LiveAtlasUIElement} from "@/index";
|
||||||
|
import LoginModal from "@/components/login/LoginModal.vue";
|
||||||
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
Map,
|
Map,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
ChatBox
|
ChatBox,
|
||||||
|
LoginModal
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
@ -48,6 +52,7 @@ export default defineComponent({
|
|||||||
currentServer = computed(() => store.state.currentServer),
|
currentServer = computed(() => store.state.currentServer),
|
||||||
configurationHash = computed(() => store.state.configurationHash),
|
configurationHash = computed(() => store.state.configurationHash),
|
||||||
chatBoxEnabled = computed(() => store.state.components.chatBox),
|
chatBoxEnabled = computed(() => store.state.components.chatBox),
|
||||||
|
loginEnabled = computed(() => store.state.components.login),
|
||||||
chatVisible = computed(() => store.state.ui.visibleElements.has('chat')),
|
chatVisible = computed(() => store.state.ui.visibleElements.has('chat')),
|
||||||
loggedIn = computed(() => store.state.loggedIn),
|
loggedIn = computed(() => store.state.loggedIn),
|
||||||
|
|
||||||
@ -79,6 +84,14 @@ export default defineComponent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Show login screen if required
|
||||||
|
if(e.message === 'login-required') {
|
||||||
|
hideSplash();
|
||||||
|
store.commit(MutationTypes.SHOW_UI_MODAL, 'login');
|
||||||
|
notify('Login required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const error = `Failed to load server configuration for '${store.state.currentServer!.id}'`;
|
const error = `Failed to load server configuration for '${store.state.currentServer!.id}'`;
|
||||||
console.error(`${error}:`, e);
|
console.error(`${error}:`, e);
|
||||||
showSplashError(`${error}\n${e}`, false, ++loadingAttempts.value);
|
showSplashError(`${error}\n${e}`, false, ++loadingAttempts.value);
|
||||||
@ -163,6 +176,12 @@ export default defineComponent({
|
|||||||
await loadConfiguration();
|
await loadConfiguration();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
watch(loggedIn, async () => {
|
||||||
|
if(!loading.value) {
|
||||||
|
console.log('Login state changed. Reloading configuration');
|
||||||
|
await loadConfiguration();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => loadConfiguration());
|
onMounted(() => loadConfiguration());
|
||||||
onBeforeUnmount(() => store.dispatch(ActionTypes.STOP_UPDATES, undefined));
|
onBeforeUnmount(() => store.dispatch(ActionTypes.STOP_UPDATES, undefined));
|
||||||
@ -184,6 +203,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
chatBoxEnabled,
|
chatBoxEnabled,
|
||||||
chatVisible,
|
chatVisible,
|
||||||
|
loginEnabled,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
1
src/assets/icons/login.svg
Normal file
1
src/assets/icons/login.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="375.12" height="369.507" version="1.0" viewBox="0 0 281.34 277.13" xmlns="http://www.w3.org/2000/svg"><g stroke-linecap="round"><path d="M117.345 0c-15.437 0-28.178 12.74-28.178 28.176v87.008h20V28.176c0-4.702 3.476-8.175 8.178-8.175h135.817c4.703 0 8.177 3.473 8.177 8.175v220.778c0 4.702-3.474 8.176-8.176 8.176H117.344c-4.702 0-8.178-3.474-8.178-8.176V167.24h-20v81.712c0 15.437 12.742 28.178 28.178 28.178h135.817c15.437 0 28.176-12.741 28.176-28.178V28.176C281.339 12.74 268.6 0 253.163 0H117.345z"/><path d="M148.458 77.933a12.5 12.5 0 0 0-8.848 3.643 12.5 12.5 0 0 0-.04 17.678l29.329 29.457H12.508a12.5 12.5 0 0 0-12.5 12.501 12.5 12.5 0 0 0 12.5 12.5h156.39l-29.328 29.457a12.5 12.5 0 0 0 .04 17.678 12.5 12.5 0 0 0 17.677-.038l50.552-50.777a12.501 12.501 0 0 0 0-17.64l-50.551-50.774a12.5 12.5 0 0 0-8.83-3.683z"/></g></svg>
|
After Width: | Height: | Size: 846 B |
1
src/assets/icons/logout.svg
Normal file
1
src/assets/icons/logout.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="375.12" height="369.507" version="1.0" viewBox="0 0 281.34 277.13" xmlns="http://www.w3.org/2000/svg"><g stroke-linecap="round"><path d="M117.345 0c-15.437 0-28.178 12.74-28.178 28.176v87.008h20V28.176c0-4.702 3.476-8.175 8.178-8.175h135.817c4.703 0 8.177 3.473 8.177 8.175v220.778c0 4.702-3.474 8.176-8.176 8.176H117.344c-4.702 0-8.178-3.474-8.178-8.176V167.24h-20v81.712c0 15.437 12.742 28.178 28.178 28.178h135.817c15.437 0 28.176-12.741 28.176-28.178V28.176C281.339 12.74 268.6 0 253.163 0z"/><path d="M63.024 77.933a12.5 12.5 0 0 1 8.848 3.643 12.5 12.5 0 0 1 .04 17.678l-29.329 29.457h156.39a12.5 12.5 0 0 1 12.5 12.501 12.5 12.5 0 0 1-12.5 12.5H42.583l29.328 29.457a12.5 12.5 0 0 1-.04 17.678 12.5 12.5 0 0 1-17.677-.038L3.642 150.032a12.501 12.501 0 0 1 0-17.64l50.552-50.774a12.5 12.5 0 0 1 8.83-3.683z"/></g></svg>
|
After Width: | Height: | Size: 836 B |
@ -25,9 +25,12 @@
|
|||||||
<CoordinatesControl v-if="coordinatesControlEnabled" :leaflet="leaflet"></CoordinatesControl>
|
<CoordinatesControl v-if="coordinatesControlEnabled" :leaflet="leaflet"></CoordinatesControl>
|
||||||
<LinkControl v-if="linkControlEnabled" :leaflet="leaflet"></LinkControl>
|
<LinkControl v-if="linkControlEnabled" :leaflet="leaflet"></LinkControl>
|
||||||
<ClockControl v-if="clockControlEnabled" :leaflet="leaflet"></ClockControl>
|
<ClockControl v-if="clockControlEnabled" :leaflet="leaflet"></ClockControl>
|
||||||
|
|
||||||
|
<LoginControl v-if="loginEnabled" :leaflet="leaflet"></LoginControl>
|
||||||
<ChatControl v-if="chatBoxEnabled" :leaflet="leaflet"></ChatControl>
|
<ChatControl v-if="chatBoxEnabled" :leaflet="leaflet"></ChatControl>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MapContextMenu :leaflet="leaflet" v-if="leaflet"></MapContextMenu>
|
<MapContextMenu :leaflet="leaflet" v-if="leaflet"></MapContextMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -48,6 +51,7 @@ import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
|||||||
import {LoadingControl} from "@/leaflet/control/LoadingControl";
|
import {LoadingControl} from "@/leaflet/control/LoadingControl";
|
||||||
import MapContextMenu from "@/components/map/MapContextMenu.vue";
|
import MapContextMenu from "@/components/map/MapContextMenu.vue";
|
||||||
import {Coordinate, LiveAtlasPlayer} from "@/index";
|
import {Coordinate, LiveAtlasPlayer} from "@/index";
|
||||||
|
import LoginControl from "@/components/map/control/LoginControl.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -59,7 +63,8 @@ export default defineComponent({
|
|||||||
ClockControl,
|
ClockControl,
|
||||||
LinkControl,
|
LinkControl,
|
||||||
ChatControl,
|
ChatControl,
|
||||||
LogoControl
|
LogoControl,
|
||||||
|
LoginControl
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
@ -75,6 +80,7 @@ export default defineComponent({
|
|||||||
clockControlEnabled = computed(() => store.getters.clockControlEnabled),
|
clockControlEnabled = computed(() => store.getters.clockControlEnabled),
|
||||||
linkControlEnabled = computed(() => store.state.components.linkControl),
|
linkControlEnabled = computed(() => store.state.components.linkControl),
|
||||||
chatBoxEnabled = computed(() => store.state.components.chatBox),
|
chatBoxEnabled = computed(() => store.state.components.chatBox),
|
||||||
|
loginEnabled = computed(() => store.state.components.login),
|
||||||
logoControls = computed(() => store.state.components.logoControls),
|
logoControls = computed(() => store.state.components.logoControls),
|
||||||
|
|
||||||
currentWorld = computed(() => store.state.currentWorld),
|
currentWorld = computed(() => store.state.currentWorld),
|
||||||
@ -102,6 +108,7 @@ export default defineComponent({
|
|||||||
clockControlEnabled,
|
clockControlEnabled,
|
||||||
linkControlEnabled,
|
linkControlEnabled,
|
||||||
chatBoxEnabled,
|
chatBoxEnabled,
|
||||||
|
loginEnabled,
|
||||||
|
|
||||||
logoControls,
|
logoControls,
|
||||||
followTarget,
|
followTarget,
|
||||||
|
101
src/components/Modal.vue
Normal file
101
src/components/Modal.vue
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<!--
|
||||||
|
- Copyright 2021 James Lyne
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="{'modal': true, 'modal--visible': visible}" role="dialog" :id="`modal--${id}`"
|
||||||
|
:aria-labelledby="`${id}__heading`" aria-modal="true" @click="onClick" ref="modal">
|
||||||
|
<div class="modal__content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
|
import {LiveAtlasUIModal} from "@/index";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
id: {
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore(),
|
||||||
|
modal = ref<HTMLElement | null>(null),
|
||||||
|
visible = computed(() => store.state.ui.visibleModal === props.id);
|
||||||
|
|
||||||
|
const onKeydown = (e: KeyboardEvent) => {
|
||||||
|
if(visible.value && e.key === 'Escape') {
|
||||||
|
store.commit(MutationTypes.HIDE_UI_MODAL, props.id as LiveAtlasUIModal);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClick = (e: MouseEvent) => {
|
||||||
|
if(e.target === modal.value) {
|
||||||
|
store.commit(MutationTypes.HIDE_UI_MODAL, props.id as LiveAtlasUIModal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', onKeydown);
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.addEventListener('keydown', onKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
visible,
|
||||||
|
modal,
|
||||||
|
onClick,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../scss/placeholders';
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 120;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
display: none;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10vh 1rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
&.modal--visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__content {
|
||||||
|
@extend %panel;
|
||||||
|
max-width: 80rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
125
src/components/login/LoginForm.vue
Normal file
125
src/components/login/LoginForm.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<!--
|
||||||
|
- Copyright 2021 James Lyne
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form :class="{'form': true, 'form--invalid': invalid}" @submit.prevent="login" ref="form" novalidate>
|
||||||
|
<h3>{{ loginHeading }}</h3>
|
||||||
|
|
||||||
|
<div class="form__group">
|
||||||
|
<label for="login-username" class="form__label" >{{ usernameLabel }}</label>
|
||||||
|
<input id="login-username" type="text" name="username" autocomplete="username"
|
||||||
|
v-model="loginUsername" required ref="usernameField" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form__group">
|
||||||
|
<label for="login-password" class="form__label" >{{ passwordLabel }}</label>
|
||||||
|
<input id="login-password" type="password" name="password" autocomplete="current-password"
|
||||||
|
v-model="loginPassword" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="alert" v-if="error" class="form__group alert">{{ error }}</div>
|
||||||
|
|
||||||
|
<div class="form__group">
|
||||||
|
<button type="submit" :disabled="submitting">{{ loginSubmit }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent} from "@vue/runtime-core";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {ActionTypes} from "@/store/action-types";
|
||||||
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const store = useStore(),
|
||||||
|
form = ref<HTMLFormElement | null>(null),
|
||||||
|
usernameField = ref<HTMLFormElement | null>(null),
|
||||||
|
|
||||||
|
loginModalVisible = computed(() => store.state.ui.visibleModal === 'login'),
|
||||||
|
|
||||||
|
heading = computed(() => store.state.messages.loginHeading),
|
||||||
|
loginHeading = computed(() => store.state.messages.loginHeading),
|
||||||
|
usernameLabel = computed(() => store.state.messages.loginUsernameLabel),
|
||||||
|
passwordLabel = computed(() => store.state.messages.loginPasswordLabel),
|
||||||
|
loginSubmit = computed(() => store.state.messages.loginSubmit),
|
||||||
|
loginSuccess = computed(() => store.state.messages.loginSuccess),
|
||||||
|
|
||||||
|
loginUsername = ref(''),
|
||||||
|
loginPassword = ref(''),
|
||||||
|
|
||||||
|
submitting = ref(false),
|
||||||
|
invalid = ref(false),
|
||||||
|
error = ref(null);
|
||||||
|
|
||||||
|
watch(loginModalVisible, (newValue) => {
|
||||||
|
if(newValue) {
|
||||||
|
requestAnimationFrame(() => usernameField.value!.focus());
|
||||||
|
} else {
|
||||||
|
loginUsername.value = '';
|
||||||
|
loginPassword.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
error.value = null;
|
||||||
|
invalid.value = !form.value!.reportValidity();
|
||||||
|
|
||||||
|
if(invalid.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
submitting.value = true;
|
||||||
|
|
||||||
|
await store.dispatch(ActionTypes.LOGIN, {
|
||||||
|
username: loginUsername.value,
|
||||||
|
password: loginPassword.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
store.commit(MutationTypes.HIDE_UI_MODAL, 'login');
|
||||||
|
notify(loginSuccess.value);
|
||||||
|
} catch(e: any) {
|
||||||
|
error.value = e;
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
usernameField,
|
||||||
|
|
||||||
|
heading,
|
||||||
|
loginHeading,
|
||||||
|
usernameLabel,
|
||||||
|
passwordLabel,
|
||||||
|
loginSubmit,
|
||||||
|
|
||||||
|
loginUsername,
|
||||||
|
loginPassword,
|
||||||
|
|
||||||
|
submitting,
|
||||||
|
invalid,
|
||||||
|
error,
|
||||||
|
|
||||||
|
login,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
70
src/components/login/LoginModal.vue
Normal file
70
src/components/login/LoginModal.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<!--
|
||||||
|
- Copyright 2021 James Lyne
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal id="login">
|
||||||
|
<h2 id="login__heading">{{ heading }}</h2>
|
||||||
|
|
||||||
|
<LoginForm></LoginForm>
|
||||||
|
<RegisterForm></RegisterForm>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent} from "@vue/runtime-core";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import LoginForm from "@/components/login/LoginForm.vue";
|
||||||
|
import RegisterForm from "@/components/login/RegisterForm.vue";
|
||||||
|
import Modal from "@/components/Modal.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {Modal, RegisterForm, LoginForm},
|
||||||
|
setup() {
|
||||||
|
const store = useStore(),
|
||||||
|
heading = computed(() => store.state.messages.loginTitle);
|
||||||
|
|
||||||
|
return {
|
||||||
|
heading
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#modal--login {
|
||||||
|
::v-deep(.modal__content) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login__heading,
|
||||||
|
#login__error {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 50%;
|
||||||
|
padding: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
163
src/components/login/RegisterForm.vue
Normal file
163
src/components/login/RegisterForm.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<!--
|
||||||
|
- Copyright 2021 James Lyne
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form :class="{'form': true, 'form--invalid': invalid}" @submit.prevent="register" ref="form" novalidate>
|
||||||
|
<h3>{{ messageHeading }}</h3>
|
||||||
|
<p>{{ messageDescription }}</p>
|
||||||
|
|
||||||
|
<div class="form__group">
|
||||||
|
<label for="register-username" class="form__label" >{{ messageUsernameLabel }}</label>
|
||||||
|
<input id="register-username" type="text" name="username" autocomplete="username"
|
||||||
|
v-model="valueUsername" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form__group">
|
||||||
|
<label for="register-password" class="form__label" >{{ messagePasswordLabel }}</label>
|
||||||
|
<input id="register-password" type="password" name="password" autocomplete="new-password"
|
||||||
|
v-model="valuePassword" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form__group">
|
||||||
|
<label for="register-confirm-password" class="form__label">{{ messageConfirmPasswordLabel }}</label>
|
||||||
|
<input id="register-confirm-password" type="password" name="confirm_password"
|
||||||
|
autocomplete="new-password" v-model="valuePassword2" required ref="confirmPasswordField"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form__group">
|
||||||
|
<label for="register-code" class="form__label">{{ messageRegisterCodeLabel }}</label>
|
||||||
|
<input id="register-code" type="text" name="code" v-model="valueCode" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="alert" v-if="error" class="form__group alert">{{ error }}</div>
|
||||||
|
|
||||||
|
<div class="form__group">
|
||||||
|
<button type="submit" :disabled="submitting">{{ messageSubmit }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent, watch} from "@vue/runtime-core";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {ActionTypes} from "@/store/action-types";
|
||||||
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const store = useStore(),
|
||||||
|
form = ref<HTMLFormElement | null>(null),
|
||||||
|
confirmPasswordField = ref<HTMLInputElement | null>(null),
|
||||||
|
|
||||||
|
loginModalVisible = computed(() => store.state.ui.visibleModal === 'login'),
|
||||||
|
|
||||||
|
messageUsernameLabel = computed(() => store.state.messages.loginUsernameLabel),
|
||||||
|
messagePasswordLabel = computed(() => store.state.messages.loginPasswordLabel),
|
||||||
|
messageConfirmPasswordLabel = computed(() => store.state.messages.registerConfirmPasswordLabel),
|
||||||
|
messageRegisterCodeLabel = computed(() => store.state.messages.registerCodeLabel),
|
||||||
|
|
||||||
|
messageHeading = computed(() => store.state.messages.registerHeading),
|
||||||
|
messageDescription = computed(() => store.state.messages.registerDescription),
|
||||||
|
messageSubmit = computed(() => store.state.messages.registerSubmit),
|
||||||
|
messagePasswordMismatch = computed(() => store.state.messages.registerErrorVerifyFailed),
|
||||||
|
|
||||||
|
valueUsername = ref(''),
|
||||||
|
valuePassword = ref(''),
|
||||||
|
valuePassword2 = ref(''),
|
||||||
|
valueCode = ref(''),
|
||||||
|
|
||||||
|
submitting = ref(false),
|
||||||
|
invalid = ref(false),
|
||||||
|
error = ref(null);
|
||||||
|
|
||||||
|
watch(loginModalVisible, (newValue) => {
|
||||||
|
if(!newValue) {
|
||||||
|
valueUsername.value = '';
|
||||||
|
valuePassword.value = '';
|
||||||
|
valuePassword2.value = '';
|
||||||
|
valueCode.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkPasswords = () => {
|
||||||
|
if(valuePassword.value !== valuePassword2.value) {
|
||||||
|
console.log(messagePasswordMismatch.value);
|
||||||
|
confirmPasswordField.value!.setCustomValidity(messagePasswordMismatch.value)
|
||||||
|
} else {
|
||||||
|
confirmPasswordField.value!.setCustomValidity('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = async () => {
|
||||||
|
error.value = null;
|
||||||
|
checkPasswords();
|
||||||
|
invalid.value = !form.value!.reportValidity();
|
||||||
|
|
||||||
|
if(invalid.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
submitting.value = true;
|
||||||
|
|
||||||
|
await store.dispatch(ActionTypes.REGISTER, {
|
||||||
|
username: valueUsername.value,
|
||||||
|
password: valuePassword.value,
|
||||||
|
code: valueCode.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
store.commit(MutationTypes.HIDE_UI_MODAL, 'login');
|
||||||
|
} catch(e: any) {
|
||||||
|
error.value = e;
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
confirmPasswordField,
|
||||||
|
|
||||||
|
messageHeading,
|
||||||
|
messageDescription,
|
||||||
|
messageUsernameLabel,
|
||||||
|
messagePasswordLabel,
|
||||||
|
messageConfirmPasswordLabel,
|
||||||
|
messageRegisterCodeLabel,
|
||||||
|
messageSubmit,
|
||||||
|
|
||||||
|
valueUsername,
|
||||||
|
valuePassword,
|
||||||
|
valuePassword2,
|
||||||
|
valueCode,
|
||||||
|
|
||||||
|
submitting,
|
||||||
|
invalid,
|
||||||
|
error,
|
||||||
|
|
||||||
|
register
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
p {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
</style>
|
43
src/components/map/control/LoginControl.vue
Normal file
43
src/components/map/control/LoginControl.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!--
|
||||||
|
- Copyright 2021 James Lyne
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
|
||||||
|
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||||
|
import {LoginControl} from "@/leaflet/control/LoginControl";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
leaflet: {
|
||||||
|
type: Object as () => LiveAtlasLeafletMap,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const control = new LoginControl({
|
||||||
|
position: 'topleft',
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => props.leaflet.addControl(control));
|
||||||
|
onUnmounted(() => props.leaflet.removeControl(control));
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
27
src/index.d.ts
vendored
27
src/index.d.ts
vendored
@ -100,6 +100,27 @@ interface LiveAtlasGlobalMessageConfig {
|
|||||||
layersTitle: string;
|
layersTitle: string;
|
||||||
copyToClipboardSuccess: string;
|
copyToClipboardSuccess: string;
|
||||||
copyToClipboardError: string;
|
copyToClipboardError: string;
|
||||||
|
loginTitle: string;
|
||||||
|
loginHeading: string;
|
||||||
|
loginUsernameLabel: string;
|
||||||
|
loginPasswordLabel: string;
|
||||||
|
loginSubmit: string;
|
||||||
|
loginErrorUnknown: string;
|
||||||
|
loginErrorDisabled: string;
|
||||||
|
loginErrorIncorrect: string;
|
||||||
|
loginSuccess: string;
|
||||||
|
registerHeading: string;
|
||||||
|
registerDescription: string;
|
||||||
|
registerConfirmPasswordLabel: string;
|
||||||
|
registerCodeLabel: string;
|
||||||
|
registerSubmit: string;
|
||||||
|
registerErrorUnknown: string;
|
||||||
|
registerErrorDisabled: string;
|
||||||
|
registerErrorVerifyFailed: string;
|
||||||
|
registerErrorIncorrect: string;
|
||||||
|
logoutTitle: string;
|
||||||
|
logoutErrorUnknown: string;
|
||||||
|
logoutSuccess: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages defined by dynmap configuration responses and can vary per server
|
// Messages defined by dynmap configuration responses and can vary per server
|
||||||
@ -122,7 +143,8 @@ interface LiveAtlasUIConfig {
|
|||||||
playersSearch: boolean;
|
playersSearch: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LiveAtlasUIElement = 'layers' | 'chat' | 'players' | 'maps' | 'settings';
|
export type LiveAtlasUIElement = 'layers' | 'chat' | 'players' | 'maps';
|
||||||
|
export type LiveAtlasUIModal = 'login' | 'settings';
|
||||||
export type LiveAtlasSidebarSection = 'servers' | 'players' | 'maps';
|
export type LiveAtlasSidebarSection = 'servers' | 'players' | 'maps';
|
||||||
export type LiveAtlasDimension = 'overworld' | 'nether' | 'end';
|
export type LiveAtlasDimension = 'overworld' | 'nether' | 'end';
|
||||||
|
|
||||||
@ -172,6 +194,9 @@ interface LiveAtlasMapProvider {
|
|||||||
startUpdates(): void;
|
startUpdates(): void;
|
||||||
stopUpdates(): void;
|
stopUpdates(): void;
|
||||||
sendChatMessage(message: string): void;
|
sendChatMessage(message: string): void;
|
||||||
|
login(formData: FormData): void;
|
||||||
|
logout(): void;
|
||||||
|
register(formData: FormData): void;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
|
|
||||||
getPlayerHeadUrl(entry: HeadQueueEntry): string;
|
getPlayerHeadUrl(entry: HeadQueueEntry): string;
|
||||||
|
100
src/leaflet/control/LoginControl.ts
Normal file
100
src/leaflet/control/LoginControl.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 James Lyne
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Control, ControlOptions, DomEvent, DomUtil} from 'leaflet';
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
|
import {watch} from "@vue/runtime-core";
|
||||||
|
|
||||||
|
import "@/assets/icons/login.svg";
|
||||||
|
import "@/assets/icons/logout.svg";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {ActionTypes} from "@/store/action-types";
|
||||||
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
|
export class LoginControl extends Control {
|
||||||
|
declare options: ControlOptions
|
||||||
|
private readonly store = useStore();
|
||||||
|
private readonly loggedIn = computed(() => this.store.state.loggedIn);
|
||||||
|
private readonly _button: HTMLButtonElement;
|
||||||
|
|
||||||
|
constructor(options: ControlOptions) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
this._button = DomUtil.create('button',
|
||||||
|
'leaflet-control-bottom leaflet-control-button leaflet-control-login') as HTMLButtonElement;
|
||||||
|
|
||||||
|
this._button.type = 'button';
|
||||||
|
|
||||||
|
this._button.addEventListener('click', async e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
await this.handleClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
//Open login on ArrowRight from button
|
||||||
|
DomEvent.on(this._button,'keydown', async (e: Event) => {
|
||||||
|
if ((e as KeyboardEvent).key === 'ArrowRight') {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
await this.handleClick();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(this.loggedIn, () => {
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(this.store.state.ui, newValue => {
|
||||||
|
this._button.setAttribute('aria-expanded', (newValue.visibleModal === 'login').toString());
|
||||||
|
}, {
|
||||||
|
deep: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd() {
|
||||||
|
return this._button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private update() {
|
||||||
|
this._button.title = this.loggedIn.value
|
||||||
|
? this.store.state.messages.logoutTitle : this.store.state.messages.loginTitle;
|
||||||
|
this._button.innerHTML = `
|
||||||
|
<svg class="svg-icon">
|
||||||
|
<use xlink:href="#icon--${this.loggedIn.value ? 'logout' : 'login'}" />
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleClick() {
|
||||||
|
const logoutSuccess = computed(() => this.store.state.messages.logoutSuccess),
|
||||||
|
logoutError = computed(() => this.store.state.messages.logoutErrorUnknown);
|
||||||
|
|
||||||
|
if (this.loggedIn.value) {
|
||||||
|
try {
|
||||||
|
await this.store.dispatch(ActionTypes.LOGOUT, undefined);
|
||||||
|
notify(logoutSuccess.value);
|
||||||
|
} catch(e) {
|
||||||
|
notify(logoutError.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.store.commit(MutationTypes.SHOW_UI_MODAL, 'login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -629,8 +629,11 @@ export default class DynmapMapProvider extends MapProvider {
|
|||||||
const response = await DynmapMapProvider.getJSON(this.config.dynmap!.configuration, this.configurationAbort.signal);
|
const response = await DynmapMapProvider.getJSON(this.config.dynmap!.configuration, this.configurationAbort.signal);
|
||||||
|
|
||||||
if(response.error === 'login-required') {
|
if(response.error === 'login-required') {
|
||||||
throw new Error("Login required");
|
this.store.commit(MutationTypes.SET_LOGGED_IN, false);
|
||||||
} else if (response.error) {
|
this.store.commit(MutationTypes.SET_COMPONENTS, {login: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -807,6 +810,106 @@ export default class DynmapMapProvider extends MapProvider {
|
|||||||
return `${this.config.dynmap!.markers}_markers_/${icon}.png`;
|
return `${this.config.dynmap!.markers}_markers_/${icon}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async login(data: any) {
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
if (!store.state.components.login) {
|
||||||
|
return Promise.reject(store.state.messages.loginErrorDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
store.commit(MutationTypes.SET_LOGGED_IN, false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = new URLSearchParams();
|
||||||
|
|
||||||
|
body.append('j_username', data.username || '');
|
||||||
|
body.append('j_password', data.password || '');
|
||||||
|
|
||||||
|
|
||||||
|
const response = await DynmapMapProvider.fetchJSON(this.config.dynmap!.login, {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
switch(response.result) {
|
||||||
|
case 'success':
|
||||||
|
store.commit(MutationTypes.SET_LOGGED_IN, true);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'loginfailed':
|
||||||
|
return Promise.reject(store.state.messages.loginErrorIncorrect);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Promise.reject(store.state.messages.loginErrorUnknown);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(store.state.messages.loginErrorUnknown);
|
||||||
|
console.trace(e);
|
||||||
|
return Promise.reject(store.state.messages.loginErrorUnknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
if (!store.state.components.login) {
|
||||||
|
return Promise.reject(store.state.messages.loginErrorDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DynmapMapProvider.fetchJSON(this.config.dynmap!.login, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
store.commit(MutationTypes.SET_LOGGED_IN, false);
|
||||||
|
} catch(e) {
|
||||||
|
return Promise.reject(store.state.messages.logoutErrorUnknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(data: any) {
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
if (!store.state.components.login) {
|
||||||
|
return Promise.reject(store.state.messages.loginErrorDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
store.commit(MutationTypes.SET_LOGGED_IN, false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = new URLSearchParams();
|
||||||
|
|
||||||
|
body.append('j_username', data.username || '');
|
||||||
|
body.append('j_password', data.password || '');
|
||||||
|
body.append('j_verify_password', data.password || '');
|
||||||
|
body.append('j_passcode', data.code || '');
|
||||||
|
|
||||||
|
const response = await DynmapMapProvider.fetchJSON(this.config.dynmap!.register, {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
switch(response.result) {
|
||||||
|
case 'success':
|
||||||
|
store.commit(MutationTypes.SET_LOGGED_IN, true);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'verifyfailed':
|
||||||
|
return Promise.reject(store.state.messages.registerErrorVerifyFailed);
|
||||||
|
|
||||||
|
case 'registerfailed':
|
||||||
|
return Promise.reject(store.state.messages.registerErrorIncorrect);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Promise.reject(store.state.messages.registerErrorUnknown);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(store.state.messages.registerErrorUnknown);
|
||||||
|
console.trace(e);
|
||||||
|
return Promise.reject(store.state.messages.registerErrorUnknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
|
|
||||||
|
@ -54,6 +54,18 @@ export default abstract class MapProvider implements LiveAtlasMapProvider {
|
|||||||
throw new Error('Provider does not support chat');
|
throw new Error('Provider does not support chat');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async login(data: any) {
|
||||||
|
throw new Error('Provider does not support logging in');
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
throw new Error('Provider does not support logging out');
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(data: any) {
|
||||||
|
throw new Error('Provider does not support registration');
|
||||||
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.currentWorldUnwatch();
|
this.currentWorldUnwatch();
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
a, button {
|
a, button {
|
||||||
@extend %button;
|
@extend %button;
|
||||||
|
@ -343,6 +343,33 @@ img {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
.form__group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__label {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-emphasis);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.form--invalid input:invalid {
|
||||||
|
border-color: var(--background-error);
|
||||||
|
outline-color: var(--background-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--background-error);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
@page {
|
@page {
|
||||||
size: 297mm 210mm;
|
size: 297mm 210mm;
|
||||||
|
@ -25,4 +25,7 @@ export enum ActionTypes {
|
|||||||
POP_LINE_UPDATES = "popLineUpdates",
|
POP_LINE_UPDATES = "popLineUpdates",
|
||||||
POP_TILE_UPDATES = "popTileUpdates",
|
POP_TILE_UPDATES = "popTileUpdates",
|
||||||
SEND_CHAT_MESSAGE = "sendChatMessage",
|
SEND_CHAT_MESSAGE = "sendChatMessage",
|
||||||
|
LOGIN = "login",
|
||||||
|
LOGOUT = "logout",
|
||||||
|
REGISTER = "register",
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,17 @@ export interface Actions {
|
|||||||
{commit}: AugmentedActionContext,
|
{commit}: AugmentedActionContext,
|
||||||
payload: string
|
payload: string
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
[ActionTypes.LOGIN](
|
||||||
|
{commit}: AugmentedActionContext,
|
||||||
|
payload: any
|
||||||
|
): Promise<void>
|
||||||
|
[ActionTypes.LOGOUT](
|
||||||
|
{commit}: AugmentedActionContext
|
||||||
|
): Promise<void>
|
||||||
|
[ActionTypes.REGISTER](
|
||||||
|
{commit}: AugmentedActionContext,
|
||||||
|
payload: any
|
||||||
|
): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions: ActionTree<State, State> & Actions = {
|
export const actions: ActionTree<State, State> & Actions = {
|
||||||
@ -240,4 +251,16 @@ export const actions: ActionTree<State, State> & Actions = {
|
|||||||
async [ActionTypes.SEND_CHAT_MESSAGE]({commit, state}, message: string): Promise<void> {
|
async [ActionTypes.SEND_CHAT_MESSAGE]({commit, state}, message: string): Promise<void> {
|
||||||
await state.currentMapProvider!.sendChatMessage(message);
|
await state.currentMapProvider!.sendChatMessage(message);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async [ActionTypes.LOGIN]({state, commit}, data: any): Promise<void> {
|
||||||
|
await state.currentMapProvider!.login(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
async [ActionTypes.LOGOUT]({state}): Promise<void> {
|
||||||
|
await state.currentMapProvider!.logout();
|
||||||
|
},
|
||||||
|
|
||||||
|
async [ActionTypes.REGISTER]({state}, data: any): Promise<void> {
|
||||||
|
await state.currentMapProvider!.register(data);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,8 @@ export enum MutationTypes {
|
|||||||
SET_SMALL_SCREEN = 'setSmallScreen',
|
SET_SMALL_SCREEN = 'setSmallScreen',
|
||||||
TOGGLE_UI_ELEMENT_VISIBILITY = 'toggleUIElementVisibility',
|
TOGGLE_UI_ELEMENT_VISIBILITY = 'toggleUIElementVisibility',
|
||||||
SET_UI_ELEMENT_VISIBILITY = 'setUIElementVisibility',
|
SET_UI_ELEMENT_VISIBILITY = 'setUIElementVisibility',
|
||||||
|
SHOW_UI_MODAL = 'showUIModal',
|
||||||
|
HIDE_UI_MODAL = 'hideUIModal',
|
||||||
|
|
||||||
TOGGLE_SIDEBAR_SECTION_COLLAPSED_STATE = 'toggleSidebarSectionCollapsedState',
|
TOGGLE_SIDEBAR_SECTION_COLLAPSED_STATE = 'toggleSidebarSectionCollapsedState',
|
||||||
SET_SIDEBAR_SECTION_COLLAPSED_STATE = 'setSidebarSectionCollapsedState',
|
SET_SIDEBAR_SECTION_COLLAPSED_STATE = 'setSidebarSectionCollapsedState',
|
||||||
|
@ -39,7 +39,7 @@ import {
|
|||||||
LiveAtlasMarker,
|
LiveAtlasMarker,
|
||||||
LiveAtlasMarkerSet,
|
LiveAtlasMarkerSet,
|
||||||
LiveAtlasServerDefinition,
|
LiveAtlasServerDefinition,
|
||||||
LiveAtlasServerConfig, LiveAtlasChat, LiveAtlasPartialComponentConfig, LiveAtlasComponentConfig
|
LiveAtlasServerConfig, LiveAtlasChat, LiveAtlasPartialComponentConfig, LiveAtlasComponentConfig, LiveAtlasUIModal
|
||||||
} from "@/index";
|
} from "@/index";
|
||||||
import DynmapMapProvider from "@/providers/DynmapMapProvider";
|
import DynmapMapProvider from "@/providers/DynmapMapProvider";
|
||||||
import Pl3xmapMapProvider from "@/providers/Pl3xmapMapProvider";
|
import Pl3xmapMapProvider from "@/providers/Pl3xmapMapProvider";
|
||||||
@ -88,6 +88,8 @@ export type Mutations<S = State> = {
|
|||||||
[MutationTypes.SET_SMALL_SCREEN](state: S, payload: boolean): void
|
[MutationTypes.SET_SMALL_SCREEN](state: S, payload: boolean): void
|
||||||
[MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY](state: S, payload: LiveAtlasUIElement): void
|
[MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY](state: S, payload: LiveAtlasUIElement): void
|
||||||
[MutationTypes.SET_UI_ELEMENT_VISIBILITY](state: S, payload: {element: LiveAtlasUIElement, state: boolean}): void
|
[MutationTypes.SET_UI_ELEMENT_VISIBILITY](state: S, payload: {element: LiveAtlasUIElement, state: boolean}): void
|
||||||
|
[MutationTypes.SHOW_UI_MODAL](state: S, payload: LiveAtlasUIModal): void
|
||||||
|
[MutationTypes.HIDE_UI_MODAL](state: S, payload: LiveAtlasUIModal): void
|
||||||
|
|
||||||
[MutationTypes.TOGGLE_SIDEBAR_SECTION_COLLAPSED_STATE](state: S, section: LiveAtlasSidebarSection): void
|
[MutationTypes.TOGGLE_SIDEBAR_SECTION_COLLAPSED_STATE](state: S, section: LiveAtlasSidebarSection): void
|
||||||
[MutationTypes.SET_SIDEBAR_SECTION_COLLAPSED_STATE](state: S, payload: {section: LiveAtlasSidebarSection, state: boolean}): void
|
[MutationTypes.SET_SIDEBAR_SECTION_COLLAPSED_STATE](state: S, payload: {section: LiveAtlasSidebarSection, state: boolean}): void
|
||||||
@ -138,6 +140,27 @@ export const mutations: MutationTree<State> & Mutations = {
|
|||||||
layersTitle: messageConfig.layersTitle || '',
|
layersTitle: messageConfig.layersTitle || '',
|
||||||
copyToClipboardSuccess: messageConfig.copyToClipboardSuccess || '',
|
copyToClipboardSuccess: messageConfig.copyToClipboardSuccess || '',
|
||||||
copyToClipboardError: messageConfig.copyToClipboardError || '',
|
copyToClipboardError: messageConfig.copyToClipboardError || '',
|
||||||
|
loginTitle: messageConfig.loginTitle || '',
|
||||||
|
loginHeading: messageConfig.loginHeading || '',
|
||||||
|
loginUsernameLabel: messageConfig.loginUsernameLabel || '',
|
||||||
|
loginPasswordLabel: messageConfig.loginPasswordLabel || '',
|
||||||
|
loginSubmit: messageConfig.loginSubmit || '',
|
||||||
|
loginErrorUnknown: messageConfig.loginErrorUnknown || '',
|
||||||
|
loginErrorDisabled: messageConfig.loginErrorDisabled || '',
|
||||||
|
loginErrorIncorrect: messageConfig.loginErrorIncorrect || '',
|
||||||
|
loginSuccess: messageConfig.loginSuccess || '',
|
||||||
|
registerHeading: messageConfig.registerHeading || '',
|
||||||
|
registerDescription: messageConfig.registerDescription || '',
|
||||||
|
registerConfirmPasswordLabel: messageConfig.registerConfirmPasswordLabel || '',
|
||||||
|
registerCodeLabel: messageConfig.registerCodeLabel || '',
|
||||||
|
registerSubmit: messageConfig.registerSubmit || '',
|
||||||
|
registerErrorUnknown: messageConfig.registerErrorUnknown || '',
|
||||||
|
registerErrorDisabled: messageConfig.registerErrorDisabled || '',
|
||||||
|
registerErrorVerifyFailed: messageConfig.registerErrorVerifyFailed || '',
|
||||||
|
registerErrorIncorrect: messageConfig.registerErrorIncorrect || '',
|
||||||
|
logoutTitle: messageConfig.logoutTitle || '',
|
||||||
|
logoutErrorUnknown: messageConfig.logoutErrorUnknown || '',
|
||||||
|
logoutSuccess: messageConfig.logoutSuccess || '',
|
||||||
}
|
}
|
||||||
|
|
||||||
state.messages = Object.assign(state.messages, messages);
|
state.messages = Object.assign(state.messages, messages);
|
||||||
@ -586,6 +609,16 @@ export const mutations: MutationTree<State> & Mutations = {
|
|||||||
payload.state ? state.ui.visibleElements.add(payload.element) : state.ui.visibleElements.delete(payload.element);
|
payload.state ? state.ui.visibleElements.add(payload.element) : state.ui.visibleElements.delete(payload.element);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[MutationTypes.SHOW_UI_MODAL](state: State, modal: LiveAtlasUIModal): void {
|
||||||
|
state.ui.visibleModal = modal;
|
||||||
|
},
|
||||||
|
|
||||||
|
[MutationTypes.HIDE_UI_MODAL](state: State, modal: LiveAtlasUIModal): void {
|
||||||
|
if(state.ui.visibleModal === modal) {
|
||||||
|
state.ui.visibleModal = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
[MutationTypes.TOGGLE_SIDEBAR_SECTION_COLLAPSED_STATE](state: State, section: LiveAtlasSidebarSection): void {
|
[MutationTypes.TOGGLE_SIDEBAR_SECTION_COLLAPSED_STATE](state: State, section: LiveAtlasSidebarSection): void {
|
||||||
if(state.ui.sidebar.collapsedSections.has(section)) {
|
if(state.ui.sidebar.collapsedSections.has(section)) {
|
||||||
state.ui.sidebar.collapsedSections.delete(section);
|
state.ui.sidebar.collapsedSections.delete(section);
|
||||||
@ -642,5 +675,7 @@ export const mutations: MutationTree<State> & Mutations = {
|
|||||||
state.components.chatBox = undefined;
|
state.components.chatBox = undefined;
|
||||||
state.components.chatBalloons = false;
|
state.components.chatBalloons = false;
|
||||||
state.components.login = false;
|
state.components.login = false;
|
||||||
|
|
||||||
|
state.ui.visibleModal = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ import {
|
|||||||
LiveAtlasPlayer,
|
LiveAtlasPlayer,
|
||||||
LiveAtlasMarkerSet,
|
LiveAtlasMarkerSet,
|
||||||
LiveAtlasComponentConfig,
|
LiveAtlasComponentConfig,
|
||||||
LiveAtlasServerConfig, LiveAtlasChat
|
LiveAtlasServerConfig, LiveAtlasChat, LiveAtlasUIModal
|
||||||
} from "@/index";
|
} from "@/index";
|
||||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||||
|
|
||||||
@ -78,6 +78,7 @@ export type State = {
|
|||||||
|
|
||||||
smallScreen: boolean;
|
smallScreen: boolean;
|
||||||
visibleElements: Set<LiveAtlasUIElement>;
|
visibleElements: Set<LiveAtlasUIElement>;
|
||||||
|
visibleModal?: LiveAtlasUIModal;
|
||||||
previouslyVisibleElements: Set<LiveAtlasUIElement>;
|
previouslyVisibleElements: Set<LiveAtlasUIElement>;
|
||||||
|
|
||||||
sidebar: {
|
sidebar: {
|
||||||
@ -144,6 +145,27 @@ export const state: State = {
|
|||||||
layersTitle: '',
|
layersTitle: '',
|
||||||
copyToClipboardSuccess: '',
|
copyToClipboardSuccess: '',
|
||||||
copyToClipboardError: '',
|
copyToClipboardError: '',
|
||||||
|
loginTitle: '',
|
||||||
|
loginHeading: '',
|
||||||
|
loginUsernameLabel: '',
|
||||||
|
loginPasswordLabel: '',
|
||||||
|
loginSubmit: '',
|
||||||
|
loginErrorUnknown: '',
|
||||||
|
loginErrorDisabled: '',
|
||||||
|
loginErrorIncorrect: '',
|
||||||
|
loginSuccess: '',
|
||||||
|
registerHeading: '',
|
||||||
|
registerDescription: '',
|
||||||
|
registerConfirmPasswordLabel: '',
|
||||||
|
registerCodeLabel: '',
|
||||||
|
registerSubmit: '',
|
||||||
|
registerErrorUnknown: '',
|
||||||
|
registerErrorDisabled: '',
|
||||||
|
registerErrorVerifyFailed: '',
|
||||||
|
registerErrorIncorrect: '',
|
||||||
|
logoutTitle: '',
|
||||||
|
logoutErrorUnknown: '',
|
||||||
|
logoutSuccess: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
@ -227,6 +249,7 @@ export const state: State = {
|
|||||||
|
|
||||||
smallScreen: false,
|
smallScreen: false,
|
||||||
visibleElements: new Set(),
|
visibleElements: new Set(),
|
||||||
|
visibleModal: undefined,
|
||||||
previouslyVisibleElements: new Set(),
|
previouslyVisibleElements: new Set(),
|
||||||
|
|
||||||
sidebar: {
|
sidebar: {
|
||||||
|
Loading…
Reference in New Issue
Block a user