Files
flyer-crawler.projectium.com/src/routes/gamification.routes.ts
Torben Sorensen 3912139273
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m24s
adr-028 and int tests
2026-01-09 12:47:41 -08:00

137 lines
4.6 KiB
TypeScript

// src/routes/gamification.routes.ts
import express, { NextFunction } from 'express';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { z } from 'zod';
import passport, { isAdmin } from './passport.routes'; // Correctly imported
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { gamificationService } from '../services/gamificationService';
// Removed: import { logger } from '../services/logger.server';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { UserProfile } from '../types';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { validateRequest } from '../middleware/validation.middleware';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { requiredString, optionalNumeric } from '../utils/zodUtils';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { publicReadLimiter, userReadLimiter, adminTriggerLimiter } from '../config/rateLimiters';
import { sendSuccess } from '../utils/apiResponse';
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('/', publicReadLimiter, async (req, res, next: NextFunction) => {
try {
const achievements = await gamificationService.getAllAchievements(req.log);
sendSuccess(res, achievements);
} catch (error) {
req.log.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',
publicReadLimiter,
validateRequest(leaderboardSchema),
async (req, res, next: NextFunction): Promise<void> => {
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);
sendSuccess(res, leaderboard);
} catch (error) {
req.log.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 }),
userReadLimiter,
async (req, res, next: NextFunction): Promise<void> => {
const userProfile = req.user as UserProfile;
try {
const userAchievements = await gamificationService.getUserAchievements(
userProfile.user.user_id,
req.log,
);
sendSuccess(res, userAchievements);
} catch (error) {
req.log.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',
adminTriggerLimiter,
validateRequest(awardAchievementSchema),
async (req, res, next: NextFunction): Promise<void> => {
// Infer type and cast request object as per ADR-003
type AwardAchievementRequest = z.infer<typeof awardAchievementSchema>;
const { body } = req as unknown as AwardAchievementRequest;
try {
await gamificationService.awardAchievement(body.userId, body.achievementName, req.log);
sendSuccess(res, {
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;