/** * Magic-link customer authentication. * * Flow: * 1. Customer submits email → POST /api/auth/magic-link * 2. Server stores a token (15-min TTL) on the customer record * 3. Email with link is sent: /account/verify?token= * 4. GET /api/auth/verify?token= validates token, sets session * * Session is stored in express-session (PostgreSQL-backed via connect-pg-simple). */ import { db } from "./db"; import { customers } from "../shared/schema"; import { eq } from "drizzle-orm"; import crypto from "crypto"; import { sendMagicLink } from "./email"; import type { Request, Response } from "express"; function generateToken(): string { return crypto.randomBytes(32).toString("hex"); } export async function requestMagicLink(req: Request, res: Response) { const { email } = req.body as { email?: string }; if (!email || !email.includes("@")) { return res.status(400).json({ error: "Valid email required" }); } const normalEmail = email.toLowerCase().trim(); const token = generateToken(); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); let [customer] = await db .select({ id: customers.id }) .from(customers) .where(eq(customers.email, normalEmail)); if (!customer) { const [created] = await db .insert(customers) .values({ email: normalEmail, magicLinkToken: token, magicLinkTokenExpiresAt: expiresAt }) .returning({ id: customers.id }); customer = created; } else { await db .update(customers) .set({ magicLinkToken: token, magicLinkTokenExpiresAt: expiresAt }) .where(eq(customers.id, customer.id)); } const baseUrl = `${req.protocol}://${req.get("host")}`; await sendMagicLink(normalEmail, token, baseUrl); res.json({ ok: true }); } export async function verifyMagicLink(req: Request, res: Response) { const { token } = req.query as { token?: string }; if (!token) return res.status(400).json({ error: "Token required" }); const [customer] = await db .select() .from(customers) .where(eq(customers.magicLinkToken, token)); if (!customer || !customer.magicLinkTokenExpiresAt) { return res.status(401).json({ error: "Invalid or expired token" }); } if (new Date() > new Date(customer.magicLinkTokenExpiresAt)) { return res.status(401).json({ error: "Token expired" }); } await db .update(customers) .set({ magicLinkToken: null, magicLinkTokenExpiresAt: null }) .where(eq(customers.id, customer.id)); (req.session as any).customerId = customer.id; res.json({ ok: true, customer: { id: customer.id, email: customer.email, firstName: customer.firstName, lastName: customer.lastName, }, }); } export function requireCustomer(req: Request, res: Response, next: Function) { if (!(req.session as any).customerId) { return res.status(401).json({ error: "Authentication required" }); } next(); } export function requireAdmin(req: Request, res: Response, next: Function) { if (!(req.session as any).adminUserId) { return res.status(401).json({ error: "Admin access required" }); } next(); } export async function logout(req: Request, res: Response) { req.session.destroy(() => { res.json({ ok: true }); }); } export async function me(req: Request, res: Response) { const customerId = (req.session as any).customerId; if (!customerId) return res.json({ customer: null }); const [customer] = await db .select({ id: customers.id, email: customers.email, firstName: customers.firstName, lastName: customers.lastName, phone: customers.phone, }) .from(customers) .where(eq(customers.id, customerId)); res.json({ customer: customer || null }); }