Change password form and several changes
This commit is contained in:
parent
ec0d783672
commit
aa4ccf65f5
|
@ -2,7 +2,9 @@ import { boot } from 'quasar/wrappers';
|
|||
import { Connection } from '../js/db/connection';
|
||||
import { userStore } from 'stores/user';
|
||||
import axios from 'axios';
|
||||
import useNotify from 'src/composables/useNotify.js';
|
||||
|
||||
const { notify } = useNotify();
|
||||
// Be careful when using SSR for cross-request state pollution
|
||||
// due to creating a Singleton instance here;
|
||||
// If any client changes this (global) instance, it might be a
|
||||
|
@ -12,9 +14,27 @@ import axios from 'axios';
|
|||
const api = axios.create({
|
||||
baseURL: `//${location.hostname}:${location.port}/api/`
|
||||
});
|
||||
|
||||
const jApi = new Connection();
|
||||
|
||||
const onRequestError = error => {
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
const onResponseError = error => {
|
||||
let message = '';
|
||||
|
||||
const response = error.response;
|
||||
const responseData = response && response.data;
|
||||
const responseError = responseData && response.data.error;
|
||||
if (responseError) {
|
||||
message = responseError.message;
|
||||
}
|
||||
|
||||
notify(message, 'negative');
|
||||
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
export default boot(({ app }) => {
|
||||
const user = userStore();
|
||||
function addToken(config) {
|
||||
|
@ -23,7 +43,9 @@ export default boot(({ app }) => {
|
|||
}
|
||||
return config;
|
||||
}
|
||||
api.interceptors.request.use(addToken);
|
||||
api.interceptors.request.use(addToken, onRequestError);
|
||||
api.interceptors.response.use(response => response, onResponseError);
|
||||
|
||||
jApi.use(addToken);
|
||||
|
||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { ref, inject, onMounted, computed } from 'vue';
|
||||
import { ref, inject, onMounted, computed, Teleport } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import useNotify from 'src/composables/useNotify.js';
|
||||
|
@ -66,9 +66,13 @@ const props = defineProps({
|
|||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showBottomActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
saveFn: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -95,6 +99,8 @@ const updatedColumns = computed(() => {
|
|||
);
|
||||
});
|
||||
|
||||
const hasChanges = computed(() => !!updatedColumns.value.length);
|
||||
|
||||
const fetchFormData = async () => {
|
||||
if (!props.fetchFormDataSql.query) return;
|
||||
loading.value = true;
|
||||
|
@ -117,20 +123,26 @@ const fetchFormData = async () => {
|
|||
loading.value = false;
|
||||
};
|
||||
|
||||
const onSubmitSuccess = () => {
|
||||
emit('onDataSaved');
|
||||
notify(t('dataSaved'), 'positive');
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (props.saveFn) {
|
||||
await props.saveFn();
|
||||
await props.saveFn(formData.value);
|
||||
} else {
|
||||
if (!hasChanges.value) {
|
||||
return;
|
||||
}
|
||||
const sqlQuery = generateSqlQuery();
|
||||
await jApi.execQuery(sqlQuery, props.pks);
|
||||
modelInfo.value.data[0] = { ...formData.value };
|
||||
}
|
||||
|
||||
emit('onDataSaved');
|
||||
notify(t('dataSaved'), 'positive');
|
||||
onSubmitSuccess();
|
||||
} catch (error) {
|
||||
console.error('Error updating address:', error);
|
||||
notify(t('addressNotUpdated'), 'negative');
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -167,18 +179,6 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport :to="$actions">
|
||||
<QBtn
|
||||
v-if="defaultActions"
|
||||
:label="t('save')"
|
||||
type="submit"
|
||||
icon="check"
|
||||
rounded
|
||||
no-caps
|
||||
:disabled="!updatedColumns.length"
|
||||
@click="addressFormRef.submit()"
|
||||
/>
|
||||
</Teleport>
|
||||
<QCard class="form-container" v-bind="$attrs">
|
||||
<QForm
|
||||
v-if="!loading"
|
||||
|
@ -190,6 +190,33 @@ defineExpose({
|
|||
{{ title }}
|
||||
</span>
|
||||
<slot name="form" :data="formData" />
|
||||
<component
|
||||
:is="showBottomActions ? 'div' : Teleport"
|
||||
:to="$actions"
|
||||
class="flex row justify-end q-gutter-x-sm"
|
||||
:class="{ 'q-mt-md': showBottomActions }"
|
||||
>
|
||||
<QBtn
|
||||
v-if="defaultActions"
|
||||
:label="t('cancel')"
|
||||
:icon="showBottomActions ? undefined : 'check'"
|
||||
rounded
|
||||
no-caps
|
||||
flat
|
||||
v-close-popup
|
||||
/>
|
||||
<QBtn
|
||||
v-if="defaultActions"
|
||||
:label="t('save')"
|
||||
type="submit"
|
||||
:icon="showBottomActions ? undefined : 'check'"
|
||||
rounded
|
||||
no-caps
|
||||
flat
|
||||
:disabled="!showBottomActions && !updatedColumns.length"
|
||||
/>
|
||||
<slot name="actions" />
|
||||
</component>
|
||||
</QForm>
|
||||
<QSpinner v-else color="primary" size="3em" :thickness="2" />
|
||||
</QCard>
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import { ref, inject, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import VnInput from 'src/components/common/VnInput.vue';
|
||||
import VnForm from 'src/components/common/VnForm.vue';
|
||||
|
||||
import { userStore as useUserStore } from 'stores/user';
|
||||
import useNotify from 'src/composables/useNotify.js';
|
||||
|
||||
const { t } = useI18n();
|
||||
const api = inject('api');
|
||||
const vnFormRef = ref(null);
|
||||
const userStore = useUserStore();
|
||||
const { notify } = useNotify();
|
||||
const router = useRouter();
|
||||
|
||||
const passwordRequirementsDialogRef = ref(null);
|
||||
const vnFormRef = ref(null);
|
||||
const repeatPassword = ref('');
|
||||
const passwordRequirements = ref(null);
|
||||
|
||||
const formData = ref({
|
||||
userId: userStore.id,
|
||||
|
@ -21,21 +27,40 @@ const formData = ref({
|
|||
});
|
||||
|
||||
const changePassword = async () => {
|
||||
if (formData.value.newPassword !== repeatPassword.value) {
|
||||
notify(t('passwordsDoNotMatch'), 'negative');
|
||||
throw new Error('Passwords do not match');
|
||||
}
|
||||
await api.patch('Accounts/change-password', formData.value);
|
||||
};
|
||||
|
||||
const getPasswordRequirements = async () => {
|
||||
try {
|
||||
// TODO: Add validation
|
||||
await api.patch('Accounts/change-password', formData.value);
|
||||
const { data } = await api.get('UserPasswords/findOne');
|
||||
passwordRequirements.value = data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await userStore.logout();
|
||||
router.push({ name: 'Login' });
|
||||
};
|
||||
|
||||
onMounted(async () => await getPasswordRequirements());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VnForm
|
||||
ref="vnFormRef"
|
||||
:title="t('changePassword')"
|
||||
:formInitialData="formData"
|
||||
:saveFn="changePassword"
|
||||
showBottomActions
|
||||
:defaultActions="false"
|
||||
style="max-width: 300px"
|
||||
@onDataSaved="logout()"
|
||||
>
|
||||
<template #form>
|
||||
<VnInput
|
||||
|
@ -53,9 +78,54 @@ const changePassword = async () => {
|
|||
:label="t('repeatPassword')"
|
||||
type="password"
|
||||
/>
|
||||
<QBtn @click="changePassword" label="saveeeee" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<QBtn
|
||||
:label="t('requirements')"
|
||||
rounded
|
||||
no-caps
|
||||
flat
|
||||
@click="passwordRequirementsDialogRef.show()"
|
||||
/>
|
||||
<QBtn :label="t('modify')" type="submit" rounded no-caps flat />
|
||||
</template>
|
||||
</VnForm>
|
||||
<QDialog ref="passwordRequirementsDialogRef">
|
||||
<QCard class="q-px-md q-py-lg column items-center">
|
||||
<span class="text-h6 text-bold q-mb-md">
|
||||
{{ t('passwordRequirements') }}
|
||||
</span>
|
||||
<div class="column" style="max-width: max-content">
|
||||
<span>
|
||||
{{
|
||||
t('charactersLong', {
|
||||
length: passwordRequirements.length
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{
|
||||
t('alphabeticCharacters', {
|
||||
nAlpha: passwordRequirements.nAlpha
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{
|
||||
t('capitalLetters', {
|
||||
nUpper: passwordRequirements.nUpper
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ t('digits', { nDigits: passwordRequirements.nDigits }) }}
|
||||
</span>
|
||||
<span>
|
||||
{{ t('symbols', { nPunct: passwordRequirements.nPunct }) }}
|
||||
</span>
|
||||
</div>
|
||||
</QCard>
|
||||
</QDialog>
|
||||
</template>
|
||||
|
||||
<i18n lang="yaml">
|
||||
|
@ -64,24 +134,69 @@ en-US:
|
|||
newPassword: New password
|
||||
oldPassword: Old password
|
||||
repeatPassword: Repeat password
|
||||
modify: Modify
|
||||
requirements: Requirements
|
||||
passwordRequirements: Password requirements
|
||||
charactersLong: '{length} characters long'
|
||||
alphabeticCharacters: '{nAlpha} alphabetic characters'
|
||||
capitalLetters: '{nUpper} capital letters'
|
||||
digits: '{nDigits} digits'
|
||||
symbols: '{nPunct} symbols. Ej: $%&.'
|
||||
passwordsDoNotMatch: Passwords do not match
|
||||
es-ES:
|
||||
changePassword: Cambiar contraseña
|
||||
newPassword: Nueva contraseña
|
||||
oldPassword: Contraseña antigua
|
||||
repeatPassword: Repetir contraseña
|
||||
modify: Modificar
|
||||
requirements: Requisitos
|
||||
passwordRequirements: Requisitos de contraseña
|
||||
charactersLong: '{length} caracteres de longitud'
|
||||
alphabeticCharacters: '{nAlpha} caracteres alfabéticos'
|
||||
capitalLetters: '{nUpper} letras mayúsculas'
|
||||
digits: '{nDigits} dígitos'
|
||||
symbols: '{nPunct} símbolos. Ej: $%&.'
|
||||
passwordsDoNotMatch: ¡Las contraseñas no coinciden!
|
||||
ca-ES:
|
||||
changePassword: Canviar contrasenya
|
||||
newPassword: Nova contrasenya
|
||||
oldPassword: Contrasenya antiga
|
||||
repeatPassword: Repetir contrasenya
|
||||
modify: Modificar
|
||||
requirements: Requisits
|
||||
passwordRequirements: Requisits de contrasenya
|
||||
charactersLong: '{length} caràcters de longitud'
|
||||
alphabeticCharacters: '{nAlpha} caràcters alfabètics'
|
||||
capitalLetters: '{nUpper} lletres majúscules'
|
||||
digits: '{nDigits} dígits'
|
||||
symbols: '{nPunct} símbols. Ej: $%&.'
|
||||
passwordsDoNotMatch: Les contrasenyes no coincideixen!
|
||||
fr-FR:
|
||||
changePassword: Changer le mot de passe
|
||||
newPassword: Nouveau mot de passe
|
||||
oldPassword: Ancien mot de passe
|
||||
repeatPassword: Répéter le mot de passe
|
||||
modify: Modifier
|
||||
requirements: Exigences
|
||||
passwordRequirements: Mot de passe exigences
|
||||
charactersLong: '{length} caractères de longueur'
|
||||
alphabeticCharacters: '{nAlpha} caractères alphabétiques'
|
||||
capitalLetters: '{nUpper} lettres majuscules'
|
||||
digits: '{nDigits} chiffres'
|
||||
symbols: '{nPunct} symboles. Ej: $%&.'
|
||||
passwordsDoNotMatch: Les mots de passe ne correspondent pas!
|
||||
pt-PT:
|
||||
changePassword: Alterar palavra-passe
|
||||
newPassword: Nova palavra-passe
|
||||
oldPassword: Palavra-passe antiga
|
||||
repeatPassword: Repetir palavra-passe
|
||||
modify: Modificar
|
||||
requirements: Requisitos
|
||||
passwordRequirements: Requisitos de palavra-passe
|
||||
charactersLong: '{length} caracteres de comprimento'
|
||||
alphabeticCharacters: '{nAlpha} caracteres alfabéticos'
|
||||
capitalLetters: '{nUpper} letras maiúsculas'
|
||||
digits: '{nDigits} dígitos'
|
||||
symbols: '{nPunct} símbolos. Ej: $%&.'
|
||||
passwordsDoNotMatch: As palavras-passe não coincidem!
|
||||
</i18n>
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
// app global css in SCSS form
|
||||
|
||||
@font-face {
|
||||
font-family: Poppins;
|
||||
src: url(./poppins.ttf) format('truetype');
|
||||
font-family: Poppins;
|
||||
src: url(./poppins.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url(./opensans.ttf) format('truetype');
|
||||
font-family: 'Open Sans';
|
||||
src: url(./opensans.ttf) format('truetype');
|
||||
}
|
||||
@mixin mobile {
|
||||
@media screen and (max-width: 960px) {
|
||||
@content;
|
||||
}
|
||||
@media screen and (max-width: 960px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Poppins', 'Verdana', 'Sans';
|
||||
background-color: #fafafa;
|
||||
font-family: 'Poppins', 'Verdana', 'Sans';
|
||||
background-color: #fafafa;
|
||||
}
|
||||
a.link {
|
||||
text-decoration: none;
|
||||
color: #6a1;
|
||||
text-decoration: none;
|
||||
color: #6a1;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.q-card {
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.6em !important;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.q-page-sticky.fixed-bottom-right {
|
||||
margin: 18px;
|
||||
margin: 18px;
|
||||
}
|
||||
|
|
|
@ -74,5 +74,7 @@ export default {
|
|||
user: 'User',
|
||||
addresses: 'Addresses',
|
||||
addressEdit: 'Edit address',
|
||||
dataSaved: 'Data saved'
|
||||
dataSaved: 'Data saved',
|
||||
save: 'Save',
|
||||
cancel: 'Cancel'
|
||||
};
|
||||
|
|
|
@ -74,5 +74,7 @@ export default {
|
|||
user: 'Usuario',
|
||||
addresses: 'Direcciones',
|
||||
addressEdit: 'Editar dirección',
|
||||
dataSaved: 'Datos guardados'
|
||||
dataSaved: 'Datos guardados',
|
||||
save: 'Guardar',
|
||||
cancel: 'Cancelar'
|
||||
};
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
const sanitizeValue = value => {
|
||||
if (typeof value === 'string') {
|
||||
return `'${value}'`;
|
||||
} else if (value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const generateUpdateSqlQuery = (
|
||||
schema,
|
||||
table,
|
||||
|
@ -6,7 +16,7 @@ export const generateUpdateSqlQuery = (
|
|||
formData
|
||||
) => {
|
||||
const setClauses = columnsUpdated
|
||||
.map(colName => `${colName} = '${formData[colName]}'`)
|
||||
.map(colName => `${colName} = ${sanitizeValue(formData[colName])}`)
|
||||
.join(', ');
|
||||
const whereClause = Object.keys(pks)
|
||||
.map(pk => `${pk} = ${pks[pk]}`)
|
||||
|
@ -30,7 +40,7 @@ export const generateInsertSqlQuery = (
|
|||
const columns = [createModelDefault.field, ...columnsUpdated].join(', ');
|
||||
const values = [
|
||||
createModelDefault.value,
|
||||
...columnsUpdated.map(colName => `'${formData[colName]}'`)
|
||||
...columnsUpdated.map(colName => sanitizeValue(formData[colName]))
|
||||
].join(', ');
|
||||
|
||||
return `
|
||||
|
|
|
@ -76,14 +76,25 @@ onMounted(() => fetchLanguagesSql());
|
|||
disable
|
||||
:clearable="false"
|
||||
/>
|
||||
<VnInput v-model="data.email" :label="t('email')" />
|
||||
<VnInput v-model="data.nickname" :label="t('nickname')" />
|
||||
<VnInput
|
||||
v-model="data.email"
|
||||
:label="t('email')"
|
||||
@keyup.enter="vnFormRef.submit()"
|
||||
@blur="vnFormRef.submit()"
|
||||
/>
|
||||
<VnInput
|
||||
v-model="data.nickname"
|
||||
:label="t('nickname')"
|
||||
@keyup.enter="vnFormRef.submit()"
|
||||
@blur="vnFormRef.submit()"
|
||||
/>
|
||||
<VnSelect
|
||||
v-model="data.lang"
|
||||
:label="t('lang')"
|
||||
option-label="name"
|
||||
option-value="code"
|
||||
:options="langOptions"
|
||||
@update:modelValue="vnFormRef.submit()"
|
||||
/>
|
||||
</template>
|
||||
</VnForm>
|
||||
|
|
|
@ -4,7 +4,7 @@ const routes = [
|
|||
component: () => import('layouts/LoginLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'login',
|
||||
name: 'Login',
|
||||
path: '/login/:email?',
|
||||
component: () => import('pages/Login/Login.vue')
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue