Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9cd83cb
fix: fix FieldPassword component to use dynamic props for name and pl…
mostafahassan04 Oct 23, 2025
f21ebad
fix(auth): enhance validation and error handling in login forms
mostafahassan04 Oct 23, 2025
c8ad8ea
test(auth): enhance tests for IdentifierStep and PasswordStep components
mostafahassan04 Oct 23, 2025
7e9ffd2
fix(auth): update identifier check function to use checkUserExists
mostafahassan04 Oct 23, 2025
5a4c1b7
feat(password-reset): add FindAccount component for user identification
mostafahassan04 Oct 23, 2025
0bfb3c5
feat(auth): add SentCode component for OTP verification
mostafahassan04 Oct 23, 2025
1b40503
feat(auth): implement ChooseNewPassword component for password reset …
mostafahassan04 Oct 23, 2025
29f0f2e
feat(i18n): add new localization strings for password recovery and ac…
mostafahassan04 Oct 23, 2025
e3b6116
feat(auth): implement password service and store for user authenticat…
mostafahassan04 Oct 23, 2025
494475b
feat(auth): add mock handlers for password management including OTP v…
mostafahassan04 Oct 23, 2025
42fbc55
feat(auth): add password management endpoints for forgot, verify, res…
mostafahassan04 Oct 23, 2025
9eac82d
refactor(login): divide login store into store and service
mostafahassan04 Oct 23, 2025
bba11c4
refactor(auth): streamline token generation and clean up login handlers
mostafahassan04 Oct 23, 2025
b852b2c
test(auth): add tests for default error handling in check-identifier …
mostafahassan04 Oct 23, 2025
d996567
test(auth): update identifier checks to use checkUserExists and add l…
mostafahassan04 Oct 23, 2025
169a5f5
test(password-reset): add unit tests for PasswordResetPage component
mostafahassan04 Oct 23, 2025
9e0a266
test(password): add unit tests for ChooseNewPassword and SentCode com…
mostafahassan04 Oct 23, 2025
08ae82c
test(password): add unit tests for password service and password stor…
mostafahassan04 Oct 23, 2025
f429259
test(password): add unit tests for password forgot, verify, resend-ot…
mostafahassan04 Oct 23, 2025
d15f4f7
chore: merge branch 'dev' into feat/forget-password
mostafahassan04 Oct 23, 2025
25c6bcb
fix(auth): fix the review comments
mostafahassan04 Oct 26, 2025
f7a9162
chore: merge branch 'dev' into feat/forget-password
mostafahassan04 Oct 26, 2025
64232c6
Merge branch 'dev' into feat/forget-password
mostafahassan04 Oct 28, 2025
e0f65b9
fix: add missing comma in error message for better JSON formatting
mostafahassan04 Oct 28, 2025
13e00d4
fix: trim password input before submission to ensure clean data
mostafahassan04 Oct 28, 2025
202f26b
fix: fix text-start for localization
mostafahassan04 Oct 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 27 additions & 35 deletions app/components/auth/login/IdentifierStep.vue
Original file line number Diff line number Diff line change
@@ -1,46 +1,31 @@
<script setup lang="ts">
import { ref } from 'vue';
import * as yup from 'yup';
import { useForm } from 'vee-validate';
import { useLoginStore } from '~/stores/auth/login';
import { showToaster } from '@/utils/showToaster';

const loginStore = useLoginStore();
const error = ref('');

const schema = yup.object({
identifier: yup.string(),
identifier: yup.string().trim().required($t('errors.IDENTIFIER_REQUIRED')),
});

