@@ -6,9 +6,11 @@ import {
66 GithubAuthProvider ,
77 signInWithEmailAndPassword ,
88 createUserWithEmailAndPassword ,
9+ sendPasswordResetEmail ,
910} from ' firebase/auth'
1011import { mdiGoogle , mdiGithub , mdiEmail } from ' @mdi/js'
1112import type { User as FirebaseUser } from ' firebase/auth'
13+ import { ErrorMessages } from ' ~/utils/errors'
1214
1315const props = withDefaults (
1416 defineProps <{
@@ -25,9 +27,12 @@ const appStore = useAppStore()
2527const loading = ref (false )
2628const showEmailForm = ref (false )
2729const isSignUp = ref (false )
30+ const showForgotPassword = ref (false )
31+ const resetEmailSent = ref (false )
2832const email = ref (' ' )
2933const password = ref (' ' )
30- const emailError = ref (' ' )
34+ const generalError = ref (' ' )
35+ const errorMessages = ref (new ErrorMessages ())
3136
3237type LoginMethod = ' google' | ' github' | ' email'
3338const 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+
4791async function signInWithGoogle() {
4892 loading .value = true
4993 try {
@@ -71,21 +115,21 @@ async function signInWithGithub() {
71115}
72116
73117async 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+
100170function 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) {
114184function 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