Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Guards w NestJS

Ave, odważni legioniści! Konsul Caesar.js prowadzi was dalej w głąb tajemnic NestJS. Dziś poznamy Guards - najwyższej klasy strażników naszego forum, którzy decydują o tym, kto może dostać się do najcenniejszych zasobów.

Guards w NestJS to jak elitarni strażnicy skarbca - nie tylko sprawdzają tożsamość, ale podejmują strategiczne decyzje o dostępie na podstawie logiki biznesowej, ról użytkowników i kontekstu żądania.

Czym są Guards?

Guards to klasy implementujące interfejs

CanActivate
, które określają, czy żądanie może być przetworzone przez handler. Działają po middleware, ale przed interceptorami i pipes.

1import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2
3@Injectable()
4export class LegionGuard implements CanActivate {
5 canActivate(context: ExecutionContext): boolean {
6 const request = context.switchToHttp().getRequest();
7 const legionaryRank = request.headers['legionary-rank'];
8
9 // Tylko legioniści o randze "centurion" lub wyżej
10 return legionaryRank === 'centurion' || legionaryRank === 'tribune';
11 }
12}

Authentication Guard

Najważniejszy strażnik - sprawdza czy legionista jest członkiem legionu:

1import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
2import { JwtService } from '@nestjs/jwt';
3
4@Injectable()
5export class AuthGuard implements CanActivate {
6 constructor(private jwtService: JwtService) {}
7
8 canActivate(context: ExecutionContext): boolean {
9 const request = context.switchToHttp().getRequest();
10 const token = this.extractTokenFromHeader(request);
11
12 if (!token) {
13 throw new UnauthorizedException('Brak tokenu! Dostęp tylko dla członków legionu!');
14 }
15
16 try {
17 const payload = this.jwtService.verify(token);
18 request['legionary'] = payload;
19 return true;
20 } catch {
21 throw new UnauthorizedException('Nieprawidłowy token! Czy na pewno jesteś jednym z nas?');
22 }
23 }
24
25 private extractTokenFromHeader(request: any): string | undefined {
26 const [type, token] = request.headers.authorization?.split(' ') ?? [];
27 return type === 'Bearer' ? token : undefined;
28 }
29}

Role-Based Access Control (RBAC)

System rang i uprawnień w legionie rzymskim:

1// decorators/roles.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4export enum LegionaryRank {
5 MILES = 'miles',
6 TESSERARIUS = 'tesserarius',
7 OPTIO = 'optio',
8 CENTURION = 'centurion',
9 TRIBUNE = 'tribune'
10}
11
12export const Ranks = (...ranks: LegionaryRank[]) => SetMetadata('ranks', ranks);
13
14// guards/roles.guard.ts
15import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
16import { Reflector } from '@nestjs/core';
17
18@Injectable()
19export class RolesGuard implements CanActivate {
20 constructor(private reflector: Reflector) {}
21
22 canActivate(context: ExecutionContext): boolean {
23 const requiredRanks = this.reflector.getAllAndOverride<LegionaryRank[]>('ranks', [
24 context.getHandler(),
25 context.getClass(),
26 ]);
27
28 if (!requiredRanks) {
29 return true; // Brak wymaganych rang = dostęp dla wszystkich
30 }
31
32 const request = context.switchToHttp().getRequest();
33 const legionary = request.legionary;
34
35 if (!legionary) {
36 return false;
37 }
38
39 return requiredRanks.includes(legionary.rank);
40 }
41}
42
43// Użycie w kontrolerze
44@Controller('tributes')
45export class TributesController {
46 @Get('centurion-only')
47 @Ranks(LegionaryRank.CENTURION, LegionaryRank.TRIBUNE)
48 @UseGuards(AuthGuard, RolesGuard)
49 getCenturionTributes() {
50 return {
51 message: 'Tributy centuriona - najcenniejsze klejnoty!',
52 tributes: ['Korona Cezara', 'Złoty Orzeł', 'Insygnia Imperatora']
53 };
54 }
55}

Custom Authorization Guard

Zaawansowany strażnik sprawdzający różne warunki dostępu:

