// src/routes/gamification.routes.ts import express, { NextFunction } from 'express'; import { z } from 'zod'; import passport, { isAdmin } from './passport.routes'; // Correctly imported import { gamificationService } from '../services/gamificationService'; import { logger } from '../services/logger.server'; import { UserProfile } from '../types'; import { validateRequest } from '../middleware/validation.middleware'; import { requiredString, optionalNumeric } from '../utils/zodUtils'; const router = express.Router(); const adminGamificationRouter = express.Router(); // Create a new router for admin-only routes. // --- Zod Schemas for Gamification Routes (as per ADR-003) --- const leaderboardQuerySchema = z.object({ limit: optionalNumeric({ default: 10, integer: true, positive: true, max: 50 }), }); const leaderboardSchema = z.object({ query: leaderboardQuerySchema, }); const awardAchievementSchema = z.object({ body: z.object({ userId: requiredString('userId is required.'), achievementName: requiredString('achievementName is required.'), }), }); // --- Public Routes --- /** * GET /api/achievements - Get the master list of all available achievements. * This is a public endpoint. */ router.get('/', async (req, res, next: NextFunction) => { try { const achievements = await gamificationService.getAllAchievements(req.log); res.json(achievements); } catch (error) { logger.error({ error }, 'Error fetching all achievements in /api/achievements:'); next(error); } }); /** * GET /api/achievements/leaderboard - Get the top users by points. * This is a public endpoint. */ router.get( '/leaderboard', validateRequest(leaderboardSchema), async (req, res, next: NextFunction): Promise => { try { // The `validateRequest` middleware ensures `req.query` is valid. // We parse it here to apply Zod's coercions (string to number) and defaults. const { limit } = leaderboardQuerySchema.parse(req.query); const leaderboard = await gamificationService.getLeaderboard(limit!, req.log); res.json(leaderboard); } catch (error) { logger.error({ error }, 'Error fetching leaderboard:'); next(error); } }, ); // --- Authenticated User Routes --- /** * GET /api/achievements/me - Get all achievements for the authenticated user. * This is a protected endpoint. */ router.get( '/me', passport.authenticate('jwt', { session: false }), async (req, res, next: NextFunction): Promise => { const userProfile = req.user as UserProfile; try { const userAchievements = await gamificationService.getUserAchievements( userProfile.user.user_id, req.log, ); res.json(userAchievements); } catch (error) { logger.error( { error, userId: userProfile.user.user_id }, 'Error fetching user achievements:', ); next(error); } }, ); // --- Admin-Only Routes --- // Apply authentication and admin-check middleware to the entire admin sub-router. adminGamificationRouter.use(passport.authenticate('jwt', { session: false }), isAdmin); /** * POST /api/achievements/award - Manually award an achievement to a user. * This is an admin-only endpoint. */ adminGamificationRouter.post( '/award', validateRequest(awardAchievementSchema), async (req, res, next: NextFunction): Promise => { // Infer type and cast request object as per ADR-003 type AwardAchievementRequest = z.infer; const { body } = req as unknown as AwardAchievementRequest; try { await gamificationService.awardAchievement(body.userId, body.achievementName, req.log); res .status(200) .json({ message: `Successfully awarded '${body.achievementName}' to user ${body.userId}.`, }); } catch (error) { next(error); } }, ); // Mount the admin sub-router onto the main gamification router. router.use(adminGamificationRouter); export default router;