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
13 changes: 13 additions & 0 deletions backend/http/endpoint.http
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsI
"password": "FatimaAminu@123"
}

GET http://localhost:3000/users


POST http://localhost:3000/auth/signIn
Content-Type: application/json
Expand All @@ -17,6 +19,16 @@ Content-Type: application/json
"password": "FatimaAminu@123"
}

### Submit puzzle answer (earns XP)
POST http://localhost:3000/progress/submit
Content-Type: application/json

{
"userId": "1",
"puzzleId": "dcebf04a-542f-464d-92e4-3ad983cde9ba",
"answer": "42"
}

POST http://localhost:3000/auth/signIn
Content-Type: application/json
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiYW1pbnVmYXRpbWFAZ21haWwuY29tIiwiaWF0IjoxNzY5MzI0Mjk0LCJleHAiOjE3NjkzMjc4OTQsImF1ZCI6ImxvY2FsaG9zdDozMDAwIiwiaXNzIjoibG9jYWxob3N0OjMwMDAifQ.vqjgnN33AMD0j1wxX6e6912PDB2VMW23eVJUQYBZRAA
Expand Down Expand Up @@ -47,6 +59,7 @@ GET http://localhost:3000/puzzles?page=1&limit=10
GET http://localhost:3000/puzzles?categoryId=397b1a88-3a41-4c14-afee-20f57554368b&difficulty=INTERMEDIATE&page=1&limit=10


GET http://localhost:3000/users/1/xp-level


