Files
spsays-admin/app/login/page.tsx
T
2026-06-16 14:50:09 +05:30

171 lines
5.0 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { toast } from "sonner";
import { api, AdminError, type Me } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [otp, setOtp] = useState("");
const [stage, setStage] = useState<"credentials" | "otp">("credentials");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
// Once authenticated, only admins may proceed. Non-admins are signed back
// out so an ordinary account can't sit in an authenticated-but-forbidden
// state against the admin origin.
async function gateAdmin(user: Me) {
if (user.role === "admin") {
router.replace("/users");
return;
}
await api.logout().catch(() => {});
setError("This account doesn't have admin access.");
setStage("credentials");
}
async function onCredentials(e: React.FormEvent) {
e.preventDefault();
setError(null);
setBusy(true);
try {
const res = await api.login(email.trim(), password);
if (res.kind === "otp_required") {
setStage("otp");
toast("We emailed you a verification code.");
} else {
await gateAdmin(res.user);
}
} catch (err) {
setError(err instanceof AdminError ? err.message : "Could not sign in.");
} finally {
setBusy(false);
}
}
async function onOtp(e: React.FormEvent) {
e.preventDefault();
setError(null);
setBusy(true);
try {
const user = await api.verifyLoginOtp(email.trim(), password, otp.trim());
await gateAdmin(user);
} catch (err) {
setError(err instanceof AdminError ? err.message : "Invalid code.");
} finally {
setBusy(false);
}
}
return (
<AuthShell title="Admin sign in">
{stage === "credentials" ? (
<form onSubmit={onCredentials} className="space-y-4">
<Field label="Email">
<Input
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</Field>
<Field label="Password">
<Input
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</Field>
{error ? <p className="text-sm text-destructive">{error}</p> : null}
<Button type="submit" disabled={busy} className="w-full">
{busy ? "Signing in…" : "Sign in"}
</Button>
<p className="text-center text-sm text-muted-foreground">
<Link href="/forgot-password" className="hover:text-foreground">
Forgot your password?
</Link>
</p>
</form>
) : (
<form onSubmit={onOtp} className="space-y-4">
<p className="text-sm text-muted-foreground">
Enter the verification code we emailed to {email}.
</p>
<Field label="Verification code">
<Input
inputMode="numeric"
autoFocus
required
value={otp}
onChange={(e) => setOtp(e.target.value)}
/>
</Field>
{error ? <p className="text-sm text-destructive">{error}</p> : null}
<Button type="submit" disabled={busy} className="w-full">
{busy ? "Verifying…" : "Verify & sign in"}
</Button>
<button
type="button"
onClick={() => {
setStage("credentials");
setOtp("");
setError(null);
}}
className="w-full text-center text-sm text-muted-foreground hover:text-foreground"
>
Back
</button>
</form>
)}
</AuthShell>
);
}
export function AuthShell({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<main className="flex min-h-screen items-center justify-center px-4">
<div className="w-full max-w-sm">
<div className="mb-6 text-center">
<p className="text-xs uppercase tracking-[0.2em] text-primary">
Prabhupāda Says
</p>
<h1 className="mt-1 text-xl font-semibold text-foreground">{title}</h1>
</div>
<div className="rounded-2xl border border-border bg-card p-6 shadow-sm">
{children}
</div>
</div>
</main>
);
}
export function Field({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<label className="block space-y-1.5">
<span className="text-sm font-medium text-foreground">{label}</span>
{children}
</label>
);
}