refactor: organize frontend by feature
This commit is contained in:
264
frontend/src/features/auth/views/ResetPasswordView.vue
Normal file
264
frontend/src/features/auth/views/ResetPasswordView.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<div class="flex min-h-full w-full items-center justify-center p-4">
|
||||
<div class="flex w-full max-w-[512px] flex-col gap-10">
|
||||
<h1 class="text-center text-2xl font-bold">
|
||||
{{ t('title') }}
|
||||
</h1>
|
||||
|
||||
<form
|
||||
class="card"
|
||||
@submit.prevent="handleResetPassword"
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="form-field">
|
||||
<label
|
||||
class="form-label"
|
||||
for="password"
|
||||
>
|
||||
{{ t('newPassword') }}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
class="password-toggle"
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
>
|
||||
<v-icon
|
||||
:icon="showPassword ? mdiEyeOff : mdiEye"
|
||||
size="small"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('passwordRequirements') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label
|
||||
class="form-label"
|
||||
for="confirmPassword"
|
||||
>
|
||||
{{ t('confirmPassword') }}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="confirmPassword"
|
||||
v-model="confirmPassword"
|
||||
:type="showConfirmPassword ? 'text' : 'password'"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
class="password-toggle"
|
||||
type="button"
|
||||
@click="showConfirmPassword = !showConfirmPassword"
|
||||
>
|
||||
<v-icon
|
||||
:icon="showConfirmPassword ? mdiEyeOff : mdiEye"
|
||||
size="small"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="error-message"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
:disabled="isLoading"
|
||||
class="primary w-full"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
v-if="isLoading"
|
||||
class="loading-spinner mr-2"
|
||||
></span>
|
||||
{{ t('resetPassword') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Success message -->
|
||||
<div
|
||||
v-if="success"
|
||||
class="success-message"
|
||||
>
|
||||
{{ t('passwordResetSuccess') }}
|
||||
<div class="mt-4">
|
||||
<router-link
|
||||
class="text-blue-500"
|
||||
to="/login"
|
||||
>
|
||||
{{ t('proceedToLogin') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
import { mdiEye, mdiEyeOff } from '@mdi/js';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const clientApi = useClient();
|
||||
|
||||
const email = ref('');
|
||||
const token = ref('');
|
||||
const password = ref('');
|
||||
const confirmPassword = ref('');
|
||||
const showPassword = ref(false);
|
||||
const showConfirmPassword = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const success = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
// Get email and token from URL query parameters
|
||||
email.value = route.query.email || '';
|
||||
token.value = route.query.token || '';
|
||||
|
||||
// Validate that we have both email and token
|
||||
if (!email.value || !token.value) {
|
||||
errorMessage.value = t('invalidResetLink');
|
||||
}
|
||||
});
|
||||
|
||||
async function handleResetPassword() {
|
||||
// Reset error message
|
||||
errorMessage.value = '';
|
||||
|
||||
// Validate passwords match
|
||||
if (password.value !== confirmPassword.value) {
|
||||
errorMessage.value = t('passwordsDoNotMatch');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate password length
|
||||
if (password.value.length < 8) {
|
||||
errorMessage.value = t('passwordTooShort');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that we have email and token
|
||||
if (!email.value || !token.value) {
|
||||
errorMessage.value = t('invalidResetLink');
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// Call password reset API
|
||||
await clientApi.post('api/users/reset-password', {
|
||||
email: email.value,
|
||||
token: token.value,
|
||||
newPassword: password.value,
|
||||
});
|
||||
|
||||
// Show success message
|
||||
success.value = true;
|
||||
|
||||
// Clear form fields
|
||||
password.value = '';
|
||||
confirmPassword.value = '';
|
||||
|
||||
// Redirect to login after a delay
|
||||
setTimeout(() => {
|
||||
router.push('/login');
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
console.error('Password reset failed:', error);
|
||||
errorMessage.value = error.response?.data || t('resetFailed');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
form {
|
||||
@apply bg-hSurface rounded-xl p-4;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
@apply flex flex-col mb-4;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@apply bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white;
|
||||
}
|
||||
|
||||
.primary {
|
||||
@apply bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg text-sm px-5 py-2.5 focus:outline-none focus:ring-4 focus:ring-blue-300 disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@apply p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-100 dark:bg-red-900 dark:text-red-300;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
@apply p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-100 dark:bg-green-900 dark:text-green-300 text-center;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
@apply absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
@apply inline-block h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite];
|
||||
}
|
||||
</style>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"title": "Reset Your Password",
|
||||
"newPassword": "New Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"passwordRequirements": "Password must be at least 8 characters",
|
||||
"resetPassword": "Reset Password",
|
||||
"passwordResetSuccess": "Your password has been reset successfully!",
|
||||
"proceedToLogin": "Proceed to Login",
|
||||
"passwordsDoNotMatch": "Passwords do not match",
|
||||
"passwordTooShort": "Password must be at least 8 characters long",
|
||||
"resetFailed": "Password reset failed. Please try again or request a new reset link.",
|
||||
"invalidResetLink": "Invalid or expired reset link. Please request a new password reset."
|
||||
},
|
||||
"fr": {
|
||||
"title": "Réinitialiser Votre Mot de Passe",
|
||||
"newPassword": "Nouveau Mot de Passe",
|
||||
"confirmPassword": "Confirmer le Mot de Passe",
|
||||
"passwordRequirements": "Le mot de passe doit comporter au moins 8 caractères",
|
||||
"resetPassword": "Réinitialiser le Mot de Passe",
|
||||
"passwordResetSuccess": "Votre mot de passe a été réinitialisé avec succès!",
|
||||
"proceedToLogin": "Procéder à la Connexion",
|
||||
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas",
|
||||
"passwordTooShort": "Le mot de passe doit comporter au moins 8 caractères",
|
||||
"resetFailed": "Échec de la réinitialisation du mot de passe. Veuillez réessayer ou demander un nouveau lien de réinitialisation.",
|
||||
"invalidResetLink": "Lien de réinitialisation invalide ou expiré. Veuillez demander une nouvelle réinitialisation de mot de passe."
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
Reference in New Issue
Block a user