Skip to content

Commit d4cacea

Browse files
AchoArnoldCopilot
andauthored
feat(web): improve login error messages and add forgot password flow (NdoleStudio#931)
* feat(web): improve login error messages and add forgot password flow - Map Firebase error codes to user-friendly messages from Firebase UI translations (e.g. 'Incorrect password' instead of raw error strings) - Add field-level error display using :error and :error-messages on v-text-field components so invalid fields turn red - Add client-side validation for empty/invalid email and empty password - Implement inline forgot password flow with email input, reset link submission via sendPasswordResetEmail, and success confirmation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: use small font for forgot password * fix(web): address PR review feedback on login error handling - Map auth/invalid-credential to both email and password fields since modern Firebase returns this code for either wrong email or password - Trim email before passing to Firebase in submitEmail to match the pattern already used in submitPasswordReset Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0cc27fb commit d4cacea

1 file changed

Lines changed: 230 additions & 25 deletions

File tree

web/app/components/FirebaseAuth.vue

Lines changed: 230 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {
66
GithubAuthProvider,
77
signInWithEmailAndPassword,
88
createUserWithEmailAndPassword,
9+
sendPasswordResetEmail,
910
} from 'firebase/auth'
1011
import { mdiGoogle, mdiGithub, mdiEmail } from '@mdi/js'
1112
import type { User as FirebaseUser } from 'firebase/auth'
13+
import { ErrorMessages } from '~/utils/errors'
1214
1315
const props = withDefaults(
1416
defineProps<{
@@ -25,9 +27,12 @@ const appStore = useAppStore()
2527
const loading = ref(false)
2628
const showEmailForm = ref(false)
2729
const isSignUp = ref(false)
30+
const showForgotPassword = ref(false)
31+
const resetEmailSent = ref(false)
2832
const email = ref('')
2933
const password = ref('')
30-
const emailError = ref('')
34+
const generalError = ref('')
35+
const errorMessages = ref(new ErrorMessages())
3136
3237
type LoginMethod = 'google' | 'github' | 'email'
3338
const LAST_LOGIN_METHOD_KEY = 'httpsms_last_login_method'
@@ -44,6 +49,45 @@ onMounted(() => {
4449
}
4550
})
4651
52+
function clearErrors() {
53+
errorMessages.value = new ErrorMessages()
54+
generalError.value = ''
55+
}
56+
57+
function validateEmail(): boolean {
58+
clearErrors()
59+
if (!email.value.trim()) {
60+
errorMessages.value.add('email', 'Please provide an email address')
61+
return false
62+
}
63+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
64+
if (!emailRegex.test(email.value.trim())) {
65+
errorMessages.value.add('email', 'Please enter a valid email address')
66+
return false
67+
}
68+
return true
69+
}
70+
71+
function validateLoginForm(): boolean {
72+
clearErrors()
73+
let valid = true
74+
if (!email.value.trim()) {
75+
errorMessages.value.add('email', 'Please provide an email address')
76+
valid = false
77+
} else {
78+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
79+
if (!emailRegex.test(email.value.trim())) {
80+
errorMessages.value.add('email', 'Please enter a valid email address')
81+
valid = false
82+
}
83+
}
84+
if (!password.value) {
85+
errorMessages.value.add('password', 'Please enter your password')
86+
valid = false
87+
}
88+
return valid
89+
}
90+
4791
async function signInWithGoogle() {
4892
loading.value = true
4993
try {
@@ -71,21 +115,21 @@ async function signInWithGithub() {
71115
}
72116
73117
async function submitEmail() {
74-
emailError.value = ''
118+
if (!validateLoginForm()) return
75119
loading.value = true
76120
try {
77121
const auth = getAuth()
78122
let result
79123
if (isSignUp.value) {
80124
result = await createUserWithEmailAndPassword(
81125
auth,
82-
email.value,
126+
email.value.trim(),
83127
password.value,
84128
)
85129
} else {
86130
result = await signInWithEmailAndPassword(
87131
auth,
88-
email.value,
132+
email.value.trim(),
89133
password.value,
90134
)
91135
}
@@ -97,6 +141,32 @@ async function submitEmail() {
97141
}
98142
}
99143
144+
async function submitPasswordReset() {
145+
if (!validateEmail()) return
146+
loading.value = true
147+
try {
148+
const auth = getAuth()
149+
await sendPasswordResetEmail(auth, email.value.trim())
150+
resetEmailSent.value = true
151+
} catch (error: unknown) {
152+
handleError(error)
153+
} finally {
154+
loading.value = false
155+
}
156+
}
157+
158+
function showForgotPasswordForm() {
159+
clearErrors()
160+
resetEmailSent.value = false
161+
showForgotPassword.value = true
162+
}
163+
164+
function backToSignIn() {
165+
clearErrors()
166+
resetEmailSent.value = false
167+
showForgotPassword.value = false
168+
}
169+
100170
function onSuccess(user: FirebaseUser, method: LoginMethod) {
101171
try {
102172
localStorage.setItem(LAST_LOGIN_METHOD_KEY, method)
@@ -114,33 +184,94 @@ function onSuccess(user: FirebaseUser, method: LoginMethod) {
114184
function handleError(error: unknown, isSocial = false) {
115185
const firebaseError = error as { code?: string; message?: string }
116186
const code = firebaseError.code || ''
117-
let message = ''
118-
if (code === 'auth/user-not-found' || code === 'auth/invalid-credential') {
119-
message = 'Invalid email or password'
120-
} else if (code === 'auth/email-already-in-use') {
121-
message = 'An account with this email already exists'
122-
} else if (code === 'auth/weak-password') {
123-
message = 'Password must be at least 6 characters'
124-
} else if (
187+
188+
if (
125189
code === 'auth/popup-closed-by-user' ||
126190
code === 'auth/cancelled-popup-request'
127191
) {
128-
// User closed the popup, no error to show
129192
return
130-
} else {
131-
message = firebaseError.message || 'An error occurred'
132193
}
133194
134195
if (isSocial) {
196+
const message = getGeneralErrorMessage(code, firebaseError.message)
135197
notificationsStore.addNotification({ message, type: 'error' })
136-
} else {
137-
emailError.value = message
198+
return
199+
}
200+
201+
clearErrors()
202+
203+
switch (code) {
204+
case 'auth/wrong-password':
205+
errorMessages.value.add('password', 'Incorrect password')
206+
break
207+
case 'auth/invalid-credential':
208+
errorMessages.value.add('email', 'Invalid email or password')
209+
errorMessages.value.add('password', 'Invalid email or password')
210+
break
211+
case 'auth/user-not-found':
212+
errorMessages.value.add(
213+
'email',
214+
'No account found with this email address',
215+
)
216+
break
217+
case 'auth/invalid-email':
218+
errorMessages.value.add('email', 'Please enter a valid email address')
219+
break
220+
case 'auth/email-already-in-use':
221+
errorMessages.value.add(
222+
'email',
223+
'An account already exists with this email',
224+
)
225+
break
226+
case 'auth/weak-password':
227+
errorMessages.value.add(
228+
'password',
229+
'Password should be at least 6 characters',
230+
)
231+
break
232+
case 'auth/user-disabled':
233+
errorMessages.value.add('email', 'This account has been disabled')
234+
break
235+
case 'auth/too-many-requests':
236+
generalError.value = 'Too many failed attempts. Please try again later'
237+
break
238+
case 'auth/network-request-failed':
239+
generalError.value =
240+
'Unable to connect to the server. Please check your internet connection'
241+
break
242+
case 'auth/missing-email':
243+
errorMessages.value.add('email', 'Please provide an email address')
244+
break
245+
default:
246+
generalError.value =
247+
firebaseError.message || 'An unexpected error occurred'
248+
}
249+
}
250+
251+
function getGeneralErrorMessage(
252+
code: string,
253+
fallback: string | undefined,
254+
): string {
255+
switch (code) {
256+
case 'auth/user-not-found':
257+
return 'No account found with this email address'
258+
case 'auth/wrong-password':
259+
case 'auth/invalid-credential':
260+
return 'The provided credentials are invalid.'
261+
case 'auth/user-disabled':
262+
return 'This account has been disabled'
263+
case 'auth/too-many-requests':
264+
return 'Too many failed attempts. Please try again later'
265+
case 'auth/network-request-failed':
266+
return 'Unable to connect to the server. Please check your internet connection'
267+
default:
268+
return fallback || 'An unexpected error occurred'
138269
}
139270
}
140271
</script>
141272

142273
<template>
143-
<div class="text-center">
274+
<div>
144275
<v-btn
145276
block
146277
color="white"
@@ -212,16 +343,78 @@ function handleError(error: unknown, isSocial = false) {
212343
Continue with email
213344
</v-btn>
214345

215-
<v-form v-if="showEmailForm" class="mt-4" @submit.prevent="submitEmail">
346+
<!-- Forgot Password Form -->
347+
<v-form
348+
v-if="showEmailForm && showForgotPassword"
349+
class="mt-4"
350+
@submit.prevent="submitPasswordReset"
351+
>
352+
<template v-if="!resetEmailSent">
353+
<p class="text-body-medium text-medium-emphasis mb-4">
354+
Enter your email address to reset your password
355+
</p>
356+
<v-text-field
357+
v-model="email"
358+
label="Email Address"
359+
color="primary"
360+
type="email"
361+
variant="outlined"
362+
density="comfortable"
363+
class="mb-2"
364+
:error="errorMessages.has('email')"
365+
:error-messages="errorMessages.get('email')"
366+
/>
367+
<v-alert
368+
v-if="generalError"
369+
type="error"
370+
density="compact"
371+
class="mb-3"
372+
>
373+
{{ generalError }}
374+
</v-alert>
375+
<v-btn
376+
block
377+
size="large"
378+
color="primary"
379+
type="submit"
380+
:loading="loading"
381+
>
382+
Send Reset Link
383+
</v-btn>
384+
</template>
385+
<template v-else>
386+
<v-alert type="success" density="compact" class="mb-3">
387+
Check your email for password reset instructions
388+
</v-alert>
389+
</template>
390+
<v-btn
391+
block
392+
variant="text"
393+
size="small"
394+
color="warning"
395+
class="mt-2"
396+
@click="backToSignIn"
397+
>
398+
Back to Sign In
399+
</v-btn>
400+
</v-form>
401+
402+
<!-- Sign In / Sign Up Form -->
403+
<v-form
404+
v-if="showEmailForm && !showForgotPassword"
405+
class="mt-4"
406+
@submit.prevent="submitEmail"
407+
>
216408
<v-text-field
217409
v-model="email"
218-
label="Email"
410+
label="Email Address"
219411
color="primary"
220412
type="email"
221413
variant="outlined"
222414
density="comfortable"
223415
class="mb-2"
224-
required
416+
:error="errorMessages.has('email')"
417+
:error-messages="errorMessages.get('email')"
225418
/>
226419
<v-text-field
227420
v-model="password"
@@ -231,11 +424,22 @@ function handleError(error: unknown, isSocial = false) {
231424
variant="outlined"
232425
density="comfortable"
233426
class="mb-2"
234-
required
427+
:error="errorMessages.has('password')"
428+
:error-messages="errorMessages.get('password')"
235429
/>
236-
<v-alert v-if="emailError" type="error" density="compact" class="mb-3">
237-
{{ emailError }}
430+
<v-alert v-if="generalError" type="error" density="compact" class="mb-3">
431+
{{ generalError }}
238432
</v-alert>
433+
<v-btn
434+
v-if="!isSignUp"
435+
variant="plain"
436+
size="small"
437+
color="primary"
438+
class="mb-3 px-0 mt-n4"
439+
@click="showForgotPasswordForm"
440+
>
441+
Forgot Password?
442+
</v-btn>
239443
<v-btn
240444
block
241445
size="large"
@@ -247,8 +451,9 @@ function handleError(error: unknown, isSocial = false) {
247451
</v-btn>
248452
<v-btn
249453
block
250-
variant="text"
454+
variant="plain"
251455
size="small"
456+
color="primary"
252457
class="mt-2"
253458
@click="isSignUp = !isSignUp"
254459
>

0 commit comments

Comments
 (0)