- Signup now creates unverified users and sends a verification email (Resend); dev falls back to [VERIFY-LINK] console log - /api/auth/verify-email: single-use token handler, mints tia_session on success, redirects to /onboarding - /api/auth/resend-verification: rate-limited (3/hr), enumeration-safe - Sign-in gated on email_verified — unverified accounts get 403 with needsVerification flag so the UI can show the resend button - Google OAuth via arctic v3: PKCE + state anti-CSRF, find-or-create user, writes accounts row, mints tia_session - Login page: Google button, check-email screen, resend link on 403 - drizzle/0005_email_verification.sql: creates email_verifications table + backfills all existing users as verified (runs automatically on container start before app boots) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
39 lines
1.5 KiB
TypeScript
39 lines
1.5 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { sql } from "@/db";
|
|
import { rateLimit, getClientIp, getRateLimitKey } from "@/lib/rate-limit";
|
|
import { randomBytes } from "crypto";
|
|
import { sendVerificationEmail } from "@/lib/email";
|
|
|
|
export async function POST(request: Request) {
|
|
const ip = getClientIp(request);
|
|
const rl = await rateLimit(getRateLimitKey("resend-verify", ip), { max: 3, windowSec: 3600 });
|
|
if (!rl.success) {
|
|
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
|
}
|
|
|
|
try {
|
|
const { email } = await request.json();
|
|
if (!email) return NextResponse.json({ error: "Email required" }, { status: 400 });
|
|
|
|
const users = await sql`SELECT id, email_verified FROM users WHERE email = ${email} LIMIT 1`;
|
|
|
|
// Generic success — never reveal whether the email exists or is verified.
|
|
if (users.length === 0 || users[0].email_verified) {
|
|
return NextResponse.json({ success: true });
|
|
}
|
|
|
|
const userId = users[0].id;
|
|
const token = randomBytes(32).toString("hex");
|
|
const expiresAt = new Date();
|
|
expiresAt.setHours(expiresAt.getHours() + 24);
|
|
await sql`
|
|
INSERT INTO email_verifications (user_id, token, expires_at)
|
|
VALUES (${userId}, ${token}, ${expiresAt.toISOString()})
|
|
`;
|
|
await sendVerificationEmail(email, token, userId);
|
|
return NextResponse.json({ success: true });
|
|
} catch (e) {
|
|
console.error("Resend verification error:", e);
|
|
return NextResponse.json({ success: true });
|
|
}
|
|
}
|