GET http://localhost:3000/users?limit=10&page=1
Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/nodemailer": "^7.0.9",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
Expand Down
1 change: 0 additions & 1 deletion backend/src/auth/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ export class AuthController {
public checkNonceStatus(@Query('nonce') nonce: string) {
return this.authservice.checkNonceStatus(nonce);
}

@Post('/forgot-password')
@Throttle({ default: { limit: 3, ttl: 60000 } }) // 3 requests per minute
@HttpCode(HttpStatus.OK)
Expand Down
8 changes: 0 additions & 8 deletions backend/src/auth/providers/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@ import { ResetPasswordProvider } from './reset-password.provider';
import { ForgotPasswordDto } from '../dtos/forgot-password.dto';
import { ResetPasswordDto } from '../dtos/reset-password.dto';

// interface OAuthUser {
// email: string;
// username: string;
// picture: string;
// accessToken: string;
// }

@Injectable()
export class AuthService {
private nonces = new Map<
Expand Down Expand Up @@ -165,7 +158,6 @@ export class AuthService {
public refreshToken(refreshTokenDto: RefreshTokenDto) {
return this.refreshTokensProvider.refreshTokens(refreshTokenDto);
}

public async forgotPassword(forgotPasswordDto: ForgotPasswordDto) {
return await this.forgotPasswordProvider.forgotPassword(forgotPasswordDto);
}
Expand Down
31 changes: 18 additions & 13 deletions backend/src/auth/providers/generate-tokens.provider.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ttouching this is dangerous

Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable prettier/prettier */
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { JwtService } from '@nestjs/jwt';
Expand Down Expand Up @@ -40,7 +39,12 @@ export class GenerateTokensProvider {
* @returns A signed JWT token
*/
@ApiOperation({ summary: 'Sign JWT Token' })
public async signToken<T>(userId: string, username: string, expiresIn: number, payload?: T) {
public async signToken<T>(
userId: string,
username: string,
expiresIn: number,
payload?: T,
) {
return await this.jwtService.signAsync(
{
sub: userId,
Expand All @@ -62,17 +66,18 @@ export class GenerateTokensProvider {
* @returns An object containing access and refresh tokens
*/
@ApiOperation({ summary: 'Generate Access and Refresh Tokens' })
public async generateTokens(user: User) {
if (!user.username) {
throw new Error('user not found');
}
public async generateTokens(user: User) {
if (!user.username) {
throw new Error('user not found');
}

const [accessToken, refreshToken] = await Promise.all([
this.signToken(user.id, user.username, this.jwtConfiguration.ttl, { email: user.email }),
this.signToken(user.id, user.username, this.jwtConfiguration.ttl)
]);
const [accessToken, refreshToken] = await Promise.all([
this.signToken(user.id, user.username, this.jwtConfiguration.ttl, {
email: user.email,
}),
this.signToken(user.id, user.username, this.jwtConfiguration.ttl),
]);

return { accessToken, refreshToken, user };
return { accessToken, refreshToken, user };
}
}

}
1 change: 0 additions & 1 deletion backend/src/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigType } from '@nestjs/config';
import jwtConfig from '../authConfig/jwt.config';

interface JwtPayload {
sub: string;
email: string;
Expand Down
2 changes: 1 addition & 1 deletion backend/src/main.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless your issue explicitly tells you to, avoid this app too

Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ async function bootstrap() {

await app.listen(3000);
}
bootstrap();
void bootstrap();
7 changes: 6 additions & 1 deletion backend/src/progress/controllers/progress.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
UseGuards,
BadRequestException,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { paginationQueryDto } from '../../common/pagination/paginationQueryDto';
import { GetProgressHistoryProvider } from '../providers/get-progress-history.provider';
Expand Down
17 changes: 17 additions & 0 deletions backend/src/progress/progress.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Controller, Post, Body } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ProgressService } from './progress.service';
import { SubmitAnswerDto } from './dtos/submit-answer.dto';

@Controller('progress')
@ApiTags('progress')
export class ProgressController {
constructor(private readonly progressService: ProgressService) {}

@Post('submit')
@ApiOperation({ summary: 'Submit a puzzle answer' })
@ApiResponse({ status: 200, description: 'Answer processed successfully' })
async submitAnswer(@Body() submitAnswerDto: SubmitAnswerDto) {
return this.progressService.submitAnswer(submitAnswerDto);
}
}
7 changes: 4 additions & 3 deletions backend/src/progress/progress.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { GetCategoryStatsProvider } from './providers/get-category-stats.provide
import { GetOverallStatsProvider } from './providers/get-overall-stats.provider';
import { ProgressCalculationProvider } from './providers/progress-calculation.provider';
import { Puzzle } from '../puzzles/entities/puzzle.entity';

import { XpLevelService } from '../users/providers/xp-level.service';

@Module({
imports: [
Expand All @@ -24,7 +24,8 @@ import { Puzzle } from '../puzzles/entities/puzzle.entity';
GetCategoryStatsProvider,
GetOverallStatsProvider,
ProgressCalculationProvider,
XpLevelService,
],
exports: [ProgressService, ProgressCalculationProvider, TypeOrmModule],
exports: [ProgressService, ProgressCalculationProvider],
})
export class ProgressModule {}
export class ProgressModule {}
24 changes: 19 additions & 5 deletions backend/src/progress/providers/get-category-stats.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserProgress } from '../entities/progress.entity';
import { CategoryStatsDto } from '../dtos/category-stats.dto';

interface CategoryStatsRaw {
categoryId: string;
totalAttempts: string;
correctAnswers: string;
}

interface CategoryNameRaw {
category_name: string;
}

@Injectable()
export class GetCategoryStatsProvider {
Expand All @@ -16,11 +25,14 @@ export class GetCategoryStatsProvider {
.createQueryBuilder('progress')
.select('progress.categoryId', 'categoryId')
.addSelect('COUNT(*)', 'totalAttempts')
.addSelect('SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)', 'correctAnswers')
.addSelect(
'SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)',
'correctAnswers',
)
.where('progress.userId = :userId', { userId })
.andWhere('progress.categoryId = :categoryId', { categoryId })
.groupBy('progress.categoryId')
.getRawOne();
.getRawOne<CategoryStatsRaw>();

if (!result) {
return {
Expand All @@ -35,7 +47,9 @@ export class GetCategoryStatsProvider {
const totalAttempts = parseInt(result.totalAttempts, 10) || 0;
const correctAnswers = parseInt(result.correctAnswers, 10) || 0;
const accuracy =
totalAttempts > 0 ? Math.round((correctAnswers / totalAttempts) * 100) : 0;
totalAttempts > 0
? Math.round((correctAnswers / totalAttempts) * 100)
: 0;

// Get category name
const category = await this.progressRepo
Expand All @@ -44,7 +58,7 @@ export class GetCategoryStatsProvider {
.where('progress.categoryId = :categoryId', { categoryId })
.select('category.name')
.limit(1)
.getRawOne();
.getRawOne<CategoryNameRaw>();

return {
categoryId,
Expand Down
18 changes: 13 additions & 5 deletions backend/src/progress/providers/get-overall-stats.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import { Repository } from 'typeorm';
import { UserProgress } from '../entities/progress.entity';
import { OverallStatsDto } from '../dtos/overall-stats.dto';

interface OverallStatsRaw {
totalAttempts: string;
totalCorrect: string;
totalPointsEarned: string;
totalTimeSpent: string;
}

@Injectable()
export class GetOverallStatsProvider {
constructor(
Expand All @@ -15,11 +22,14 @@ export class GetOverallStatsProvider {
const result = await this.progressRepo
.createQueryBuilder('progress')
.select('COUNT(*)', 'totalAttempts')
.addSelect('SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)', 'totalCorrect')
.addSelect(
'SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)',
'totalCorrect',
)
.addSelect('SUM(progress.pointsEarned)', 'totalPointsEarned')
.addSelect('SUM(progress.timeSpent)', 'totalTimeSpent')
.where('progress.userId = :userId', { userId })
.getRawOne();
.getRawOne<OverallStatsRaw>();

if (!result) {
return {
Expand All @@ -34,9 +44,7 @@ export class GetOverallStatsProvider {
const totalAttempts = parseInt(result.totalAttempts, 10) || 0;
const totalCorrect = parseInt(result.totalCorrect, 10) || 0;
const accuracy =
totalAttempts > 0
? Math.round((totalCorrect / totalAttempts) * 100)
: 0;
totalAttempts > 0 ? Math.round((totalCorrect / totalAttempts) * 100) : 0;

return {
totalAttempts,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserProgress } from '../entities/progress.entity';
import { paginate } from '../../common/pagination/paginate';
import { ProgressHistoryDto } from '../dtos/progress-history.dto';

@Injectable()
export class GetProgressHistoryProvider {
Expand Down
16 changes: 16 additions & 0 deletions backend/src/progress/providers/progress-calculation.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MoreThan, Repository } from 'typeorm';
import { Puzzle } from '../../puzzles/entities/puzzle.entity';
import { UserProgress } from '../entities/progress.entity';
import { SubmitAnswerDto } from '../dtos/submit-answer.dto';
import { XpLevelService } from '../../users/providers/xp-level.service';
import { User } from '../../users/user.entity';
import { Streak } from '../../streak/entities/streak.entity';
import { DailyQuest } from '../../quests/entities/daily-quest.entity';
Expand Down Expand Up @@ -34,6 +35,7 @@ export class ProgressCalculationProvider {
private readonly puzzleRepository: Repository<Puzzle>,
@InjectRepository(UserProgress)
private readonly userProgressRepository: Repository<UserProgress>,
private readonly xpLevelService: XpLevelService,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(Streak)
Expand Down Expand Up @@ -229,6 +231,10 @@ export class ProgressCalculationProvider {
// Save to database
await this.userProgressRepository.save(userProgress);

if (validation.isCorrect && pointsEarned > 0) {
await this.xpLevelService.addXp(submitAnswerDto.userId, pointsEarned);
}

return {
userProgress,
validation,
Expand All @@ -252,6 +258,16 @@ export class ProgressCalculationProvider {
.andWhere('progress.categoryId = :categoryId', { categoryId })
.getRawOne<ProgressStatsRaw>();

if (!stats) {
return {
totalAttempts: 0,
correctAttempts: 0,
totalPoints: 0,
averageTimeSpent: 0,
accuracy: 0,
};
}

return {
totalAttempts: Number(stats?.totalAttempts) || 0,
correctAttempts: parseInt(stats?.correctAttempts || '0', 10),
Expand Down
2 changes: 1 addition & 1 deletion backend/src/puzzles/controllers/puzzles.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class PuzzlesController {
findAll(@Query() query: PuzzleQueryDto) {
return this.puzzlesService.findAll(query);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not ment to remove any endpoint


@ApiOperation({ summary: 'Get a puzzle by ID' })
@ApiResponse({
status: 200,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/quests/controllers/daily-quest.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,4 @@ export class DailyQuestController {
}
return this.dailyQuestService.completeDailyQuest(userId);
}
}
}
2 changes: 1 addition & 1 deletion backend/src/quests/dtos/complete-daily-quest.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ export class CompleteDailyQuestResponseDto {
example: '2026-01-28T15:30:00Z',
})
completedAt: Date;
}
}
11 changes: 4 additions & 7 deletions backend/src/quests/providers/complete-daily-quest.provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// backend/src/quests/providers/complete-daily-quest.provider.ts
import {
Injectable,
Logger,
Expand Down Expand Up @@ -67,9 +66,8 @@ export class CompleteDailyQuestProvider {
);

// Return existing completion data
const streak = await this.updateStreakProvider.getStreak(
userIdNumber,
);
const streak =
await this.updateStreakProvider.getStreak(userIdNumber);
return {
isAlreadyCompleted: true,
success: true,
Expand Down Expand Up @@ -147,8 +145,7 @@ export class CompleteDailyQuestProvider {
streakInfo: {
currentStreak: streak.currentStreak,
longestStreak: streak.longestStreak,
lastActivityDate:
streak.lastActivityDate || this.getTodayDateString(),
lastActivityDate: streak.lastActivityDate || this.getTodayDateString(),
},
completedAt: transactionResult.completedAt,
};
Expand All @@ -158,4 +155,4 @@ export class CompleteDailyQuestProvider {
const now = new Date();
return now.toISOString().split('T')[0];
}
}
}
Loading