104 lines
2.8 KiB
TypeScript
104 lines
2.8 KiB
TypeScript
import { NextResponse } from "next/server";
|
||
import { cookies } from "next/headers";
|
||
import { randomUUID } from "crypto";
|
||
import { and, eq, gt, isNull } from "drizzle-orm";
|
||
|
||
import { db } from "@/lib/db";
|
||
import { loginTokens, sessions, users } from "@/lib/schema";
|
||
import { hashToken } from "@/lib/auth/tokens";
|
||
|
||
export async function POST(req: Request) {
|
||
// --------------------------------------------------
|
||
// 1️⃣ Parse token from request
|
||
// --------------------------------------------------
|
||
const { token } = await req.json();
|
||
|
||
if (!token) {
|
||
return NextResponse.json(
|
||
{ error: "Missing token" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// --------------------------------------------------
|
||
// 2️⃣ Validate login token (magic link)
|
||
// --------------------------------------------------
|
||
const tokenHash = hashToken(token);
|
||
|
||
const loginToken = await db
|
||
.select()
|
||
.from(loginTokens)
|
||
.where(
|
||
and(
|
||
eq(loginTokens.tokenHash, tokenHash),
|
||
isNull(loginTokens.usedAt),
|
||
gt(loginTokens.expiresAt, new Date())
|
||
)
|
||
)
|
||
.limit(1)
|
||
.then((rows) => rows[0]);
|
||
|
||
if (!loginToken) {
|
||
return NextResponse.json(
|
||
{ error: "Invalid or expired token" },
|
||
{ status: 401 }
|
||
);
|
||
}
|
||
|
||
// --------------------------------------------------
|
||
// 3️⃣ Ensure user still exists
|
||
// --------------------------------------------------
|
||
const user = await db
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, loginToken.userId))
|
||
.limit(1)
|
||
.then((rows) => rows[0]);
|
||
|
||
if (!user) {
|
||
return NextResponse.json(
|
||
{ error: "User not found" },
|
||
{ status: 404 }
|
||
);
|
||
}
|
||
|
||
// --------------------------------------------------
|
||
// 4️⃣ Consume login token (one-time use)
|
||
// --------------------------------------------------
|
||
await db
|
||
.update(loginTokens)
|
||
.set({ usedAt: new Date() })
|
||
.where(eq(loginTokens.id, loginToken.id));
|
||
|
||
// --------------------------------------------------
|
||
// 5️⃣ Create DB-backed session
|
||
// --------------------------------------------------
|
||
const sessionId = randomUUID();
|
||
const expiresAt = new Date(
|
||
Date.now() + 1000 * 60 * 60 * 24 * 14 // 14 days
|
||
);
|
||
|
||
await db.insert(sessions).values({
|
||
id: sessionId,
|
||
userId: user.id,
|
||
expiresAt,
|
||
});
|
||
|
||
// --------------------------------------------------
|
||
// 6️⃣ Set secure session cookie
|
||
// --------------------------------------------------
|
||
const cookieStore = await cookies();
|
||
|
||
cookieStore.set("session", sessionId, {
|
||
httpOnly: true,
|
||
sameSite: "strict",
|
||
secure: process.env.NODE_ENV === "production",
|
||
path: "/",
|
||
maxAge: 60 * 60 * 24 * 14, // 14 days
|
||
});
|
||
|
||
// --------------------------------------------------
|
||
// 7️⃣ Done
|
||
// --------------------------------------------------
|
||
return NextResponse.json({ ok: true });
|
||
}
|