diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index 295af21..6268bcf 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -768,6 +768,12 @@ export const translations = { passwordResetRequestSent: "If that account exists, a reset link has been sent.", passwordResetRequestFailed: "Could not send the reset link.", loginFailed: "Login failed.", + emailRequired: "Enter your email address.", + invalidEmail: "Enter a valid email address.", + passwordRequired: "Enter your password.", + passwordTooShort: "Password must be at least 8 characters, with a number and a lowercase letter.", + confirmPassword: "Confirm password", + passwordsDoNotMatch: "Passwords do not match.", resetPasswordTitle: "Reset password", resetPasswordBody: "Set a new password for your account.", missingResetLinkInfo: "Missing email/token in link.", @@ -1816,6 +1822,12 @@ export const translations = { passwordResetRequestSent: "Hvis kontoen finnes, er en nullstillingslenke sendt.", passwordResetRequestFailed: "Kunne ikke sende nullstillingslenken.", loginFailed: "Innlogging mislyktes.", + emailRequired: "Skriv inn e-postadressen din.", + invalidEmail: "Skriv inn en gyldig e-postadresse.", + passwordRequired: "Skriv inn passordet ditt.", + passwordTooShort: "Passordet må være minst 8 tegn, med et tall og en liten bokstav.", + confirmPassword: "Bekreft passord", + passwordsDoNotMatch: "Passordene stemmer ikke overens.", resetPasswordTitle: "Tilbakestill passord", resetPasswordBody: "Sett et nytt passord for kontoen din.", missingResetLinkInfo: "Mangler e-post/token i lenken.", diff --git a/job-tracker-ui/src/views/ForgotPasswordPage.tsx b/job-tracker-ui/src/views/ForgotPasswordPage.tsx index 239dfa9..f9a63e9 100644 --- a/job-tracker-ui/src/views/ForgotPasswordPage.tsx +++ b/job-tracker-ui/src/views/ForgotPasswordPage.tsx @@ -16,6 +16,8 @@ export default function ForgotPasswordPage() { const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); const [submitted, setSubmitted] = useState(false); + const [emailError, setEmailError] = useState(undefined); + const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; useEffect(() => { const params = new URLSearchParams(window.location.search); @@ -46,10 +48,16 @@ export default function ForgotPasswordPage() { component="form" onSubmit={(e) => { e.preventDefault(); - if (!email.trim()) { - toast(t("passwordResetEnterEmail"), "info"); + const trimmed = email.trim(); + if (!trimmed) { + setEmailError(t("emailRequired")); return; } + if (!EMAIL_PATTERN.test(trimmed)) { + setEmailError(t("invalidEmail")); + return; + } + setEmailError(undefined); setLoading(true); api .post("/auth/request-password-reset", { email: email.trim() }) @@ -65,7 +73,17 @@ export default function ForgotPasswordPage() { sx={{ display: "flex", flexDirection: "column", gap: 1.5 }} > {submitted ? {t("passwordResetRequestSent")} : null} - setEmail(e.target.value)} autoComplete="email" fullWidth /> + { setEmail(e.target.value); setEmailError(undefined); }} + autoComplete="email" + autoFocus + error={Boolean(emailError)} + helperText={emailError} + fullWidth + /> - + )} - - {rememberMe ? t("rememberMeHelpPersistent") : t("rememberMeHelpSession")} - + {!registerMode && ( + + setRememberMe(e.target.checked)} />} + label={t("rememberMe")} + /> + + + )} - + {!registerMode && ( + + {rememberMe ? t("rememberMeHelpPersistent") : t("rememberMeHelpSession")} + + )} + + {allowReg && ( - )} - diff --git a/job-tracker-ui/src/views/ResetPasswordPage.tsx b/job-tracker-ui/src/views/ResetPasswordPage.tsx index 5526a5e..53be715 100644 --- a/job-tracker-ui/src/views/ResetPasswordPage.tsx +++ b/job-tracker-ui/src/views/ResetPasswordPage.tsx @@ -4,7 +4,7 @@ import { Alert, Box, Button, Paper, TextField, Typography } from "@mui/material" import { useNavigate } from "react-router-dom"; -import { api } from "../api"; +import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; @@ -16,7 +16,9 @@ export default function ResetPasswordPage() { const [email, setEmail] = useState(""); const [token, setToken] = useState(""); const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); const [loading, setLoading] = useState(false); + const [fieldErrors, setFieldErrors] = useState<{ newPassword?: string; confirmPassword?: string }>({}); useEffect(() => { const params = new URLSearchParams(window.location.search); @@ -54,6 +56,12 @@ export default function ResetPasswordPage() { toast(t("missingResetLinkInfo"), "error"); return; } + const errors: { newPassword?: string; confirmPassword?: string } = {}; + if (!newPassword || newPassword.length < 8) errors.newPassword = t("passwordTooShort"); + if (confirmPassword !== newPassword) errors.confirmPassword = t("passwordsDoNotMatch"); + setFieldErrors(errors); + if (Object.keys(errors).length > 0) return; + setLoading(true); api .post("/auth/reset-password", { email, token, newPassword }) @@ -62,16 +70,34 @@ export default function ResetPasswordPage() { navigate("/login", { replace: true }); }) .catch((e2: any) => { - const msg = e2?.response?.data || e2?.message || t("resetFailed"); - toast(String(msg), "error"); + toast(getApiErrorMessage(e2, t("resetFailed")), "error"); }) .finally(() => setLoading(false)); }} sx={{ display: "flex", flexDirection: "column", gap: 1.5 }} > {missingResetInfo ? {t("missingResetLinkInfo")} : null} - setEmail(e.target.value)} disabled={!missingResetInfo} fullWidth /> - setNewPassword(e.target.value)} fullWidth /> + setEmail(e.target.value)} disabled={!missingResetInfo} fullWidth /> + { setNewPassword(e.target.value); setFieldErrors((f) => ({ ...f, newPassword: undefined, confirmPassword: undefined })); }} + autoComplete="new-password" + error={Boolean(fieldErrors.newPassword)} + helperText={fieldErrors.newPassword || t("passwordTooShort")} + fullWidth + /> + { setConfirmPassword(e.target.value); setFieldErrors((f) => ({ ...f, confirmPassword: undefined })); }} + autoComplete="new-password" + error={Boolean(fieldErrors.confirmPassword)} + helperText={fieldErrors.confirmPassword} + fullWidth + />