add magic link back

This commit is contained in:
Jun-te Kim 2026-02-01 21:43:56 +00:00
parent b864da766b
commit 0504460f5b
2 changed files with 101 additions and 20 deletions

View file

@ -1,20 +1,104 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { sessions } from "@/lib/schema"; 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 sessionId = randomUUID();
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 14); // 14 days const expiresAt = new Date(
Date.now() + 1000 * 60 * 60 * 24 * 14 // 14 days
);
await db.insert(sessions).values({ await db.insert(sessions).values({
id: sessionId, id: sessionId,
userId: loginToken.userId, userId: user.id,
expiresAt, expiresAt,
}); });
// --------------------------------------------------
// 6⃣ Set secure session cookie
// --------------------------------------------------
const cookieStore = await cookies(); const cookieStore = await cookies();
cookieStore.set("session", sessionId, { cookieStore.set("session", sessionId, {
httpOnly: true, httpOnly: true,
sameSite: "strict", sameSite: "strict",
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
path: "/", path: "/",
maxAge: 60 * 60 * 24 * 14, maxAge: 60 * 60 * 24 * 14, // 14 days
}); });
// --------------------------------------------------
// 7⃣ Done
// --------------------------------------------------
return NextResponse.json({ ok: true });
}

View file

@ -1,10 +1,7 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig = { const nextConfig = {
output: 'standalone', output: 'standalone'
experimental: {
turbo: false, // disables Turbopack in prod builds
},
}; };
export default nextConfig; export default nextConfig;