const { defineField, handleSubmit, resetForm } = useForm({
const { defineField, handleSubmit, isSubmitting, meta } = useForm({
validationSchema: schema,
initialValues: {
identifier: '',
},
validateOnMount: true,
validateOnMount: false,
});

const onSubmit = handleSubmit(async (values) => {
error.value = '';

try {
if (!values.identifier || values.identifier.trim() === '') {
throw new Error($t('errors.IDENTIFIER_REQUIRED'));
}
const success = await loginStore.checkIdentifierExists(values.identifier);
const success = await loginStore.checkUserExists(values.identifier.trim());
if (!success) {
throw new Error($t('errors.USER_NOT_FOUND'));
}
resetForm({ values: { identifier: '' } });
} catch (err: unknown) {
if (err instanceof Error) {
error.value = err.message || $t('errors.GENERIC_ERROR');
showToaster('error', error.value);
} else {
error.value = $t('errors.GENERIC_ERROR');
showToaster('error', error.value);
}
console.error('Identifier check error:', error.value);
showToaster('error', (err as Error).message || $t('errors.GENERIC_ERROR'));
}
});
const [_identifier, identifierAttrs] = defineField('identifier');
Expand All @@ -50,7 +35,7 @@ const [_identifier, identifierAttrs] = defineField('identifier');
<form @submit.prevent="onSubmit">
<UiDialogHeader class="mt-3 px-8 py-4">
<UiDialogTitle class="mx-auto w-75 text-3xl font-bold">{{
$t('Sign in to Raven')
$t('login.identifier-step.title')
}}</UiDialogTitle>
</UiDialogHeader>
<div class="mx-auto mt-7 w-75">
Expand All @@ -62,7 +47,7 @@ const [_identifier, identifierAttrs] = defineField('identifier');
data-testid="google-button"
>
<Icon name="devicon:google" width="128" height="128"></Icon>
{{ $t('root.auth.google-signin') }}</UiButton
{{ $t('login.identifier-step.google-signin') }}</UiButton
>
<UiButton
class="bg-oauth dark:hover:bg-oauth/80 hover:bg-oauth/110 border-foreground mb-1 border text-black"
Expand All @@ -71,23 +56,28 @@ const [_identifier, identifierAttrs] = defineField('identifier');
data-testid="github-button"
>
<Icon name="devicon:github" width="128" height="128"></Icon>
{{ $t('root.auth.github-signin') }}</UiButton
{{ $t('login.identifier-step.github-signin') }}</UiButton
>
</section>
<p class="py-2 text-center">{{ $t('root.auth.separator') }}</p>
<section class="flex flex-col gap-4">
<UiFormFieldInput
class="mb-4"
placeholder="email or username"
:placeholder="$t('login.email-or-username')"
type="text"
name="identifier"
v-bind="identifierAttrs"
data-testid="identifier-input"
></UiFormFieldInput>
</section>
</div>
<UiDialogFooter>
<UiButton class="mb-1 w-75" size="lg" type="submit" data-testid="submit-button">
<UiDialogFooter class="absolute end-0 bottom-15 w-full">
<UiButton
class="mb-1 w-75"
size="lg"
type="submit"
data-testid="submit-button"
:disabled="!meta.valid || isSubmitting"
>
{{ $t('ui.next') }}
</UiButton>
<UiButton
Expand All @@ -96,18 +86,20 @@ const [_identifier, identifierAttrs] = defineField('identifier');
variant="outline"
type="button"
data-testid="forgot-password-button"
@click="loginStore.openForgotPasswordDialog"
>
{{ $t('root.auth.forget-password') }}
{{ $t('login.forgot-password') }}
</UiButton>
<p class="mt-6">
{{ $t('root.auth.dont-have-account') }}
<NuxtLink to="/auth/signup" class="text-primary" data-testid="signup-link">
{{ $t('root.auth.signup') }}
</NuxtLink>
{{ $t('login.dont-have-account') }}
<span
class="text-primary cursor-pointer hover:underline"
data-testid="signup-link"
@click="loginStore.openSignupDialog"
>
{{ $t('login.signup') }}
</span>
</p>
<!-- <Transition name="fade"
><p v-if="error" class="mt-3 text-red-500">{{ error }}</p></Transition
> -->
</UiDialogFooter>
</form>
</template>
Expand Down
10 changes: 8 additions & 2 deletions app/components/auth/login/LoginDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@ const handleDialogChange = (val: boolean) => {
<UiDialog :open="loginStore.open" @update:open="handleDialogChange">
<UiDialogContent class="pt-2">
<UiSpinner v-if="loginStore.loading" class="mx-auto my-auto"></UiSpinner>
<AuthLoginIdentifierStep v-if="loginStore.step === 0" id="identifier-step-test" />
<AuthLoginPasswordStep v-if="loginStore.step === 1" id="password-step-test" />
<AuthLoginIdentifierStep
v-if="loginStore.step === 0 && !loginStore.loading"
id="identifier-step-test"
/>
<AuthLoginPasswordStep
v-if="loginStore.step === 1 && !loginStore.loading"
id="password-step-test"
/>
</UiDialogContent>
</UiDialog>
</template>
95 changes: 47 additions & 48 deletions app/components/auth/login/PasswordStep.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import { ref } from 'vue';
import * as yup from 'yup';
import { useForm } from 'vee-validate';
import { storeToRefs } from 'pinia';
Expand All @@ -8,45 +7,28 @@ import { showToaster } from '@/utils/showToaster';

const loginStore = useLoginStore();
const { identifier } = storeToRefs(loginStore);
const error = ref('');

const schema = yup.object({
password: yup.string(),
password: yup.string().trim().required($t('errors.PASSWORD_REQUIRED')),
});

const { defineField, handleSubmit, resetForm } = useForm({
const { defineField, handleSubmit, isSubmitting, meta } = useForm({
validationSchema: schema,
initialValues: {
password: '',
},
validateOnMount: true,
validateOnMount: false,
});

const onSubmit = handleSubmit(async (values) => {
error.value = '';

try {
if (!values.password || values.password.trim() === '') {
throw new Error($t('errors.PASSWORD_REQUIRED'));
}

const submissionValues = {
identifier: identifier.value,
password: values.password,
password: values.password.trim(),
};

await loginStore.submitLogin(submissionValues);

resetForm({ values: { password: '' } });
} catch (err: unknown) {
if (err instanceof Error) {
error.value = err.message || $t('errors.GENERIC_ERROR');
showToaster('error', error.value);
} else {
error.value = $t('errors.GENERIC_ERROR');
showToaster('error', error.value);
}
console.error('Login error:', error.value);
showToaster('error', (err as Error).message || $t('errors.GENERIC_ERROR'));
}
});

Expand All @@ -57,40 +39,57 @@ const [_password, passwordAttrs] = defineField('password');
<form @submit.prevent="onSubmit">
<UiDialogHeader class="mt-3 px-8 py-4">
<UiDialogTitle class="mx-auto w-100 text-3xl font-bold">
{{ $t('Enter your password') }}
{{ $t('login.password-step.title') }}
</UiDialogTitle>
</UiDialogHeader>

<div class="mx-auto mt-6 w-100">
<UiFormFieldInput
class="input-readonly mb-7"
name="identifier"
type="text"
:model-value="identifier"
:placeholder="loginStore.type ? loginStore.type : ''"
readonly
/>
<UiFormFieldPassword name="password" v-bind="passwordAttrs" />
<p class="text-primary ms-1 mt-1 block text-sm" data-testid="forgot-password-link">
<NuxtLink to="/auth/forgot-password">
{{ $t('root.auth.forget-password') }}
</NuxtLink>
<section class="flex flex-col gap-6">
<UiFormFieldInput
class="input-readonly"
name="identifier"
type="text"
:model-value="identifier"
:placeholder="
loginStore.type ? $t(`login.${loginStore.type}`) : $t('login.email-or-username')
"
readonly
/>
<UiFormFieldPassword
name="password"
:placeholder="$t('login.password')"
v-bind="passwordAttrs"
/>
</section>
<p
class="text-primary ms-1 mt-2 block w-fit cursor-pointer text-sm hover:underline"
data-testid="forgot-password-link"
@click="loginStore.openForgotPasswordDialog"
>
{{ $t('login.forgot-password') }}
</p>
</div>

<UiDialogFooter class="mt-40">
<UiButton class="mb-1 w-100" size="lg" type="submit" data-testid="submit-button">
{{ $t('root.auth.signin') }}
<UiDialogFooter class="absolute end-0 bottom-15 w-full">
<UiButton
class="mb-1 w-100"
size="lg"
type="submit"
data-testid="submit-button"
:disabled="!meta.valid || isSubmitting"
>
{{ $t('login.password-step.signin') }}
</UiButton>
<p class="mt-4 w-fit">
{{ $t('root.auth.dont-have-account') }}
<NuxtLink to="/auth/signup" class="text-primary" data-testid="signup-link">
{{ $t('root.auth.signup') }}
</NuxtLink>
<p class="mt-6">
{{ $t('login.dont-have-account') }}
<span
class="text-primary cursor-pointer hover:underline"
data-testid="signup-link"
@click="loginStore.openSignupDialog"
>
{{ $t('login.signup') }}
</span>
</p>
<!-- <Transition name="fade"
><p v-if="error" class="mt-3 text-red-500">{{ error }}</p></Transition
> -->
</UiDialogFooter>
</form>
</template>
Expand Down
80 changes: 80 additions & 0 deletions app/components/auth/password/ChooseNewPassword.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script setup lang="ts">
import * as yup from 'yup';
import { useForm } from 'vee-validate';
import { usePasswordStore } from '~/stores/auth/password';
import { showToaster } from '@/utils/showToaster';
import { createPasswordSchema } from '~/schemas/auth';

const passwordStore = usePasswordStore();
const { t } = useI18n();

const schema = yup.object({
newPassword: createPasswordSchema(t),
confirmPassword: createPasswordSchema(t).oneOf(
[yup.ref('newPassword')],
t('errors.PASSWORD_MISMATCH'),
),
});

const { defineField, handleSubmit, isSubmitting, meta } = useForm({
validationSchema: schema,
initialValues: { newPassword: '', confirmPassword: '' },
validateOnMount: false,
});

const [_newPassword, newPasswordAttrs] = defineField('newPassword');
const [_confirmPassword, confirmPasswordAttrs] = defineField('confirmPassword');

const onSubmit = handleSubmit(async (values) => {
try {
await passwordStore.resetPassword(values.newPassword.trim());
} catch (err: unknown) {
showToaster('error', (err as Error).message || t('errors.GENERIC_ERROR'));
}
});
</script>

<template>
<form @submit.prevent="onSubmit">
<UiDialogHeader class="mt-3 w-fit px-8 py-4">
<UiDialogTitle class="text-3xl font-bold">
{{ $t('forgot-password.new-password.title') }}
</UiDialogTitle>
<p class="text-muted-foreground mx-auto mt-2 text-sm">
{{ $t('forgot-password.new-password.description') }}
</p>
<p class="text-muted-foreground mx-auto mt-2 text-sm">
{{ $t('forgot-password.new-password.warning') }}
</p>
</UiDialogHeader>

<div class="mx-auto mt-2 px-8">
<section class="flex flex-col gap-6">
<UiFormFieldPassword
:placeholder="$t('forgot-password.new-password.password.label')"
name="newPassword"
v-bind="newPasswordAttrs"
data-testid="new-password-input"
/>
<UiFormFieldPassword
:placeholder="$t('forgot-password.new-password.confirm-password.label')"
name="confirmPassword"
v-bind="confirmPasswordAttrs"
data-testid="confirm-password-input"
/>
</section>
</div>

<UiDialogFooter class="absolute end-0 bottom-8 w-full">
<UiButton
class="w-100"
size="xl"
type="submit"
data-testid="submit-button"
:disabled="!meta.valid || isSubmitting"
>
{{ $t('forgot-password.new-password.change-password-button') }}
</UiButton>
</UiDialogFooter>
</form>
</template>
Loading
Loading