chore: add server/auth.ts
This commit is contained in:
parent
893ad6558d
commit
0382d22c97
1 changed files with 129 additions and 0 deletions
129
server/auth.ts
Normal file
129
server/auth.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
/**
|
||||||
|
* 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=<token>
|
||||||
|
* 4. GET /api/auth/verify?token=<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 });
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue