167 lines
6.0 KiB
TypeScript
167 lines
6.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Header } from '@/components/layouts/header';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Button, Input, Label } from '@horux/shared-ui';
|
|
import { changePassword, logoutAll } from '@/lib/api/auth';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import { KeyRound, LogOut, Loader2, AlertCircle, CheckCircle2 } from 'lucide-react';
|
|
|
|
export default function SeguridadPage() {
|
|
const router = useRouter();
|
|
const { logout } = useAuthStore();
|
|
|
|
const [currentPassword, setCurrentPassword] = useState('');
|
|
const [newPassword, setNewPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [changing, setChanging] = useState(false);
|
|
const [changeError, setChangeError] = useState<string | null>(null);
|
|
const [changeOk, setChangeOk] = useState<string | null>(null);
|
|
|
|
const [loggingOut, setLoggingOut] = useState(false);
|
|
|
|
const handleChangePassword = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setChangeError(null);
|
|
setChangeOk(null);
|
|
|
|
if (newPassword.length < 8) {
|
|
setChangeError('La nueva contraseña debe tener al menos 8 caracteres');
|
|
return;
|
|
}
|
|
if (newPassword !== confirmPassword) {
|
|
setChangeError('Las contraseñas no coinciden');
|
|
return;
|
|
}
|
|
if (currentPassword === newPassword) {
|
|
setChangeError('La nueva contraseña debe ser distinta a la actual');
|
|
return;
|
|
}
|
|
|
|
setChanging(true);
|
|
try {
|
|
const res = await changePassword(currentPassword, newPassword);
|
|
setChangeOk(res.message);
|
|
setCurrentPassword('');
|
|
setNewPassword('');
|
|
setConfirmPassword('');
|
|
setTimeout(() => {
|
|
logout();
|
|
router.push('/login');
|
|
}, 2500);
|
|
} catch (err: any) {
|
|
setChangeError(err?.response?.data?.message || 'Error al cambiar contraseña');
|
|
} finally {
|
|
setChanging(false);
|
|
}
|
|
};
|
|
|
|
const handleLogoutAll = async () => {
|
|
if (!confirm('Esto cerrará todas tus sesiones activas, incluyendo esta. Tendrás que iniciar sesión de nuevo. ¿Continuar?')) return;
|
|
setLoggingOut(true);
|
|
try {
|
|
await logoutAll();
|
|
logout();
|
|
router.push('/login');
|
|
} catch (err: any) {
|
|
alert(err?.response?.data?.message || 'Error al cerrar sesiones');
|
|
setLoggingOut(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Header title="Seguridad" />
|
|
<main className="p-6 space-y-6 max-w-3xl">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<KeyRound className="h-4 w-4" />
|
|
Cambiar contraseña
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Al cambiar tu contraseña, todas tus sesiones (incluyendo esta) serán cerradas por seguridad.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleChangePassword} className="space-y-4">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="current">Contraseña actual</Label>
|
|
<Input
|
|
id="current"
|
|
type="password"
|
|
value={currentPassword}
|
|
onChange={e => setCurrentPassword(e.target.value)}
|
|
autoComplete="current-password"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="new">Nueva contraseña</Label>
|
|
<Input
|
|
id="new"
|
|
type="password"
|
|
value={newPassword}
|
|
onChange={e => setNewPassword(e.target.value)}
|
|
autoComplete="new-password"
|
|
minLength={8}
|
|
required
|
|
/>
|
|
<p className="text-xs text-muted-foreground">Mínimo 8 caracteres.</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="confirm">Confirmar nueva contraseña</Label>
|
|
<Input
|
|
id="confirm"
|
|
type="password"
|
|
value={confirmPassword}
|
|
onChange={e => setConfirmPassword(e.target.value)}
|
|
autoComplete="new-password"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{changeError && (
|
|
<div className="flex items-start gap-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2">
|
|
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
|
<span>{changeError}</span>
|
|
</div>
|
|
)}
|
|
{changeOk && (
|
|
<div className="flex items-start gap-2 text-sm text-green-700 bg-green-50 border border-green-200 rounded px-3 py-2">
|
|
<CheckCircle2 className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
|
<span>{changeOk}</span>
|
|
</div>
|
|
)}
|
|
|
|
<Button type="submit" disabled={changing || !!changeOk}>
|
|
{changing && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
|
Actualizar contraseña
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<LogOut className="h-4 w-4" />
|
|
Cerrar todas las sesiones
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Útil si perdiste un dispositivo o sospechas que alguien accedió a tu cuenta. Tendrás que iniciar sesión de nuevo en todos tus dispositivos, incluyendo este.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Button variant="outline" onClick={handleLogoutAll} disabled={loggingOut}>
|
|
{loggingOut && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
|
Cerrar todas mis sesiones
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|