import { FastifyInstance } from "fastify" import { db } from "../db" import { users, auditLogs } from "../db/schema" import { eq, max } 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) }) const UserDeleteSchema = z.object({ password: z.string() }) export default async function userRoutes(fastify: FastifyInstance) { fastify.addHook("preHandler", async (request, reply) => { try { await request.jwtVerify() const userId = (request.user as { sub: string } | undefined)?.sub if (userId) { // Touch last active via audit log entry await db.insert(auditLogs).values({ userId, action: "heartbeat", timestamp: new Date(), }) } } 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.delete("/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 = UserDeleteSchema.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.password) if (!isPasswordCorrect) { return reply.code(401).send({ message: "Incorrect password" }) } await db .delete(users) .where(eq(users.id, userId)) return reply.send({ message: "User account deleted 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.get("/presence", async (request, reply) => { const userId = (request.user as { sub: string } | undefined)?.sub if (!userId) { return reply.code(401).send({ message: "Unauthorized" }) } if (!isAdmin(request)) { // Non-admins only see their own presence const [presence] = await db .select({ userId: auditLogs.userId, lastActiveAt: max(auditLogs.timestamp), }) .from(auditLogs) .where(eq(auditLogs.userId, userId)) .groupBy(auditLogs.userId) return presence ? { [presence.userId]: presence.lastActiveAt } : {} } // Admins see everyone const results = await db .select({ userId: auditLogs.userId, lastActiveAt: max(auditLogs.timestamp), }) .from(auditLogs) .groupBy(auditLogs.userId) const presenceMap: Record = {} results.forEach(row => { if (row.userId) presenceMap[row.userId] = row.lastActiveAt! }) return presenceMap }) }