From c3522d470f86613b422a6d5e6026ed34201caf5a Mon Sep 17 00:00:00 2001 From: Acho Arnold Ewin Date: Mon, 29 Jun 2026 20:13:11 +0300 Subject: [PATCH 1/3] 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> --- web/app/components/FirebaseAuth.vue | 247 +++++++++++++++++++++++++--- 1 file changed, 224 insertions(+), 23 deletions(-) diff --git a/web/app/components/FirebaseAuth.vue b/web/app/components/FirebaseAuth.vue index 1ee90027..2e1fbca1 100644 --- a/web/app/components/FirebaseAuth.vue +++ b/web/app/components/FirebaseAuth.vue @@ -6,9 +6,11 @@ import { GithubAuthProvider, signInWithEmailAndPassword, createUserWithEmailAndPassword, + sendPasswordResetEmail, } from 'firebase/auth' import { mdiGoogle, mdiGithub, mdiEmail } from '@mdi/js' import type { User as FirebaseUser } from 'firebase/auth' +import { ErrorMessages } from '~/utils/errors' const props = withDefaults( defineProps<{ @@ -25,9 +27,12 @@ const appStore = useAppStore() const loading = ref(false) const showEmailForm = ref(false) const isSignUp = ref(false) +const showForgotPassword = ref(false) +const resetEmailSent = ref(false) const email = ref('') const password = ref('') -const emailError = ref('') +const generalError = ref('') +const errorMessages = ref(new ErrorMessages()) type LoginMethod = 'google' | 'github' | 'email' const LAST_LOGIN_METHOD_KEY = 'httpsms_last_login_method' @@ -44,6 +49,45 @@ onMounted(() => { } }) +function clearErrors() { + errorMessages.value = new ErrorMessages() + generalError.value = '' +} + +function validateEmail(): boolean { + clearErrors() + if (!email.value.trim()) { + errorMessages.value.add('email', 'Please provide an email address') + return false + } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(email.value.trim())) { + errorMessages.value.add('email', 'Please enter a valid email address') + return false + } + return true +} + +function validateLoginForm(): boolean { + clearErrors() + let valid = true + if (!email.value.trim()) { + errorMessages.value.add('email', 'Please provide an email address') + valid = false + } else { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(email.value.trim())) { + errorMessages.value.add('email', 'Please enter a valid email address') + valid = false + } + } + if (!password.value) { + errorMessages.value.add('password', 'Please enter your password') + valid = false + } + return valid +} + async function signInWithGoogle() { loading.value = true try { @@ -71,7 +115,7 @@ async function signInWithGithub() { } async function submitEmail() { - emailError.value = '' + if (!validateLoginForm()) return loading.value = true try { const auth = getAuth() @@ -97,6 +141,32 @@ async function submitEmail() { } } +async function submitPasswordReset() { + if (!validateEmail()) return + loading.value = true + try { + const auth = getAuth() + await sendPasswordResetEmail(auth, email.value.trim()) + resetEmailSent.value = true + } catch (error: unknown) { + handleError(error) + } finally { + loading.value = false + } +} + +function showForgotPasswordForm() { + clearErrors() + resetEmailSent.value = false + showForgotPassword.value = true +} + +function backToSignIn() { + clearErrors() + resetEmailSent.value = false + showForgotPassword.value = false +} + function onSuccess(user: FirebaseUser, method: LoginMethod) { try { localStorage.setItem(LAST_LOGIN_METHOD_KEY, method) @@ -114,33 +184,91 @@ function onSuccess(user: FirebaseUser, method: LoginMethod) { function handleError(error: unknown, isSocial = false) { const firebaseError = error as { code?: string; message?: string } const code = firebaseError.code || '' - let message = '' - if (code === 'auth/user-not-found' || code === 'auth/invalid-credential') { - message = 'Invalid email or password' - } else if (code === 'auth/email-already-in-use') { - message = 'An account with this email already exists' - } else if (code === 'auth/weak-password') { - message = 'Password must be at least 6 characters' - } else if ( + + if ( code === 'auth/popup-closed-by-user' || code === 'auth/cancelled-popup-request' ) { - // User closed the popup, no error to show return - } else { - message = firebaseError.message || 'An error occurred' } if (isSocial) { + const message = getGeneralErrorMessage(code, firebaseError.message) notificationsStore.addNotification({ message, type: 'error' }) - } else { - emailError.value = message + return + } + + clearErrors() + + switch (code) { + case 'auth/wrong-password': + case 'auth/invalid-credential': + errorMessages.value.add('password', 'Incorrect password') + break + case 'auth/user-not-found': + errorMessages.value.add( + 'email', + 'No account found with this email address', + ) + break + case 'auth/invalid-email': + errorMessages.value.add('email', 'Please enter a valid email address') + break + case 'auth/email-already-in-use': + errorMessages.value.add( + 'email', + 'An account already exists with this email', + ) + break + case 'auth/weak-password': + errorMessages.value.add( + 'password', + 'Password should be at least 6 characters', + ) + break + case 'auth/user-disabled': + errorMessages.value.add('email', 'This account has been disabled') + break + case 'auth/too-many-requests': + generalError.value = 'Too many failed attempts. Please try again later' + break + case 'auth/network-request-failed': + generalError.value = + 'Unable to connect to the server. Please check your internet connection' + break + case 'auth/missing-email': + errorMessages.value.add('email', 'Please provide an email address') + break + default: + generalError.value = + firebaseError.message || 'An unexpected error occurred' + } +} + +function getGeneralErrorMessage( + code: string, + fallback: string | undefined, +): string { + switch (code) { + case 'auth/user-not-found': + return 'No account found with this email address' + case 'auth/wrong-password': + case 'auth/invalid-credential': + return 'The provided credentials are invalid.' + case 'auth/user-disabled': + return 'This account has been disabled' + case 'auth/too-many-requests': + return 'Too many failed attempts. Please try again later' + case 'auth/network-request-failed': + return 'Unable to connect to the server. Please check your internet connection' + default: + return fallback || 'An unexpected error occurred' } }