From 2a69ee8258844aaea6b259c86129d48d616d83d4 Mon Sep 17 00:00:00 2001 From: tracer Date: Tue, 24 Feb 2026 17:59:51 +0100 Subject: [PATCH] Add functional forgot-password flow and login modal UX updates --- CHANGELOG.md | 8 ++ resources/js/App.jsx | 2 + resources/js/api/client.js | 14 ++++ resources/js/pages/Login.jsx | 4 +- resources/js/pages/ResetPassword.jsx | 113 +++++++++++++++++++++++++++ resources/lang/de.json | 11 +++ resources/lang/en.json | 11 +++ 7 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 resources/js/pages/ResetPassword.jsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 68261a5..ee0b460 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2026-02-24 +- Added login modal actions: `Cancel` button and accent-styled, right-aligned `Sign in` button. +- Added functional `Forgot password?` flow with dedicated SPA route/page at `/reset-password`. +- Implemented reset-link request UI wired to `POST /api/forgot-password`. +- Implemented token-based new-password submission (`?token=...&email=...`) wired to `POST /api/reset-password`. +- Updated reset flow UX to return to `/login` after successful reset-link request and after successful password update. +- Added English and German translations for password reset screens/messages. + ## 2026-02-18 - Added CLI default PHP version detection to system status (`php_default_version`) using the CLI `php` binary. - Updated ACP System -> CLI to show the CLI default PHP path/version in the panel header with sufficiency indicator and warning tooltip. diff --git a/resources/js/App.jsx b/resources/js/App.jsx index 99520a3..9fe833b 100644 --- a/resources/js/App.jsx +++ b/resources/js/App.jsx @@ -7,6 +7,7 @@ import ForumView from './pages/ForumView' import ThreadView from './pages/ThreadView' import Login from './pages/Login' import Register from './pages/Register' +import ResetPassword from './pages/ResetPassword' import { Acp } from './pages/Acp' import BoardIndex from './pages/BoardIndex' import Ucp from './pages/Ucp' @@ -466,6 +467,7 @@ function AppShell() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/resources/js/api/client.js b/resources/js/api/client.js index a3d31c2..d3ace48 100644 --- a/resources/js/api/client.js +++ b/resources/js/api/client.js @@ -62,6 +62,20 @@ export async function registerUser({ email, username, plainPassword }) { }) } +export async function requestPasswordReset(email) { + return apiFetch('/forgot-password', { + method: 'POST', + body: JSON.stringify({ email }), + }) +} + +export async function resetPassword({ token, email, password, password_confirmation }) { + return apiFetch('/reset-password', { + method: 'POST', + body: JSON.stringify({ token, email, password, password_confirmation }), + }) +} + export async function logoutUser() { return apiFetch('/logout', { method: 'POST', diff --git a/resources/js/pages/Login.jsx b/resources/js/pages/Login.jsx index 0068aad..f97672e 100644 --- a/resources/js/pages/Login.jsx +++ b/resources/js/pages/Login.jsx @@ -57,11 +57,11 @@ export default function Login() { {t('auth.forgot_password')} -
+
-
diff --git a/resources/js/pages/ResetPassword.jsx b/resources/js/pages/ResetPassword.jsx new file mode 100644 index 0000000..9e7ccb8 --- /dev/null +++ b/resources/js/pages/ResetPassword.jsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from 'react' +import { Button, Card, Container, Form } from 'react-bootstrap' +import { Link, useNavigate, useSearchParams } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { requestPasswordReset, resetPassword } from '../api/client' + +export default function ResetPassword() { + const { t } = useTranslation() + const navigate = useNavigate() + const [searchParams] = useSearchParams() + const token = searchParams.get('token') || '' + const emailFromLink = searchParams.get('email') || '' + const isResetFlow = token.length > 0 + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [passwordConfirmation, setPasswordConfirmation] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (emailFromLink) { + setEmail(emailFromLink) + } + }, [emailFromLink]) + + const handleSubmit = async (event) => { + event.preventDefault() + setError('') + setLoading(true) + try { + if (isResetFlow) { + await resetPassword({ + token, + email, + password, + password_confirmation: passwordConfirmation, + }) + navigate('/login') + } else { + await requestPasswordReset(email) + navigate('/login') + } + } catch (err) { + setError(err.message) + } finally { + setLoading(false) + } + } + + return ( + + + + + {isResetFlow ? t('auth.reset_password_title') : t('auth.forgot_password')} + + + {isResetFlow ? t('auth.reset_password_hint') : t('auth.forgot_password_hint')} + + {error &&

{error}

} +
+ + {t('form.email')} + setEmail(event.target.value)} + placeholder={t('auth.reset_email_placeholder')} + required + /> + + {isResetFlow && ( + <> + + {t('form.password')} + setPassword(event.target.value)} + required + /> + + + {t('auth.confirm_password')} + setPasswordConfirmation(event.target.value)} + required + /> + + + )} +
+ + +
+
+
+
+
+ ) +} diff --git a/resources/lang/de.json b/resources/lang/de.json index 2a56605..96fdf73 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -82,6 +82,17 @@ "auth.login_identifier": "E-Mail oder Benutzername", "auth.login_placeholder": "name@example.com oder benutzername", "auth.forgot_password": "Passwort vergessen?", + "auth.forgot_password_hint": "Gib die E-Mail-Adresse deines Kontos ein, dann senden wir dir einen Link zum Zuruecksetzen.", + "auth.reset_email_placeholder": "name@example.com", + "auth.send_reset_link": "Reset-Link senden", + "auth.sending_reset_link": "Wird gesendet...", + "auth.reset_link_sent": "Falls ein Konto existiert, wurde ein Passwort-Reset-Link gesendet.", + "auth.reset_password_title": "Passwort zuruecksetzen", + "auth.reset_password_hint": "Gib deine E-Mail-Adresse ein und waehle ein neues Passwort.", + "auth.reset_password_submit": "Passwort zuruecksetzen", + "auth.resetting_password": "Wird zurueckgesetzt...", + "auth.password_reset_success": "Passwort erfolgreich zurueckgesetzt. Du kannst dich jetzt anmelden.", + "auth.confirm_password": "Passwort bestaetigen", "auth.register_hint": "Registriere dich mit E-Mail und einem eindeutigen Benutzernamen.", "auth.verify_notice": "Bitte bestätige deine E-Mail-Adresse, bevor du dich anmeldest.", "auth.register_title": "Konto erstellen", diff --git a/resources/lang/en.json b/resources/lang/en.json index 8fdb815..df87488 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -82,6 +82,17 @@ "auth.login_identifier": "Email or username", "auth.login_placeholder": "name@example.com or username", "auth.forgot_password": "Forgot password?", + "auth.forgot_password_hint": "Enter your account email and we will send you a password reset link.", + "auth.reset_email_placeholder": "name@example.com", + "auth.send_reset_link": "Send reset link", + "auth.sending_reset_link": "Sending...", + "auth.reset_link_sent": "If an account exists, a password reset link has been sent.", + "auth.reset_password_title": "Reset password", + "auth.reset_password_hint": "Enter your email and choose a new password.", + "auth.reset_password_submit": "Reset password", + "auth.resetting_password": "Resetting...", + "auth.password_reset_success": "Password reset successful. You can now sign in.", + "auth.confirm_password": "Confirm password", "auth.register_hint": "Register with an email and a unique username.", "auth.verify_notice": "Check your email to verify your account before logging in.", "auth.register_title": "Create account",