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
26 changes: 26 additions & 0 deletions docs/security/admin-ip-whitelist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Admin IP Whitelisting

Admin endpoints (`/admin/*`) are additionally protected by `IpWhitelistGuard`
(`src/common/guards/ip-whitelist.guard.ts`), which runs after JWT and role
checks.

## Configuration

Set the `ADMIN_IP_WHITELIST` environment variable to a comma-separated list of
trusted IP addresses:

```
ADMIN_IP_WHITELIST=203.0.113.10,198.51.100.4
```

- When the variable is **empty or unset**, the guard allows all requests, so the
control is opt-in and does not interfere with local development or CI.
- When it is set, any request whose client IP is not in the list receives a
`403 Forbidden`.

## Notes

- Place the app behind a trusted proxy and ensure the real client IP is
forwarded so the guard evaluates the correct address.
- Combine with the existing `JwtAuthGuard` and `RolesGuard` (role-based access);
IP whitelisting is a defence-in-depth layer, not a replacement for them.
3 changes: 2 additions & 1 deletion src/audit/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { VerifyArtistDto } from './dto/verify-artist.dto';
import { ResolveDisputeDto } from './dto/resolve-dispute.dto';
import { JwtAuthGuard } from '../auth/sync/jwt.auth.guard';
import { RolesGuard } from '../auth/sync/roles.guard';
import { IpWhitelistGuard } from '../common/guards/ip-whitelist.guard';
import { Roles } from '../auth/decorators/roles.decorators';
import { Role, CommissionStatus } from '@prisma/client';

import { PaymentsService } from '../payments/payments.service';

@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@UseGuards(JwtAuthGuard, RolesGuard, IpWhitelistGuard)
export class AdminController {
constructor(
private auditService: AuditService,
Expand Down
18 changes: 0 additions & 18 deletions src/auth/dto copy/login.dto.ts

This file was deleted.

36 changes: 0 additions & 36 deletions src/auth/dto copy/register.dto.ts

This file was deleted.

46 changes: 46 additions & 0 deletions src/common/guards/ip-whitelist.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';

interface RequestWithIp {
ip?: string;
socket?: { remoteAddress?: string };
}

/**
* Restricts access to routes it guards to a configured set of trusted IPs.
*
* The allowlist is read from the `ADMIN_IP_WHITELIST` environment variable as a
* comma-separated list of IP addresses. When the variable is empty or unset the
* guard allows the request, so the control is opt-in and does not break local
* development. See `docs/security/admin-ip-whitelist.md`.
*/
@Injectable()
export class IpWhitelistGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const raw = process.env.ADMIN_IP_WHITELIST;
if (!raw || raw.trim() === '') {
return true;
}

const allowed = raw
.split(',')
.map((ip) => ip.trim())
.filter(Boolean);

const request = context.switchToHttp().getRequest<RequestWithIp>();
const clientIp = (request.ip || request.socket?.remoteAddress || '').replace(
'::ffff:',
'',
);

if (!allowed.includes(clientIp)) {
throw new ForbiddenException('Access denied: IP address is not whitelisted');
}

return true;
}
}
3 changes: 2 additions & 1 deletion src/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';
import { UsersController } from './users.controller';
import { PrismaModule } from '../prisma/prisma.module';

@Module({
imports: [PrismaModule],
controllers: [UsersController],
providers: [UsersService],
providers: [UsersService, UsersRepository],
exports: [UsersService],
})
export class UsersModule {}
32 changes: 32 additions & 0 deletions src/users/users.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

/**
* Data-access layer for the User entity.
*
* Encapsulates all Prisma queries for users so that services depend on this
* repository rather than on Prisma directly. This keeps persistence concerns in
* one place and makes the service layer easier to test in isolation.
*/
@Injectable()
export class UsersRepository {
constructor(private readonly prisma: PrismaService) {}

/**
* Find a single user by their unique id.
* @param id - The user's unique identifier.
* @returns The matching user, or `null` if none exists.
*/
findById(id: string) {
return this.prisma.user.findUnique({ where: { id } });
}

/**
* Find a single user by their unique email address.
* @param email - The user's email address.
* @returns The matching user, or `null` if none exists.
*/
findByEmail(email: string) {
return this.prisma.user.findUnique({ where: { email } });
}
}
25 changes: 21 additions & 4 deletions src/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { UsersRepository } from './users.repository';

/**
* Business-logic layer for user operations.
*
* Delegates all persistence to {@link UsersRepository} so this service stays
* focused on application logic and remains easy to unit-test with a mocked
* repository.
*/
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
constructor(private readonly usersRepository: UsersRepository) {}

/**
* Retrieve a user by their unique id.
* @param id - The user's unique identifier.
* @returns The matching user, or `null` if none exists.
*/
async findById(id: string) {
return this.prisma.user.findUnique({ where: { id } });
return this.usersRepository.findById(id);
}

/**
* Retrieve a user by their email address.
* @param email - The user's email address.
* @returns The matching user, or `null` if none exists.
*/
async findByEmail(email: string) {
return this.prisma.user.findUnique({ where: { email } });
return this.usersRepository.findByEmail(email);
}
}