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
2 changes: 1 addition & 1 deletion .github/workflows/ci-test-subgraph.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ jobs:
- name: Build core package
run: yarn build:core
- name: Generate manifest for Polygon for tests
- run: NETWORK=polygon yarn workspace @human-protocol/subgraph generate
run: NETWORK=polygon yarn workspace @human-protocol/subgraph generate
- name: Run subgraph test
run: yarn workspace @human-protocol/subgraph test
3 changes: 2 additions & 1 deletion packages/apps/fortune/exchange-oracle/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
"axios": "^1.3.1",
"class-transformer": "^0.5.1",
"class-validator": "0.14.1",
"ethers": "~6.13.5",
"joi": "^17.13.3",
"pg": "8.13.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.2.0",
"typeorm": "^0.3.23",
Expand All @@ -60,7 +62,6 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"jest": "29.7.0",
"pg": "8.13.1",
"prettier": "^3.4.2",
"source-map-support": "^0.5.20",
"supertest": "^7.0.0",
Expand Down
32 changes: 21 additions & 11 deletions packages/apps/fortune/exchange-oracle/server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { JobModule } from './modules/job/job.module';
import { ConfigModule } from '@nestjs/config';
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
import { ScheduleModule } from '@nestjs/schedule';
import { AppController } from './app.controller';
import { envValidator } from './common/config';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { EnvConfigModule } from './common/config/config.module';
import { ExceptionFilter } from './common/exceptions/exception.filter';
import { JwtHttpStrategy } from './common/guards/strategy';
import { SnakeCaseInterceptor } from './common/interceptors/snake-case';
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
import { DatabaseModule } from './database/database.module';
import { WebhookModule } from './modules/webhook/webhook.module';
import { JwtHttpStrategy } from './common/guards/strategy';
import { Web3Module } from './modules/web3/web3.module';
import { UserModule } from './modules/user/user.module';
import { StatsModule } from './modules/stats/stats.module';
import { AssignmentModule } from './modules/assignment/assignment.module';
import { CronJobModule } from './modules/cron-job/cron-job.module';
import { HealthModule } from './modules/health/health.module';
import { EnvConfigModule } from './common/config/config.module';
import { ScheduleModule } from '@nestjs/schedule';
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
import { JobModule } from './modules/job/job.module';
import { StatsModule } from './modules/stats/stats.module';
import { UserModule } from './modules/user/user.module';
import { Web3Module } from './modules/web3/web3.module';
import { WebhookModule } from './modules/webhook/webhook.module';
import { HttpValidationPipe } from './common/pipes';

