advance
This commit is contained in:
123
components/auth/LoginForm.tsx
Normal file → Executable file
123
components/auth/LoginForm.tsx
Normal file → Executable file
@@ -1,95 +1,152 @@
|
||||
"use client";
|
||||
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import { FormEvent, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { supabaseBrowser } from "@/lib/supabase/browser";
|
||||
|
||||
type LoginFormProps = {
|
||||
redirectTo: string;
|
||||
role?: string;
|
||||
showForgot?: boolean;
|
||||
};
|
||||
|
||||
// Helper to prevent open redirect vulnerabilities
|
||||
const normalizeRedirect = (redirectTo: string) => {
|
||||
if (!redirectTo.startsWith("/")) return "/courses";
|
||||
return redirectTo;
|
||||
if (redirectTo.startsWith("/") && !redirectTo.startsWith("//")) {
|
||||
return redirectTo;
|
||||
}
|
||||
return "/home"; // Default fallback
|
||||
};
|
||||
|
||||
export default function LoginForm({ redirectTo }: LoginFormProps) {
|
||||
export default function LoginForm({ redirectTo, role, showForgot }: 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);
|
||||
|
||||
// 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);
|
||||
|
||||
const client = supabaseBrowser();
|
||||
if (!client) {
|
||||
setLoading(false);
|
||||
setError("Supabase is not configured. Add NEXT_PUBLIC_SUPABASE_* to .env.local.");
|
||||
return;
|
||||
}
|
||||
// 1. Initialize the Supabase Client (Browser side)
|
||||
const supabase = createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
const { error: signInError } = await client.auth.signInWithPassword({ email, password });
|
||||
setLoading(false);
|
||||
// 2. Attempt Real Login
|
||||
const { error: signInError } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (signInError) {
|
||||
setError(signInError.message);
|
||||
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
|
||||
router.push(safeRedirect);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="acve-panel mx-auto w-full max-w-md p-6">
|
||||
<h1 className="acve-heading text-4xl">Login</h1>
|
||||
<p className="mt-1 text-base text-slate-600">Sign in to access protected learning routes.</p>
|
||||
<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>
|
||||
|
||||
<form className="mt-5 space-y-4" onSubmit={onSubmit}>
|
||||
{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 text-slate-700">Email</span>
|
||||
<span className="mb-1 block text-sm font-medium text-slate-700">Email</span>
|
||||
<input
|
||||
className="w-full rounded-xl border border-slate-300 px-3 py-2 outline-none focus:border-brand"
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
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 text-slate-700">Password</span>
|
||||
<span className="mb-1 block text-sm font-medium text-slate-700">Contraseña</span>
|
||||
<input
|
||||
className="w-full rounded-xl border border-slate-300 px-3 py-2 outline-none focus:border-brand"
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
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 ? <p className="text-sm text-red-600">{error}</p> : null}
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-3 text-sm text-red-600 border border-red-100">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="acve-button-primary w-full px-4 py-2 font-semibold hover:brightness-105 disabled:opacity-60"
|
||||
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 ? "Signing in..." : "Login"}
|
||||
{loading ? "Entrando..." : "Entrar"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-4 text-sm text-slate-600">
|
||||
New here?{" "}
|
||||
<Link className="font-semibold text-brand" href="/auth/signup">
|
||||
Create an account
|
||||
</Link>
|
||||
</p>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user