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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ npm --workspace frontend run lint
npm --workspace backend run lint

npm --workspace frontend exec -- tsc --noEmit -p tsconfig.json
npm --workspace backend exec -- tsc --noEmit -p tsconfig.json.
npm --workspace backend exec -- tsc --noEmit -p tsconfig.json
```

## Branch Protection
Expand Down
3 changes: 2 additions & 1 deletion backend/http/endpoint.http
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
POST http://localhost:3000/users
Content-Type: application/json
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiYW1pbnVmYXRpbWFAZ21haWwuY29tIiwiaWF0IjoxNzY5MzI0Mjk0LCJleHAiOjE3NjkzMjc4OTQsImF1ZCI6ImxvY2FsaG9zdDozMDAwIiwiaXNzIjoibG9jYWxob3N0OjMwMDAifQ.vqjgnN33AMD0j1wxX6e6912PDB2VMW23eVJUQYBZRAA
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiYW1pbnVmYXRpbWFAZ21haWwuY29tIiwiaWF0IjoxNzY5MzI0Mjk0LCJleHAiOjE3NjkzMjc4OTQsImF1ZCI6ImxvY2FsaG9zdDozMDAwIiwiaXNzIjoibG9jYWxob3N0OjMwMDAifQ.vqjgnN33AMD0j1wxX6e6912PDB2VMW23eVJUQYBZRAA

{
"username": "Fatee",
"fullname": "Fatima Aminu",
Expand Down
12 changes: 6 additions & 6 deletions backend/src/auth/providers/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ 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;
}
// interface OAuthUser {
// email: string;
// username: string;
// picture: string;
// accessToken: string;
// }

@Injectable()
export class AuthService {
Expand Down
27 changes: 27 additions & 0 deletions backend/src/quests/controllers/daily-quest.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { DailyQuestService } from '../providers/daily-quest.service';
import { DailyQuestResponseDto } from '../dtos/daily-quest-response.dto';
import { DailyQuestStatusDto } from '../dtos/daily-quest-status.dto';
import { ActiveUser } from '../../auth/decorators/activeUser.decorator';
import { Auth } from '../../auth/decorators/auth.decorator';
import { authType } from '../../auth/enum/auth-type.enum';
Expand Down Expand Up @@ -49,4 +50,30 @@ export class DailyQuestController {
}
return this.dailyQuestService.getTodaysDailyQuest(userId);
}

@Get('status')
@Auth(authType.Bearer)
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: "Get today's daily quest progress status",
description:
"Returns the current progress state of today's Daily Quest. This is a lightweight, read-only endpoint suitable for dashboard polling and UI consumption. If no quest exists yet, one is automatically generated.",
})
@ApiResponse({
status: 200,
description: 'Daily quest status retrieved successfully',
type: DailyQuestStatusDto,
})
@ApiResponse({
status: 401,
description: 'Unauthorized - valid authentication required',
})
async getTodaysDailyQuestStatus(
@ActiveUser('sub') userId: string,
): Promise<DailyQuestStatusDto> {
if (!userId) {
throw new UnauthorizedException('User ID not found in token');
}
return this.dailyQuestService.getTodaysDailyQuestStatus(userId);
}
}
25 changes: 25 additions & 0 deletions backend/src/quests/dtos/daily-quest-status.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';

/**
* Response DTO for the Daily Quest status endpoint.
* Returns only essential progress information for dashboard/UI consumption.
*/
export class DailyQuestStatusDto {
@ApiProperty({
description: "Total number of questions in today's daily quest",
example: 5,
})
totalQuestions: number;

@ApiProperty({
description: 'Number of questions completed so far (0-5)',
example: 2,
})
completedQuestions: number;

@ApiProperty({
description: 'Whether the entire daily quest has been completed',
example: false,
})
isCompleted: boolean;
}
12 changes: 12 additions & 0 deletions backend/src/quests/providers/daily-quest.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { Injectable } from '@nestjs/common';
import { DailyQuestResponseDto } from '../dtos/daily-quest-response.dto';
import { DailyQuestStatusDto } from '../dtos/daily-quest-status.dto';
import { GetTodaysDailyQuestProvider } from './getTodaysDailyQuest.provider';
import { GetTodaysDailyQuestStatusProvider } from './getTodaysDailyQuestStatus.provider';

@Injectable()
export class DailyQuestService {
constructor(
private readonly getTodaysDailyQuestProvider: GetTodaysDailyQuestProvider,
private readonly getTodaysDailyQuestStatusProvider: GetTodaysDailyQuestStatusProvider,
) {}

async getTodaysDailyQuest(userId: string): Promise<DailyQuestResponseDto> {
return this.getTodaysDailyQuestProvider.execute(userId);
}

/**
* Returns the status of today's Daily Quest (read-only, lightweight)
*/
async getTodaysDailyQuestStatus(
userId: string,
): Promise<DailyQuestStatusDto> {
return this.getTodaysDailyQuestStatusProvider.execute(userId);
}
}
87 changes: 87 additions & 0 deletions backend/src/quests/providers/getTodaysDailyQuestStatus.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DailyQuest } from '../entities/daily-quest.entity';
import { DailyQuestStatusDto } from '../dtos/daily-quest-status.dto';
import { GetTodaysDailyQuestProvider } from './getTodaysDailyQuest.provider';

/**
* Provider for fetching the status of today's Daily Quest.
* Returns minimal data (totalQuestions, completedQuestions, isCompleted) for fast, cache-friendly lookups.
*
* This is read-only and does not mutate state.
* If no quest exists, it auto-generates one using the existing generation logic.
*/
@Injectable()
export class GetTodaysDailyQuestStatusProvider {
private readonly logger = new Logger(GetTodaysDailyQuestStatusProvider.name);

constructor(
@InjectRepository(DailyQuest)
private readonly dailyQuestRepository: Repository<DailyQuest>,
private readonly getTodaysDailyQuestProvider: GetTodaysDailyQuestProvider,
) {}

/**
* Fetches the status of today's Daily Quest.
* Auto-generates a quest if one doesn't exist.
*
* @param userId - The user's ID
* @returns DailyQuestStatusDto with totalQuestions, completedQuestions, isCompleted
*/
async execute(userId: string): Promise<DailyQuestStatusDto> {
const todayDate = this.getTodayDateString();
this.logger.log(
`Fetching daily quest status for user ${userId} on ${todayDate}`,
);

// Try to find existing quest for today
let dailyQuest = await this.dailyQuestRepository.findOne({
where: { userId, questDate: todayDate },
select: ['id', 'totalQuestions', 'completedQuestions', 'isCompleted'],
});

// If no quest exists, auto-generate one
if (!dailyQuest) {
this.logger.log(
`No quest found for user ${userId}, auto-generating quest`,
);
// Use the existing provider to generate the full quest
// This ensures consistency with the main getTodaysDailyQuest endpoint
// const fullQuest = await this.getTodaysDailyQuestProvider.execute(userId);

// Fetch the newly created quest with status fields
dailyQuest = await this.dailyQuestRepository.findOne({
where: { userId, questDate: todayDate },
select: ['id', 'totalQuestions', 'completedQuestions', 'isCompleted'],
});

if (!dailyQuest) {
throw new Error(
`Failed to retrieve created daily quest for user ${userId}`,
);
}
}

return this.buildStatusResponse(dailyQuest);
}

/**
* Returns today's date as YYYY-MM-DD string (timezone-safe)
*/
private getTodayDateString(): string {
const now = new Date();
return now.toISOString().split('T')[0];
}

/**
* Converts DailyQuest entity to DailyQuestStatusDto
*/
private buildStatusResponse(dailyQuest: DailyQuest): DailyQuestStatusDto {
return {
totalQuestions: dailyQuest.totalQuestions,
completedQuestions: dailyQuest.completedQuestions,
isCompleted: dailyQuest.isCompleted,
};
}
}
7 changes: 6 additions & 1 deletion backend/src/quests/quests.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DailyQuestPuzzle } from './entities/daily-quest-puzzle.entity';
import { DailyQuestController } from './controllers/daily-quest.controller';
import { DailyQuestService } from './providers/daily-quest.service';
import { GetTodaysDailyQuestProvider } from './providers/getTodaysDailyQuest.provider';
import { GetTodaysDailyQuestStatusProvider } from './providers/getTodaysDailyQuestStatus.provider';
import { PuzzlesModule } from '../puzzles/puzzles.module';
import { ProgressModule } from '../progress/progress.module';
import { UsersModule } from '../users/users.module';
Expand All @@ -17,7 +18,11 @@ import { UsersModule } from '../users/users.module';
UsersModule,
],
controllers: [DailyQuestController],
providers: [DailyQuestService, GetTodaysDailyQuestProvider],
providers: [
DailyQuestService,
GetTodaysDailyQuestProvider,
GetTodaysDailyQuestStatusProvider,
],
exports: [TypeOrmModule, DailyQuestService],
})
export class QuestsModule {}