From 0cef1fa3dfa878998bf58e40ee5e188623d403b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Minelli?= <michael@minelli.me> Date: Wed, 28 Feb 2024 17:53:24 +0100 Subject: [PATCH] Sonar => Resolve issues --- ExpressAPI/prisma/seed.ts | 2 +- ExpressAPI/src/app.ts | 4 +- ExpressAPI/src/config/Config.ts | 2 +- ExpressAPI/src/controllers/Session.ts | 25 ++++---- ExpressAPI/src/express/API.ts | 6 +- ExpressAPI/src/helpers/DatabaseHelper.ts | 4 +- .../src/helpers/DojoCliVersionHelper.ts | 2 - ExpressAPI/src/helpers/DojoModelsHelper.ts | 3 +- ExpressAPI/src/helpers/DojoValidators.ts | 13 ++-- ExpressAPI/src/helpers/GlobalHelper.ts | 4 +- .../Extensions/AssignmentResultExtension.ts | 4 +- .../Prisma/Extensions/UserResultExtension.ts | 8 +-- ExpressAPI/src/logging/MorganMiddleware.ts | 6 +- ExpressAPI/src/managers/GitlabManager.ts | 22 ++++--- ExpressAPI/src/managers/HttpManager.ts | 12 ++-- .../src/middlewares/SecurityMiddleware.ts | 6 +- ExpressAPI/src/process/ClusterManager.ts | 5 +- ExpressAPI/src/routes/AssignmentRoutes.ts | 9 ++- ExpressAPI/src/routes/ExerciseRoutes.ts | 59 +++++++++++++------ ExpressAPI/src/shared | 2 +- sonar-project.properties | 3 +- 21 files changed, 107 insertions(+), 94 deletions(-) diff --git a/ExpressAPI/prisma/seed.ts b/ExpressAPI/prisma/seed.ts index 20249cf..fc8dce5 100644 --- a/ExpressAPI/prisma/seed.ts +++ b/ExpressAPI/prisma/seed.ts @@ -16,7 +16,7 @@ async function main() { main().then(async () => { await db.$disconnect(); -}).catch(async (e) => { +}).catch(async e => { logger.error(e); await db.$disconnect(); process.exit(1); diff --git a/ExpressAPI/src/app.ts b/ExpressAPI/src/app.ts index 9317f38..487035a 100644 --- a/ExpressAPI/src/app.ts +++ b/ExpressAPI/src/app.ts @@ -13,7 +13,5 @@ HttpManager.registerAxiosInterceptor(); role : WorkerRole.API, quantity : ClusterManager.CORES, restartOnFail: true, - loadTask : () => { - return new API(); - } + loadTask : () => new API() } ])).run(); diff --git a/ExpressAPI/src/config/Config.ts b/ExpressAPI/src/config/Config.ts index f5a736c..a768fde 100644 --- a/ExpressAPI/src/config/Config.ts +++ b/ExpressAPI/src/config/Config.ts @@ -19,7 +19,7 @@ class Config { version: { [client: string]: string } - }; // { version: { CLIENT: CONDITION } } + }; public readonly dojoCLI: { versionUpdatePeriodMs: number diff --git a/ExpressAPI/src/controllers/Session.ts b/ExpressAPI/src/controllers/Session.ts index 17efe68..7be2dea 100644 --- a/ExpressAPI/src/controllers/Session.ts +++ b/ExpressAPI/src/controllers/Session.ts @@ -19,30 +19,27 @@ class Session { this._profile = newProfile; } - constructor() { } - async initSession(req: express.Request, res: express.Response) { const authorization = req.headers.authorization; - if ( authorization ) { - if ( authorization.startsWith('Bearer ') ) { - const jwtToken = authorization.replace('Bearer ', ''); + if ( authorization && authorization.startsWith('Bearer ') ) { + const jwtToken = authorization.replace('Bearer ', ''); - try { - const jwtData = jwt.verify(jwtToken, Config.jwtConfig.secret) as JwtPayload; + try { + const jwtData = jwt.verify(jwtToken, Config.jwtConfig.secret) as JwtPayload; - if ( jwtData.profile ) { - this.profile = jwtData.profile; - this.profile = await UserManager.getById(this.profile.id!) ?? this.profile; - } - } catch ( err ) { - res.sendStatus(StatusCodes.UNAUTHORIZED).end(); + if ( jwtData.profile ) { + this.profile = jwtData.profile; + this.profile = await UserManager.getById(this.profile.id!) ?? this.profile; } + } catch ( err ) { + res.sendStatus(StatusCodes.UNAUTHORIZED).end(); } } } private static getToken(profileJson: unknown): string | null { - return profileJson === null ? null : jwt.sign({ profile: profileJson }, Config.jwtConfig.secret, Config.jwtConfig.expiresIn > 0 ? { expiresIn: Config.jwtConfig.expiresIn } : {}); + const options = Config.jwtConfig.expiresIn > 0 ? { expiresIn: Config.jwtConfig.expiresIn } : {}; + return profileJson === null ? null : jwt.sign({ profile: profileJson }, Config.jwtConfig.secret, options); } private async getResponse<T>(code: number, data: T, descriptionOverride?: string): Promise<DojoBackendResponse<T>> { diff --git a/ExpressAPI/src/express/API.ts b/ExpressAPI/src/express/API.ts index 0d61b88..a55896a 100644 --- a/ExpressAPI/src/express/API.ts +++ b/ExpressAPI/src/express/API.ts @@ -45,7 +45,7 @@ class API implements WorkerTask { this.backend.use(cors()); //Allow CORS requests this.backend.use(compression()); //Compress responses - this.backend.use(async (req, res, next) => { + this.backend.use(async (_req, res, next) => { res.header('dojocli-latest-version', await DojoCliVersionHelper.getLatestVersion()); next(); }); @@ -59,9 +59,9 @@ class API implements WorkerTask { url: '../OpenAPI.yaml' } }; - this.backend.get('/docs/OpenAPI.yaml', (req, res) => res.sendFile(path.resolve(__dirname + '/../../assets/OpenAPI/OpenAPI.yaml'))); + this.backend.get('/docs/OpenAPI.yaml', (_req, res) => res.sendFile(path.resolve(__dirname + '/../../assets/OpenAPI/OpenAPI.yaml'))); this.backend.use('/docs/swagger', swaggerUi.serveFiles(undefined, options), swaggerUi.setup(undefined, options)); - this.backend.get('/docs/redoc.html', (req, res) => res.sendFile(path.resolve(__dirname + '/../../assets/OpenAPI/redoc.html'))); + this.backend.get('/docs/redoc.html', (_req, res) => res.sendFile(path.resolve(__dirname + '/../../assets/OpenAPI/redoc.html'))); this.backend.get('/docs/', (req, res) => { const prefix = req.url.slice(-1) === '/' ? '' : 'docs/'; diff --git a/ExpressAPI/src/helpers/DatabaseHelper.ts b/ExpressAPI/src/helpers/DatabaseHelper.ts index 9dc14bb..797615f 100644 --- a/ExpressAPI/src/helpers/DatabaseHelper.ts +++ b/ExpressAPI/src/helpers/DatabaseHelper.ts @@ -31,7 +31,7 @@ prisma.$on('warn', e => logger.warn(`Prisma => ${ e.message }`)); prisma.$on('error', e => logger.error(`Prisma => ${ e.message }`)); -const db = prisma.$extends(UserQueryExtension).$extends(UserResultExtension).$extends(AssignmentResultExtension).$extends(ExerciseResultExtension); +const DatabaseHelper = prisma.$extends(UserQueryExtension).$extends(UserResultExtension).$extends(AssignmentResultExtension).$extends(ExerciseResultExtension); -export default db; \ No newline at end of file +export default DatabaseHelper; \ No newline at end of file diff --git a/ExpressAPI/src/helpers/DojoCliVersionHelper.ts b/ExpressAPI/src/helpers/DojoCliVersionHelper.ts index a44e1ec..962bf1c 100644 --- a/ExpressAPI/src/helpers/DojoCliVersionHelper.ts +++ b/ExpressAPI/src/helpers/DojoCliVersionHelper.ts @@ -7,8 +7,6 @@ class DojoCliVersionHelper { private latestUpdate: Date | undefined; private latestVersion: string | undefined; - constructor() { } - private async updateVersion(): Promise<void> { const releases: Array<GitlabRelease> = await GitlabManager.getRepositoryReleases(Config.dojoCLI.repositoryId); for ( const release of releases ) { diff --git a/ExpressAPI/src/helpers/DojoModelsHelper.ts b/ExpressAPI/src/helpers/DojoModelsHelper.ts index 8fa9e5f..3b3c35c 100644 --- a/ExpressAPI/src/helpers/DojoModelsHelper.ts +++ b/ExpressAPI/src/helpers/DojoModelsHelper.ts @@ -9,8 +9,7 @@ class DojoModelsHelper { * @param depth The depth of the search for LazyVal instances */ async getFullSerializableObject<T extends NonNullable<unknown>>(obj: T, depth: number = 0): Promise<unknown> { - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - const result: any = {}; + const result: { [key: string]: unknown } = {}; for ( const key in obj ) { let value: unknown = obj[key]; diff --git a/ExpressAPI/src/helpers/DojoValidators.ts b/ExpressAPI/src/helpers/DojoValidators.ts index 3846541..2f3797f 100644 --- a/ExpressAPI/src/helpers/DojoValidators.ts +++ b/ExpressAPI/src/helpers/DojoValidators.ts @@ -31,9 +31,9 @@ class DojoValidators { } readonly nullSanitizer = this.toValidatorSchemaOptions({ - options: (value) => { + options: value => { try { - return value == 'null' || value == 'undefined' || value == '' ? null : value; + return value === 'null' || value === 'undefined' || value === '' ? null : value; } catch ( error ) { logger.error(`null sanitizer error: ${ error }`); @@ -43,7 +43,7 @@ class DojoValidators { }); readonly jsonSanitizer = this.toValidatorSchemaOptions({ - options: (value) => { + options: value => { try { return JSON.parse(value as string); } catch ( e ) { @@ -62,7 +62,7 @@ class DojoValidators { return new Promise((resolve, reject) => { const template = this.getParamValue(req, path) as string; if ( template ) { - GitlabManager.checkTemplateAccess(template, req).then((templateAccess) => { + GitlabManager.checkTemplateAccess(template, req).then(templateAccess => { templateAccess !== StatusCodes.OK ? reject() : resolve(true); }); } @@ -79,7 +79,8 @@ class DojoValidators { try { const template = this.getParamValue(req, path); if ( template ) { - return `${ Config.gitlab.urls[0].replace(/^([a-z]{3,5}:\/{2})?(.*)/, `$1${ Config.gitlab.account.username }:${ Config.gitlab.account.token }@$2`) }${ template }.git`; + const gitlabUrlWithCredentials = Config.gitlab.urls[0].replace(/^([a-z]{3,5}:\/{2})?(.*)/, `$1${ Config.gitlab.account.username }:${ Config.gitlab.account.token }@$2`); + return `${ gitlabUrlWithCredentials }${ template }.git`; } else { return Config.assignment.default.template; } @@ -121,7 +122,7 @@ class DojoValidators { if ( exerciseIdOrUrl ) { ParamsCallbackManager.initBoundParams(req); - ExerciseManager.get(exerciseIdOrUrl).then((exercise) => { + ExerciseManager.get(exerciseIdOrUrl).then(exercise => { req.boundParams.exercise = exercise; exercise !== undefined ? resolve(true) : reject(); diff --git a/ExpressAPI/src/helpers/GlobalHelper.ts b/ExpressAPI/src/helpers/GlobalHelper.ts index 8a425a0..e7e6e34 100644 --- a/ExpressAPI/src/helpers/GlobalHelper.ts +++ b/ExpressAPI/src/helpers/GlobalHelper.ts @@ -16,9 +16,9 @@ class GlobalHelper { if ( repositoryToRemove ) { await GitlabManager.deleteRepository(repositoryToRemove.id); } - } catch ( error ) { + } catch ( deleteError ) { logger.error('Repository deletion error'); - logger.error(error); + logger.error(deleteError); } if ( error instanceof AxiosError ) { diff --git a/ExpressAPI/src/helpers/Prisma/Extensions/AssignmentResultExtension.ts b/ExpressAPI/src/helpers/Prisma/Extensions/AssignmentResultExtension.ts index a5f8139..4c1d8be 100644 --- a/ExpressAPI/src/helpers/Prisma/Extensions/AssignmentResultExtension.ts +++ b/ExpressAPI/src/helpers/Prisma/Extensions/AssignmentResultExtension.ts @@ -30,9 +30,7 @@ export default Prisma.defineExtension(client => { assignment: { corrections: { compute(assignment) { - return new LazyVal<Array<Partial<Exercise>> | undefined>(() => { - return getCorrections(assignment); - }); + return new LazyVal<Array<Partial<Exercise>> | undefined>(() => getCorrections(assignment)); } } } diff --git a/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts b/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts index 9ece43d..8a8640e 100644 --- a/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts +++ b/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts @@ -13,7 +13,7 @@ export default Prisma.defineExtension(client => { role: true }, compute(user) { - return user.role == UserRole.TEACHING_STAFF || user.role == UserRole.ADMIN; + return user.role === UserRole.TEACHING_STAFF || user.role === UserRole.ADMIN; } }, isAdmin : { @@ -21,14 +21,12 @@ export default Prisma.defineExtension(client => { role: true }, compute(user) { - return user.role == UserRole.ADMIN; + return user.role === UserRole.ADMIN; } }, gitlabProfile : { compute(user) { - return new LazyVal<GitlabUser | undefined>(() => { - return GitlabManager.getUserById(user.id); - }); + return new LazyVal<GitlabUser | undefined>(() => GitlabManager.getUserById(user.id)); } } } diff --git a/ExpressAPI/src/logging/MorganMiddleware.ts b/ExpressAPI/src/logging/MorganMiddleware.ts index d1a1f72..6da9b45 100644 --- a/ExpressAPI/src/logging/MorganMiddleware.ts +++ b/ExpressAPI/src/logging/MorganMiddleware.ts @@ -3,12 +3,10 @@ import logger from '../shared/logging/WinstonLogger'; const stream: StreamOptions = { - write: (message) => logger.http(message) + write: message => logger.http(message) }; -const skip = () => { - return false; //SharedConfig.production; -}; +const skip = () => false; const morganMiddleware = morgan(':method :url :status :res[content-length] - :response-time ms', { stream, diff --git a/ExpressAPI/src/managers/GitlabManager.ts b/ExpressAPI/src/managers/GitlabManager.ts index 3455f8b..ceadae5 100644 --- a/ExpressAPI/src/managers/GitlabManager.ts +++ b/ExpressAPI/src/managers/GitlabManager.ts @@ -97,11 +97,11 @@ class GitlabManager { } } - async createRepository(name: string, description: string, visibility: string, initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, import_url: string): Promise<GitlabRepository> { + async createRepository(name: string, description: string, visibility: string, initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, importUrl: string): Promise<GitlabRepository> { const response = await axios.post<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_CREATE), { name : name, description : description, - import_url : import_url, + import_url : importUrl, initialize_with_readme: initializeWithReadme, namespace_id : namespace, shared_runners_enabled: sharedRunnersEnabled, @@ -112,8 +112,8 @@ class GitlabManager { return response.data; } - async deleteRepository(repoId: number): Promise<void> { - return await axios.delete(this.getApiUrl(GitlabRoute.REPOSITORY_DELETE).replace('{{id}}', String(repoId))); + deleteRepository(repoId: number): Promise<void> { + return axios.delete(this.getApiUrl(GitlabRoute.REPOSITORY_DELETE).replace('{{id}}', String(repoId))); } async forkRepository(forkId: number, name: string, path: string, description: string, visibility: string, namespace: number): Promise<GitlabRepository> { @@ -134,8 +134,8 @@ class GitlabManager { return response.data; } - async changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<GitlabRepository> { - return await this.editRepository(repoId, { visibility: visibility.toString() }); + changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<GitlabRepository> { + return this.editRepository(repoId, { visibility: visibility.toString() }); } async addRepositoryMember(repoId: number, userId: number, accessLevel: GitlabAccessLevel): Promise<GitlabMember> { @@ -240,8 +240,12 @@ class GitlabManager { return results; } + private getRepositoryFileUrl(repoId: number, filePath: string): string { + return this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)); + } + async getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<GitlabFile> { - const response = await axios.get<GitlabFile>(this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), { + const response = await axios.get<GitlabFile>(this.getRepositoryFileUrl(repoId, filePath), { params: { ref: branch } @@ -253,7 +257,7 @@ class GitlabManager { private async createUpdateFile(create: boolean, repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) { const axiosFunction = create ? axios.post : axios.put; - await axiosFunction(this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), { + await axiosFunction(this.getRepositoryFileUrl(repoId, filePath), { encoding : 'base64', branch : branch, commit_message: commitMessage, @@ -272,7 +276,7 @@ class GitlabManager { } async deleteFile(repoId: number, filePath: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) { - await axios.delete(this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), { + await axios.delete(this.getRepositoryFileUrl(repoId, filePath), { data: { branch : branch, commit_message: commitMessage, diff --git a/ExpressAPI/src/managers/HttpManager.ts b/ExpressAPI/src/managers/HttpManager.ts index 075e727..e67828d 100644 --- a/ExpressAPI/src/managers/HttpManager.ts +++ b/ExpressAPI/src/managers/HttpManager.ts @@ -12,15 +12,13 @@ class HttpManager { } private registerRequestInterceptor() { - axios.interceptors.request.use((config) => { + axios.interceptors.request.use(config => { if ( config.data instanceof FormData ) { config.headers = { ...config.headers, ...(config.data as FormData).getHeaders() } as AxiosRequestHeaders; } - if ( config.url && config.url.indexOf(SharedConfig.gitlab.apiURL) !== -1 ) { - if ( !config.headers.DojoOverrideAuthorization ) { - config.headers['PRIVATE-TOKEN'] = Config.gitlab.account.token; - } + if ( config.url && config.url.indexOf(SharedConfig.gitlab.apiURL) !== -1 && !config.headers.DojoOverrideAuthorization ) { + config.headers['PRIVATE-TOKEN'] = Config.gitlab.account.token; } if ( config.headers.DojoOverrideAuthorization && 'DojoAuthorizationHeader' in config.headers && 'DojoAuthorizationValue' in config.headers ) { @@ -36,9 +34,7 @@ class HttpManager { } private registerResponseInterceptor() { - axios.interceptors.response.use((response) => { - return response; - }, (error) => { + axios.interceptors.response.use(response => response, error => { if ( error instanceof AxiosError ) { logger.error(`${ JSON.stringify(error.response?.data) }`); } else { diff --git a/ExpressAPI/src/middlewares/SecurityMiddleware.ts b/ExpressAPI/src/middlewares/SecurityMiddleware.ts index 37e6e4a..6b4a075 100644 --- a/ExpressAPI/src/middlewares/SecurityMiddleware.ts +++ b/ExpressAPI/src/middlewares/SecurityMiddleware.ts @@ -9,10 +9,8 @@ class SecurityMiddleware { // First check if connected then check if at least ONE rule match. It's NOT an AND but it's a OR function. check(checkIfConnected: boolean, ...checkTypes: Array<SecurityCheckType>): (req: express.Request, res: express.Response, next: express.NextFunction) => void { return async (req: express.Request, res: express.Response, next: express.NextFunction) => { - if ( checkIfConnected ) { - if ( req.session.profile === null || req.session.profile === undefined ) { - return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED); - } + if ( checkIfConnected && (req.session.profile === null || req.session.profile === undefined) ) { + return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED); } let isAllowed = checkTypes.length === 0; diff --git a/ExpressAPI/src/process/ClusterManager.ts b/ExpressAPI/src/process/ClusterManager.ts index 945f755..fd061eb 100644 --- a/ExpressAPI/src/process/ClusterManager.ts +++ b/ExpressAPI/src/process/ClusterManager.ts @@ -11,10 +11,13 @@ import logger from '../shared/logging/WinstonLogger'; */ class ClusterManager { public static readonly CORES = os.cpus().length; + private readonly strategy: ClusterStrategy; private workers: { [pid: number]: WorkerRole; } = []; - constructor(private strategy: ClusterStrategy) {} + constructor(strategy: ClusterStrategy) { + this.strategy = strategy; + } private getWorkerPool(role: WorkerRole): WorkerPool | undefined { return this.strategy.find(elem => elem.role === role); diff --git a/ExpressAPI/src/routes/AssignmentRoutes.ts b/ExpressAPI/src/routes/AssignmentRoutes.ts index a11d53c..5b607ab 100644 --- a/ExpressAPI/src/routes/AssignmentRoutes.ts +++ b/ExpressAPI/src/routes/AssignmentRoutes.ts @@ -98,7 +98,7 @@ class AssignmentRoutes implements RoutesManager { logger.error(error); if ( error instanceof AxiosError ) { - if ( error.response?.data.message.name && error.response.data.message.name == 'has already been taken' ) { + if ( error.response?.data.message.name && error.response.data.message.name === 'has already been taken' ) { return res.status(StatusCodes.CONFLICT).send(); } @@ -174,7 +174,8 @@ class AssignmentRoutes implements RoutesManager { if ( publish ) { const isPublishable = await SharedAssignmentHelper.isPublishable(req.boundParams.assignment!.gitlabId); if ( !isPublishable.isPublishable ) { - return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, { lastPipeline: isPublishable.lastPipeline }, isPublishable.status?.message, isPublishable.status?.code); + req.session.sendResponse(res, StatusCodes.BAD_REQUEST, { lastPipeline: isPublishable.lastPipeline }, isPublishable.status?.message, isPublishable.status?.code); + return; } } @@ -200,12 +201,14 @@ class AssignmentRoutes implements RoutesManager { logger.error(error); res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); } + + return; }; } private linkUpdateAssignmentCorrection(isUpdate: boolean): (req: express.Request, res: express.Response) => Promise<void> { return async (req: express.Request, res: express.Response): Promise<void> => { - if ( req.boundParams.exercise?.assignmentName != req.boundParams.assignment?.name ) { + if ( req.boundParams.exercise?.assignmentName !== req.boundParams.assignment?.name ) { return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'The exercise does not belong to the assignment', DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED); } diff --git a/ExpressAPI/src/routes/ExerciseRoutes.ts b/ExpressAPI/src/routes/ExerciseRoutes.ts index 1317f5b..ff7cad6 100644 --- a/ExpressAPI/src/routes/ExerciseRoutes.ts +++ b/ExpressAPI/src/routes/ExerciseRoutes.ts @@ -78,62 +78,83 @@ class ExerciseRoutes implements RoutesManager { } private getExerciseName(assignment: Assignment, members: Array<GitlabUser>, suffix: number): string { - return `DojoEx - ${ assignment.name } - ${ members.map(member => member.username).sort((a, b) => a.localeCompare(b)).join(' + ') }${ suffix > 0 ? ` - ${ suffix }` : '' }`; + const memberNames: string = members.map(member => member.username).sort((a, b) => a.localeCompare(b)).join(' + '); + const suffixString: string = suffix > 0 ? ` - ${ suffix }` : ''; + return `DojoEx - ${ assignment.name } - ${ memberNames }${ suffixString }`; } private getExercisePath(assignment: Assignment, exerciseId: string): string { return `dojo-ex_${ (assignment.gitlabLastInfo as unknown as GitlabRepository).path }_${ exerciseId }`; } - private async createExercise(req: express.Request, res: express.Response) { - const params: { members: Array<GitlabUser> } = req.body; - params.members = [ await req.session.profile.gitlabProfile!.value, ...params.members ].removeObjectDuplicates(gitlabUser => gitlabUser.id); - const assignment: Assignment = req.boundParams.assignment!; - - + private async checkExerciseLimit(assignment: Assignment, members: Array<GitlabUser>): Promise<Array<GitlabUser>> { const exercises: Array<Exercise> | undefined = await ExerciseManager.getFromAssignment(assignment.name, { members: true }); const reachedLimitUsers: Array<GitlabUser> = []; if ( exercises ) { - for ( const member of params.members ) { + for ( const member of members ) { const exerciseCount: number = exercises.filter(exercise => exercise.members.findIndex(exerciseMember => exerciseMember.id === member.id) !== -1).length; if ( exerciseCount >= Config.exercise.maxPerAssignment ) { reachedLimitUsers.push(member); } } } - if ( reachedLimitUsers.length > 0 ) { - return req.session.sendResponse(res, StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE, reachedLimitUsers, 'Max exercise per assignment reached', DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED); - } + return reachedLimitUsers; + } - const exerciseId: string = uuidv4(); - const secret: string = uuidv4(); + private async createExerciseRepository(assignment: Assignment, members: Array<GitlabUser>, exerciseId: string, req: express.Request, res: express.Response): Promise<GitlabRepository | undefined> { let repository!: GitlabRepository; let suffix: number = 0; do { try { - repository = await GitlabManager.forkRepository((assignment.gitlabCreationInfo as unknown as GitlabRepository).id, this.getExerciseName(assignment, params.members, suffix), this.getExercisePath(req.boundParams.assignment!, exerciseId), Config.exercise.default.description.replace('{{ASSIGNMENT_NAME}}', assignment.name), Config.exercise.default.visibility, Config.gitlab.group.exercises); + repository = await GitlabManager.forkRepository((assignment.gitlabCreationInfo as unknown as GitlabRepository).id, this.getExerciseName(assignment, members, suffix), this.getExercisePath(req.boundParams.assignment!, exerciseId), Config.exercise.default.description.replace('{{ASSIGNMENT_NAME}}', assignment.name), Config.exercise.default.visibility, Config.gitlab.group.exercises); break; } catch ( error ) { logger.error('Repo creation error'); logger.error(error); if ( error instanceof AxiosError ) { - if ( error.response?.data.message.name && error.response.data.message.name == 'has already been taken' ) { + if ( error.response?.data.message.name && error.response.data.message.name === 'has already been taken' ) { suffix++; } else { - return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown gitlab error while forking repository', DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR); + req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown gitlab error while forking repository', DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR); + return; } } else { - return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown error while forking repository', DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR); + req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown error while forking repository', DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR); + return; } } } while ( suffix < Config.exercise.maxSameName ); if ( suffix >= Config.exercise.maxSameName ) { logger.error('Max exercise with same name reached'); - return res.status(StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE).send(); + req.session.sendResponse(res, StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE, undefined, 'Max exercise per assignment reached', DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED); + return; + } + + return repository; + } + + private async createExercise(req: express.Request, res: express.Response) { + const params: { members: Array<GitlabUser> } = req.body; + params.members = [ await req.session.profile.gitlabProfile!.value, ...params.members ].removeObjectDuplicates(gitlabUser => gitlabUser.id); + const assignment: Assignment = req.boundParams.assignment!; + + + const reachedLimitUsers: Array<GitlabUser> = await this.checkExerciseLimit(assignment, params.members); + if ( reachedLimitUsers.length > 0 ) { + return req.session.sendResponse(res, StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE, reachedLimitUsers, 'Max exercise per assignment reached', DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED); + } + + + const exerciseId: string = uuidv4(); + const secret: string = uuidv4(); + const repository: GitlabRepository | undefined = await this.createExerciseRepository(assignment, params.members, exerciseId, req, res); + + if ( !repository ) { + return; } await new Promise(resolve => setTimeout(resolve, Config.gitlab.repository.timeoutAfterCreation)); @@ -219,7 +240,7 @@ class ExerciseRoutes implements RoutesManager { const immutablePaths = dojoAssignmentFile.immutable.map(fileDescriptor => fileDescriptor.path); await Promise.all(repoTree.map(async gitlabTreeFile => { - if ( gitlabTreeFile.type == GitlabTreeFileType.BLOB ) { + if ( gitlabTreeFile.type === GitlabTreeFileType.BLOB ) { for ( const immutablePath of immutablePaths ) { if ( gitlabTreeFile.path.startsWith(immutablePath) ) { immutableFiles.push(await GitlabManager.getFile(req.boundParams.exercise!.assignment.gitlabId, gitlabTreeFile.path)); diff --git a/ExpressAPI/src/shared b/ExpressAPI/src/shared index 9e3f29d..da76a3b 160000 --- a/ExpressAPI/src/shared +++ b/ExpressAPI/src/shared @@ -1 +1 @@ -Subproject commit 9e3f29d2f313ef96944a199da0db39f1827c496a +Subproject commit da76a3b35471ec1e9862ae3b57f5905a909058b5 diff --git a/sonar-project.properties b/sonar-project.properties index f59019d..51242d1 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,2 +1,3 @@ sonar.projectKey=DojoBackendAPI -sonar.qualitygate.wait=true \ No newline at end of file +sonar.qualitygate.wait=true +sonar.exclusions=ExpressAPI/prisma/seed.ts \ No newline at end of file -- GitLab