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
27 changes: 13 additions & 14 deletions backend/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,56 +51,56 @@ 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')
@RequirePermission(PERMISSIONS.REMOVE_TRACKS)
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')
Expand All @@ -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,
);
}
Expand Down
15 changes: 12 additions & 3 deletions backend/src/admin/guards/admin-role.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ExecutionContext,
ForbiddenException,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { InjectRepository } from '@nestjs/typeorm';
Expand All @@ -20,6 +21,7 @@ export class AdminRoleGuard implements CanActivate {
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const logger = new Logger(AdminRoleGuard.name);
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
PERMISSIONS_KEY,
[context.getHandler(), context.getClass()],
Expand All @@ -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}`,
);
Comment on lines +47 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging raw userId values on authorization failures.

These warning paths persist stable user identifiers in auth logs. That is unnecessary for denial auditing and creates a privacy/compliance risk. Prefer a request/correlation ID, or redact/hash the principal before logging.

🛠️ Minimal fix
-      logger.warn(
-        `Denied admin access: no admin role for userId=${user.userId}`,
-      );
+      logger.warn(
+        'Denied admin access: no admin role for authenticated principal',
+      );
...
-      logger.warn(
-        `Denied admin access: missing permissions for userId=${user.userId} required=${requiredPermissions.join(',')}`,
-      );
+      logger.warn(
+        `Denied admin access: missing permissions required=${requiredPermissions.join(',')}`,
+      );

Also applies to: 58-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/admin/guards/admin-role.guard.ts` around lines 47 - 49, Remove
logging of raw user.userId in the AdminRoleGuard logger.warn calls and replace
with a privacy-safe identifier: either use the existing request/correlation id
from the execution context (if available) or log a redacted/hashed form of the
principal (e.g., SHA-256 or truncated HMAC of user.userId) so the original
stable identifier is not persisted; update both the warn at the `Denied admin
access: no admin role for userId=${user.userId}` site and the similar warn/error
calls around the other occurrence (lines referenced in the review) to include
the correlation id or the hashed/redacted user id instead of user.userId, and
ensure the hashing uses a constant salt/config so the resulting token can be
correlated for troubleshooting without exposing the raw value.

throw new ForbiddenException('User does not have admin privileges');
}

Expand All @@ -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',
);
Expand Down
4 changes: 4 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -126,6 +128,8 @@ import { validate } from "./config/env.validation";
EmbedModule,
ReferralModule,
PayoutsModule,
AdminModule,
VerificationModule,
],
controllers: [],
providers: [
Expand Down
2 changes: 1 addition & 1 deletion backend/src/artiste-payout/payouts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down
17 changes: 9 additions & 8 deletions backend/src/artists/artists.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
21 changes: 16 additions & 5 deletions backend/src/auth/guards/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -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<User>,
) {}
Comment on lines +9 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "RolesGuard references in modules:"
rg -n -C2 '\bRolesGuard\b' backend/src --glob '**/*.module.ts' || true

echo
echo "RolesGuard use sites:"
rg -n -C2 '@UseGuards\([^)]*\bRolesGuard\b' backend/src --type=ts || true

echo
echo "AuthModule wiring:"
sed -n '1,200p' backend/src/auth/auth.module.ts

Repository: OlufunbiIK/tip-tune

Length of output: 6982


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding RolesGuard file ==="
fd -type f -name '*roles.guard*' backend/src

echo
echo "=== RolesGuard implementation ==="
cat -n backend/src/auth/guards/roles.guard.ts

echo
echo "=== Checking if RolesGuard is exported from any index/barrel file ==="
rg -n 'RolesGuard' backend/src --glob '**/*index*' || true

echo
echo "=== Checking if RolesGuard appears in any .module.ts ==="
rg -n 'RolesGuard' backend/src --glob '**/*.module.ts' || true

echo
echo "=== Checking RolesGuard in auth.module.ts (full file) ==="
wc -l backend/src/auth/auth.module.ts

Repository: OlufunbiIK/tip-tune

Length of output: 282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding RolesGuard file ==="
find backend/src -name '*roles.guard*'

echo
echo "=== RolesGuard implementation ==="
cat -n backend/src/auth/guards/roles.guard.ts

echo
echo "=== Checking all .module.ts files for RolesGuard ==="
rg 'RolesGuard' backend/src --type ts -l

echo
echo "=== Full AuthModule file ==="
cat -n backend/src/auth/auth.module.ts

Repository: OlufunbiIK/tip-tune

Length of output: 3269


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking app.module.ts ==="
find backend/src -name 'app.module.ts' -exec cat -n {} \;

echo
echo "=== Searching for RolesGuard in any import/provider context ==="
rg -B3 -A3 'RolesGuard' backend/src --type ts --glob '**/*.ts' | grep -E '(providers|imports|@Module)' || true

Repository: OlufunbiIK/tip-tune

Length of output: 6788


RolesGuard must be added to AuthModule's providers and exports.

RolesGuard is @Injectable() and depends on @InjectRepository(User), but is not registered in any module's providers array. It is currently used via @UseGuards(RolesGuard) across 15+ routes in ReportsModule, ModerationModule, FeesModule, platinum-fee, and VerificationModule, but these controllers cannot resolve it at runtime.

To fix, add RolesGuard to AuthModule.providers and AuthModule.exports:

`@Module`({
  imports: [TypeOrmModule.forFeature([User]), ...],
  providers: [AuthService, WalletStrategy, JwtAuthGuard, RolesGuard],
  exports: [AuthService, JwtAuthGuard, WalletStrategy, RolesGuard],
})
export class AuthModule {}

Then import AuthModule into any module that uses @UseGuards(RolesGuard), or export RolesGuard from a shared guards module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/auth/guards/roles.guard.ts` around lines 9 - 12, RolesGuard is
injectable and injects User repository but isn't provided by any module, causing
runtime resolution failures where controllers use `@UseGuards`(RolesGuard); add
RolesGuard to the AuthModule providers and exports (alongside
TypeOrmModule.forFeature([User]) and existing providers like AuthService and
JwtAuthGuard) so it can be injected, then ensure modules using
`@UseGuards`(RolesGuard) import AuthModule (or export RolesGuard from a shared
guards module) so RolesGuard can be resolved at runtime.


canActivate(context: ExecutionContext): boolean {
async canActivate(context: ExecutionContext): Promise<boolean> {
const roles = this.reflector.get<UserRole[]>('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);
}
}
23 changes: 5 additions & 18 deletions backend/src/recommendations/recommendations.controller.ts
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion backend/src/social-sharing/referral.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
@ApiTags("Referrals")
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller("api/referrals")
@Controller("referrals")
export class ReferralController {
constructor(private readonly referralService: ReferralService) {}

Expand Down
Loading
Loading