diff --git a/src/api/copilot/copilot-opportunity.controller.ts b/src/api/copilot/copilot-opportunity.controller.ts index ca6c849..9c154f5 100644 --- a/src/api/copilot/copilot-opportunity.controller.ts +++ b/src/api/copilot/copilot-opportunity.controller.ts @@ -84,7 +84,7 @@ export class CopilotOpportunityController { @Req() req: Request, @Res({ passthrough: true }) res: Response, @Query() query: ListOpportunitiesQueryDto, - @CurrentUser() user: JwtUser, + @CurrentUser() user: JwtUser | undefined, ): Promise { const result = await this.service.listOpportunities(query, user); @@ -131,7 +131,7 @@ export class CopilotOpportunityController { @ApiResponse({ status: 404, description: 'Not found' }) async getOpportunity( @Param('id') id: string, - @CurrentUser() user: JwtUser, + @CurrentUser() user: JwtUser | undefined, ): Promise { return this.service.getOpportunity(id, user); } diff --git a/src/api/copilot/copilot-opportunity.service.ts b/src/api/copilot/copilot-opportunity.service.ts index 3d8c7ba..bfcbe96 100644 --- a/src/api/copilot/copilot-opportunity.service.ts +++ b/src/api/copilot/copilot-opportunity.service.ts @@ -85,12 +85,12 @@ export class CopilotOpportunityService { * Admin/manager responses also include minimal project metadata for v5 compatibility. * * @param query Pagination, sort, and noGrouping parameters. - * @param user Authenticated JWT user. + * @param user Authenticated JWT user, or undefined for anonymous `@Public()` callers. * @returns Paginated opportunity response payload. */ async listOpportunities( query: ListOpportunitiesQueryDto, - user: JwtUser, + user: JwtUser | undefined, ): Promise { // TODO [SECURITY]: No permission check is applied here; this is intentional for authenticated browsing and should remain explicitly documented. const [sortField, sortDirection] = parseSortExpression( @@ -160,14 +160,14 @@ export class CopilotOpportunityService { * Admin/manager responses also include minimal project metadata for v5 compatibility. * * @param opportunityId Opportunity id path value. - * @param user Authenticated JWT user. + * @param user Authenticated JWT user, or undefined for anonymous `@Public()` callers. * @returns One formatted opportunity response. * @throws BadRequestException If id is non-numeric. * @throws NotFoundException If opportunity does not exist. */ async getOpportunity( opportunityId: string, - user: JwtUser, + user: JwtUser | undefined, ): Promise { // TODO [SECURITY]: No permission check is applied; any authenticated user can access any opportunity by id. const parsedOpportunityId = parseNumericId(opportunityId, 'Opportunity'); @@ -208,7 +208,7 @@ export class CopilotOpportunityService { ); const canApplyAsCopilot = - user.userId && user.userId.trim().length > 0 + user?.userId && user.userId.trim().length > 0 ? !members.includes(user.userId) : true; @@ -622,9 +622,9 @@ export class CopilotOpportunityService { */ private async getMembershipProjectIds( opportunities: CopilotOpportunity[], - user: JwtUser, + user: JwtUser | undefined, ): Promise> { - if (!user.userId || !/^\d+$/.test(user.userId)) { + if (!user?.userId || !/^\d+$/.test(user.userId)) { return new Set(); } diff --git a/src/api/copilot/copilot.utils.ts b/src/api/copilot/copilot.utils.ts index 68eb382..2d25c45 100644 --- a/src/api/copilot/copilot.utils.ts +++ b/src/api/copilot/copilot.utils.ts @@ -81,10 +81,13 @@ export function getCopilotTypeLabel(type: CopilotOpportunityType): string { /** * Returns true if user is admin, project manager, or manager. * - * @param user Authenticated JWT user. + * @param user Authenticated JWT user (undefined on anonymous `@Public()` routes). * @returns Whether the user is admin-or-manager scoped. */ -export function isAdminOrManager(user: JwtUser): boolean { +export function isAdminOrManager(user: JwtUser | undefined): boolean { + if (!user) { + return false; + } const userRoles = user.roles || []; return [ @@ -98,10 +101,13 @@ export function isAdminOrManager(user: JwtUser): boolean { /** * Returns true if user is admin or project manager. * - * @param user Authenticated JWT user. + * @param user Authenticated JWT user (undefined on anonymous `@Public()` routes). * @returns Whether the user is admin-or-pm scoped. */ -export function isAdminOrPm(user: JwtUser): boolean { +export function isAdminOrPm(user: JwtUser | undefined): boolean { + if (!user) { + return false; + } const userRoles = user.roles || []; return [