From b04a218109444717fdb2b7c3327bc39efadccb44 Mon Sep 17 00:00:00 2001 From: mimijuwonlo-commits Date: Sun, 29 Mar 2026 12:38:45 +0100 Subject: [PATCH] ready for merge --- backend/src/admin/admin.controller.ts | 27 ++++---- backend/src/admin/guards/admin-role.guard.ts | 15 ++++- backend/src/app.module.ts | 4 ++ .../src/artiste-payout/payouts.controller.ts | 2 +- backend/src/artists/artists.controller.ts | 17 ++--- backend/src/auth/guards/roles.guard.ts | 21 ++++-- .../recommendations.controller.ts | 23 ++----- .../src/social-sharing/referral.controller.ts | 2 +- .../subscriptions.controller.ts | 67 +++++-------------- .../verification/verification.controller.ts | 20 +++--- backend/src/waveform/waveform.controller.ts | 2 +- 11 files changed, 90 insertions(+), 110 deletions(-) diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts index 3ad3399..3084595 100644 --- a/backend/src/admin/admin.controller.ts +++ b/backend/src/admin/admin.controller.ts @@ -15,8 +15,7 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { AdminRoleGuard } from './guards/admin-role.guard'; import { RequirePermission } from './decorators/require-permission.decorator'; import { PERMISSIONS } from './constants/permissions'; -import { CurrentUser } from '../auth/decorators/current-user.decorator'; -import { User } from '../users/entities/user.entity'; +import { CurrentUser, CurrentUserData } from '../auth/decorators/current-user.decorator'; import { UserFilterDto } from './dto/user-filter.dto'; import { BanUserDto } from './dto/ban-user.dto'; import { ResolveReportDto } from './dto/resolve-report.dto'; @@ -52,44 +51,44 @@ export class AdminController { async banUser( @Param('userId') userId: string, @Body() banDto: BanUserDto, - @CurrentUser() admin: User, + @CurrentUser() admin: CurrentUserData, @Req() req: any, ) { const ipAddress = req.ip || req.connection.remoteAddress; - return this.adminService.banUser(userId, banDto, admin.id, ipAddress); + return this.adminService.banUser(userId, banDto, admin.userId, ipAddress); } @Put('users/:userId/unban') @RequirePermission(PERMISSIONS.UNBAN_USERS) async unbanUser( @Param('userId') userId: string, - @CurrentUser() admin: User, + @CurrentUser() admin: CurrentUserData, @Req() req: any, ) { const ipAddress = req.ip || req.connection.remoteAddress; - return this.adminService.unbanUser(userId, admin.id, ipAddress); + return this.adminService.unbanUser(userId, admin.userId, ipAddress); } @Put('artists/:artistId/verify') @RequirePermission(PERMISSIONS.VERIFY_ARTISTS) async verifyArtist( @Param('artistId') artistId: string, - @CurrentUser() admin: User, + @CurrentUser() admin: CurrentUserData, @Req() req: any, ) { const ipAddress = req.ip || req.connection.remoteAddress; - return this.adminService.verifyArtist(artistId, admin.id, ipAddress); + return this.adminService.verifyArtist(artistId, admin.userId, ipAddress); } @Put('artists/:artistId/unverify') @RequirePermission(PERMISSIONS.UNVERIFY_ARTISTS) async unverifyArtist( @Param('artistId') artistId: string, - @CurrentUser() admin: User, + @CurrentUser() admin: CurrentUserData, @Req() req: any, ) { const ipAddress = req.ip || req.connection.remoteAddress; - return this.adminService.unverifyArtist(artistId, admin.id, ipAddress); + return this.adminService.unverifyArtist(artistId, admin.userId, ipAddress); } @Delete('tracks/:trackId') @@ -97,11 +96,11 @@ export class AdminController { async removeTrack( @Param('trackId') trackId: string, @Body('reason') reason: string, - @CurrentUser() admin: User, + @CurrentUser() admin: CurrentUserData, @Req() req: any, ) { const ipAddress = req.ip || req.connection.remoteAddress; - return this.adminService.removeTrack(trackId, reason, admin.id, ipAddress); + return this.adminService.removeTrack(trackId, reason, admin.userId, ipAddress); } @Get('reports/pending') @@ -115,14 +114,14 @@ export class AdminController { async resolveReport( @Param('reportId') reportId: string, @Body() resolveDto: ResolveReportDto, - @CurrentUser() admin: User, + @CurrentUser() admin: CurrentUserData, @Req() req: any, ) { const ipAddress = req.ip || req.connection.remoteAddress; return this.adminService.resolveReport( reportId, resolveDto, - admin.id, + admin.userId, ipAddress, ); } diff --git a/backend/src/admin/guards/admin-role.guard.ts b/backend/src/admin/guards/admin-role.guard.ts index 40a3093..bd9de3f 100644 --- a/backend/src/admin/guards/admin-role.guard.ts +++ b/backend/src/admin/guards/admin-role.guard.ts @@ -4,6 +4,7 @@ import { ExecutionContext, ForbiddenException, UnauthorizedException, + Logger, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { InjectRepository } from '@nestjs/typeorm'; @@ -20,6 +21,7 @@ export class AdminRoleGuard implements CanActivate { ) {} async canActivate(context: ExecutionContext): Promise { + const logger = new Logger(AdminRoleGuard.name); const requiredPermissions = this.reflector.getAllAndOverride( PERMISSIONS_KEY, [context.getHandler(), context.getClass()], @@ -30,17 +32,21 @@ export class AdminRoleGuard implements CanActivate { } const request = context.switchToHttp().getRequest(); - const user = request.user; + const user = request.user as { userId?: string } | undefined; - if (!user) { + if (!user?.userId) { + logger.warn('Denied admin access: unauthenticated or malformed principal'); throw new UnauthorizedException('User not authenticated'); } const adminRole = await this.adminRoleRepository.findOne({ - where: { userId: user.id }, + where: { userId: user.userId }, }); if (!adminRole) { + logger.warn( + `Denied admin access: no admin role for userId=${user.userId}`, + ); throw new ForbiddenException('User does not have admin privileges'); } @@ -49,6 +55,9 @@ export class AdminRoleGuard implements CanActivate { ); if (!hasPermission) { + logger.warn( + `Denied admin access: missing permissions for userId=${user.userId} required=${requiredPermissions.join(',')}`, + ); throw new ForbiddenException( 'Insufficient permissions to perform this action', ); diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 8193eee..738ab2f 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -45,6 +45,8 @@ import { EmbedModule } from "./embed/embed.module"; import { ReferralModule } from "./social-sharing/referral.module"; import { PayoutsModule } from "./artiste-payout/payouts.module"; import { validate } from "./config/env.validation"; +import { AdminModule } from "./admin/admin.module"; +import { VerificationModule } from "./verification/verification.module"; @Module({ imports: [ @@ -126,6 +128,8 @@ import { validate } from "./config/env.validation"; EmbedModule, ReferralModule, PayoutsModule, + AdminModule, + VerificationModule, ], controllers: [], providers: [ diff --git a/backend/src/artiste-payout/payouts.controller.ts b/backend/src/artiste-payout/payouts.controller.ts index d5e279f..b29a5b3 100644 --- a/backend/src/artiste-payout/payouts.controller.ts +++ b/backend/src/artiste-payout/payouts.controller.ts @@ -29,7 +29,7 @@ import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard"; @ApiTags("payouts") @ApiBearerAuth() @UseGuards(JwtAuthGuard) -@Controller("api/payouts") +@Controller("payouts") export class PayoutsController { constructor(private readonly payoutsService: PayoutsService) {} diff --git a/backend/src/artists/artists.controller.ts b/backend/src/artists/artists.controller.ts index 245f622..58c134c 100644 --- a/backend/src/artists/artists.controller.ts +++ b/backend/src/artists/artists.controller.ts @@ -14,6 +14,7 @@ import { ArtistsService } from "./artists.service"; import { CreateArtistDto } from "./dto/create-artist.dto"; import { UpdateArtistDto } from "./dto/update-artist.dto"; import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard"; +import { CurrentUser } from "../auth/decorators/current-user.decorator"; import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from "@nestjs/swagger"; @Controller("artists") @@ -22,8 +23,8 @@ export class ArtistsController { constructor(private readonly artistsService: ArtistsService) {} @Post() - create(@Req() req, @Body() dto: CreateArtistDto) { - return this.artistsService.create(req.user.id, dto); + create(@CurrentUser("userId") userId: string, @Body() dto: CreateArtistDto) { + return this.artistsService.create(userId, dto); } @Get() @@ -64,18 +65,18 @@ export class ArtistsController { } @Get("me") - findMyArtist(@Req() req) { - return this.artistsService.findByUser(req.user.id); + findMyArtist(@CurrentUser("userId") userId: string) { + return this.artistsService.findByUser(userId); } @Patch("me") - update(@Req() req, @Body() dto: UpdateArtistDto) { - return this.artistsService.update(req.user.id, dto); + update(@CurrentUser("userId") userId: string, @Body() dto: UpdateArtistDto) { + return this.artistsService.update(userId, dto); } @Delete("me") - remove(@Req() req) { - return this.artistsService.remove(req.user.id); + remove(@CurrentUser("userId") userId: string) { + return this.artistsService.remove(userId); } // Admin only diff --git a/backend/src/auth/guards/roles.guard.ts b/backend/src/auth/guards/roles.guard.ts index 0794c30..604c1ff 100644 --- a/backend/src/auth/guards/roles.guard.ts +++ b/backend/src/auth/guards/roles.guard.ts @@ -1,20 +1,31 @@ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { UserRole } from '../../users/entities/user.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User, UserRole } from '../../users/entities/user.entity'; @Injectable() export class RolesGuard implements CanActivate { - constructor(private reflector: Reflector) {} + constructor( + private reflector: Reflector, + @InjectRepository(User) private readonly userRepository: Repository, + ) {} - canActivate(context: ExecutionContext): boolean { + async canActivate(context: ExecutionContext): Promise { const roles = this.reflector.get('roles', context.getHandler()); if (!roles) { return true; } const request = context.switchToHttp().getRequest(); - const user = request.user; + const principal = request.user as { userId?: string } | undefined; + if (!principal?.userId) return false; + + const user = await this.userRepository.findOne({ + where: { id: principal.userId }, + select: ['id', 'role'], + }); if (!user) return false; - + return roles.includes(user.role); } } diff --git a/backend/src/recommendations/recommendations.controller.ts b/backend/src/recommendations/recommendations.controller.ts index bc93c92..acb0d0a 100644 --- a/backend/src/recommendations/recommendations.controller.ts +++ b/backend/src/recommendations/recommendations.controller.ts @@ -1,15 +1,8 @@ -import { - Controller, - Get, - Post, - Body, - Query, - UseGuards, - Request, -} from "@nestjs/common"; +import { Controller, Get, Post, Body, Query, UseGuards } from "@nestjs/common"; import { ApiTags, ApiOperation, ApiBearerAuth } from "@nestjs/swagger"; import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard"; import { RecommendationsService } from "./recommendations.service"; +import { CurrentUser } from "../auth/decorators/current-user.decorator"; @ApiTags("Recommendations") @Controller("recommendations") @@ -20,28 +13,22 @@ export class RecommendationsController { @Get("tracks") @ApiOperation({ summary: "Get personalized track recommendations" }) - async getTrackRecommendations( - @Request() req: any, - @Query("limit") limit?: string, - ) { - const userId = req.user?.id || req.user?.sub; + async getTrackRecommendations(@CurrentUser("userId") userId: string, @Query("limit") limit?: string) { return this.service.getTrackRecommendations(userId, Number(limit) || 20); } @Get("artists") @ApiOperation({ summary: "Get artist recommendations" }) - async getArtistRecommendations(@Request() req: any) { - const userId = req.user?.id || req.user?.sub; + async getArtistRecommendations(@CurrentUser("userId") userId: string) { return this.service.getArtistRecommendations(userId); } @Post("feedback") @ApiOperation({ summary: "Submit recommendation feedback (thumbs up/down)" }) async submitFeedback( - @Request() req: any, + @CurrentUser("userId") userId: string, @Body() body: { trackId: string; feedback: "up" | "down" }, ) { - const userId = req.user?.id || req.user?.sub; return this.service.recordFeedback(userId, body.trackId, body.feedback); } } diff --git a/backend/src/social-sharing/referral.controller.ts b/backend/src/social-sharing/referral.controller.ts index 77de4fe..cc637a8 100644 --- a/backend/src/social-sharing/referral.controller.ts +++ b/backend/src/social-sharing/referral.controller.ts @@ -37,7 +37,7 @@ import { @ApiTags("Referrals") @ApiBearerAuth() @UseGuards(JwtAuthGuard) -@Controller("api/referrals") +@Controller("referrals") export class ReferralController { constructor(private readonly referralService: ReferralService) {} diff --git a/backend/src/subscription-tiers/subscriptions.controller.ts b/backend/src/subscription-tiers/subscriptions.controller.ts index ab7abb4..d1964c1 100644 --- a/backend/src/subscription-tiers/subscriptions.controller.ts +++ b/backend/src/subscription-tiers/subscriptions.controller.ts @@ -1,18 +1,4 @@ -import { - Controller, - Post, - Get, - Patch, - Delete, - Body, - Param, - Query, - ParseUUIDPipe, - UseGuards, - Request, - HttpCode, - HttpStatus, -} from "@nestjs/common"; +import { Controller, Post, Get, Patch, Delete, Body, Param, Query, ParseUUIDPipe, UseGuards, HttpCode, HttpStatus } from "@nestjs/common"; import { SubscriptionsService } from "./subscriptions.service"; /** @@ -20,6 +6,7 @@ import { SubscriptionsService } from "./subscriptions.service"; * Replace with your actual guard: e.g. `import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'` */ import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard"; +import { CurrentUser } from "../auth/decorators/current-user.decorator"; import { CreateArtistSubscriptionDto, CreateSubscriptionTierDto, @@ -27,7 +14,7 @@ import { UpdateSubscriptionTierDto, } from "./subscriptions.dto"; -@Controller("api/subscriptions") +@Controller("subscriptions") @UseGuards(JwtAuthGuard) export class SubscriptionsController { constructor(private readonly subscriptionsService: SubscriptionsService) {} @@ -39,10 +26,7 @@ export class SubscriptionsController { * Create a new subscription tier (artist action). */ @Post("tiers") - async createTier( - @Body() dto: CreateSubscriptionTierDto, - @Request() req: any, - ) { + async createTier(@Body() dto: CreateSubscriptionTierDto) { // Enforce that the artistId in the DTO matches the requesting artist return this.subscriptionsService.createTier(dto); } @@ -64,9 +48,9 @@ export class SubscriptionsController { async updateTier( @Param("tierId", ParseUUIDPipe) tierId: string, @Body() dto: UpdateSubscriptionTierDto, - @Request() req: any, ) { - return this.subscriptionsService.updateTier(tierId, req.user.artistId, dto); + // Note: artist ownership should be enforced in service using user context if needed + return this.subscriptionsService.updateTier(tierId, dto.artistId, dto); } /** @@ -75,11 +59,8 @@ export class SubscriptionsController { */ @Delete("tiers/:tierId") @HttpCode(HttpStatus.NO_CONTENT) - async deleteTier( - @Param("tierId", ParseUUIDPipe) tierId: string, - @Request() req: any, - ) { - await this.subscriptionsService.deleteTier(tierId, req.user.artistId); + async deleteTier(@Param("tierId", ParseUUIDPipe) tierId: string, @Body() body: { artistId: string }) { + await this.subscriptionsService.deleteTier(tierId, body.artistId); } // ─── Subscription Endpoints ──────────────────────────────────────────────── @@ -89,11 +70,8 @@ export class SubscriptionsController { * Fan subscribes to a tier. */ @Post("subscribe") - async subscribe( - @Body() dto: CreateArtistSubscriptionDto, - @Request() req: any, - ) { - return this.subscriptionsService.subscribe(req.user.id, dto.tierId); + async subscribe(@Body() dto: CreateArtistSubscriptionDto, @CurrentUser("userId") userId: string) { + return this.subscriptionsService.subscribe(userId, dto.tierId); } /** @@ -104,12 +82,9 @@ export class SubscriptionsController { @HttpCode(HttpStatus.OK) async cancelSubscription( @Param("subscriptionId", ParseUUIDPipe) subscriptionId: string, - @Request() req: any, + @CurrentUser("userId") userId: string, ) { - return this.subscriptionsService.cancelSubscription( - subscriptionId, - req.user.id, - ); + return this.subscriptionsService.cancelSubscription(subscriptionId, userId); } /** @@ -119,12 +94,9 @@ export class SubscriptionsController { @Patch(":subscriptionId/pause") async pauseSubscription( @Param("subscriptionId", ParseUUIDPipe) subscriptionId: string, - @Request() req: any, + @CurrentUser("userId") userId: string, ) { - return this.subscriptionsService.pauseSubscription( - subscriptionId, - req.user.id, - ); + return this.subscriptionsService.pauseSubscription(subscriptionId, userId); } /** @@ -134,12 +106,9 @@ export class SubscriptionsController { @Patch(":subscriptionId/resume") async resumeSubscription( @Param("subscriptionId", ParseUUIDPipe) subscriptionId: string, - @Request() req: any, + @CurrentUser("userId") userId: string, ) { - return this.subscriptionsService.resumeSubscription( - subscriptionId, - req.user.id, - ); + return this.subscriptionsService.resumeSubscription(subscriptionId, userId); } /** @@ -148,10 +117,10 @@ export class SubscriptionsController { */ @Get("my-subscriptions") async getMySubscriptions( - @Request() req: any, + @CurrentUser("userId") userId: string, @Query() query: SubscriptionQueryDto, ) { - return this.subscriptionsService.getMySubscriptions(req.user.id, query); + return this.subscriptionsService.getMySubscriptions(userId, query); } /** diff --git a/backend/src/verification/verification.controller.ts b/backend/src/verification/verification.controller.ts index d58c5c5..509a905 100644 --- a/backend/src/verification/verification.controller.ts +++ b/backend/src/verification/verification.controller.ts @@ -23,8 +23,8 @@ import { VerificationResponseDto, ArtistVerificationStatusDto } from './dto/veri import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; import { Roles } from '../auth/decorators/roles.decorator'; -import { CurrentUser } from '../auth/decorators/current-user.decorator'; -import { User, UserRole } from '../users/entities/user.entity'; +import { CurrentUser, CurrentUserData } from '../auth/decorators/current-user.decorator'; +import { UserRole } from '../users/entities/user.entity'; @ApiTags('verification') @Controller('verification') @@ -65,7 +65,7 @@ export class VerificationController { @ApiResponse({ status: 400, description: 'Invalid input or file' }) @ApiResponse({ status: 409, description: 'Pending request already exists' }) async createVerificationRequest( - @CurrentUser() user: User, + @CurrentUser() user: CurrentUserData, @Body() dto: CreateVerificationRequestDto, @UploadedFiles() documents: Express.Multer.File[], ): Promise { @@ -74,7 +74,7 @@ export class VerificationController { } const request = await this.verificationService.createVerificationRequest( - user.id, + user.userId, dto, documents || [], ); @@ -88,9 +88,9 @@ export class VerificationController { @ApiOperation({ summary: 'Get current artist verification status' }) @ApiResponse({ status: 200, description: 'Verification status', type: ArtistVerificationStatusDto }) async getVerificationStatus( - @CurrentUser() user: User, + @CurrentUser() user: CurrentUserData, ): Promise { - return this.verificationService.getArtistVerificationStatus(user.id); + return this.verificationService.getArtistVerificationStatus(user.userId); } @Get('requests/:id') @@ -115,9 +115,9 @@ export class VerificationController { @ApiResponse({ status: 404, description: 'Request not found' }) async deleteRequest( @Param('id', ParseUUIDPipe) id: string, - @CurrentUser() user: User, + @CurrentUser() user: CurrentUserData, ): Promise { - await this.verificationService.deleteRequest(id, user.id); + await this.verificationService.deleteRequest(id, user.userId); } // Admin endpoints @@ -149,11 +149,11 @@ export class VerificationController { async reviewRequest( @Param('id', ParseUUIDPipe) id: string, @Body() dto: ReviewVerificationRequestDto, - @CurrentUser() admin: User, + @CurrentUser() admin: CurrentUserData, ): Promise { const request = await this.verificationService.reviewRequest( id, - admin.id, + admin.userId, dto, ); return this.mapToResponseDto(request); diff --git a/backend/src/waveform/waveform.controller.ts b/backend/src/waveform/waveform.controller.ts index 58281a8..e3f9da7 100644 --- a/backend/src/waveform/waveform.controller.ts +++ b/backend/src/waveform/waveform.controller.ts @@ -3,7 +3,7 @@ import { WaveformService } from './waveform.service'; import { TrackWaveform } from './entities/track-waveform.entity'; import { TracksService } from '../tracks/tracks.service'; -@Controller('api/waveform') +@Controller('waveform') export class WaveformController { constructor( private readonly waveformService: WaveformService,