import { FastifyInstance } from "fastify" import { db } from "../db" import { users } from "../db/schema" import { eq } from "drizzle-orm" import { z } from "zod" import argon2 from "argon2" const UserUpdateSchema = z.object({ name: z.string().min(1).optional(), role: z.string().optional() }) const UserCreateSchema = z.object({ email: z.string().email(), name: z.string().min(1), role: z.string(), password: z.string().min(8) }) const PasswordChangeSchema = z.object({ oldPassword: z.string(), newPassword: z.string().min(8) }) export default async function userRoutes(fastify: FastifyInstance) { fastify.addHook("preHandler", async (request, reply) => { try { await request.jwtVerify() } catch (err) { return reply.code(401).send({ message: "Unauthorized" }) } }) const isAdmin = (request: any) => { return (request.user as { sub: string, role: string })?.role === "admin" } fastify.get("/me", async (request, reply) => { const userId = (request.user as { sub: string } | undefined)?.sub if (!userId) { return reply.code(401).send({ message: "User ID not found in token" }) } const [user] = await db .select() .from(users) .where(eq(users.id, userId)) .limit(1) if (!user) { return reply.code(404).send({ message: "User not found" }) } return user }) fastify.patch("/me", async (request, reply) => { const userId = (request.user as { sub: string } | undefined)?.sub if (!userId) { return reply.code(401).send({ message: "User ID not found in token" }) } const body = UserUpdateSchema.parse(request.body) const [user] = await db .update(users) .set(body) .where(eq(users.id, userId)) .returning() if (!user) { return reply.code(404).send({ message: "User not found" }) } return user }) fastify.post("/me/password", async (request, reply) => { const userId = (request.user as { sub: string } | undefined)?.sub if (!userId) { return reply.code(401).send({ message: "User ID not found in token" }) } const body = PasswordChangeSchema.parse(request.body) const [user] = await db .select() .from(users) .where(eq(users.id, userId)) .limit(1) if (!user) { return reply.code(404).send({ message: "User not found" }) } const isPasswordCorrect = await argon2.verify(user.passwordHash, body.oldPassword) if (!isPasswordCorrect) { return reply.code(401).send({ message: "Incorrect old password" }) } const newPasswordHash = await argon2.hash(body.newPassword) await db .update(users) .set({ passwordHash: newPasswordHash }) .where(eq(users.id, userId)) return reply.send({ message: "Password updated successfully" }) }) fastify.get("/", async (request, reply) => { if (!isAdmin(request)) { return reply.code(403).send({ message: "Forbidden: Admin role required" }) } return await db.select().from(users) }) fastify.post("/", async (request, reply) => { if (!isAdmin(request)) { return reply.code(403).send({ message: "Forbidden: Admin role required" }) } const body = UserCreateSchema.parse(request.body) const passwordHash = await argon2.hash(body.password) const [user] = await db .insert(users) .values({ email: body.email, name: body.name, role: body.role, passwordHash }) .returning() return reply.code(201).send(user) }) fastify.patch("/:id", async (request, reply) => { if (!isAdmin(request)) { return reply.code(403).send({ message: "Forbidden: Admin role required" }) } const { id } = request.params as { id: string } const body = UserUpdateSchema.parse(request.body) const [user] = await db .update(users) .set(body) .where(eq(users.id, id)) .returning() if (!user) { return reply.code(404).send({ message: "User not found" }) } return user }) fastify.delete("/:id", async (request, reply) => { if (!isAdmin(request)) { return reply.code(403).send({ message: "Forbidden: Admin role required" }) } const { id } = request.params as { id: string } const result = await db .delete(users) .where(eq(users.id, id)) .returning() if (result.length === 0) { return reply.code(404).send({ message: "User not found" }) } return reply.code(204).send() }) }