138 lines
4.3 KiB
TypeScript
138 lines
4.3 KiB
TypeScript
// src/routes/gamification.routes.ts
|
|
import express, { NextFunction } from 'express';
|
|
import { z } from 'zod';
|
|
import passport, { isAdmin } from './passport.routes';
|
|
import { gamificationRepo } from '../services/db/index.db';
|
|
import { logger } from '../services/logger.server';
|
|
import { UserProfile } from '../types';
|
|
import { ForeignKeyConstraintError } from '../services/db/errors.db';
|
|
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 leaderboardSchema = z.object({
|
|
query: z.object({
|
|
limit: optionalNumeric({ default: 10, integer: true, positive: true, max: 50 }),
|
|
}),
|
|
});
|
|
|
|
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 gamificationRepo.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<void> => {
|
|
// Apply ADR-003 pattern for type safety.
|
|
// Explicitly coerce query params to ensure numbers are passed to the repo,
|
|
// as validateRequest might not replace req.query in all test environments.
|
|
const query = req.query as unknown as { limit?: string };
|
|
const limit = query.limit ? Number(query.limit) : 10;
|
|
|
|
try {
|
|
const leaderboard = await gamificationRepo.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<void> => {
|
|
const userProfile = req.user as UserProfile;
|
|
try {
|
|
const userAchievements = await gamificationRepo.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<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 gamificationRepo.awardAchievement(body.userId, body.achievementName, req.log);
|
|
res
|
|
.status(200)
|
|
.json({
|
|
message: `Successfully awarded '${body.achievementName}' to user ${body.userId}.`,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ForeignKeyConstraintError) {
|
|
res.status(400).json({ message: error.message });
|
|
return;
|
|
}
|
|
logger.error(
|
|
{ error, userId: body.userId, achievementName: body.achievementName },
|
|
'Error awarding achievement via admin endpoint:',
|
|
);
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
// Mount the admin sub-router onto the main gamification router.
|
|
router.use(adminGamificationRouter);
|
|
|
|
export default router;
|