eyrun-api/src/routes/signup.ts
2026-02-08 13:39:33 +01:00

172 lines
5.1 KiB
TypeScript

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 });
},
);
}