Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 40 additions & 34 deletions backend/src/milestone/controllers/milestone.controller.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,83 @@
import {
Body,
Controller,
Get,
Post,
Param,
HttpCode,
HttpStatus,
Body,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { CurrentUser } from '../../auth/decorators/current-user.decorator';
import { APIKeyGuard } from '../../api-key/api-key.guard';
import type { MilestoneService } from '../services/milestone.service';

/**
* Identity rules (issue #482):
* - The `/my` routes resolve the user id from the verified JWT principal
* (`req.user.id` via the JwtStrategy validate() hook) — never from a
* placeholder or any client-supplied value. Unauthenticated calls are
* rejected with 401.
* - The `/trigger/*` routes are server-to-server endpoints that mint
* milestone awards for the user named in the body, so they sit behind
* the API key guard (`x-api-key` header) which a browser user cannot
* satisfy. They intentionally do NOT accept JWT auth.
*/
@Controller('milestones')
export class MilestoneController {
constructor(private readonly milestoneService: MilestoneService) {}

@Get('my')
async getMyMilestones() {
// In a real app, you'd get userId from JWT token
const req = { user: { id: 'user-id-placeholder' } }; // Mock request object
const userId = req.user.id;

@UseGuards(JwtAuthGuard)
async getMyMilestones(@CurrentUser('id') userId: string) {
return this.milestoneService.getUserMilestones(userId);
}

@Get('my/stats')
async getMyMilestoneStats() {
// In a real app, you'd get userId from JWT token
const req = { user: { id: 'user-id-placeholder' } }; // Mock request object
const userId = req.user.id;

@UseGuards(JwtAuthGuard)
async getMyMilestoneStats(@CurrentUser('id') userId: string) {
return this.milestoneService.getUserMilestoneStats(userId);
}

@Get('my/next')
async getMyNextMilestones() {
// In a real app, you'd get userId from JWT token
const req = { user: { id: 'user-id-placeholder' } }; // Mock request object
const userId = req.user.id;

@UseGuards(JwtAuthGuard)
async getMyNextMilestones(@CurrentUser('id') userId: string) {
return this.milestoneService.getNextMilestones(userId);
}

/**
* Marks a milestone as viewed for the *calling* user only: the update is
* scoped by `{ id, userId }` in the assignment service, so one user can
* never mutate another user's unread state.
*/
@Post('my/:milestoneId/view')
@HttpCode(HttpStatus.OK)
async markMilestoneAsViewed(@Param('milestoneId') milestoneId: string) {
// In a real app, you'd get userId from JWT token
const req = { user: { id: 'user-id-placeholder' } }; // Mock request object
const userId = req.user.id;

@UseGuards(JwtAuthGuard)
async markMilestoneAsViewed(
@CurrentUser('id') userId: string,
@Param('milestoneId') milestoneId: string,
) {
await this.milestoneService.markMilestoneAsViewed(userId, milestoneId);
return { success: true };
}

// Endpoint for external systems to trigger milestone checks
// Server-to-server endpoints for external systems to trigger milestone
// checks. Guarded by API key auth, not JWT (see class docblock).
@Post('trigger/puzzle-completed')
@HttpCode(HttpStatus.OK)
@UseGuards(APIKeyGuard)
async triggerPuzzleCompleted(
@Body() body: { userId: string; puzzleData?: any },
) {
return this.milestoneService.onPuzzleCompleted(
body.userId,
body.puzzleData,
);
return this.milestoneService.onPuzzleCompleted(body.userId, body.puzzleData);
}

@Post('trigger/streak-updated')
@HttpCode(HttpStatus.OK)
@UseGuards(APIKeyGuard)
async triggerStreakUpdated(
@Body()
body: {
userId: string;
currentStreak: number;
longestStreak: number;
},
@Body() body: { userId: string; currentStreak: number; longestStreak: number },
) {
return this.milestoneService.onStreakUpdated(
body.userId,
Expand Down
4 changes: 4 additions & 0 deletions backend/src/milestone/milestone.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import { MilestoneAssignmentService } from './services/milestone-assignment.serv
import { MilestoneService } from './services/milestone.service';
import { MilestoneController } from './controllers/milestone.controller';
import { UserMilestoneController } from './controllers/user-milestone.controller';
import { ApiKeyModule } from '../api-key/api-key.module';

@Module({
imports: [
TypeOrmModule.forFeature([MilestoneTemplate, UserMilestone, UserProgress]),
// Provides ApiKeyService for the APIKeyGuard that protects the
// server-to-server `/milestones/trigger/*` routes (issue #482).
ApiKeyModule,
],
controllers: [MilestoneController, UserMilestoneController],
providers: [
Expand Down
Loading