172 lines
5.1 KiB
TypeScript
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 });
|
|
},
|
|
);
|
|
}
|