Files
16gagent/cases/bookshelf/backend/src/routes/auth.ts
T
2026-06-06 10:40:48 +08:00

122 lines
3.6 KiB
TypeScript

// 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<void> {
// 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<User>(
"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<User & { password_hash: string }>(
"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<User>(
"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 };
});
}