app/api/sign-in/route.ts
import { cookies } from "next/headers";
// A route handler lives at a fixed URL, so a page left open across a deploy
// still posts to something that exists. No action id to go stale.
function sameOrigin(request: Request) {
const origin = request.headers.get("origin");
const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host");
return origin !== null && host !== null && new URL(origin).host === host;
}
export async function POST(request: Request) {
if (!sameOrigin(request)) return new Response("Forbidden", { status: 403 });
const form = await request.formData();
const email = String(form.get("email") ?? "").trim();
const password = String(form.get("password") ?? "");
const wantsJson = request.headers.get("accept")?.includes("application/json") ?? false;
const session = await verify(email, password);
if (!session) {
return wantsJson
? Response.json({ message: "Wrong email or password." }, { status: 401 })
: new Response(null, { status: 303, headers: { Location: "/login?error=1" } });
}
(await cookies()).set("session", session, { httpOnly: true, secure: true, sameSite: "lax", path: "/" });
return wantsJson ? Response.json({ ok: true }) : new Response(null, { status: 303, headers: { Location: "/dashboard" } });
}
async function verify(email: string, password: string): Promise<string | null> {
// your auth here: Supabase, Auth.js, Clerk's backend API or your own users table
void email;
void password;
return null;
}
lib/sign-in.ts
// What the login form's submit handler calls. It throws on a wrong password,
// so the form can show the message and shake.
export async function signIn(email: string, password: string): Promise<void> {
const body = new FormData();
body.set("email", email);
body.set("password", password);
const res = await fetch("/api/sign-in", { method: "POST", headers: { Accept: "application/json" }, body });
if (!res.ok) {
const data: { message?: string } = await res.json().catch(() => ({}));
throw new Error(data.message ?? "Sign in failed. Try again.");
}
window.location.assign("/dashboard");
}