Skip to content

Commit b22f74e

Browse files
authored
[Reputation Oracle] refactor: properly use auth strategies (#3168)
1 parent 3c7eacf commit b22f74e

33 files changed

Lines changed: 191 additions & 154 deletions

packages/apps/reputation-oracle/server/src/app.module.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,44 @@
11
import { Module } from '@nestjs/common';
2-
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
2+
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
33
import { ConfigModule } from '@nestjs/config';
44
import { ScheduleModule } from '@nestjs/schedule';
55
import { ServeStaticModule } from '@nestjs/serve-static';
66
import { join } from 'path';
7+
8+
import { envValidator } from './config';
9+
import { EnvConfigModule } from './config/config.module';
10+
711
import { DatabaseModule } from './database/database.module';
12+
13+
import { JwtAuthGuard } from './common/guards';
14+
import { ExceptionFilter } from './common/filters/exception.filter';
15+
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
816
import { HttpValidationPipe } from './common/pipes';
17+
918
import { HealthModule } from './modules/health/health.module';
1019
import { ReputationModule } from './modules/reputation/reputation.module';
1120
import { Web3Module } from './modules/web3/web3.module';
12-
import { envValidator } from './config';
1321
import { AuthModule } from './modules/auth/auth.module';
14-
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
1522
import { KycModule } from './modules/kyc/kyc.module';
1623
import { CronJobModule } from './modules/cron-job/cron-job.module';
1724
import { PayoutModule } from './modules/payout/payout.module';
18-
import { EnvConfigModule } from './config/config.module';
19-
import { ExceptionFilter } from './common/filters/exception.filter';
2025
import { QualificationModule } from './modules/qualification/qualification.module';
2126
import { EscrowCompletionModule } from './modules/escrow-completion/escrow-completion.module';
2227
import { WebhookIncomingModule } from './modules/webhook/webhook-incoming.module';
2328
import { WebhookOutgoingModule } from './modules/webhook/webhook-outgoing.module';
24-
import { UserModule } from './modules/user/user.module';
29+
import { UserModule } from './modules/user';
2530
import { EmailModule } from './modules/email/module';
2631
import { NDAModule } from './modules/nda/nda.module';
2732
import { StorageModule } from './modules/storage/storage.module';
33+
2834
import Environment from './utils/environment';
2935

3036
@Module({
3137
providers: [
38+
{
39+
provide: APP_GUARD,
40+
useClass: JwtAuthGuard,
41+
},
3242
{
3343
provide: APP_PIPE,
3444
useClass: HttpValidationPipe,
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { SetMetadata } from '@nestjs/common';
2-
import { Role } from '../enums/user';
1+
import { Reflector } from '@nestjs/core';
2+
import { UserRole } from '../../modules/user';
33

4-
export const Public = (): ((target: any, key?: any, descriptor?: any) => any) =>
5-
SetMetadata('isPublic', true);
4+
export const Public = Reflector.createDecorator<boolean>({
5+
key: 'isPublic',
6+
transform: () => true,
7+
});
68

7-
export const ROLES_KEY = 'roles';
8-
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
9+
export const Roles = Reflector.createDecorator<UserRole[]>({
10+
key: 'roles',
11+
});

packages/apps/reputation-oracle/server/src/common/enums/user.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
export enum UserStatus {
2-
ACTIVE = 'active',
3-
INACTIVE = 'inactive',
4-
PENDING = 'pending',
5-
}
6-
7-
export enum Role {
8-
OPERATOR = 'operator',
9-
WORKER = 'worker',
10-
HUMAN_APP = 'human_app',
11-
ADMIN = 'admin',
12-
}
13-
141
export enum KycStatus {
152
NONE = 'none',
163
APPROVED = 'approved',

packages/apps/reputation-oracle/server/src/common/filters/exception.filter.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { Request, Response } from 'express';
99
import { DatabaseError } from '../errors/database';
1010
import logger from '../../logger';
11+
import { transformKeysFromCamelToSnake } from '../../utils/case-converters';
1112

1213
@Catch()
1314
export class ExceptionFilter implements IExceptionFilter {
@@ -33,8 +34,24 @@ export class ExceptionFilter implements IExceptionFilter {
3334
const exceptionResponse = exception.getResponse();
3435
if (typeof exceptionResponse === 'string') {
3536
responseBody.message = exceptionResponse;
37+
} else if (
38+
'error' in exceptionResponse &&
39+
exceptionResponse.error === exception.message
40+
) {
41+
/**
42+
* This is the case for "sugar" exception classes
43+
* (e.g. UnauthorizedException) that have custom message
44+
*/
45+
responseBody.message = exceptionResponse.error;
3646
} else {
37-
Object.assign(responseBody, exceptionResponse);
47+
/**
48+
* Exception filters called after interceptors,
49+
* so it's just a safety belt
50+
*/
51+
Object.assign(
52+
responseBody,
53+
transformKeysFromCamelToSnake(exceptionResponse),
54+
);
3855
}
3956
} else {
4057
this.logger.error('Unhandled exception', exception);
Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
CanActivate,
32
ExecutionContext,
43
HttpException,
54
HttpStatus,
@@ -8,30 +7,45 @@ import {
87
import { Reflector } from '@nestjs/core';
98
import { AuthGuard } from '@nestjs/passport';
109
import { JWT_STRATEGY_NAME } from '../constants';
10+
import { Public } from '../decorators';
1111

1212
@Injectable()
13-
export class JwtAuthGuard
14-
extends AuthGuard(JWT_STRATEGY_NAME)
15-
implements CanActivate
16-
{
13+
export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) {
1714
constructor(private readonly reflector: Reflector) {
1815
super();
1916
}
2017

21-
public async canActivate(context: ExecutionContext): Promise<boolean> {
22-
// `super` has to be called to set `user` on `request`
23-
// see https://github.com/nestjs/passport/blob/master/lib/auth.guard.ts
24-
return (super.canActivate(context) as Promise<boolean>).catch((_error) => {
25-
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
26-
context.getHandler(),
27-
context.getClass(),
28-
]);
18+
canActivate(context: ExecutionContext) {
19+
const isPublic = this.reflector.getAllAndOverride(Public, [
20+
context.getHandler(),
21+
context.getClass(),
22+
]);
2923

30-
if (isPublic) {
31-
return true;
32-
}
24+
if (isPublic) {
25+
return true;
26+
}
3327

28+
return super.canActivate(context);
29+
}
30+
31+
handleRequest(error: any, user: any) {
32+
if (error) {
33+
/**
34+
* Error happened while "validate" in "passport" strategy
35+
*/
36+
throw error;
37+
}
38+
39+
/**
40+
* There is no error and user in different cases, e.g.:
41+
* - jwt is not provided - strategy does not validate it
42+
* - token is expired
43+
* - etc.
44+
*/
45+
if (!user) {
3446
throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
35-
});
47+
}
48+
49+
return user;
3650
}
3751
}

packages/apps/reputation-oracle/server/src/common/guards/roles.auth.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,34 @@ import {
66
HttpException,
77
} from '@nestjs/common';
88
import { Reflector } from '@nestjs/core';
9-
import { Role } from '../enums/user';
10-
import { ROLES_KEY } from '../decorators';
9+
import { Roles } from '../decorators';
1110

1211
@Injectable()
1312
export class RolesAuthGuard implements CanActivate {
1413
constructor(private reflector: Reflector) {}
1514

1615
canActivate(context: ExecutionContext): boolean {
17-
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
16+
const allowedRoles = this.reflector.getAllAndOverride(Roles, [
1817
context.getHandler(),
1918
context.getClass(),
2019
]);
2120

22-
if (!requiredRoles) {
23-
return true;
21+
/**
22+
* We don't use this guard globally, only on specific routes,
23+
* so it's just a safety belt
24+
*/
25+
if (!allowedRoles?.length) {
26+
throw new Error(
27+
'Allowed roles must be specified when using RolesAuthGuard',
28+
);
2429
}
2530

2631
const { user } = context.switchToHttp().getRequest();
27-
const isAllowed = requiredRoles.some((role) => user.role === role);
2832

29-
if (isAllowed) {
33+
if (allowedRoles.includes(user.role)) {
3034
return true;
3135
}
3236

33-
throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
37+
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
3438
}
3539
}

packages/apps/reputation-oracle/server/src/common/interfaces/user.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/apps/reputation-oracle/server/src/modules/auth/auth.controller.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
2-
ApiBearerAuth,
32
ApiOperation,
43
ApiResponse,
54
ApiTags,
65
ApiBody,
6+
ApiBearerAuth,
77
} from '@nestjs/swagger';
88
import {
99
Body,
@@ -18,7 +18,6 @@ import {
1818
} from '@nestjs/common';
1919
import { Public } from '../../common/decorators';
2020
import { AuthService } from './auth.service';
21-
import { JwtAuthGuard } from '../../common/guards';
2221
import { RequestWithUser } from '../../common/interfaces/request';
2322
import { HCaptchaGuard } from '../../integrations/hcaptcha/hcaptcha.guard';
2423
import { TokenRepository } from './token.repository';
@@ -147,7 +146,7 @@ export class AuthController {
147146
description: 'Verification email resent successfully',
148147
})
149148
@ApiBearerAuth()
150-
@UseGuards(HCaptchaGuard, JwtAuthGuard)
149+
@UseGuards(HCaptchaGuard)
151150
@Post('/web2/resend-verification-email')
152151
@HttpCode(200)
153152
async resendEmailVerification(
@@ -223,7 +222,6 @@ export class AuthController {
223222
description: 'User logged out successfully',
224223
})
225224
@ApiBearerAuth()
226-
@UseGuards(JwtAuthGuard)
227225
@Post('/logout')
228226
@HttpCode(200)
229227
async logout(@Req() request: RequestWithUser): Promise<void> {

packages/apps/reputation-oracle/server/src/modules/auth/auth.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { JwtModule } from '@nestjs/jwt';
44
import { AuthConfigService } from '../../config/auth-config.service';
55
import { HCaptchaModule } from '../../integrations/hcaptcha/hcaptcha.module';
66
import { EmailModule } from '../email/module';
7-
import { UserModule } from '../user/user.module';
7+
import { UserModule } from '../user';
88
import { Web3Module } from '../web3/web3.module';
99

1010
import { JwtHttpStrategy } from './strategy';

packages/apps/reputation-oracle/server/src/modules/auth/auth.service.spec.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import { HCaptchaConfigService } from '../../config/hcaptcha-config.service';
2727
import { ServerConfigService } from '../../config/server-config.service';
2828
import { Web3ConfigService } from '../../config/web3-config.service';
2929
import { JobRequestType } from '../../common/enums';
30-
import { Role, UserStatus } from '../../common/enums/user';
3130
import { SignatureType } from '../../common/enums/web3';
3231
import {
3332
generateNonce,
@@ -37,9 +36,13 @@ import {
3736
import { HCaptchaService } from '../../integrations/hcaptcha/hcaptcha.service';
3837
import { SiteKeyRepository } from '../user/site-key.repository';
3938
import { PrepareSignatureDto } from '../user/user.dto';
40-
import { UserEntity } from '../user/user.entity';
41-
import { UserRepository } from '../user/user.repository';
42-
import { UserService } from '../user/user.service';
39+
import {
40+
UserStatus,
41+
UserRole,
42+
UserEntity,
43+
UserRepository,
44+
UserService,
45+
} from '../user';
4346
import { Web3Service } from '../web3/web3.service';
4447
import {
4548
AuthError,
@@ -730,7 +733,7 @@ describe('AuthService', () => {
730733

731734
const result = await authService.web3Signup({
732735
address: web3PreSignUpDto.address,
733-
type: Role.WORKER,
736+
type: UserRole.WORKER,
734737
signature,
735738
});
736739

@@ -754,7 +757,7 @@ describe('AuthService', () => {
754757
await expect(
755758
authService.web3Signup({
756759
...web3PreSignUpDto,
757-
type: Role.WORKER,
760+
type: UserRole.WORKER,
758761
signature: invalidSignature,
759762
}),
760763
).rejects.toThrow(
@@ -769,7 +772,7 @@ describe('AuthService', () => {
769772
await expect(
770773
authService.web3Signup({
771774
...web3PreSignUpDto,
772-
type: Role.WORKER,
775+
type: UserRole.WORKER,
773776
signature: signature,
774777
}),
775778
).rejects.toThrow(new InvalidOperatorRoleError(''));
@@ -784,7 +787,7 @@ describe('AuthService', () => {
784787
await expect(
785788
authService.web3Signup({
786789
...web3PreSignUpDto,
787-
type: Role.WORKER,
790+
type: UserRole.WORKER,
788791
signature: signature,
789792
}),
790793
).rejects.toThrow(new InvalidOperatorFeeError(''));
@@ -800,7 +803,7 @@ describe('AuthService', () => {
800803
await expect(
801804
authService.web3Signup({
802805
...web3PreSignUpDto,
803-
type: Role.WORKER,
806+
type: UserRole.WORKER,
804807
signature: signature,
805808
}),
806809
).rejects.toThrow(new InvalidOperatorUrlError(''));
@@ -817,7 +820,7 @@ describe('AuthService', () => {
817820
await expect(
818821
authService.web3Signup({
819822
...web3PreSignUpDto,
820-
type: Role.WORKER,
823+
type: UserRole.WORKER,
821824
signature: signature,
822825
}),
823826
).rejects.toThrow(new InvalidOperatorJobTypesError(''));

0 commit comments

Comments
 (0)