feat(auth): adds local account authentication
This commit is contained in:
@@ -3,10 +3,12 @@ import {ref, markRaw} from 'vue';
|
||||
import {useCreatorProfileStore} from '@/stores/creatorProfileStore.js';
|
||||
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import {useRouter} from 'vue-router';
|
||||
import SocialsDialog from './creators/SocialsDialog.vue';
|
||||
import AliasDialog from "@/views/profile/account/AliasDialog.vue";
|
||||
import FullnameDialog from "@/views/profile/account/FullnameDialog.vue";
|
||||
import EmailDialog from "@/views/profile/account/EmailDialog.vue";
|
||||
import ChangePasswordDialog from "@/views/profile/account/ChangePasswordDialog.vue";
|
||||
import ChangeStripeIdDialog from '@/views/profile/creators/ChangeStripeIdDialog.vue';
|
||||
import ChangeNameDialog from '@/views/profile/creators/ChangeNameDialog.vue';
|
||||
import ChangeSlugDialog from '@/views/profile/creators/ChangeSlugDialog.vue';
|
||||
@@ -25,6 +27,7 @@ import {useI18n} from 'vue-i18n';
|
||||
import QRCodeVue from 'qrcode.vue';
|
||||
|
||||
const {t} = useI18n();
|
||||
const router = useRouter();
|
||||
const userProfileStore = useUserProfileStore()
|
||||
const creatorProfileStore = useCreatorProfileStore();
|
||||
const baseURL = window.location.origin;
|
||||
@@ -102,6 +105,7 @@ const deleteDialogShown = ref(false);
|
||||
|
||||
const componentsMap = {
|
||||
EmailDialog: markRaw(EmailDialog),
|
||||
ChangePasswordDialog: markRaw(ChangePasswordDialog),
|
||||
SocialsDialog: markRaw(SocialsDialog),
|
||||
ChangeSlugDialog: markRaw(ChangeSlugDialog),
|
||||
ChangeNameDialog: markRaw(ChangeNameDialog),
|
||||
@@ -251,6 +255,14 @@ async function deconfigureStripe() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<button class="action" @click="openDialog('ChangePasswordDialog')">
|
||||
<span class="label">{{ t('changePassword') }}</span>
|
||||
<span class="value">••••••••</span>
|
||||
<span class="chevron"><v-icon>mdi-chevron-right</v-icon></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<template v-if="creatorProfileStore.hasCreator">
|
||||
@@ -665,6 +677,7 @@ async function deconfigureStripe() {
|
||||
"fullName": "Full Name",
|
||||
"alias": "Alias",
|
||||
"email": "Email",
|
||||
"changePassword": "Set Password",
|
||||
"creatorInfo": "Creator Information",
|
||||
"dangerZone": "Danger Zone",
|
||||
"dangerZoneWarning": "The actions below can have significant impacts on your creator page. Please proceed with caution.",
|
||||
@@ -693,6 +706,7 @@ async function deconfigureStripe() {
|
||||
"fullName": "Nom Complet",
|
||||
"alias": "Alias",
|
||||
"email": "Email",
|
||||
"changePassword": "Définir le mot de passe",
|
||||
"creatorInfo": "Informations du Créateur",
|
||||
"dangerZone": "Zone de Danger",
|
||||
"dangerZoneWarning": "Les actions ci-dessous peuvent avoir des impacts significatifs sur votre page de créateur. Veuillez procéder avec précaution.",
|
||||
@@ -721,6 +735,7 @@ async function deconfigureStripe() {
|
||||
"fullName": "Nombre Completo",
|
||||
"alias": "Alias",
|
||||
"email": "Correo Electrónico",
|
||||
"changePassword": "Establecer contraseña",
|
||||
"creatorInfo": "Información del Creador",
|
||||
"dangerZone": "Zona de Peligro",
|
||||
"dangerZoneWarning": "Las acciones a continuación pueden tener impactos significativos en tu página de creador. Por favor, procede con precaución.",
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div class="card dialog">
|
||||
<div class="card-title">
|
||||
{{ t('title') }}
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="address"
|
||||
:label="t('label')"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button class="secondary" @click="requestClose">
|
||||
{{ t('cancel') }}
|
||||
</button>
|
||||
<button class="primary" @click="requestSave">
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps(['address'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const address = ref(props.address);
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', address.value)
|
||||
</script>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"title": "Address",
|
||||
"label": "Your address"
|
||||
},
|
||||
"fr": {
|
||||
"title": "Adresse",
|
||||
"label": "Votre adresse"
|
||||
},
|
||||
"es": {
|
||||
"title": "Dirección",
|
||||
"label": "Tu dirección"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div class="card dialog">
|
||||
<div class="card-title">
|
||||
{{ t('title') }}
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="birthDate"
|
||||
:label="t('label')"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button class="secondary" @click="requestClose">
|
||||
{{ t('cancel') }}
|
||||
</button>
|
||||
<button class="primary" @click="requestSave">
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
|
||||
const {t} = useI18n();
|
||||
const props = defineProps(['birthDate'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const birthDate = ref(props.birthDate)
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', birthDate.value)
|
||||
</script>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"title": "Birthday",
|
||||
"label": "Your birthday"
|
||||
},
|
||||
"fr": {
|
||||
"title": "Date de naissance",
|
||||
"label": "Votre date de naissance"
|
||||
},
|
||||
"es": {
|
||||
"title": "Fecha de nacimiento",
|
||||
"label": "Tu fecha de nacimiento"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
183
frontend/src/views/profile/account/ChangePasswordDialog.vue
Normal file
183
frontend/src/views/profile/account/ChangePasswordDialog.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div class="card dialog">
|
||||
|
||||
<div class="card-title">
|
||||
{{ t('changePassword') }}
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="description mb-4">{{ t('passwordDescription') }}</p>
|
||||
|
||||
<v-text-field
|
||||
v-model="newPassword"
|
||||
:label="t('newPassword')"
|
||||
:type="showNewPassword ? 'text' : 'password'"
|
||||
variant="outlined"
|
||||
required
|
||||
:hint="t('passwordRequirements')"
|
||||
>
|
||||
<template v-slot:append-inner>
|
||||
<v-icon
|
||||
@click="showNewPassword = !showNewPassword"
|
||||
class="visibility-toggle"
|
||||
size="small"
|
||||
>
|
||||
{{ showNewPassword ? 'mdi-eye-off' : 'mdi-eye' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="confirmPassword"
|
||||
:label="t('confirmPassword')"
|
||||
:type="showConfirmPassword ? 'text' : 'password'"
|
||||
variant="outlined"
|
||||
required
|
||||
>
|
||||
<template v-slot:append-inner>
|
||||
<v-icon
|
||||
@click="showConfirmPassword = !showConfirmPassword"
|
||||
class="visibility-toggle"
|
||||
size="small"
|
||||
>
|
||||
{{ showConfirmPassword ? 'mdi-eye-off' : 'mdi-eye' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<div v-if="errorMessage" class="error-message mb-4">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button class="secondary" @click="$emit('closeRequested')">
|
||||
{{ t('cancel') }}
|
||||
</button>
|
||||
<button class="primary" @click="handleChangePassword" :disabled="isLoading">
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useAuthStore } from '@/stores/authStore.js';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const authStore = useAuthStore();
|
||||
const emit = defineEmits(['closeRequested']);
|
||||
|
||||
const newPassword = ref('');
|
||||
const confirmPassword = ref('');
|
||||
const isLoading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const showNewPassword = ref(false);
|
||||
const showConfirmPassword = ref(false);
|
||||
|
||||
async function handleChangePassword() {
|
||||
// Clear previous error
|
||||
errorMessage.value = '';
|
||||
|
||||
// Validate passwords match
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
errorMessage.value = t('passwordsDoNotMatch');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate password length
|
||||
if (newPassword.value.length < 8) {
|
||||
errorMessage.value = t('passwordTooShort');
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Pass empty string for current password since we're already authenticated
|
||||
// This will use the set-password endpoint for OAuth users
|
||||
await authStore.changePassword(newPassword.value);
|
||||
|
||||
// Success - close dialog
|
||||
emit('closeRequested');
|
||||
|
||||
// You could also emit a success event if needed
|
||||
// emit('success');
|
||||
} catch (error) {
|
||||
console.error('Failed to change password:', error);
|
||||
// Use error message from response if available, or the error message itself, or fallback
|
||||
errorMessage.value = error.response?.data || error.message || t('passwordUpdateFailed');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.dialog {
|
||||
@apply max-w-md mx-auto;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@apply text-red-500 text-sm mt-2;
|
||||
}
|
||||
|
||||
.visibility-toggle {
|
||||
@apply cursor-pointer;
|
||||
@apply transition-opacity duration-300;
|
||||
@apply opacity-60 hover:opacity-100;
|
||||
@apply absolute right-2 top-1/2 transform -translate-y-1/2;
|
||||
@apply z-10;
|
||||
}
|
||||
|
||||
/* Override Vuetify's default padding to accommodate our icon */
|
||||
:deep(.v-field__append-inner) {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"changePassword": "Set Password",
|
||||
"newPassword": "New Password",
|
||||
"confirmPassword": "Confirm New Password",
|
||||
"passwordRequirements": "Password must be at least 8 characters",
|
||||
"passwordDescription": "Setting a password allows you to log in directly with your email and password, even if you originally signed up with Google.",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"passwordsDoNotMatch": "New passwords do not match",
|
||||
"passwordTooShort": "Password must be at least 8 characters long",
|
||||
"passwordUpdateFailed": "Failed to update password. Please try again."
|
||||
},
|
||||
"fr": {
|
||||
"changePassword": "Définir le mot de passe",
|
||||
"newPassword": "Nouveau mot de passe",
|
||||
"confirmPassword": "Confirmer le nouveau mot de passe",
|
||||
"passwordRequirements": "Le mot de passe doit comporter au moins 8 caractères",
|
||||
"passwordDescription": "La définition d'un mot de passe vous permet de vous connecter directement avec votre email et mot de passe, même si vous vous êtes initialement inscrit avec Google.",
|
||||
"save": "Enregistrer",
|
||||
"cancel": "Annuler",
|
||||
"passwordsDoNotMatch": "Les nouveaux mots de passe ne correspondent pas",
|
||||
"passwordTooShort": "Le mot de passe doit comporter au moins 8 caractères",
|
||||
"passwordUpdateFailed": "Échec de la mise à jour du mot de passe. Veuillez réessayer."
|
||||
},
|
||||
"es": {
|
||||
"changePassword": "Establecer contraseña",
|
||||
"newPassword": "Nueva contraseña",
|
||||
"confirmPassword": "Confirmar nueva contraseña",
|
||||
"passwordRequirements": "La contraseña debe tener al menos 8 caracteres",
|
||||
"passwordDescription": "Establecer una contraseña le permite iniciar sesión directamente con su correo electrónico y contraseña, incluso si originalmente se registró con Google.",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"passwordsDoNotMatch": "Las nuevas contraseñas no coinciden",
|
||||
"passwordTooShort": "La contraseña debe tener al menos 8 caracteres",
|
||||
"passwordUpdateFailed": "Error al actualizar la contraseña. Por favor, inténtelo de nuevo."
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div class="card dialog">
|
||||
<div class="card-title">
|
||||
{{ t('title') }}
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="phone"
|
||||
:label="t('label')"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button class="secondary" @click="requestClose">
|
||||
{{ t('cancel') }}
|
||||
</button>
|
||||
<button class="primary" @click="requestSave">
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps(['phone'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const phone = ref(props.phone)
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', phone.value)
|
||||
</script>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"title": "Phone Number",
|
||||
"label": "Your phone number"
|
||||
},
|
||||
"fr": {
|
||||
"title": "Numéro de téléphone",
|
||||
"label": "Votre numéro de téléphone"
|
||||
},
|
||||
"es": {
|
||||
"title": "Número de teléfono",
|
||||
"label": "Tu número de teléfono"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
@@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<div class="card dialog">
|
||||
<div class="card-title">
|
||||
{{ t('title') }}
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<img
|
||||
:src="portraitData"
|
||||
class="mb-5 w-full transition duration-200 ease-in-out transform"
|
||||
:alt="t('preview')"
|
||||
/>
|
||||
|
||||
<v-file-input
|
||||
@change="onSelectedFileChanged"
|
||||
v-model="selectedFile"
|
||||
variant="outlined"
|
||||
accept="image/*"
|
||||
:label="t('label')"
|
||||
></v-file-input>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button class="secondary" @click="requestClose">
|
||||
{{ t('cancel') }}
|
||||
</button>
|
||||
<button class="primary" @click="requestSave">
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps(['portraitUrl'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const portraitData = ref(props.portraitUrl)
|
||||
|
||||
const selectedFile = ref({})
|
||||
const onSelectedFileChanged = () => {
|
||||
if (selectedFile.value) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
portraitData.value = event.target.result
|
||||
}
|
||||
reader.readAsDataURL(selectedFile.value)
|
||||
} else {
|
||||
portraitData.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', selectedFile.value)
|
||||
</script>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"title": "Profile Picture",
|
||||
"preview": "Profile picture preview",
|
||||
"label": "Choose a profile picture"
|
||||
},
|
||||
"fr": {
|
||||
"title": "Photo de profil",
|
||||
"preview": "Aperçu de la photo de profil",
|
||||
"label": "Choisir une photo de profil"
|
||||
},
|
||||
"es": {
|
||||
"title": "Foto de perfil",
|
||||
"preview": "Vista previa de la foto de perfil",
|
||||
"label": "Elegir una foto de perfil"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
Reference in New Issue
Block a user