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
76 changes: 75 additions & 1 deletion src/api/ai-workflow/ai-workflow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {
Post,
Body,
Get,
Param,
Patch,
ValidationPipe,
Query,
Param,
} from '@nestjs/common';
import {
ApiBearerAuth,
Expand All @@ -14,6 +15,7 @@ import {
ApiResponse,
ApiParam,
ApiBody,
ApiQuery,
} from '@nestjs/swagger';
import { AiWorkflowService } from './ai-workflow.service';
import {
Expand All @@ -25,6 +27,8 @@ import { Scopes } from 'src/shared/decorators/scopes.decorator';
import { UserRole } from 'src/shared/enums/userRole.enum';
import { Scope } from 'src/shared/enums/scopes.enum';
import { Roles } from 'src/shared/guards/tokenRoles.guard';
import { JwtUser } from 'src/shared/modules/global/jwt.service';
import { User } from 'src/shared/decorators/user.decorator';

@ApiTags('ai_workflow')
@ApiBearerAuth()
Expand Down Expand Up @@ -95,4 +99,74 @@ export class AiWorkflowController {
) {
return this.aiWorkflowService.createWorkflowRun(workflowId, body);
}

@Get('/:workflowId/runs')
@Roles(
UserRole.Admin,
UserRole.Copilot,
UserRole.ProjectManager,
UserRole.Reviewer,
UserRole.Submitter,
)
@Scopes(Scope.ReadWorkflowRuns)
@ApiOperation({
summary: 'Get all the AI workflow runs for a given submission ID',
})
@ApiQuery({
name: 'submissionId',
description: 'The ID of the submission to fetch AI workflow runs for',
required: true,
})
@ApiResponse({
status: 200,
description: 'The AI workflow runs for the given submission ID.',
})
@ApiResponse({ status: 403, description: 'Forbidden.' })
getRuns(
@Param('workflowId') workflowId: string,
@Query('submissionId') submissionId: string,
@User() user: JwtUser,
) {
return this.aiWorkflowService.getWorkflowRuns(workflowId, user, {
submissionId,
});
}

@Get('/:workflowId/runs/:runId')
@Roles(
UserRole.Admin,
UserRole.Copilot,
UserRole.ProjectManager,
UserRole.Reviewer,
UserRole.Submitter,
)
@Scopes(Scope.ReadWorkflowRuns)
@ApiOperation({
summary: 'Get an AI workflow run by its ID',
})
@ApiParam({
name: 'runId',
description: 'The ID of the run to fetch AI workflow run',
required: true,
})
@ApiResponse({
status: 200,
description: 'The AI workflow run for the given ID.',
})
@ApiResponse({ status: 403, description: 'Forbidden.' })
async getRun(
@Param('workflowId') workflowId: string,
@Param('runId') runId: string,
@User() user: JwtUser,
) {
const runs = await this.aiWorkflowService.getWorkflowRuns(
workflowId,
user,
{
runId,
},
);

return runs[0];
}
}
103 changes: 102 additions & 1 deletion src/api/ai-workflow/ai-workflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
BadRequestException,
Logger,
NotFoundException,
InternalServerErrorException,
ForbiddenException,
} from '@nestjs/common';
import { PrismaService } from '../../shared/modules/global/prisma.service';
import {
Expand All @@ -11,11 +13,24 @@ import {
UpdateAiWorkflowDto,
} from '../../dto/aiWorkflow.dto';
import { ScorecardStatus } from 'src/dto/scorecard.dto';
import { JwtUser } from 'src/shared/modules/global/jwt.service';
import {
ChallengeApiService,
ChallengeData,
} from 'src/shared/modules/global/challenge.service';
import { ResourceApiService } from 'src/shared/modules/global/resource.service';
import { UserRole } from 'src/shared/enums/userRole.enum';
import { ChallengeStatus } from 'src/shared/enums/challengeStatus.enum';

@Injectable()
export class AiWorkflowService {
private readonly logger: Logger = new Logger(AiWorkflowService.name);
constructor(private readonly prisma: PrismaService) {}

constructor(
private readonly prisma: PrismaService,
private readonly challengeApiService: ChallengeApiService,
private readonly resourceApiService: ResourceApiService,
) {}

async scorecardExists(scorecardId: string): Promise<boolean> {
const count = await this.prisma.scorecard.count({
Expand Down Expand Up @@ -154,4 +169,90 @@ export class AiWorkflowService {
throw e;
}
}

async getWorkflowRuns(
workflowId: string,
user: JwtUser,
filter: { submissionId?: string; runId?: string },
) {
// validate workflowId
try {
await this.getWorkflowById(workflowId);
} catch (e) {
if (e instanceof NotFoundException) {
throw new BadRequestException(
`Invalid workflow id provided! Workflow with id ${workflowId} does not exist!`,
);
}
}

const runs = await this.prisma.aiWorkflowRun.findMany({
where: {
workflowId,
id: filter.runId,
submissionId: filter.submissionId,
},
include: {
submission: true,
},
});

if (filter.runId && !runs.length) {
throw new NotFoundException(
`AI Workflow run with id ${filter.runId} not found!`,
);
}

const submission = runs[0]?.submission;
const challengeId = submission?.challengeId;
const challenge: ChallengeData =
await this.challengeApiService.getChallengeDetail(challengeId!);

if (!challenge) {
throw new InternalServerErrorException(
`Challenge with id ${challengeId} was not found!`,
);
}

const isM2mOrAdmin = user.isMachine || user.roles?.includes(UserRole.Admin);
if (!isM2mOrAdmin) {
const requiredRoles = [
UserRole.Reviewer,
UserRole.ProjectManager,
UserRole.Copilot,
UserRole.Submitter,
].map((r) => r.toLowerCase());

const memberRoles = (
await this.resourceApiService.getMemberResourcesRoles(
challengeId!,
user.userId,
)
).filter((resource) =>
requiredRoles.some(
(role) =>
resource.roleName!.toLowerCase().indexOf(role.toLowerCase()) >= 0,
),
);

if (!memberRoles.length) {
throw new ForbiddenException('Insufficient permissions');
}

if (
challenge.status !== ChallengeStatus.COMPLETED &&
memberRoles.some(
(r) => r.roleName?.toLowerCase() === UserRole.Submitter.toLowerCase(),
) &&
user.userId !== submission.memberId
) {
this.logger.log(
`Submitter ${user.userId} trying to access AI workflow run for other submitters.`,
);
throw new ForbiddenException('Insufficient permissions');
}
}

return runs;
}
}
8 changes: 7 additions & 1 deletion src/dto/aiWorkflow.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { ApiProperty, PartialType } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsDateString, IsNumber, IsOptional } from 'class-validator';
import {
IsString,
IsNotEmpty,
IsDateString,
IsNumber,
IsOptional,
} from 'class-validator';

export class CreateAiWorkflowDto {
@ApiProperty()
Expand Down
17 changes: 17 additions & 0 deletions src/shared/enums/challengeStatus.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export enum ChallengeStatus {
NEW = 'NEW',
DRAFT = 'DRAFT',
APPROVED = 'APPROVED',
ACTIVE = 'ACTIVE',
COMPLETED = 'COMPLETED',
DELETED = 'DELETED',
CANCELLED = 'CANCELLED',
CANCELLED_FAILED_REVIEW = 'CANCELLED_FAILED_REVIEW',
CANCELLED_FAILED_SCREENING = 'CANCELLED_FAILED_SCREENING',
CANCELLED_ZERO_SUBMISSIONS = 'CANCELLED_ZERO_SUBMISSIONS',
CANCELLED_WINNER_UNRESPONSIVE = 'CANCELLED_WINNER_UNRESPONSIVE',
CANCELLED_CLIENT_REQUEST = 'CANCELLED_CLIENT_REQUEST',
CANCELLED_REQUIREMENTS_INFEASIBLE = 'CANCELLED_REQUIREMENTS_INFEASIBLE',
CANCELLED_ZERO_REGISTRATIONS = 'CANCELLED_ZERO_REGISTRATIONS',
CANCELLED_PAYMENT_FAILED = 'CANCELLED_PAYMENT_FAILED',
}
2 changes: 2 additions & 0 deletions src/shared/enums/userRole.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ export enum UserRole {
Admin = 'administrator',
Copilot = 'copilot',
Reviewer = 'reviewer',
Submitter = 'Submitter',
ProjectManager = 'Manager',
User = 'Topcoder User',
}
2 changes: 2 additions & 0 deletions src/shared/modules/global/challenge.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AxiosError } from 'axios';
import { M2MService } from './m2m.service';
import { Injectable, Logger } from '@nestjs/common';
import { CommonConfig } from 'src/shared/config/common.config';
import { ChallengeStatus } from 'src/shared/enums/challengeStatus.enum';

export class PhaseData {
id: string;
Expand All @@ -24,6 +25,7 @@ export class ChallengeData {
track?: string | undefined;
subTrack?: string | undefined;
};
status: ChallengeStatus;
numOfSubmissions?: number | undefined;
track: string;
legacyId: number;
Expand Down
46 changes: 29 additions & 17 deletions src/shared/modules/global/resource.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class ResourceApiService {
/**
* Fetch list of resource roles
*
* @returns resolves to list of resouce role
* @returns resolves to list of resource role
*/
async getResourceRoles(): Promise<{
[key: string]: ResourceRole;
Expand Down Expand Up @@ -54,7 +54,7 @@ export class ResourceApiService {
/**
* Fetch list of resource
*
* @returns resolves to list of resouce info
* @returns resolves to list of resource info
*/
async getResources(query: {
challengeId?: string;
Expand Down Expand Up @@ -90,7 +90,30 @@ export class ResourceApiService {
}

/**
* Validate resource fole
* Fetch list of role resources
*
* @returns resolves to list of resource info
*/
async getMemberResourcesRoles(
challengeId?: string,
memberId?: string,
): Promise<ResourceInfo[]> {
const resourceRoles = await this.getResourceRoles();
return (
await this.getResources({
challengeId: challengeId,
memberId: memberId,
})
)
.filter((resource) => resource.memberId === memberId)
.map((resource) => ({
...resource,
roleName: resourceRoles?.[resource.roleId]?.name ?? '',
}));
}

/**
* Validate resource role
*
* @param requiredRoles list of require roles
* @param authUser login user info
Expand All @@ -104,25 +127,14 @@ export class ResourceApiService {
challengeId: string,
resourceId: string,
): Promise<boolean> {
const resourceRoles = await this.getResourceRoles();
const myResources = (
await this.getResources({
challengeId: challengeId,
memberId: authUser.userId,
})
await this.getMemberResourcesRoles(challengeId, authUser.userId)
)
.filter(
(resource) =>
resource.id === resourceId && resource.memberId === authUser.userId,
)
.map((resource) => ({
...resource,
roleName: resourceRoles?.[resource.roleId]?.name ?? '',
}))
.filter((resource) => resource.id === resourceId)
.filter((resource) =>
some(
requiredRoles.map((item) => item.toLowerCase()),
(role: string) => resource.roleName.toLowerCase().indexOf(role) >= 0,
(role: string) => resource.roleName!.toLowerCase().indexOf(role) >= 0,
),
);
if (!myResources.length) {
Expand Down