Files
ACVE/components/auth/LoginForm.tsx
2026-03-15 13:52:11 +00:00

178 lines
5.7 KiB
TypeScript
Executable File

"use client";
import { createBrowserClient } from "@supabase/ssr";
import { FormEvent, useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
type LoginFormProps = {
redirectTo: string;
role?: string;
showForgot?: boolean;
skipAuthedRedirect?: boolean;
};
// Helper to prevent open redirect vulnerabilities
const normalizeRedirect = (redirectTo: string) => {
if (!redirectTo.startsWith("/") || redirectTo.startsWith("//")) {
return "/courses";
}
// Never redirect back into auth routes after successful login.
if (redirectTo.startsWith("/auth/")) {
return "/courses";
}
return redirectTo;
};
export default function LoginForm({ redirectTo, role, showForgot, skipAuthedRedirect }: LoginFormProps) {
const router = useRouter();
const safeRedirect = normalizeRedirect(redirectTo);
const isTeacher = role === "teacher";
const showForgotNotice = Boolean(showForgot);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (skipAuthedRedirect) {
return;
}
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
supabase.auth.getUser().then(({ data }) => {
if (data.user) {
router.replace(safeRedirect);
}
});
}, [router, safeRedirect, skipAuthedRedirect]);
// Construct the "Forgot Password" link to preserve context
const forgotHref = `/auth/login?redirectTo=${encodeURIComponent(safeRedirect)}${isTeacher ? "&role=teacher" : ""
}&forgot=1`;
const onSubmit = async (event: FormEvent) => {
event.preventDefault();
setError(null);
setLoading(true);
// 1. Initialize the Supabase Client (Browser side)
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// 2. Attempt Real Login
const { error: signInError } = await supabase.auth.signInWithPassword({
email,
password,
});
if (signInError) {
setLoading(false);
setError(signInError.message); // e.g. "Invalid login credentials"
return;
}
// 3. CRITICAL: Refresh the Server Context
// This forces Next.js to re-run the Middleware and Server Components
// so they see the new cookie immediately.
router.refresh();
// 4. Navigate to the protected page and release button state.
setLoading(false);
router.replace(safeRedirect);
};
return (
<div className="acve-panel mx-auto w-full max-w-md p-6 bg-white rounded-xl shadow-sm border border-slate-200">
<h1 className="text-3xl font-bold text-slate-900 mb-2">
{isTeacher ? "Acceso Profesores" : "Iniciar Sesión"}
</h1>
<p className="text-slate-600 mb-6">
{isTeacher
? "Gestiona tus cursos y estudiantes."
: "Ingresa para continuar aprendiendo."}
</p>
{showForgotNotice && (
<div className="mb-4 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900">
El restablecimiento de contraseña no está disponible en este momento. Contacta a soporte.
</div>
)}
<form className="space-y-4" onSubmit={onSubmit}>
<label className="block">
<span className="mb-1 block text-sm font-medium text-slate-700">Email</span>
<input
className="w-full rounded-lg border border-slate-300 px-3 py-2 outline-none focus:border-black focus:ring-1 focus:ring-black transition-all"
onChange={(e) => setEmail(e.target.value)}
required
type="email"
value={email}
placeholder="tu@email.com"
/>
</label>
<label className="block">
<span className="mb-1 block text-sm font-medium text-slate-700">Contraseña</span>
<input
className="w-full rounded-lg border border-slate-300 px-3 py-2 outline-none focus:border-black focus:ring-1 focus:ring-black transition-all"
onChange={(e) => setPassword(e.target.value)}
required
type="password"
value={password}
placeholder="••••••••"
/>
</label>
{error && (
<div className="rounded-md bg-red-50 p-3 text-sm text-red-600 border border-red-100">
{error}
</div>
)}
<button
className="w-full rounded-lg bg-black px-4 py-2.5 font-semibold text-white hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
disabled={loading}
type="submit"
>
{loading ? "Entrando..." : "Entrar"}
</button>
</form>
<div className="mt-6 space-y-2 text-center text-sm text-slate-600">
<div>
<Link className="font-semibold text-black hover:underline" href={forgotHref}>
¿Olvidaste tu contraseña?
</Link>
</div>
<div>
¿Nuevo aquí?{" "}
<Link className="font-semibold text-black hover:underline" href="/auth/signup">
Crear cuenta
</Link>
</div>
{!isTeacher && (
<div className="pt-2 border-t border-slate-100 mt-4">
¿Eres profesor?{" "}
<Link
className="font-semibold text-black hover:underline"
href={`/auth/login?role=teacher&redirectTo=${encodeURIComponent(safeRedirect)}`}
>
Ingresa aquí
</Link>
</div>
)}
</div>
</div>
);
}