Skip to content

Commit 1be8d60

Browse files
committed
PM-3697 - handle EngagementPaymentApprover role
1 parent a4617c8 commit 1be8d60

12 files changed

Lines changed: 227 additions & 46 deletions

src/api/admin/admin.controller.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,14 @@ import { TopcoderMembersService } from 'src/shared/topcoder/members.service';
2424
import { Role } from 'src/core/auth/auth.constants';
2525
import { Roles, User } from 'src/core/auth/decorators';
2626

27-
import { UserInfo } from 'src/dto/user.type';
28-
2927
import { AdminService } from './admin.service';
3028
import { ResponseDto, ResponseStatusType } from 'src/dto/api-response.dto';
3129
import { WinningAuditDto, AuditPayoutDto } from './dto/audit.dto';
3230

3331
import { WinningRequestDto, SearchWinningResult } from 'src/dto/winning.dto';
3432
import { WinningsRepository } from '../repository/winnings.repo';
3533
import { WinningUpdateRequestDto } from './dto/winnings.dto';
34+
import { AccessControlService } from 'src/shared/access-control';
3635

3736
@ApiTags('AdminWinnings')
3837
@Controller('/admin')
@@ -42,20 +41,14 @@ export class AdminController {
4241
private readonly adminService: AdminService,
4342
private readonly winningsRepo: WinningsRepository,
4443
private readonly tcMembersService: TopcoderMembersService,
44+
private readonly accessControlService: AccessControlService,
4545
) {}
4646

