Skip to content

Commit cb7153a

Browse files
authored
[Job Launcher] Crypto payments for whitelisted users (#2594)
* Crypto payments for whitelisted users * UserId not null in whitelist table
1 parent 2704f85 commit cb7153a

File tree

11 files changed

+178
-0
lines changed

11 files changed

+178
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
CanActivate,
3+
ExecutionContext,
4+
HttpStatus,
5+
Injectable,
6+
} from '@nestjs/common';
7+
import { WhitelistService } from '../../modules/whitelist/whitelist.service';
8+
import { ControlledError } from '../errors/controlled';
9+
10+
@Injectable()
11+
export class WhitelistAuthGuard implements CanActivate {
12+
constructor(private readonly whitelistService: WhitelistService) {}
13+
14+
async canActivate(context: ExecutionContext): Promise<boolean> {
15+
const request = context.switchToHttp().getRequest();
16+
const user = request.user;
17+
18+
if (!user) {
19+
throw new ControlledError('User not found.', HttpStatus.UNAUTHORIZED);
20+
}
21+
22+
const isWhitelisted = await this.whitelistService.isUserWhitelisted(
23+
user.id,
24+
);
25+
if (!isWhitelisted) {
26+
throw new ControlledError('Unauthorized.', HttpStatus.UNAUTHORIZED);
27+
}
28+
29+
return true;
30+
}
31+
}

packages/apps/job-launcher/server/src/database/database.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ApiKeyEntity } from '../modules/auth/apikey.entity';
1515
import { WebhookEntity } from '../modules/webhook/webhook.entity';
1616
import { LoggerOptions } from 'typeorm';
1717
import { CronJobEntity } from '../modules/cron-job/cron-job.entity';
18+
import { WhitelistEntity } from 'src/modules/whitelist/whitelist.entity';
1819

1920
@Module({
2021
imports: [
@@ -47,6 +48,7 @@ import { CronJobEntity } from '../modules/cron-job/cron-job.entity';
4748
PaymentEntity,
4849
WebhookEntity,
4950
CronJobEntity,
51+
WhitelistEntity,
5052
],
5153
// We are using migrations, synchronize should be set to false.
5254
synchronize: false,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class Whitelist1728374018070 implements MigrationInterface {
4+
name = 'Whitelist1728374018070';
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`
8+
CREATE TABLE "hmt"."whitelist" (
9+
"id" SERIAL NOT NULL,
10+
"user_id" integer NOT NULL,
11+
CONSTRAINT "REL_963f4d9041f735ed614cf1f3ac" UNIQUE ("user_id"),
12+
CONSTRAINT "PK_0169bfbd49b0511243f7a068cec" PRIMARY KEY ("id")
13+
)
14+
`);
15+
await queryRunner.query(`
16+
ALTER TABLE "hmt"."whitelist"
17+
ADD CONSTRAINT "FK_963f4d9041f735ed614cf1f3acf" FOREIGN KEY ("user_id") REFERENCES "hmt"."users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION
18+
`);
19+
}
20+
21+
public async down(queryRunner: QueryRunner): Promise<void> {
22+
await queryRunner.query(`
23+
ALTER TABLE "hmt"."whitelist" DROP CONSTRAINT "FK_963f4d9041f735ed614cf1f3acf"
24+
`);
25+
await queryRunner.query(`
26+
DROP TABLE "hmt"."whitelist"
27+
`);
28+
}
29+
}

packages/apps/job-launcher/server/src/modules/payment/payment.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { HEADER_SIGNATURE_KEY } from '../../common/constants';
3030
import { ControlledError } from '../../common/errors/controlled';
3131
import { ServerConfigService } from '../../common/config/server-config.service';
3232
import { RateService } from './rate.service';
33+
import { WhitelistAuthGuard } from 'src/common/guards/whitelist.auth';
3334

