user module + translations

This commit is contained in:
2026-02-05 16:38:34 +02:00
parent 4d95a40c37
commit 6e95ac7999
9 changed files with 185 additions and 35 deletions
+1 -6
View File
@@ -69,12 +69,7 @@ class UsersController {
await user.update(req, req.body); await user.update(req, req.body);
res.json({ status: 'OK' }); res.json({ status: 'OK' });
} catch (err) { } catch (err) {
if (err.message == 'unauthorized') { res.json({ status: 'error', message: err.message});
res.status(401).json({ status: 'error', message: 'Unauthorized' })
} else {
console.error(err);
res.status(500).json({ status: 'error' })
}
} }
}) })
+6
View File
@@ -1,6 +1,12 @@
<template> <template>
<v-app> <v-app>
<router-view :key="$route.fullPath" /> <router-view :key="$route.fullPath" />
<v-snackbar v-model="store.snackbar.show" :color="store.snackbar.color" :timeout="store.snackbar.timeout">
{{ store.snackbar.text }}
<template v-slot:actions>
<v-btn variant="text" @click="store.snackbar.show = false"> Close </v-btn>
</template>
</v-snackbar>
</v-app> </v-app>
</template> </template>
+10 -6
View File
@@ -1,16 +1,20 @@
<template> <template>
<v-app-bar color="blue" scroll-behavior="elevate" scroll-threshold="20"> <v-app-bar color="blue" scroll-behavior="elevate" scroll-threshold="20">
<!-- <v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon> --> <!-- <v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon> -->
<v-app-bar-title>ProNature Playground</v-app-bar-title> <v-app-bar-title>{{ l.playground }}</v-app-bar-title>
<v-btn to="/manage" icon="mdi-wrench-cog-outline" v-if="roles.editor"></v-btn> <v-btn to="/manage" icon="mdi-wrench-cog-outline" v-if="roles.editor" v-tooltip="l.workshop"></v-btn>
<v-dialog max-width="400"> <v-dialog max-width="480">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn icon="mdi-account" variant="text" v-bind="props"></v-btn> <v-btn icon="mdi-account" variant="text" v-bind="props"></v-btn>
</template> </template>
<v-card class="pa-3" :title="user? 'Profile' : 'Sign in' "> <template v-slot:default="{ isActive }">
<Auth v-if="!user"></Auth> <v-card class="pa-3" :title="user? l.profile : l.signin ">
<Profile v-else></Profile> <v-card-text>
<Profile v-if="user"></Profile>
<Auth v-show="!user" @login-success="isActive.value = false"></Auth>
</v-card-text>
</v-card> </v-card>
</template>
</v-dialog > </v-dialog >
</v-app-bar> </v-app-bar>
</template> </template>
+2 -2
View File
@@ -1,8 +1,8 @@
<template> <template>
<v-app-bar color="primary" scroll-behavior="elevate" scroll-threshold="20"> <v-app-bar color="primary" scroll-behavior="elevate" scroll-threshold="20">
<!-- <v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon> --> <!-- <v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon> -->
<v-app-bar-title>ProNature Games Workshop</v-app-bar-title> <v-app-bar-title>{{ l.workshop }}</v-app-bar-title>
<v-btn to="/" icon="mdi-seesaw" v-tooltip="'To playground'"></v-btn> <v-btn to="/" icon="mdi-seesaw" v-tooltip="l.playground"></v-btn>
<v-menu> <v-menu>
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn icon="mdi-plus" variant="text" v-bind="props"></v-btn> <v-btn icon="mdi-plus" variant="text" v-bind="props"></v-btn>
+18 -8
View File
@@ -1,14 +1,14 @@
<template> <template>
<v-form> <v-form>
<v-text-field label="Email" v-model="form.email" type="email" required></v-text-field> <v-text-field :label="l.email" v-model="form.email" type="email" required></v-text-field>
<v-text-field label="Password" v-model="form.password" type="password" required></v-text-field> <v-text-field :label="l.password" v-model="form.password" type="password" required></v-text-field>
<template v-if="mode==='register'"> <template v-if="mode==='register'">
<v-text-field label="Confirm Password" v-model="form.passConfirm" type="password" required></v-text-field> <v-text-field :label="l.passwordRetype" v-model="form.passConfirm" type="password" required></v-text-field>
<v-img :src="`/api/user/captcha?${captchaIter}`" class="my-2" height="100px"></v-img> <v-img :src="`/api/user/captcha?${captchaIter}`" class="my-2" height="100px"></v-img>
<v-text-field label="Enter Captcha" v-model="form.captcha" required></v-text-field> <v-text-field :label="l.confirmCaptcha" v-model="form.captcha" required></v-text-field>
</template> </template>
<v-btn @click="login" :color="mode==='login' ? 'green' : 'grey'">Sign in</v-btn> <v-btn @click="login" :color="mode==='login' ? 'green' : 'grey'">{{l.signin}}</v-btn>
<v-btn @click="register" :color="mode==='register' ? 'green' : 'grey'" class="float-right">Sign up</v-btn> <v-btn @click="register" :color="mode==='register' ? 'green' : 'grey'" class="float-right">{{l.signup}}</v-btn>
</v-form> </v-form>
</template> </template>
@@ -30,8 +30,13 @@ export default {
// Implement login logic here // Implement login logic here
if (this.mode == 'login') { if (this.mode == 'login') {
// Perform login // Perform login
await this.$api.user.signin(this.form); let response = await this.$api.user.signin(this.form);
if(response.data?.status === 'OK') {
await this.loadUser(); await this.loadUser();
this.$emit('login-success');
} else {
this.toast(this.getErrorText(response.data), 'red');
}
} else { } else {
this.mode = 'login'; this.mode = 'login';
} }
@@ -39,8 +44,13 @@ export default {
async register() { async register() {
if (this.mode == 'register') { if (this.mode == 'register') {
// Implement registration logic here // Implement registration logic here
await this.$api.user.signup(this.form); let response = await this.$api.user.signup(this.form);
if(response.data?.status === 'OK') {
this.toast(this.l.signupSuccess, 'green');
await this.loadUser(); await this.loadUser();
} else {
this.toast(this.getErrorText(response.data), 'red');
}
} else { } else {
this.mode = 'register'; this.mode = 'register';
} }
+12 -6
View File
@@ -1,14 +1,15 @@
<template> <template>
<v-form> <v-form>
<v-text-field label="Email" disabled v-model="user.email" type="email" required></v-text-field> <v-text-field :label="l.email" disabled v-model="user.email" type="email" required></v-text-field>
<v-text-field label="Change Password" v-model="form.password" type="password" required></v-text-field> <v-text-field :label="l.passwordChange" v-model="form.password" type="password" required></v-text-field>
<v-text-field label="Confirm Password" v-model="form.passConfirm" type="password" required></v-text-field> <v-text-field :label="l.passwordRetype" v-model="form.passConfirm" type="password" required></v-text-field>
<v-btn @click="update" color="green">Update Profile</v-btn> <v-btn @click="update" color="green">{{ l.update }}</v-btn>
<v-btn @click="signout" color="grey" class="float-right">Sign out</v-btn> <v-btn @click="signout" color="grey" class="float-right">{{ l.signout }}</v-btn>
</v-form> </v-form>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
@@ -24,7 +25,12 @@ export default {
await this.loadUser(); await this.loadUser();
}, },
async update() { async update() {
await this.$api.user.update({...this.form, _id: this.user._id}); let response = await this.$api.user.update({...this.form, _id: this.user._id});
if(response.data.status === 'OK') {
this.toast('Profile updated successfully', 'green');
} else {
this.toast(this.getErrorText(response.data), 'red');
}
} }
} }
} }
+21 -1
View File
@@ -3,7 +3,7 @@ import { useAppStore } from '@/stores/app';
export default { export default {
data(){ data(){
return { return {
store: null store: null,
} }
}, },
created(){ created(){
@@ -32,5 +32,25 @@ export default {
this.user = response.data.user; this.user = response.data.user;
return this.user; return this.user;
}, },
getErrorText(error){
let msg = error?.response?.data?.error || error?.message;
if (msg){
if (typeof msg == 'object'){
return JSON.stringify(msg);
}else if (this.l.errors[msg]) {
return this.l.errors[msg]
}else {
return msg;
}
}else{
return error;
}
},
toast(text, color, timeout=3000){
this.store.snackbar.text = text;
this.store.snackbar.color = color;
this.store.snackbar.timeout = timeout;
this.store.snackbar.show = true;
}
} }
} }
+104 -2
View File
@@ -1,6 +1,8 @@
const lang = { const lang = {
en: { en: {
_code: 'en', _code: 'en',
playground: 'ProNature Playground',
workshop: 'ProNature Workshop',
createGameObject: 'Add game object', createGameObject: 'Add game object',
editGameObject: 'Edit game object', editGameObject: 'Edit game object',
createGame: 'Add game', createGame: 'Add game',
@@ -38,10 +40,61 @@ const lang = {
editScenario: 'Edit scenario', editScenario: 'Edit scenario',
editScenes: 'Edit scenes', editScenes: 'Edit scenes',
addScene: 'Add scene', addScene: 'Add scene',
addTask: 'Add task' addTask: 'Add task',
date: 'Date modified',
update: 'Update',
signin: 'Sign in',
signup: 'Sign up',
signupSuccess: 'Successful sign-up',
profile: 'Profile',
'reset-password': 'Reset password',
'change-password': 'Change password',
signout: 'Sign out',
faq: 'Help',
email:'E-mail',
passwordChange: 'Change password',
password:'Password',
passwordRetype: 'Password (confirm)',
passwordCurrent: 'Your current password',
passwordForgotten: 'Forgotten password',
recoveryMailSent: 'Password reset mail was sent. Please check your e-mail.',
confirmCaptcha: 'Enter the text you see',
'validate-email': 'E-mail validation',
emailValidated: 'Your e-mail address was successfully validated.',
validationMailContent: v1 => `Activation link: <a href="${v1}">${v1}</a>`,
validationMailSent: 'Activation link was sent.',
forgottenPassMailContent: v1 => `Recover your account by following this link: <a href="${v1}">${v1}</a>`,
emailNotValidated: 'This email is not validated',
resendValidationMail: 'Resend verification e-mail',
displayName:'Display Name',
firstName:'First Name',
lastName:'Last Name',
errors:{
unauthorized: 'Unauthorized',
notFound: 'Object not found',
noReadPermissions: 'You don\'t have access to this content',
noEditPermissions:'Missing edit permissions',
noCreatePermissions:'Missing create permissions',
noDeletePermissions:'Missing delete permissions',
noPermissions: 'No permissions',
ftsUnavailable: 'Full text search service is not available',
systemReadOnly: 'Request rejected. The system is in read-only mode.',
invalidEmail: 'Invalid email',
emailExists: 'This email is already registered',
invalidPassword: 'Invalid password',
invalidUsername: 'Invalid username',
passwordMismatch: 'Password mismatch',
invalidCaptcha: 'Invalid captcha',
invalidValidationLink: 'Invalid/inactive validation link',
activationLinkExpired: 'Activation link has expired',
invalidActivationLink: 'Invalid activation link',
objectUpdateCollision: 'Save failed. Object was altered by another user. Please refresh the page.'
},
}, },
bg: { bg: {
_code: 'bg', _code: 'bg',
playground: 'ProNature игрище',
workshop: 'ProNature работилница',
createGameObject: 'Добавяне на игрови обект', createGameObject: 'Добавяне на игрови обект',
editGameObject: 'Редактиране на игрови обект', editGameObject: 'Редактиране на игрови обект',
createGame: 'Добавяне на игра', createGame: 'Добавяне на игра',
@@ -79,7 +132,56 @@ const lang = {
editScenario: 'Редактиране на сценарий', editScenario: 'Редактиране на сценарий',
editScenes: 'Редактиране на сцени', editScenes: 'Редактиране на сцени',
addScene: 'Добавяне на сцена', addScene: 'Добавяне на сцена',
addTask: 'Добавяне на задача' addTask: 'Добавяне на задача',
date: 'Промяна',
update: 'Обнови',
signin: 'Вход',
signup: 'Регистрация',
signupSuccess: 'Успешна регистрация',
profile: 'Профил',
'reset-password': 'Възстановяване на парола',
'change-password': 'Промяна на парола',
signout: 'Изход',
faq: 'Помощ',
email:'Имейл',
passwordChange: 'Смяна на парола',
password:'Парола',
passwordRetype:'Парола (отново)',
passwordCurrent: 'Текуща парола',
passwordForgotten: 'Забравена парола',
recoveryMailSent: 'На посочения от Вас адрес е изпратен мейл за възстановяване на парола',
confirmCaptcha: 'Въведете текста от картинката',
'validate-email': 'Валидиране на имейл',
emailValidated: 'Вашият имейл е валидиран.',
validationMailContent: v1 => `Линк за активиране: <a href="${v1}">${v1}</a>`,
validationMailSent: 'На посочената поща е изпратен линк за активиране',
forgottenPassMailContent: v1 => `Линк за възстановяване на парола: <a href="${v1}">${v1}</a>`,
emailNotValidated: 'Този имейл не е валидиран',
resendValidationMail: 'Повторно изпращане на имейл за верификация',
displayName:'Псевдоним',
firstName:'Име',
lastName:'Фамилия',
errors:{
unauthorized: 'Отказан достъп',
notFound: 'Обектът не е намерен',
noReadPermissions: 'Нямате достъп до това съдържание',
noEditPermissions:'Нямате права за редакция на този обект',
noCreatePermissions:'Нямате права за създаване на обект',
noDeletePermissions:'Нямате права за изтриване на този обект',
noPermissions: 'Нямате права',
ftsUnavailable: 'Услугата за пълнотекстово търсене не е налична',
systemReadOnly: 'Записът е отказан. Системата работи в read-only режим.',
invalidEmail: 'Невалиден имейл',
emailExists: 'Този имейл вече е регистриран',
invalidPassword: 'Грешна парола',
invalidUsername: 'Грешно потребителско име',
passwordMismatch: 'Паролите не съвпадат',
invalidCaptcha: 'Невалиден текст от картинката',
invalidValidationLink: 'Невалиден линк за валидация',
activationLinkExpired: 'Изтекъл линк за активация',
invalidActivationLink: 'Невалиден линк за активация',
objectUpdateCollision: 'Неуспешен запис. Обектът е бил редактиран от друг потребител. Моля опресенете страницата.'
},
}, },
} }
+8 -1
View File
@@ -9,10 +9,17 @@ prefs = reactive(prefs ? JSON.parse(prefs) : {
} }
}) })
let snackbar = reactive({
show: false,
text: '',
color: 'info',
timeout: 3000,
})
watch(prefs, (newPrefs) => { watch(prefs, (newPrefs) => {
localStorage.setItem('prefs', JSON.stringify(newPrefs)) localStorage.setItem('prefs', JSON.stringify(newPrefs))
}, { deep: true }) }, { deep: true })
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
state: () => ({ prefs }), state: () => ({ prefs, snackbar }),
}) })