diff --git a/web-next/app/[locale]/reset-password/page.tsx b/web-next/app/[locale]/reset-password/page.tsx index d61c4d8..31063c4 100644 --- a/web-next/app/[locale]/reset-password/page.tsx +++ b/web-next/app/[locale]/reset-password/page.tsx @@ -1,7 +1,12 @@ import { getTranslations, setRequestLocale } from 'next-intl/server' import { PageShell } from '@/components/PageShell' +import { getRealmName } from '@/lib/realm' +import { isResetTokenValid } from '@/lib/recover' import { ResetForm } from './ResetForm' +// Consulta el token en la BD al abrirse: no se puede prerenderizar. +export const dynamic = 'force-dynamic' + export default async function ResetPasswordPage({ params, searchParams, @@ -14,6 +19,23 @@ export default async function ResetPasswordPage({ const { token } = await searchParams const t = await getTranslations('Reset') + // El enlace es de un solo uso y dura 1 hora. Antes el formulario se pintaba + // igualmente y solo al enviarlo te enterabas de que el enlace ya no servía. + if (!(await isResetTokenValid(token ?? ''))) { + const realm = await getRealmName() + return ( + +
+
+

{t('invalidLink')}

+
+

{t('needHelp', { realm })}

+
+
+
+ ) + } + return (
diff --git a/web-next/lib/recover.ts b/web-next/lib/recover.ts index edc3401..3bd7a09 100644 --- a/web-next/lib/recover.ts +++ b/web-next/lib/recover.ts @@ -144,6 +144,26 @@ export async function requestRecovery(type: string, value: string): Promise { + if (!token) return false + try { + const [rows] = await db(DB.default).query( + 'SELECT created_at FROM passwordreset WHERE token = ? AND used = 0', + [token], + ) + const pr = rows[0] + if (!pr) return false + return Date.now() - new Date(pr.created_at).getTime() <= 3600_000 + } catch { + return false + } +} + export async function resetPassword(token: string, newPassword: string, confPassword: string): Promise { const [rows] = await db(DB.default).query( 'SELECT id, email, created_at FROM passwordreset WHERE token = ? AND used = 0', diff --git a/web-next/messages/en.json b/web-next/messages/en.json index 9f7c051..f43323c 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -276,8 +276,9 @@ "saving": "Saving…", "success": "Password reset. You can now sign in.", "goLogin": "Go to sign in", - "invalidLink": "The link is invalid or has already been used.", - "expiredLink": "The link has expired. Please request a new one.", + "invalidLink": "The password reset link is invalid", + "expiredLink": "The password reset link has expired", + "needHelp": "If you need help creating an account, you can contact the {realm} team.", "passwordMismatch": "Passwords do not match.", "passwordTooLong": "The password must not exceed 16 characters.", "accountNotFound": "Account not found.", diff --git a/web-next/messages/es.json b/web-next/messages/es.json index 08d7f47..d37faf1 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -276,8 +276,9 @@ "saving": "Guardando…", "success": "Contraseña restablecida. Ya puedes iniciar sesión.", "goLogin": "Ir a iniciar sesión", - "invalidLink": "El enlace no es válido o ya se ha usado.", - "expiredLink": "El enlace ha caducado. Solicita uno nuevo.", + "invalidLink": "El enlace de restablecer la contraseña es inválido", + "expiredLink": "El enlace de restablecer la contraseña ha caducado", + "needHelp": "Si necesitas ayuda para crear una cuenta, puedes contactar con el equipo de {realm}.", "passwordMismatch": "Las contraseñas no coinciden.", "passwordTooLong": "La contraseña no debe exceder los 16 caracteres.", "accountNotFound": "No se ha encontrado la cuenta.",