feat(F-083): completed feature

This commit is contained in:
chattie
2026-08-20 06:08:32 +02:00
parent 23e84efe80
commit 22ad15325f
20 changed files with 1041 additions and 25 deletions

View File

@@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from 'next/server';
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
/** Proxy to backend POST /auth/password-reset/confirm. */
export async function POST(req: NextRequest) {
const body = await req.text();
try {
const backendRes = await fetch(`${API}/auth/password-reset/confirm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});
const data = await backendRes.json().catch(() => null);
return NextResponse.json(data ?? { error: 'Bad response' }, { status: backendRes.status });
} catch {
return NextResponse.json({ error: 'Proxy error' }, { status: 502 });
}
}

View File

@@ -0,0 +1,112 @@
'use client';
import { useState, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
function RestablecerForm() {
const router = useRouter();
const params = useSearchParams();
const token = params.get('token') ?? '';
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [done, setDone] = useState(false);
if (!token) {
return (
<div className="p-8 bg-white border border-gray-200 rounded-xl text-sm text-gray-700">
Enlace inválido. Solicita un nuevo enlace desde la página de inicio de sesión.
</div>
);
}
const submit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (password.length < 8) {
setError('La contraseña debe tener al menos 8 caracteres.');
return;
}
if (password !== confirm) {
setError('Las contraseñas no coinciden.');
return;
}
setSaving(true);
try {
const res = await fetch('/api/auth/password-reset/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, password }),
});
if (!res.ok) {
const data = await res.json().catch(() => null);
setError(data?.message ?? 'Enlace inválido o caducado. Solicita uno nuevo.');
return;
}
setDone(true);
setTimeout(() => router.push('/cuenta/iniciar-sesion'), 2500);
} catch {
setError('Error de red. Inténtalo de nuevo.');
} finally {
setSaving(false);
}
};
if (done) {
return (
<div className="p-8 bg-white border border-gray-200 rounded-xl text-sm text-green-700">
Tu contraseña se ha actualizado. Te llevamos a iniciar sesión...
</div>
);
}
return (
<form onSubmit={submit} className="p-8 bg-white border border-gray-200 rounded-xl space-y-4 max-w-md w-full">
<div>
<h1 className="text-xl font-bold text-gray-900">Restablecer contraseña</h1>
<p className="text-sm text-gray-500 mt-1">Introduce tu nueva contraseña.</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Nueva contraseña</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
autoFocus
className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Confirmar contraseña</label>
<input
type="password"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
required
minLength={8}
className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none"
/>
</div>
{error && <p className="text-sm text-red-600 bg-red-50 rounded-xl px-4 py-2">{error}</p>}
<button
type="submit"
disabled={saving}
className="w-full px-5 py-2.5 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white text-sm font-semibold rounded-xl transition-colors"
>
{saving ? 'Guardando...' : 'Restablecer contraseña'}
</button>
</form>
);
}
export default function RestablecerPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<Suspense fallback={<div className="text-gray-400 text-sm">Cargando...</div>}>
<RestablecerForm />
</Suspense>
</div>
);
}