Skip to content

Commit f7f28a1

Browse files
committed
Merge branch 'dev' of github.com:topcoder-platform/tc-finance-api into dev
# Conflicts: # src/api/admin/admin.controller.ts # src/api/admin/admin.service.ts
2 parents f260de7 + 409b628 commit f7f28a1

11 files changed

Lines changed: 234 additions & 75 deletions

src/api/admin/admin.controller.ts

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

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

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

3837
@ApiTags('AdminWinnings')
3938
@Controller('/admin')
@@ -45,20 +44,14 @@ export class AdminController {
4544
private readonly adminService: AdminService,
4645
private readonly winningsRepo: WinningsRepository,
4746
private readonly tcMembersService: TopcoderMembersService,
47+
private readonly accessControlService: AccessControlService,
4848
) {}
4949

50-
private isBaAdmin(user?: { roles?: string[] }) {
51-
return (user?.roles || []).some(
52-
(r) =>
53-
r &&
54-
r.trim().toLowerCase() === Role.PaymentBaAdmin.trim().toLowerCase(),
55-
);
56-
}
57-
5850
@Post('/winnings/search')
5951
@Roles(
6052
Role.PaymentAdmin,
6153
Role.PaymentBaAdmin,
54+
Role.EngagementPaymentApprover,
6255
Role.PaymentEditor,
6356
Role.PaymentViewer,
6457
)
@@ -80,13 +73,14 @@ export class AdminController {
8073
@Body() body: WinningRequestDto,
8174
@User() user: any,
8275
): Promise<ResponseDto<SearchWinningResult>> {
83-
const result = await this.winningsRepo.searchWinnings(
84-
await this.adminService.applyBaAdminUserFilters(
76+
const filters =
77+
await this.accessControlService.applyFilters<WinningRequestDto>(
8578
user.id,
86-
this.isBaAdmin(user),
79+
user.roles,
8780
body,
88-
),
89-
);
81+
);
82+
83+
const result = await this.winningsRepo.searchWinnings(filters);
9084

9185
if (result.error) {
9286
result.status = ResponseStatusType.ERROR;
@@ -101,6 +95,7 @@ export class AdminController {
10195
@Roles(
10296
Role.PaymentAdmin,
10397
Role.PaymentBaAdmin,
98+
Role.EngagementPaymentApprover,
10499
Role.PaymentEditor,
105100
Role.PaymentViewer,
106101
)
@@ -121,15 +116,16 @@ export class AdminController {
121116
@Header('Content-Type', 'text/csv')
122117
@Header('Content-Disposition', 'attachment; filename="winnings.csv"')
123118
async exportWinnings(@Body() body: WinningRequestDto, @User() user: any) {
124-
const baseFilters = await this.adminService.applyBaAdminUserFilters(
125-
user.id,
126-
this.isBaAdmin(user),
127-
{
128-
...body,
129-
limit: undefined,
130-
offset: undefined,
131-
},
132-
);
119+
const baseFilters =
120+
await this.accessControlService.applyFilters<WinningRequestDto>(
121+
user.id,
122+
user.roles,
123+
{
124+
...body,
125+
limit: undefined,
126+
offset: undefined,
127+
},
128+
);
133129

134130
const winnings: SearchWinningResult['winnings'] = [];
135131
let offset = 0;
@@ -214,7 +210,12 @@ export class AdminController {
214210
}
215211

216212
@Patch('/winnings')
217-
@Roles(Role.PaymentAdmin, Role.PaymentBaAdmin, Role.PaymentEditor)
213+
@Roles(
214+
Role.PaymentAdmin,
215+
Role.PaymentBaAdmin,
216+
Role.EngagementPaymentApprover,
217+
Role.PaymentEditor,
218+
)
218219
@ApiOperation({
219220
summary: 'Update winnings with given parameter',
220221
description:
@@ -227,7 +228,7 @@ export class AdminController {
227228
})
228229
async updateWinning(
229230
@Body() body: WinningUpdateRequestDto,
230-
@User() user: UserInfo,
231+
@User() user: any,
231232
): Promise<ResponseDto<string>> {
232233
if (
233234
!body.paymentAmount &&
@@ -243,7 +244,7 @@ export class AdminController {
243244
const result = await this.adminService.updateWinnings(
244245
body,
245246
user.id,
246-
this.isBaAdmin(user),
247+
user.roles,
247248
);
248249

249250
result.status = ResponseStatusType.SUCCESS;
@@ -258,6 +259,7 @@ export class AdminController {
258259
@Roles(
259260
Role.PaymentAdmin,
260261
Role.PaymentBaAdmin,
262+
Role.EngagementPaymentApprover,
261263
Role.PaymentEditor,
262264
Role.PaymentViewer,
263265
)
@@ -279,9 +281,11 @@ export class AdminController {
279281
@Param('winningID') winningId: string,
280282
@User() user: any,
281283
): Promise<ResponseDto<WinningAuditDto[]>> {
282-
if (this.isBaAdmin(user)) {
283-
await this.adminService.verifyBaAdminAccessToWinning(winningId, user.id);
284-
}
284+
await this.adminService.verifyUserAccessToWinning(
285+
winningId,
286+
user.id,
287+
user.roles,
288+
);
285289

286290
const result = await this.adminService.getWinningAudit(winningId);
287291

@@ -297,6 +301,7 @@ export class AdminController {
297301
@Roles(
298302
Role.PaymentAdmin,
299303
Role.PaymentBaAdmin,
304+
Role.EngagementPaymentApprover,
300305
Role.PaymentEditor,
301306
Role.PaymentViewer,
302307
)
@@ -319,9 +324,11 @@ export class AdminController {
319324
@Param('winningID') winningId: string,
320325
@User() user: any,
321326
): Promise<ResponseDto<AuditPayoutDto[]>> {
322-
if (this.isBaAdmin(user)) {
323-
await this.adminService.verifyBaAdminAccessToWinning(winningId, user.id);
324-
}
327+
await this.adminService.verifyUserAccessToWinning(
328+
winningId,
329+
user.id,
330+
user.roles,
331+
);
325332

326333
const result = await this.adminService.getWinningAuditPayout(winningId);
327334

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: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,20 @@ 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';
16-
import { TopcoderChallengesService } from 'src/shared/topcoder/challenges.service';
17-
import { Logger, getBaClient } from 'src/shared/global';
18-
import { WinningRequestDto } from 'src/dto/winning.dto';
19-
20-
function formatDate(date = new Date()) {
21-
const pad = (n, z = 2) => String(n).padStart(z, '0');
22-
23-
return (
24-
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
25-
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.` +
26-
`${pad(date.getMilliseconds(), 3)}`
27-
);
28-
}
18+
import { Logger } from 'src/shared/global';
19+
import { BillingAccountsService } from 'src/shared/topcoder/billing-accounts.service';
2920

3021
/**
3122
* The admin winning service.
@@ -41,33 +32,20 @@ export class AdminService {
4132
constructor(
4233
private readonly prisma: PrismaService,
4334
private readonly paymentsService: PaymentsService,
44-
private readonly tcChallengesService: TopcoderChallengesService,
35+
private readonly baService: BillingAccountsService,
36+
private readonly accessControlService: AccessControlService,
4537
) {}
4638

47-
async getBillingAccountsForUser(userId: string): Promise<string[]> {
48-
const baPrisma = getBaClient();
49-
const baRows = await baPrisma.billingAccountAccess.findMany({
50-
where: {
51-
userId,
52-
},
53-
});
54-
55-
return baRows.map((r) => `${r.billingAccountId}`);
56-
}
57-
58-
async applyBaAdminUserFilters(
39+
async verifyUserAccessToWinning(
40+
winningsId: string,
5941
userId: string,
60-
isBaAdmin?: boolean,
61-
filters: WinningRequestDto = {},
62-
) {
63-
if (!isBaAdmin) {
64-
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');
6548
}
66-
67-
return {
68-
...filters,
69-
billingAccounts: await this.getBillingAccountsForUser(userId),
70-
};
7149
}
7250

7351
private getWinningById(winningId: string) {
@@ -117,7 +95,7 @@ export class AdminService {
11795
return;
11896
}
11997

120-
const allowedBAs = await this.getBillingAccountsForUser(userId);
98+
const allowedBAs = await this.baService.getBillingAccountsForUser(userId);
12199
const paymentBAs = payments
122100
.map((p) => p.billing_account)
123101
.filter((b) => b !== null && b !== undefined);
@@ -142,7 +120,7 @@ export class AdminService {
142120
async updateWinnings(
143121
body: WinningUpdateRequestDto,
144122
userId: string,
145-
isBaAdmin?: boolean,
123+
roles: string[] = [],
146124
): Promise<ResponseDto<string>> {
147125
const result = new ResponseDto<string>();
148126

@@ -153,9 +131,7 @@ export class AdminService {
153131
);
154132
this.logger.log(`updateWinnings payload: ${JSON.stringify(body)}`);
155133

156-
if (isBaAdmin) {
157-
await this.verifyBaAdminAccessToWinning(body.winningsId, userId);
158-
}
134+
await this.verifyUserAccessToWinning(body.winningsId, userId, roles);
159135

160136
try {
161137
const payments = await this.getPaymentsByWinningsId(

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',
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)