47-
private isBaAdmin(user?: { roles?: string[] }) {
48-
return (user?.roles || []).some(
49-
(r) =>
50-
r &&
51-
r.trim().toLowerCase() === Role.PaymentBaAdmin.trim().toLowerCase(),
52-
);
53-
}
54-
5547
@Post('/winnings/search')
5648
@Roles(
5749
Role.PaymentAdmin,
5850
Role.PaymentBaAdmin,
51+
Role.EngagementPaymentApprover,
5952
Role.PaymentEditor,
6053
Role.PaymentViewer,
6154
)
@@ -77,13 +70,14 @@ export class AdminController {
7770
@Body() body: WinningRequestDto,
7871
@User() user: any,
7972
): Promise<ResponseDto<SearchWinningResult>> {
80-
const result = await this.winningsRepo.searchWinnings(
81-
await this.adminService.applyBaAdminUserFilters(
73+
const filters =
74+
await this.accessControlService.applyFilters<WinningRequestDto>(
8275
user.id,
83-
this.isBaAdmin(user),
76+
user.roles,
8477
body,
85-
),
86-
);
78+
);
79+
80+
const result = await this.winningsRepo.searchWinnings(filters);
8781

8882
if (result.error) {
8983
result.status = ResponseStatusType.ERROR;
@@ -98,6 +92,7 @@ export class AdminController {
9892
@Roles(
9993
Role.PaymentAdmin,
10094
Role.PaymentBaAdmin,
95+
Role.EngagementPaymentApprover,
10196
Role.PaymentEditor,
10297
Role.PaymentViewer,
10398
)
@@ -118,16 +113,16 @@ export class AdminController {
118113
@Header('Content-Type', 'text/csv')
119114
@Header('Content-Disposition', 'attachment; filename="winnings.csv"')
120115
async exportWinnings(@Body() body: WinningRequestDto, @User() user: any) {
121-
const result = await this.winningsRepo.searchWinnings(
122-
await this.adminService.applyBaAdminUserFilters(
116+
const filters =
117+
await this.accessControlService.applyFilters<WinningRequestDto>(
123118
user.id,
124-
this.isBaAdmin(user),
119+
user.roles,
125120
{
126121
...body,
127122
limit: 999,
128123
},
129-
),
130-
);
124+
);
125+
const result = await this.winningsRepo.searchWinnings(filters);
131126

132127
const handles = await this.tcMembersService.getHandlesByUserIds(
133128
result.data.winnings.map((d) => d.winnerId),
@@ -181,7 +176,12 @@ export class AdminController {
181176
}
182177

183178
@Patch('/winnings')
184-
@Roles(Role.PaymentAdmin, Role.PaymentBaAdmin, Role.PaymentEditor)
179+
@Roles(
180+
Role.PaymentAdmin,
181+
Role.PaymentBaAdmin,
182+
Role.EngagementPaymentApprover,
183+
Role.PaymentEditor,
184+
)
185185
@ApiOperation({
186186
summary: 'Update winnings with given parameter',
187187
description:
@@ -194,7 +194,7 @@ export class AdminController {
194194
})
195195
async updateWinning(
196196
@Body() body: WinningUpdateRequestDto,
197-
@User() user: UserInfo,
197+
@User() user: any,
198198
): Promise<ResponseDto<string>> {
199199
if (
200200
!body.paymentAmount &&
@@ -210,7 +210,7 @@ export class AdminController {
210210
const result = await this.adminService.updateWinnings(
211211
body,
212212
user.id,
213-
this.isBaAdmin(user),
213+
user.roles,
214214
);
215215

216216
result.status = ResponseStatusType.SUCCESS;
@@ -225,6 +225,7 @@ export class AdminController {
225225
@Roles(
226226
Role.PaymentAdmin,
227227
Role.PaymentBaAdmin,
228+
Role.EngagementPaymentApprover,
228229
Role.PaymentEditor,
229230
Role.PaymentViewer,
230231
)
@@ -246,9 +247,11 @@ export class AdminController {
246247
@Param('winningID') winningId: string,
247248
@User() user: any,
248249
): Promise<ResponseDto<WinningAuditDto[]>> {
249-
if (this.isBaAdmin(user)) {
250-
await this.adminService.verifyBaAdminAccessToWinning(winningId, user.id);
251-
}
250+
await this.adminService.verifyUserAccessToWinning(
251+
winningId,
252+
user.id,
253+
user.roles,
254+
);
252255

253256
const result = await this.adminService.getWinningAudit(winningId);
254257

@@ -264,6 +267,7 @@ export class AdminController {
264267
@Roles(
265268
Role.PaymentAdmin,
266269
Role.PaymentBaAdmin,
270+
Role.EngagementPaymentApprover,
267271
Role.PaymentEditor,
268272
Role.PaymentViewer,
269273
)
@@ -286,9 +290,11 @@ export class AdminController {
286290
@Param('winningID') winningId: string,
287291
@User() user: any,
288292
): Promise<ResponseDto<AuditPayoutDto[]>> {
289-
if (this.isBaAdmin(user)) {
290-
await this.adminService.verifyBaAdminAccessToWinning(winningId, user.id);
291-
}
293+
await this.adminService.verifyUserAccessToWinning(
294+
winningId,
295+
user.id,
296+
user.roles,
297+
);
292298

293299
const result = await this.adminService.getWinningAuditPayout(winningId);
294300

src/api/admin/admin.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { AdminService } from './admin.service';
44
import { WinningsRepository } from '../repository/winnings.repo';
55
import { TopcoderModule } from 'src/shared/topcoder/topcoder.module';
66
import { PaymentsModule } from 'src/shared/payments';
7+
import { AccessControlModule } from 'src/shared/access-control';
78

89
@Module({
9-
imports: [TopcoderModule, PaymentsModule],
10+
imports: [TopcoderModule, PaymentsModule, AccessControlModule],
1011
controllers: [AdminController],
1112
providers: [AdminService, WinningsRepository],
1213
})

src/api/admin/admin.service.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ import {
33
HttpStatus,
44
NotFoundException,
55
BadRequestException,
6+
UnauthorizedException,
67
} from '@nestjs/common';
78

89
import { Prisma } from '@prisma/client';
910
import { PrismaService } from 'src/shared/global/prisma.service';
1011
import { PaymentsService } from 'src/shared/payments';
12+
import { AccessControlService } from 'src/shared/access-control/access-control.service';
1113

1214
import { ResponseDto } from 'src/dto/api-response.dto';
1315
import { PaymentStatus } from 'src/dto/payment.dto';
1416
import { WinningAuditDto, AuditPayoutDto } from './dto/audit.dto';
1517
import { WinningUpdateRequestDto } from './dto/winnings.dto';
1618
import { Logger } from 'src/shared/global';
17-
import { WinningRequestDto } from 'src/dto/winning.dto';
1819
import { BillingAccountsService } from 'src/shared/topcoder/billing-accounts.service';
1920

2021
/**
@@ -32,21 +33,19 @@ export class AdminService {
3233
private readonly prisma: PrismaService,
3334
private readonly paymentsService: PaymentsService,
3435
private readonly baService: BillingAccountsService,
36+
private readonly accessControlService: AccessControlService,
3537
) {}
3638

37-
async applyBaAdminUserFilters(
39+
async verifyUserAccessToWinning(
40+
winningsId: string,
3841
userId: string,
39-
isBaAdmin?: boolean,
40-
filters: WinningRequestDto = {},
41-
) {
42-
if (!isBaAdmin) {
43-
return filters;
42+
roles: string[] = [],
43+
): Promise<void> {
44+
try {
45+
await this.accessControlService.verifyAccess(winningsId, userId, roles);
46+
} catch (err) {
47+
throw new UnauthorizedException(err?.message ?? 'access denied');
4448
}
45-
46-
return {
47-
...filters,
48-
billingAccounts: await this.baService.getBillingAccountsForUser(userId),
49-
};
5049
}
5150

5251
private getWinningById(winningId: string) {
@@ -121,7 +120,7 @@ export class AdminService {
121120
async updateWinnings(
122121
body: WinningUpdateRequestDto,
123122
userId: string,
124-
isBaAdmin?: boolean,
123+
roles: string[] = [],
125124
): Promise<ResponseDto<string>> {
126125
const result = new ResponseDto<string>();
127126

@@ -132,9 +131,7 @@ export class AdminService {
132131
);
133132
this.logger.log(`updateWinnings payload: ${JSON.stringify(body)}`);
134133

135-
if (isBaAdmin) {
136-
await this.verifyBaAdminAccessToWinning(body.winningsId, userId);
137-
}
134+
await this.verifyUserAccessToWinning(body.winningsId, userId, roles);
138135

139136
try {
140137
const payments = await this.getPaymentsByWinningsId(

src/api/repository/winnings.repo.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,17 @@ export class WinningsRepository {
196196
};
197197
}
198198

199+
if ((searchProps as any).paymentMethodTypes) {
200+
(queryWhere as any).payment.some = {
201+
...queryWhere.payment!.some,
202+
payment_method: {
203+
payment_method_type: {
204+
in: (searchProps as any).paymentMethodTypes,
205+
},
206+
},
207+
};
208+
}
209+
199210
const orderBy = this.getOrderByWithWinnerId(
200211
searchProps.sortBy,
201212
searchProps.sortOrder,

src/core/auth/auth.constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export enum Role {
22
Administrator = 'Administrator',
33
PaymentAdmin = 'Payment Admin',
44
PaymentBaAdmin = 'Payment BA Admin',
5+
EngagementPaymentApprover = 'Engagement Payment Approver',
56
PaymentEditor = 'Payment Editor',
67
PaymentViewer = 'Payment Viewer',
78
TaskManager = 'Task Manager',

src/dto/winning.dto.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,16 @@ export class WinningRequestDto extends SortPagination {
200200
@IsString({ each: true })
201201
@IsNotEmpty({ each: true })
202202
billingAccounts?: string[];
203+
204+
@ApiProperty({
205+
description: 'Filter by payment method types (e.g. taas)',
206+
example: ['taas'],
207+
required: false,
208+
})
209+
@IsOptional()
210+
@IsArray()
211+
@IsString({ each: true })
212+
paymentMethodTypes?: string[];
203213
}
204214

205215
export class WinningCreateRequestDto {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Module } from '@nestjs/common';
2+
import { AccessControlService } from 'src/shared/access-control/access-control.service';
3+
import { PaymentBaProvider } from 'src/shared/access-control/payment-ba.provider';
4+
import { EngagementPaymentApproverProvider } from 'src/shared/access-control/engagement-pa.provider';
5+
import { Injectable } from '@nestjs/common';
6+
import { TopcoderModule } from '../topcoder/topcoder.module';
7+
8+
@Injectable()
9+
class AccessControlRegistrar {
10+
constructor(
11+
accessControlService: AccessControlService,
12+
paymentBaProvider: PaymentBaProvider,
13+
engagementPaymentApproverProvider: EngagementPaymentApproverProvider,
14+
) {
15+
accessControlService.register(paymentBaProvider);
16+
accessControlService.register(engagementPaymentApproverProvider);
17+
}
18+
}
19+
20+
@Module({
21+
imports: [TopcoderModule],
22+
controllers: [],
23+
providers: [
24+
AccessControlService,
25+
PaymentBaProvider,
26+
EngagementPaymentApproverProvider,
27+
AccessControlRegistrar,
28+
],
29+
exports: [AccessControlService],
30+
})
31+
export class AccessControlModule {}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { RoleAccessProvider } from './role-access.interface';
3+
4+
@Injectable()
5+
export class AccessControlService {
6+
private providers = new Map<string, RoleAccessProvider<any>>();
7+
8+
register(provider: RoleAccessProvider) {
9+
this.providers.set(provider.roleName.trim().toLowerCase(), provider);
10+
}
11+
12+
async applyFilters<T>(
13+
userId: string,
14+
roles: string[] = [],
15+
req: any,
16+
): Promise<T> {
17+
let out = { ...req };
18+
for (const r of roles || []) {
19+
const p = this.providers.get(r?.trim().toLowerCase());
20+
if (p?.applyFilter) {
21+
out = await p.applyFilter(userId, out);
22+
}
23+
}
24+
return out as T;
25+
}
26+
27+
async verifyAccess(resourceId: string, userId: string, roles: string[] = []) {
28+
for (const r of roles || []) {
29+
const p = this.providers.get(r?.trim().toLowerCase());
30+
if (p?.verifyAccessToResource) {
31+
await p.verifyAccessToResource(resourceId, userId);
32+
}
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)