feat(F-083): completed feature
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
112
project/storefront/src/app/cuenta/restablecer/page.tsx
Normal file
112
project/storefront/src/app/cuenta/restablecer/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user