Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m10s
261 lines
8.3 KiB
TypeScript
261 lines
8.3 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 '../config/passport';
|
|
// 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 ---
|
|
|
|
/**
|
|
* @openapi
|
|
* /achievements:
|
|
* get:
|
|
* summary: Get all achievements
|
|
* description: Returns the master list of all available achievements in the system. This is a public endpoint.
|
|
* tags:
|
|
* - Achievements
|
|
* responses:
|
|
* 200:
|
|
* description: List of all achievements
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: array
|
|
* items:
|
|
* $ref: '#/components/schemas/Achievement'
|
|
*/
|
|
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);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /achievements/leaderboard:
|
|
* get:
|
|
* summary: Get leaderboard
|
|
* description: Returns the top users ranked by total points earned from achievements. This is a public endpoint.
|
|
* tags:
|
|
* - Achievements
|
|
* parameters:
|
|
* - in: query
|
|
* name: limit
|
|
* schema:
|
|
* type: integer
|
|
* minimum: 1
|
|
* maximum: 50
|
|
* default: 10
|
|
* description: Maximum number of users to return
|
|
* responses:
|
|
* 200:
|
|
* description: Leaderboard entries
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: array
|
|
* items:
|
|
* $ref: '#/components/schemas/LeaderboardUser'
|
|
*/
|
|
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 ---
|
|
|
|
/**
|
|
* @openapi
|
|
* /achievements/me:
|
|
* get:
|
|
* summary: Get my achievements
|
|
* description: Returns all achievements earned by the authenticated user.
|
|
* tags:
|
|
* - Achievements
|
|
* security:
|
|
* - bearerAuth: []
|
|
* responses:
|
|
* 200:
|
|
* description: List of user's earned achievements
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: array
|
|
* items:
|
|
* $ref: '#/components/schemas/UserAchievement'
|
|
* 401:
|
|
* description: Unauthorized - JWT token missing or invalid
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* $ref: '#/components/schemas/ErrorResponse'
|
|
*/
|
|
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);
|
|
|
|
/**
|
|
* @openapi
|
|
* /achievements/award:
|
|
* post:
|
|
* summary: Award achievement to user (Admin only)
|
|
* description: Manually award an achievement to a specific user. Requires admin role.
|
|
* tags:
|
|
* - Achievements
|
|
* - Admin
|
|
* security:
|
|
* - bearerAuth: []
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - userId
|
|
* - achievementName
|
|
* properties:
|
|
* userId:
|
|
* type: string
|
|
* format: uuid
|
|
* description: The user ID to award the achievement to
|
|
* achievementName:
|
|
* type: string
|
|
* description: The name of the achievement to award
|
|
* example: First-Upload
|
|
* responses:
|
|
* 200:
|
|
* description: Achievement awarded successfully
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Successfully awarded 'First-Upload' to user abc123.
|
|
* 401:
|
|
* description: Unauthorized - JWT token missing or invalid
|
|
* 403:
|
|
* description: Forbidden - User is not an admin
|
|
*/
|
|
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;
|