1@Injectable()
2export class TributeAccessGuard implements CanActivate {
3 constructor(
4 private tributeService: TributeService,
5 private legionService: LegionService
6 ) {}
7
8 async canActivate(context: ExecutionContext): Promise<boolean> {
9 const request = context.switchToHttp().getRequest();
10 const legionary = request.legionary;
11 const tributeId = request.params.id;
12
13 // Sprawdzenie czy tribut istnieje
14 const tribute = await this.tributeService.findById(tributeId);
15 if (!tribute) {
16 throw new NotFoundException('Tribut nie został znaleziony!');
17 }
18
19 // Różne poziomy dostępu
20 switch (tribute.securityLevel) {
21 case 'public':
22 return true; // Dostępny dla wszystkich
23
24 case 'legion':
25 return this.isLegionMember(legionary);
26
27 case 'officers':
28 return this.isOfficer(legionary);
29
30 case 'centurion':
31 return this.isCenturion(legionary);
32
33 case 'personal':
34 return tribute.ownerId === legionary.id;
35
36 default:
37 return false;
38 }
39 }
40
41 private isLegionMember(legionary: any): boolean {
42 return legionary && legionary.status === 'active';
43 }
44
45 private isOfficer(legionary: any): boolean {
46 const officerRanks = [
47 LegionaryRank.OPTIO,
48 LegionaryRank.CENTURION,
49 LegionaryRank.TRIBUNE
50 ];
51 return this.isLegionMember(legionary) && officerRanks.includes(legionary.rank);
52 }
53
54 private isCenturion(legionary: any): boolean {
55 return this.isLegionMember(legionary) &&
56 [LegionaryRank.CENTURION, LegionaryRank.TRIBUNE].includes(legionary.rank);
57 }
58}

Time-Based Access Guard

Strażnik kontrolujący dostęp w zależności od czasu:

1@Injectable()
2export class TimeBasedGuard implements CanActivate {
3 canActivate(context: ExecutionContext): boolean {
4 const now = new Date();
5 const hour = now.getHours();
6 const dayOfWeek = now.getDay();
7
8 const request = context.switchToHttp().getRequest();
9 const route = request.route.path;
10
11 // Skarbiec dostępny tylko w godzinach dziennych
12 if (route.includes('/vault')) {
13 if (hour < 8 || hour > 18) {
14 throw new ForbiddenException(
15 'Skarbiec zamknięty! Dostępny tylko w godzinach 8:00-18:00'
16 );
17 }
18 }
19
20 // Termy zamknięte w niedziele
21 if (route.includes('/thermae') && dayOfWeek === 0) {
22 throw new ForbiddenException(
23 'Termy zamknięte w niedziele! Nawet legioniści potrzebują odpoczynku!'
24 );
25 }
26
27 // Nocna warta - ograniczony dostęp po 22:00
28 if (hour >= 22 || hour <= 6) {
29 const legionary = request.legionary;
30 if (!legionary.nightShiftAccess) {
31 throw new ForbiddenException(
32 'Nocny dostęp tylko dla straży nocnej!'
33 );
34 }
35 }
36
37 return true;
38 }
39}

IP Whitelist Guard

Kontrola dostępu na podstawie adresu IP:

1@Injectable()
2export class IpWhitelistGuard implements CanActivate {
3 private readonly allowedIps = [
4 '192.168.1.100', // Komputer konsula
5 '192.168.1.101', // Komputer centuriona
6 '10.0.0.50', // Serwer inżynieryjny
7 '127.0.0.1' // Localhost dla rozwoju
8 ];
9
10 canActivate(context: ExecutionContext): boolean {
11 const request = context.switchToHttp().getRequest();
12 const clientIp = this.getClientIp(request);
13
14 if (!this.allowedIps.includes(clientIp)) {
15 console.log(`⚠️ Próba dostępu z nieautoryzowanego IP: ${clientIp}`);
16 throw new ForbiddenException(
17 'Dostęp z tej prowincji nie jest dozwolony! Tylko zaufane lokacje mogą się łączyć!'
18 );
19 }
20
21 return true;
22 }
23
24 private getClientIp(request: any): string {
25 return request.headers['x-forwarded-for'] ||
26 request.connection.remoteAddress ||
27 request.socket.remoteAddress ||
28 request.ip;
29 }
30}

Feature Flag Guard

Kontrola dostępu do nowych funkcji:

1@Injectable()
2export class FeatureFlagGuard implements CanActivate {
3 constructor(private configService: ConfigService) {}
4
5 canActivate(context: ExecutionContext): boolean {
6 const handler = context.getHandler();
7 const featureFlag = this.reflector.get<string>('feature', handler);
8
9 if (!featureFlag) {
10 return true; // Brak flagi = dostęp bez ograniczeń
11 }
12
13 const isEnabled = this.configService.get<boolean>(`features.${featureFlag}`);
14
15 if (!isEnabled) {
16 throw new ForbiddenException(
17 `Funkcja '${featureFlag}' jest obecnie niedostępna. Inżynierowie jeszcze nad nią pracują!`
18 );
19 }
20
21 return true;
22 }
23}
24
25// Decorator dla feature flags
26export const Feature = (flag: string) => SetMetadata('feature', flag);
27
28// Użycie
29@Controller('experimental')
30export class ExperimentalController {
31 @Get('new-weapon')
32 @Feature('advanced-ballistae')
33 @UseGuards(FeatureFlagGuard)
34 getNewWeapon() {
35 return {
36 name: 'Kwantowa Ballista',
37 damage: 9999,
38 status: 'Eksperymentalna'
39 };
40 }
41}

