122 lines
3.6 KiB
TypeScript
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 };
|
|
});
|
|
}
|