-
Notifications
You must be signed in to change notification settings - Fork 1
PM-3575 - handle BA Admin user #138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,8 +44,21 @@ export class AdminController { | |
| private readonly tcMembersService: TopcoderMembersService, | ||
| ) {} | ||
|
|
||
| private isBaAdmin(user?: { roles?: string[] }) { | ||
| return (user?.roles || []).some( | ||
| (r) => | ||
| r && | ||
| r.trim().toLowerCase() === Role.PaymentBaAdmin.trim().toLowerCase(), | ||
| ); | ||
| } | ||
|
|
||
| @Post('/winnings/search') | ||
| @Roles(Role.PaymentAdmin, Role.PaymentEditor, Role.PaymentViewer) | ||
| @Roles( | ||
| Role.PaymentAdmin, | ||
| Role.PaymentBaAdmin, | ||
| Role.PaymentEditor, | ||
| Role.PaymentViewer, | ||
| ) | ||
| @ApiOperation({ | ||
| summary: 'Search winnings with parameters', | ||
| description: 'Roles: Payment Admin, Payment Editor, Payment Viewer', | ||
|
|
@@ -62,8 +75,16 @@ export class AdminController { | |
| @HttpCode(HttpStatus.OK) | ||
| async searchWinnings( | ||
| @Body() body: WinningRequestDto, | ||
| @User() user: any, | ||
|
kkartunov marked this conversation as resolved.
|
||
| ): Promise<ResponseDto<SearchWinningResult>> { | ||
| const result = await this.winningsRepo.searchWinnings(body); | ||
| const result = await this.winningsRepo.searchWinnings( | ||
| await this.adminService.applyBaAdminUserFilters( | ||
|
kkartunov marked this conversation as resolved.
|
||
| user.id, | ||
| this.isBaAdmin(user), | ||
| body, | ||
| ), | ||
| ); | ||
|
|
||
| if (result.error) { | ||
| result.status = ResponseStatusType.ERROR; | ||
| } | ||
|
|
@@ -74,7 +95,12 @@ export class AdminController { | |
| } | ||
|
|
||
| @Post('/winnings/export') | ||
| @Roles(Role.PaymentAdmin, Role.PaymentEditor, Role.PaymentViewer) | ||
| @Roles( | ||
| Role.PaymentAdmin, | ||
| Role.PaymentBaAdmin, | ||
| Role.PaymentEditor, | ||
| Role.PaymentViewer, | ||
| ) | ||
| @ApiOperation({ | ||
| summary: 'Export search winnings result in csv file format', | ||
| description: 'Roles: Payment Admin, Payment Editor, Payment Viewer', | ||
|
|
@@ -91,11 +117,17 @@ export class AdminController { | |
| @HttpCode(HttpStatus.OK) | ||
| @Header('Content-Type', 'text/csv') | ||
| @Header('Content-Disposition', 'attachment; filename="winnings.csv"') | ||
| async exportWinnings(@Body() body: WinningRequestDto) { | ||
| const result = await this.winningsRepo.searchWinnings({ | ||
| ...body, | ||
| limit: 999, | ||
| }); | ||
| async exportWinnings(@Body() body: WinningRequestDto, @User() user: any) { | ||
|
kkartunov marked this conversation as resolved.
|
||
| const result = await this.winningsRepo.searchWinnings( | ||
| await this.adminService.applyBaAdminUserFilters( | ||
| user.id, | ||
| this.isBaAdmin(user), | ||
|
kkartunov marked this conversation as resolved.
|
||
| { | ||
| ...body, | ||
| limit: 999, | ||
| }, | ||
| ), | ||
| ); | ||
|
|
||
| const handles = await this.tcMembersService.getHandlesByUserIds( | ||
| result.data.winnings.map((d) => d.winnerId), | ||
|
|
@@ -149,7 +181,7 @@ export class AdminController { | |
| } | ||
|
|
||
| @Patch('/winnings') | ||
| @Roles(Role.PaymentAdmin, Role.PaymentEditor) | ||
| @Roles(Role.PaymentAdmin, Role.PaymentBaAdmin, Role.PaymentEditor) | ||
|
kkartunov marked this conversation as resolved.
|
||
| @ApiOperation({ | ||
| summary: 'Update winnings with given parameter', | ||
| description: | ||
|
|
@@ -175,7 +207,11 @@ export class AdminController { | |
| ); | ||
| } | ||
|
|
||
| const result = await this.adminService.updateWinnings(body, user.id); | ||
| const result = await this.adminService.updateWinnings( | ||
| body, | ||
| user.id, | ||
| this.isBaAdmin(user), | ||
|
vas3a marked this conversation as resolved.
|
||
| ); | ||
|
|
||
| result.status = ResponseStatusType.SUCCESS; | ||
| if (result.error) { | ||
|
|
@@ -186,7 +222,12 @@ export class AdminController { | |
| } | ||
|
|
||
| @Get('/winnings/:winningID/audit') | ||
| @Roles(Role.PaymentAdmin, Role.PaymentEditor, Role.PaymentViewer) | ||
| @Roles( | ||
| Role.PaymentAdmin, | ||
| Role.PaymentBaAdmin, | ||
| Role.PaymentEditor, | ||
| Role.PaymentViewer, | ||
| ) | ||
| @ApiOperation({ | ||
| summary: 'List winning audit logs with given winning id', | ||
| description: 'Roles: Payment Admin, Payment Editor, Payment Viewer', | ||
|
|
@@ -203,7 +244,12 @@ export class AdminController { | |
| }) | ||
| async getWinningAudit( | ||
| @Param('winningID') winningId: string, | ||
| @User() user: any, | ||
| ): Promise<ResponseDto<WinningAuditDto[]>> { | ||
| if (this.isBaAdmin(user)) { | ||
|
vas3a marked this conversation as resolved.
|
||
| await this.adminService.verifyBaAdminAccessToWinning(winningId, user.id); | ||
|
vas3a marked this conversation as resolved.
|
||
| } | ||
|
|
||
| const result = await this.adminService.getWinningAudit(winningId); | ||
|
|
||
| result.status = ResponseStatusType.SUCCESS; | ||
|
|
@@ -215,7 +261,12 @@ export class AdminController { | |
| } | ||
|
|
||
| @Get('/winnings/:winningID/audit-payout') | ||
| @Roles(Role.PaymentAdmin, Role.PaymentEditor, Role.PaymentViewer) | ||
| @Roles( | ||
| Role.PaymentAdmin, | ||
| Role.PaymentBaAdmin, | ||
| Role.PaymentEditor, | ||
| Role.PaymentViewer, | ||
| ) | ||
| @ApiOperation({ | ||
| summary: 'Fetch winnings payout audit logs with given winning id.', | ||
| description: | ||
|
|
@@ -233,7 +284,12 @@ export class AdminController { | |
| }) | ||
| async getWinningAuditPayout( | ||
| @Param('winningID') winningId: string, | ||
| @User() user: any, | ||
|
vas3a marked this conversation as resolved.
|
||
| ): Promise<ResponseDto<AuditPayoutDto[]>> { | ||
| if (this.isBaAdmin(user)) { | ||
|
vas3a marked this conversation as resolved.
|
||
| await this.adminService.verifyBaAdminAccessToWinning(winningId, user.id); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| } | ||
|
|
||
| const result = await this.adminService.getWinningAuditPayout(winningId); | ||
|
|
||
| result.status = ResponseStatusType.SUCCESS; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,8 @@ import { PaymentStatus } from 'src/dto/payment.dto'; | |
| import { WinningAuditDto, AuditPayoutDto } from './dto/audit.dto'; | ||
| import { WinningUpdateRequestDto } from './dto/winnings.dto'; | ||
| import { TopcoderChallengesService } from 'src/shared/topcoder/challenges.service'; | ||
| import { Logger } from 'src/shared/global'; | ||
| import { Logger, getBaClient } from 'src/shared/global'; | ||
| import { WinningRequestDto } from 'src/dto/winning.dto'; | ||
|
|
||
| function formatDate(date = new Date()) { | ||
| const pad = (n, z = 2) => String(n).padStart(z, '0'); | ||
|
|
@@ -43,6 +44,32 @@ export class AdminService { | |
| private readonly tcChallengesService: TopcoderChallengesService, | ||
| ) {} | ||
|
|
||
| async getBillingAccountsForUser(userId: string): Promise<string[]> { | ||
| const baPrisma = getBaClient(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| const baRows = await baPrisma.billingAccountAccess.findMany({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| where: { | ||
| userId, | ||
| } | ||
| }); | ||
|
|
||
| return baRows.map(r => `${r.billingAccountId}`); | ||
| } | ||
|
|
||
| async applyBaAdminUserFilters( | ||
| userId: string, | ||
| isBaAdmin?: boolean, | ||
| filters: WinningRequestDto = {}, | ||
| ) { | ||
| if (!isBaAdmin) { | ||
| return filters; | ||
| } | ||
|
|
||
| return { | ||
| ...filters, | ||
| billingAccounts: await this.getBillingAccountsForUser(userId), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| }; | ||
| } | ||
|
|
||
| private getWinningById(winningId: string) { | ||
| return this.prisma.winnings.findFirst({ where: { winning_id: winningId } }); | ||
| } | ||
|
|
@@ -65,6 +92,47 @@ export class AdminService { | |
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Verify that a BA admin user has access to the billing account(s) | ||
| * associated with the given winningsId. Throws BadRequestException when | ||
| * access is not allowed. | ||
| */ | ||
| async verifyBaAdminAccessToWinning( | ||
| winningsId: string, | ||
| userId: string, | ||
| ): Promise<void> { | ||
| const payments = await this.prisma.payment.findMany({ | ||
| where: { | ||
| winnings_id: { | ||
| equals: winningsId, | ||
| }, | ||
| }, | ||
| select: { | ||
| billing_account: true, | ||
| } | ||
| });; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
|
|
||
| if (!payments || payments.length === 0) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| // nothing to check | ||
| return; | ||
| } | ||
|
|
||
| const allowedBAs = await this.getBillingAccountsForUser(userId); | ||
| const paymentBAs = payments | ||
| .map((p) => p.billing_account) | ||
| .filter((b) => b !== null && b !== undefined); | ||
|
|
||
| const unauthorized = paymentBAs.some((ba) => !allowedBAs.includes(`${ba}`)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| if (unauthorized) { | ||
| this.logger.warn( | ||
| `BA admin ${userId} attempted to access winnings ${winningsId} for unauthorized billing account(s)`, | ||
| ); | ||
| throw new BadRequestException( | ||
| 'BA admin user does not have access to the billing account for this winnings', | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Update winnings with parameters | ||
| * @param body the request body | ||
|
|
@@ -74,6 +142,7 @@ export class AdminService { | |
| async updateWinnings( | ||
| body: WinningUpdateRequestDto, | ||
| userId: string, | ||
| isBaAdmin?: boolean, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| ): Promise<ResponseDto<string>> { | ||
| const result = new ResponseDto<string>(); | ||
|
|
||
|
|
@@ -84,6 +153,10 @@ export class AdminService { | |
| ); | ||
| this.logger.log(`updateWinnings payload: ${JSON.stringify(body)}`); | ||
|
|
||
| if (isBaAdmin) { | ||
| await this.verifyBaAdminAccessToWinning(body.winningsId, userId); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| } | ||
|
|
||
| try { | ||
| const payments = await this.getPaymentsByWinningsId( | ||
| winningsId, | ||
|
|
@@ -100,6 +173,8 @@ export class AdminService { | |
| throw new NotFoundException('failed to get current payments'); | ||
| } | ||
|
|
||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
|
|
||
| let releaseDate; | ||
| if (body.paymentStatus) { | ||
| releaseDate = await this.getPaymentReleaseDateByWinningsId(winningsId); | ||
|
|
@@ -385,7 +460,9 @@ export class AdminService { | |
| // Run all transaction tasks in a single prisma transaction | ||
| await this.prisma.$transaction(async (tx) => { | ||
| for (let i = 0; i < transactions.length; i++) { | ||
| this.logger.log(`Executing transaction ${i + 1}/${transactions.length}`); | ||
| this.logger.log( | ||
| `Executing transaction ${i + 1}/${transactions.length}`, | ||
| ); | ||
| await transactions[i](tx); | ||
| } | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,7 +62,7 @@ export class WinningsRepository { | |
| winnerIds?: string[], | ||
| externalIds?: string[], | ||
| date?: DateFilterType, | ||
| ): Prisma.winningsFindManyArgs['where'] { | ||
| ): Prisma.winningsWhereInput { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| const typeFilter = type | ||
| ? { | ||
| equals: type as winnings_type, | ||
|
|
@@ -185,6 +185,17 @@ export class WinningsRepository { | |
| searchProps.date, | ||
| ); | ||
|
|
||
| if (searchProps.billingAccounts) { | ||
| // override payment filter to include billing account constraint | ||
| // while preserving status/installment constraints | ||
| (queryWhere as any).payment.some = { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ...queryWhere.payment!.some, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| billing_account: { | ||
| in: searchProps.billingAccounts, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| const orderBy = this.getOrderByWithWinnerId( | ||
| searchProps.sortBy, | ||
| searchProps.sortOrder, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,6 +88,7 @@ export class RolesGuard implements CanActivate { | |
| id: userId, | ||
| handle: userHandle, | ||
| email: request.email, | ||
| roles: userRoles, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| }; | ||
|
|
||
| return true; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -189,6 +189,17 @@ export class WinningRequestDto extends SortPagination { | |
| @IsOptional() | ||
| @IsEnum(DateFilterType) | ||
| date?: DateFilterType; | ||
|
|
||
| @ApiProperty({ | ||
| description: 'The array of billing account ids', | ||
| example: ['1234'], | ||
| }) | ||
| @IsOptional() | ||
| @IsArray() | ||
| @ArrayNotEmpty() | ||
| @IsString({ each: true }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| @IsNotEmpty({ each: true }) | ||
| billingAccounts?: string[]; | ||
| } | ||
|
|
||
| export class WinningCreateRequestDto { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { PrismaClientOptions } from '@prisma/client/runtime/library'; | ||
| import { PrismaClient as BaPrismaClient } from '@topcoder/billing-accounts-api-v6/packages/ba-prisma-client'; | ||
| import { ENV_CONFIG } from 'src/config'; | ||
|
|
||
| const clientOptions = { | ||
| transactionOptions: { | ||
| timeout: 20000, | ||
| }, | ||
| log: [ | ||
| { level: 'query', emit: 'event' }, | ||
| { level: 'info', emit: 'event' }, | ||
| { level: 'warn', emit: 'event' }, | ||
| { level: 'error', emit: 'event' }, | ||
| ] as PrismaClientOptions['log'], | ||
| }; | ||
|
|
||
| let baPrismaClient: BaPrismaClient; | ||
| export const getBaClient = () => { | ||
| if (!baPrismaClient) { | ||
| if (!ENV_CONFIG.BILLING_ACCOUNTS_DB_URL) { | ||
| throw new Error( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| 'BILLING_ACCOUNTS_DB_URL must be set for challenges Prisma client', | ||
| ); | ||
| } | ||
| baPrismaClient = new BaPrismaClient({ | ||
| ...clientOptions, | ||
| datasources: { db: { url: ENV_CONFIG.BILLING_ACCOUNTS_DB_URL } }, | ||
| }); | ||
| } | ||
| return baPrismaClient; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export * from './logger'; | ||
| export * from './ba-prisma.client'; |
Uh oh!
There was an error while loading. Please reload this page.