@Module({
providers: [
{
provide: APP_PIPE,
useClass: HttpValidationPipe,
},
{
provide: APP_INTERCEPTOR,
useClass: SnakeCaseInterceptor,
Expand All @@ -28,6 +34,10 @@ import { TransformEnumInterceptor } from './common/interceptors/transform-enum.i
provide: APP_INTERCEPTOR,
useClass: TransformEnumInterceptor,
},
{
provide: APP_FILTER,
useClass: ExceptionFilter,
},
JwtHttpStrategy,
],
imports: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { QueryFailedError } from 'typeorm';
import { PostgresErrorCodes } from './database.enum';

export class DatabaseError extends Error {
constructor(message: string, stack: string) {
super(message);
this.stack = stack;
}
}
import { DatabaseError } from '.';
import { PostgresErrorCodes } from '../enums/database';

export function handleQueryFailedError(error: QueryFailedError): DatabaseError {
const stack = error.stack || '';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export * from './database';

export class BaseError extends Error {
constructor(message: string, stack?: string) {
super(message);
this.name = this.constructor.name;
if (stack) {
this.stack = stack;
}
}
}

export class ValidationError extends BaseError {
constructor(message: string, stack?: string) {
super(message, stack);
}
}

export class AuthError extends BaseError {
constructor(message: string, stack?: string) {
super(message, stack);
}
}

export class ForbiddenError extends BaseError {
constructor(message: string, stack?: string) {
super(message, stack);
}
}

export class NotFoundError extends BaseError {
constructor(message: string, stack?: string) {
super(message, stack);
}
}

export class ConflictError extends BaseError {
constructor(message: string, stack?: string) {
super(message, stack);
}
}

export class ServerError extends BaseError {
constructor(message: string, stack?: string) {
super(message, stack);
}
}

export class DatabaseError extends BaseError {
constructor(message: string, stack?: string) {
super(message, stack);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter as IExceptionFilter,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import {
ValidationError,
AuthError,
ForbiddenError,
NotFoundError,
ConflictError,
ServerError,
DatabaseError,
} from '../errors';

@Catch()
export class ExceptionFilter implements IExceptionFilter {
private logger = new Logger(ExceptionFilter.name);

private getStatus(exception: any): number {
if (exception instanceof ValidationError) {
return HttpStatus.BAD_REQUEST;
} else if (exception instanceof AuthError) {
return HttpStatus.UNAUTHORIZED;
} else if (exception instanceof ForbiddenError) {
return HttpStatus.FORBIDDEN;
} else if (exception instanceof NotFoundError) {
return HttpStatus.NOT_FOUND;
} else if (exception instanceof ConflictError) {
return HttpStatus.CONFLICT;
} else if (exception instanceof ServerError) {
return HttpStatus.UNPROCESSABLE_ENTITY;
} else if (exception instanceof DatabaseError) {
return HttpStatus.UNPROCESSABLE_ENTITY;
} else if (exception.statusCode) {
return exception.statusCode;
}
return HttpStatus.INTERNAL_SERVER_ERROR;
}

catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();

const status = this.getStatus(exception);
const message = exception.message || 'Internal server error';

this.logger.error(
`Exception caught: ${message}`,
exception.stack || 'No stack trace available',
);

response.status(status).json({
status_code: status,
timestamp: new Date().toISOString(),
message: message,
path: request.url,
});
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Role } from '../enums/role';
import { AuthError } from '../errors';
import { JwtUser } from '../types/jwt';

@Injectable()
Expand All @@ -27,10 +23,7 @@ export class JwtAuthGuard extends AuthGuard('jwt-http') implements CanActivate {
}

// Try to authenticate with JWT
const canActivate = (await super.canActivate(context)) as boolean;
if (!canActivate) {
throw new UnauthorizedException('JWT authentication failed');
}
await super.canActivate(context);

// Roles verification
let roles = this.reflector.get<Role[]>('roles', context.getHandler());
Expand All @@ -39,11 +32,11 @@ export class JwtAuthGuard extends AuthGuard('jwt-http') implements CanActivate {
const request = context.switchToHttp().getRequest();
const user = request.user as JwtUser;
if (!user) {
throw new UnauthorizedException('User not found in request');
throw new AuthError('User not found in request');
}

if (!roles.includes(user.role)) {
throw new UnauthorizedException('Invalid role');
throw new AuthError('Invalid role');
}

return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { SignatureAuthGuard } from './signature.auth';
import { verifySignature } from '../utils/signature';
import { ChainId, EscrowUtils } from '@human-protocol/sdk';
import { AuthSignatureRole } from '../enums/role';
import { HEADER_SIGNATURE_KEY } from '../constant';
import { AssignmentRepository } from '../../modules/assignment/assignment.repository';
import { ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { AssignmentRepository } from '../../modules/assignment/assignment.repository';
import { HEADER_SIGNATURE_KEY } from '../constant';
import { AuthSignatureRole } from '../enums/role';
import { AuthError, NotFoundError } from '../errors';
import { verifySignature } from '../utils/signature';
import { SignatureAuthGuard } from './signature.auth';

jest.mock('../utils/signature');
jest.mock('@human-protocol/sdk', () => ({
Expand Down Expand Up @@ -84,7 +85,7 @@ describe('SignatureAuthGuard', () => {
);
});

it('should throw UnauthorizedException if signature is not verified', async () => {
it('should throw AuthError if signature is not verified', async () => {
reflector.get = jest
.fn()
.mockReturnValue([AuthSignatureRole.JobLauncher]);
Expand All @@ -96,9 +97,7 @@ describe('SignatureAuthGuard', () => {
};
(verifySignature as jest.Mock).mockReturnValue(false);

await expect(guard.canActivate(context)).rejects.toThrow(
UnauthorizedException,
);
await expect(guard.canActivate(context)).rejects.toThrow(AuthError);
});

it('should handle Worker role and verify signature', async () => {
Expand All @@ -118,7 +117,7 @@ describe('SignatureAuthGuard', () => {
expect(assignmentRepository.findOneById).toHaveBeenCalledWith('1');
});

it('should throw UnauthorizedException if assignment is not found for Worker role', async () => {
it('should throw AuthError if assignment is not found for Worker role', async () => {
reflector.get = jest.fn().mockReturnValue([AuthSignatureRole.Worker]);

mockRequest.headers[HEADER_SIGNATURE_KEY] = 'validSignature';
Expand All @@ -128,9 +127,7 @@ describe('SignatureAuthGuard', () => {
(verifySignature as jest.Mock).mockReturnValue(true);
assignmentRepository.findOneById.mockResolvedValue(null);

await expect(guard.canActivate(context)).rejects.toThrow(
UnauthorizedException,
);
await expect(guard.canActivate(context)).rejects.toThrow(NotFoundError);
});

it('should handle multiple roles and verify signature', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { EscrowUtils } from '@human-protocol/sdk';
import {
CanActivate,
ExecutionContext,
Injectable,
NotImplementedException,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { verifySignature } from '../utils/signature';
import { HEADER_SIGNATURE_KEY } from '../constant';
import { EscrowUtils } from '@human-protocol/sdk';
import { AuthSignatureRole } from '../enums/role';
import { Reflector } from '@nestjs/core';
import { AssignmentRepository } from '../../modules/assignment/assignment.repository';
import { HEADER_SIGNATURE_KEY } from '../constant';
import { ErrorAssignment, ErrorSignature } from '../constant/errors';
import { AuthSignatureRole } from '../enums/role';
import { AuthError, NotFoundError } from '../errors';
import { verifySignature } from '../utils/signature';

@Injectable()
export class SignatureAuthGuard implements CanActivate {
private readonly logger = new Logger(SignatureAuthGuard.name);
constructor(
private reflector: Reflector,
private readonly assignmentRepository: AssignmentRepository,
Expand All @@ -25,7 +26,7 @@ export class SignatureAuthGuard implements CanActivate {
'roles',
context.getHandler(),
);
if (!roles) throw new NotImplementedException(ErrorSignature.MissingRoles);
if (!roles) throw new Error(ErrorSignature.MissingRoles);
const request = context.switchToHttp().getRequest();
const data = request.body;
const signature = request.headers[HEADER_SIGNATURE_KEY];
Expand All @@ -38,7 +39,7 @@ export class SignatureAuthGuard implements CanActivate {
if (assignment) {
oracleAdresses.push(assignment.workerAddress);
} else {
throw new UnauthorizedException(ErrorAssignment.NotFound);
throw new NotFoundError(ErrorAssignment.NotFound);
}
} else {
const escrowData = await EscrowUtils.getEscrow(
Expand All @@ -64,9 +65,9 @@ export class SignatureAuthGuard implements CanActivate {
return true;
}
} catch (error) {
console.error(error);
this.logger.error(error);
}

throw new UnauthorizedException('Unauthorized');
throw new AuthError('Unauthorized');
}
}
Loading
Loading