/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { FastifyPluginAsync } from "fastify"; import { User, AuditAction, AuditCategory } from "@library/shared-types"; import { UserService } from "../../services/user.service"; import { AuditService } from "../../services/audit.service"; import { adminGuard } from "../../middleware/admin-guard"; interface UpdateUserSettingsBody { slug?: string; displayName?: string; bio?: string; profilePublic?: boolean; } interface UserProfileResponse { id: string; username: string; displayName?: string; avatar?: string; bio?: string; slug?: string; badges: { isStaff: boolean; isMod: boolean; isVip: boolean; inDiscord: boolean; }; stats: { suggestionsCount: number; suggestionsAcceptedCount: number; likesCount: number; commentsCount: number; }; createdAt: Date; } const usersRoutes: FastifyPluginAsync = async (app) => { const userService = new UserService(); app.get<{ Reply: User[] }>( "/", { preValidation: [app.authenticate, adminGuard], }, async () => { return userService.getAllUsers(); } ); app.get<{ Reply: User }>( "/me", { preValidation: [app.authenticate], }, async (request) => { const currentUser = request.user as { id: string }; const user = await userService.getUserById(currentUser.id); if (!user) { throw new Error("User not found"); } return user; } ); app.put<{ Body: UpdateUserSettingsBody; Reply: User | { error: string } }>( "/me", { preValidation: [app.authenticate], preHandler: [app.csrfProtection], }, async (request, reply) => { const currentUser = request.user as { id: string }; const updates = request.body; // If slug is being updated, check if it's unique if (updates.slug) { const existingUser = await userService.getUserBySlug(updates.slug); if (existingUser && existingUser.id !== currentUser.id) { return reply.code(400).send({ error: "Slug already taken" }); } } const updatedUser = await userService.updateUserSettings( currentUser.id, updates ); if (!updatedUser) { return reply.code(404).send({ error: "User not found" }); } return updatedUser; } ); app.get<{ Params: { identifier: string }; Reply: UserProfileResponse | { error: string }; }>( "/profile/:identifier", async (request, reply) => { const { identifier } = request.params; try { const profile = await userService.getUserProfile(identifier); if (!profile) { return reply.code(404).send({ error: "User not found" }); } if (!profile.profilePublic) { // Check if the requesting user is viewing their own profile const currentUser = request.user as { id: string } | undefined; if (!currentUser || currentUser.id !== profile.id) { return reply .code(403) .send({ error: "This profile is private" }); } } return { id: profile.id, username: profile.username, displayName: profile.displayName, avatar: profile.avatar, bio: profile.bio, slug: profile.slug, badges: { isStaff: profile.isStaff, isMod: profile.isMod, isVip: profile.isVip, inDiscord: profile.inDiscord, }, stats: profile.stats, createdAt: profile.createdAt, }; } catch (error) { console.error("Error fetching profile:", error); return reply.code(500).send({ error: "Failed to fetch profile" }); } } ); app.get<{ Params: { id: string }; Reply: User | null }>( "/:id", { preValidation: [app.authenticate, adminGuard], }, async (request) => { const { id } = request.params; return userService.getUserById(id); } ); app.post<{ Params: { id: string }; Reply: User | { error: string } }>( "/:id/ban", { preValidation: [app.authenticate, adminGuard], preHandler: [app.csrfProtection], }, async (request, reply) => { const { id } = request.params; const currentUser = request.user as { id: string }; if (currentUser.id === id) { return reply.code(400).send({ error: "You cannot ban yourself" }); } const user = await userService.banUser(id); if (!user) { return reply.code(404).send({ error: "User not found" }); } await AuditService.logFromRequest(request, { action: AuditAction.userBan, category: AuditCategory.admin, targetUserId: id, details: `Banned user: ${user.username}`, }); return user; } ); app.post<{ Params: { id: string }; Reply: User | { error: string } }>( "/:id/unban", { preValidation: [app.authenticate, adminGuard], preHandler: [app.csrfProtection], }, async (request, reply) => { const { id } = request.params; const user = await userService.unbanUser(id); if (!user) { return reply.code(404).send({ error: "User not found" }); } await AuditService.logFromRequest(request, { action: AuditAction.userUnban, category: AuditCategory.admin, targetUserId: id, details: `Unbanned user: ${user.username}`, }); return user; } ); }; export default usersRoutes;