3435
@ApiBearerAuth()
3536
@UseGuards(JwtAuthGuard)
@@ -128,6 +129,7 @@ export class PaymentController {
128129
status: 409,
129130
description: 'Conflict. Conflict with the current state of the server.',
130131
})
132+
@UseGuards(WhitelistAuthGuard)
131133
@Post('/crypto')
132134
public async createCryptoPayment(
133135
@Headers(HEADER_SIGNATURE_KEY) signature: string,

packages/apps/job-launcher/server/src/modules/payment/payment.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import { PaymentRepository } from './payment.repository';
1010
import { HttpModule } from '@nestjs/axios';
1111
import { Web3Module } from '../web3/web3.module';
1212
import { RateService } from './rate.service';
13+
import { WhitelistModule } from '../whitelist/whitelist.module';
1314

1415
@Module({
1516
imports: [
1617
HttpModule,
1718
TypeOrmModule.forFeature([PaymentEntity]),
1819
ConfigModule,
1920
Web3Module,
21+
WhitelistModule,
2022
MinioModule.registerAsync({
2123
imports: [ConfigModule],
2224
inject: [ConfigService],

packages/apps/job-launcher/server/src/modules/user/user.entity.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { UserStatus, UserType } from '../../common/enums/user';
88
import { PaymentEntity } from '../payment/payment.entity';
99
import { JobEntity } from '../job/job.entity';
1010
import { ApiKeyEntity } from '../auth/apikey.entity';
11+
import { WhitelistEntity } from '../whitelist/whitelist.entity';
1112

1213
@Entity({ schema: NS, name: 'users' })
1314
export class UserEntity extends BaseEntity implements IUser {
@@ -37,4 +38,9 @@ export class UserEntity extends BaseEntity implements IUser {
3738
nullable: true,
3839
})
3940
public apiKey: ApiKeyEntity;
41+
42+
@OneToOne(() => WhitelistEntity, (whitelist) => whitelist.user, {
43+
nullable: true,
44+
})
45+
public whitelist: WhitelistEntity;
4046
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { NS } from '../../common/constants';
2+
import { Entity, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm';
3+
import { UserEntity } from '../user/user.entity';
4+
5+
@Entity({ schema: NS, name: 'whitelist' })
6+
export class WhitelistEntity {
7+
@PrimaryGeneratedColumn()
8+
id: number;
9+
10+
@JoinColumn()
11+
@OneToOne(() => UserEntity, { nullable: false })
12+
public user: UserEntity;
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { WhitelistService } from './whitelist.service';
4+
import { WhitelistEntity } from './whitelist.entity';
5+
import { WhitelistRepository } from './whitelist.repository';
6+
7+
@Module({
8+
imports: [TypeOrmModule.forFeature([WhitelistEntity])],
9+
providers: [WhitelistService, WhitelistRepository],
10+
exports: [WhitelistService, WhitelistRepository],
11+
})
12+
export class WhitelistModule {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { DataSource } from 'typeorm';
3+
import { BaseRepository } from '../../database/base.repository';
4+
import { WhitelistEntity } from './whitelist.entity';
5+
6+
@Injectable()
7+
export class WhitelistRepository extends BaseRepository<WhitelistEntity> {
8+
constructor(private dataSource: DataSource) {
9+
super(WhitelistEntity, dataSource);
10+
}
11+
12+
public findOneByUserId(userId: number): Promise<WhitelistEntity | null> {
13+
return this.findOne({
14+
where: { user: { id: userId } },
15+
relations: ['user'],
16+
});
17+
}
18+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Test } from '@nestjs/testing';
2+
import { WhitelistService } from './whitelist.service';
3+
import { WhitelistRepository } from './whitelist.repository';
4+
import { createMock } from '@golevelup/ts-jest';
5+
6+
describe('WhitelistService', () => {
7+
let whitelistService: WhitelistService;
8+
let whitelistRepository: WhitelistRepository;
9+
10+
beforeAll(async () => {
11+
const moduleRef = await Test.createTestingModule({
12+
providers: [
13+
WhitelistService,
14+
{
15+
provide: WhitelistRepository,
16+
useValue: createMock<WhitelistRepository>(),
17+
},
18+
],
19+
}).compile();
20+
21+
whitelistService = moduleRef.get<WhitelistService>(WhitelistService);
22+
whitelistRepository =
23+
moduleRef.get<WhitelistRepository>(WhitelistRepository);
24+
});
25+
26+
describe('isUserWhitelisted', () => {
27+
it('should return true if user is whitelisted', async () => {
28+
const userId = 1;
29+
jest
30+
.spyOn(whitelistRepository, 'findOneByUserId')
31+
.mockResolvedValue({ id: 1, user: { id: userId } as any });
32+
33+
const result = await whitelistService.isUserWhitelisted(userId);
34+
35+
expect(whitelistRepository.findOneByUserId).toHaveBeenCalledWith(userId);
36+
expect(result).toBe(true);
37+
});
38+
39+
it('should return false if user is not whitelisted', async () => {
40+
const userId = 2;
41+
jest
42+
.spyOn(whitelistRepository, 'findOneByUserId')
43+
.mockResolvedValue(null);
44+
45+
const result = await whitelistService.isUserWhitelisted(userId);
46+
47+
expect(whitelistRepository.findOneByUserId).toHaveBeenCalledWith(userId);
48+
expect(result).toBe(false);
49+
});
50+
});
51+
});

0 commit comments

Comments
 (0)