Rate Limiting Guard

Guard kontrolujący częstotliwość żądań:

1@Injectable()
2export class RateLimitGuard implements CanActivate {
3 private requests = new Map<string, { count: number; resetTime: number }>();
4
5 constructor(
6 @Inject('RATE_LIMIT_OPTIONS')
7 private options: { max: number; windowMs: number }
8 ) {}
9
10 canActivate(context: ExecutionContext): boolean {
11 const request = context.switchToHttp().getRequest();
12 const key = this.generateKey(request);
13 const now = Date.now();
14
15 let userRequests = this.requests.get(key);
16
17 if (!userRequests || now > userRequests.resetTime) {
18 userRequests = {
19 count: 0,
20 resetTime: now + this.options.windowMs
21 };
22 }
23
24 userRequests.count++;
25 this.requests.set(key, userRequests);
26
27 if (userRequests.count > this.options.max) {
28 throw new TooManyRequestsException(
29 'Zbyt wiele żądań! Forum nie wytrzyma takiego natarcia!'
30 );
31 }
32
33 return true;
34 }
35
36 private generateKey(request: any): string {
37 const legionary = request.legionary;
38 return legionary ? `legionary-${legionary.id}` : `ip-${request.ip}`
1 }
2}

Composite Guard

Guard łączący wiele warunków:

1@Injectable()
2export class TributeVaultGuard implements CanActivate {
3 constructor(
4 private tributeService: TributeService,
5 private auditService: AuditService
6 ) {}
7
8 async canActivate(context: ExecutionContext): Promise<boolean> {
9 const request = context.switchToHttp().getRequest();
10 const legionary = request.legionary;
11 const tributeId = request.params.id;
12
13 // Warunek 1: Sprawdź rangę legionisty
14 const hasRequiredRank = this.checkRank(legionary);
15 if (!hasRequiredRank) {
16 this.auditService.logUnauthorizedAccess(legionary.id, 'insufficient_rank');
17 return false;
18 }
19
20 // Warunek 2: Sprawdź czy legionista ma aktywny dostęp
21 const hasActiveAccess = await this.checkActiveAccess(legionary);
22 if (!hasActiveAccess) {
23 this.auditService.logUnauthorizedAccess(legionary.id, 'inactive_access');
24 return false;
25 }
26
27 // Warunek 3: Sprawdź limit dzienny
28 const withinDailyLimit = await this.checkDailyLimit(legionary);
29 if (!withinDailyLimit) {
30 this.auditService.logUnauthorizedAccess(legionary.id, 'daily_limit_exceeded');
31 throw new ForbiddenException(
32 'Przekroczono dzienny limit dostępu do tributów!'
33 );
34 }
35
36 // Warunek 4: Sprawdź czy tribut nie jest obecnie używany
37 const tributeAvailable = await this.checkTributeAvailability(tributeId);
38 if (!tributeAvailable) {
39 throw new ConflictException(
40 'Tribut jest obecnie używany przez innego legionistę!'
41 );
42 }
43
44 // Logowanie udanego dostępu
45 this.auditService.logSuccessfulAccess(legionary.id, tributeId);
46
47 return true;
48 }
49
50 private checkRank(legionary: any): boolean {
51 return [LegionaryRank.OPTIO, LegionaryRank.CENTURION, LegionaryRank.TRIBUNE]
52 .includes(legionary.rank);
53 }
54
55 private async checkActiveAccess(legionary: any): Promise<boolean> {
56 // Sprawdzenie w bazie danych czy legionista ma aktywny dostęp
57 return legionary.status === 'active' && !legionary.suspended;
58 }
59
60 private async checkDailyLimit(legionary: any): Promise<boolean> {
61 const today = new Date().toDateString();
62 const accessCount = await this.auditService.getDailyAccessCount(legionary.id, today);
63 return accessCount < 5; // Maksymalnie 5 dostępów dziennie
64 }
65
66 private async checkTributeAvailability(tributeId: string): Promise<boolean> {
67 return await this.tributeService.isAvailable(tributeId);
68 }
69}

Globalny Guard

1// main.ts
2import { NestFactory } from '@nestjs/core';
3import { AppModule } from './app.module';
4
5async function bootstrap() {
6 const app = await NestFactory.create(AppModule);
7 
8 // Globalny guard dla całej aplikacji
9 app.useGlobalGuards(new AuthGuard());
10 
11 await app.listen(3000);
12}
13bootstrap();
14
15// Lub w module
16@Module({
17 providers: [
18 {
19 provide: APP_GUARD,
20 useClass: AuthGuard,
21 },
22 ],
23})
24export class AppModule {}
Vai a CodeWorlds