import type { FastifyInstance } from "fastify"; import { eq } from "drizzle-orm"; import { db } from "../db/index.js"; import { users, accounts, memberships } from "../db/schema.js"; import { requestOtp, OtpRateLimitError, verifyOtp, OtpError } from "../lib/otp.js"; import { createSession } from "../lib/sessions.js"; import { getUserAccounts } from "../lib/accounts.js"; export async function signupRoutes(app: FastifyInstance) { // POST /signup — send OTP for a new user app.post<{ Body: { email: string } }>( "/signup", { schema: { description: "Request an OTP for a new user signup", tags: ["Authentication"], body: { type: "object", required: ["email"], properties: { email: { type: "string", format: "email" }, }, }, response: { 200: { description: "OTP sent successfully", type: "object", properties: { message: { type: "string" }, }, }, 409: { description: "User already exists", type: "object", properties: { error: { type: "string" }, }, }, 429: { description: "Too many OTP requests", type: "object", properties: { error: { type: "string" }, }, }, }, }, }, async (request, reply) => { const { email } = request.body; const [existingUser] = await db.select().from(users).where(eq(users.email, email)); if (existingUser) { return reply.status(409).send({ error: "User already exists. Please log in." }); } try { await requestOtp(email); } catch (err) { if (err instanceof OtpRateLimitError) { return reply.status(429).send({ error: err.message }); } throw err; } return { message: "OTP sent to your email" }; }, ); // POST /signup/verify — verify OTP, create user + account + session app.post<{ Body: { email: string; code: string; accountName: string; username: string } }>( "/signup/verify", { schema: { description: "Verify OTP and create a new user with an account", tags: ["Authentication"], body: { type: "object", required: ["email", "code", "accountName", "username"], properties: { email: { type: "string", format: "email" }, code: { type: "string", minLength: 6, maxLength: 6 }, accountName: { type: "string", minLength: 1 }, username: { type: "string", minLength: 1 }, }, }, response: { 201: { description: "User created successfully", type: "object", properties: { accessToken: { type: "string" }, refreshToken: { type: "string" }, user: { type: "object", properties: { id: { type: "string" }, email: { type: "string" }, name: { type: "string" }, }, }, accounts: { type: "array", items: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, role: { type: "string" }, }, }, }, }, }, 400: { description: "Invalid or expired OTP", type: "object", properties: { error: { type: "string" }, }, }, 409: { description: "User already exists", type: "object", properties: { error: { type: "string" }, }, }, }, }, }, async (request, reply) => { const { email, code, accountName, username } = request.body; try { await verifyOtp(email, code); } catch (err) { if (err instanceof OtpError) { return reply.status(400).send({ error: err.message }); } throw err; } const [existingUser] = await db.select().from(users).where(eq(users.email, email)); if (existingUser) { return reply.status(409).send({ error: "User already exists. Please log in." }); } const { user, tokens } = await db.transaction(async (tx) => { const [created] = await tx .insert(users) .values({ email, name: username }) .returning(); const [account] = await tx.insert(accounts).values({ name: accountName }).returning(); await tx.insert(memberships).values({ userId: created.id, accountId: account.id, role: "owner", }); const t = await createSession(created.id, tx); return { user: created, tokens: t }; }); const userAccounts = await getUserAccounts(user.id); return reply.status(201).send({ ...tokens, user, accounts: userAccounts }); }, ); }