diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c556c9..0db5929 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/backend/http/endpoint.http b/backend/http/endpoint.http index 9a39481..0b124a0 100644 --- a/backend/http/endpoint.http +++ b/backend/http/endpoint.http @@ -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", diff --git a/backend/src/auth/providers/auth.service.ts b/backend/src/auth/providers/auth.service.ts index 165eae7..cce6f6d 100644 --- a/backend/src/auth/providers/auth.service.ts +++ b/backend/src/auth/providers/auth.service.ts @@ -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 { diff --git a/backend/src/quests/controllers/daily-quest.controller.ts b/backend/src/quests/controllers/daily-quest.controller.ts index b070058..359db93 100644 --- a/backend/src/quests/controllers/daily-quest.controller.ts +++ b/backend/src/quests/controllers/daily-quest.controller.ts @@ -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'; @@ -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 { + if (!userId) { + throw new UnauthorizedException('User ID not found in token'); + } + return this.dailyQuestService.getTodaysDailyQuestStatus(userId); + } } diff --git a/backend/src/quests/dtos/daily-quest-status.dto.ts b/backend/src/quests/dtos/daily-quest-status.dto.ts new file mode 100644 index 0000000..bd50604 --- /dev/null +++ b/backend/src/quests/dtos/daily-quest-status.dto.ts @@ -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; +} diff --git a/backend/src/quests/providers/daily-quest.service.ts b/backend/src/quests/providers/daily-quest.service.ts index 37b383b..a37c6b8 100644 --- a/backend/src/quests/providers/daily-quest.service.ts +++ b/backend/src/quests/providers/daily-quest.service.ts @@ -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 { return this.getTodaysDailyQuestProvider.execute(userId); } + + /** + * Returns the status of today's Daily Quest (read-only, lightweight) + */ + async getTodaysDailyQuestStatus( + userId: string, + ): Promise { + return this.getTodaysDailyQuestStatusProvider.execute(userId); + } } diff --git a/backend/src/quests/providers/getTodaysDailyQuestStatus.provider.ts b/backend/src/quests/providers/getTodaysDailyQuestStatus.provider.ts new file mode 100644 index 0000000..773ebde --- /dev/null +++ b/backend/src/quests/providers/getTodaysDailyQuestStatus.provider.ts @@ -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, + 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 { + 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, + }; + } +} diff --git a/backend/src/quests/quests.module.ts b/backend/src/quests/quests.module.ts index 8d4a2da..8572299 100644 --- a/backend/src/quests/quests.module.ts +++ b/backend/src/quests/quests.module.ts @@ -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'; @@ -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 {}