// Authentication routes import type { FastifyInstance } from "fastify"; import bcrypt from "bcrypt"; import { queryOne, execute } from "../db/client.js"; import { authenticate } from "../middleware/auth.js"; import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; const SALT_ROUNDS = 10; export async function authRoutes(app: FastifyInstance): Promise { // POST /api/auth/register app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { const { username, password, nickname } = request.body; if (!username || !password) { return reply.status(400).send({ error: "Bad Request", message: "Username and password are required", statusCode: 400, }); } if (password.length < 6) { return reply.status(400).send({ error: "Bad Request", message: "Password must be at least 6 characters", statusCode: 400, }); } const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); if (existing) { return reply.status(409).send({ error: "Conflict", message: "Username already exists", statusCode: 409, }); } const id = crypto.randomUUID(); const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); const now = new Date().toISOString(); execute( "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", [id, username, passwordHash, nickname || username, now, now] ); const user = queryOne( "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", [id] ); if (!user) { return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); } const token = app.jwt.sign({ userId: id, username, role: user.role }); return reply.status(201).send({ token, user } satisfies AuthResponse); }); // POST /api/auth/login app.post<{ Body: LoginInput }>("/login", async (request, reply) => { const { username, password } = request.body; if (!username || !password) { return reply.status(400).send({ error: "Bad Request", message: "Username and password are required", statusCode: 400, }); } const user = queryOne( "SELECT * FROM users WHERE username = ?", [username] ); if (!user) { return reply.status(401).send({ error: "Unauthorized", message: "Invalid username or password", statusCode: 401, }); } const valid = await bcrypt.compare(password, user.password_hash); if (!valid) { return reply.status(401).send({ error: "Unauthorized", message: "Invalid username or password", statusCode: 401, }); } const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); const { password_hash, ...safeUser } = user; return { token, user: safeUser } satisfies AuthResponse; }); // GET /api/auth/me — current user info app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { const jwtUser = request.user as unknown as { userId: string }; const user = queryOne( "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", [jwtUser.userId] ); if (!user) { return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404, }); } return { data: user }; }); }