diff --git a/.gitignore b/.gitignore index a4c94313afddb57eb82d0c85ab475143d760a4ee..3d3345425a8616324ffadec9f97a5a6e85a840ee 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ ExpressAPI/src/config/Version.ts redoc.html OpenAPI.yaml-r +sonarlint.xml +sonarlint/ ############################ MacOS # General diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 80d804e66e077b6989c957f0d0be22520ea87a25..66b479718ed10400062d9182ca3b1b863fb0d7e9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -89,6 +89,26 @@ code_quality:lint: - npm run lint +code_quality:sonarqube: + stage: code_quality + tags: + - code_quality + image: + name: leadrien/isc-sonar-scanner-cli + entrypoint: [ "" ] + variables: + SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache + GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task + cache: + key: "${CI_JOB_NAME}" + paths: + - .sonar/cache + script: + - sonar-scanner + rules: + - if: '$CI_COMMIT_TAG =~ "/^$/"' + + test:build: stage: test image: node:latest diff --git a/ExpressAPI/.idea/DojoBackendAPI.iml b/ExpressAPI/.idea/DojoBackendAPI.iml index f94b98967da5ba1253b527d008b563510c3da242..565eef92b6c08946f17ac6a0e8a3c6e2e0a3bc00 100644 --- a/ExpressAPI/.idea/DojoBackendAPI.iml +++ b/ExpressAPI/.idea/DojoBackendAPI.iml @@ -15,4 +15,7 @@ <orderEntry type="inheritedJdk" /> <orderEntry type="sourceFolder" forTests="false" /> </component> + <component name="SonarLintModuleSettings"> + <option name="uniqueId" value="2749ea0f-74a8-42c0-9fd6-d6a4b4cd75a4" /> + </component> </module> \ No newline at end of file diff --git a/ExpressAPI/prisma/seed.ts b/ExpressAPI/prisma/seed.ts index 20249cf1ee8d9779400f3483413eb0b10d643205..fc8dce5183e470dafbfd99caa6eca2de6faec393 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 9317f3869137d6789fd35a2dc18fb7c313d48f2b..487035a9d069e91c7a147e0a94219137a9f31cc5 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 f5a736c7eddf25b332fc5cd62d8fdab2d63714e4..a768fded7ca22aae497741693e813a785157a61f 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 17efe6873ccc9edd605e74571b31cd9dc63fe6f3..d264c7aa4a86ffdeea59c8f9cf55a82d7e049022 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>> { @@ -67,12 +64,14 @@ class Session { Send a response to the client Information: Data could be a promise or an object. If it's a promise, we wait on the data to be resolved before sending the response */ - sendResponse(res: express.Response, code: number, data?: unknown, descriptionOverride?: string, internalCode?: number) { - Promise.resolve(data).then((toReturn: unknown) => { - this.getResponse(internalCode ?? code, toReturn, descriptionOverride).then(response => { - res.status(code).json(response); + sendResponse(res: express.Response | undefined, code: number, data?: unknown, descriptionOverride?: string, internalCode?: number) { + if ( res ) { + Promise.resolve(data).then((toReturn: unknown) => { + this.getResponse(internalCode ?? code, toReturn, descriptionOverride).then(response => { + res.status(code).json(response); + }); }); - }); + } } } diff --git a/ExpressAPI/src/express/API.ts b/ExpressAPI/src/express/API.ts index 0d61b88e4f9d007c26b7d1ef13f98a4fa5ae3e85..0f885ecf1ca1297c12abbc42b0c384a20521ccf7 100644 --- a/ExpressAPI/src/express/API.ts +++ b/ExpressAPI/src/express/API.ts @@ -38,14 +38,16 @@ class API implements WorkerTask { private initBaseMiddlewares() { this.backend.use(multer({ - limits: { fieldSize: 100 * 1024 * 1024 } + limits: { + fieldSize: 15728640 // 15MB + } }).none()); //Used for extract params from body with format "form-data", The none is for say that we do not wait a file in params this.backend.use(morganMiddleware); //Log API accesses this.backend.use(helmet()); //Help to secure express, https://helmetjs.github.io/ 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 +61,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 9dc14bb292ef7e2921617c135b1e4f8d014e2a78..797615fea9171ed184be24a024416fd139a17f3b 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 a44e1ec54ed64170f491339b5d91f39423372ba6..962bf1c1d41ed2d9da7c8dfbac9fb9c51be8396e 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 8fa9e5f2d28180bdf5711fb65f572218e253160b..3b3c35cd4e9197fdd9bb722095e9c73fd409b879 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 38465417c46274cff0e46ee3dbdc2929cb17f05b..442f1608730d62f94772846d00fdfc28235d4406 100644 --- a/ExpressAPI/src/helpers/DojoValidators.ts +++ b/ExpressAPI/src/helpers/DojoValidators.ts @@ -1,5 +1,4 @@ import Config from '../config/Config'; -import { StatusCodes } from 'http-status-codes'; import { CustomValidator, ErrorMessage, FieldMessageFactory, Meta } from 'express-validator/src/base'; import { BailOptions, ValidationChain } from 'express-validator/src/chain'; import GitlabManager from '../managers/GitlabManager'; @@ -31,9 +30,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 +42,7 @@ class DojoValidators { }); readonly jsonSanitizer = this.toValidatorSchemaOptions({ - options: (value) => { + options: value => { try { return JSON.parse(value as string); } catch ( e ) { @@ -62,8 +61,8 @@ class DojoValidators { return new Promise((resolve, reject) => { const template = this.getParamValue(req, path) as string; if ( template ) { - GitlabManager.checkTemplateAccess(template, req).then((templateAccess) => { - templateAccess !== StatusCodes.OK ? reject() : resolve(true); + GitlabManager.checkTemplateAccess(template, req).then(templateAccess => { + templateAccess ? resolve(true) : reject(); }); } resolve(true); @@ -79,7 +78,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 +121,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 8a425a0d6e27b94ba6078da3b0e9bfa4f1e0ea88..e7e6e344f29769d48a8517d77ec7eb0b7493aa2d 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 a5f813993c56c3d5c4824854231a2a6486cddd3b..4c1d8bea9160b8497ce33b05372595e8c15a2cae 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 9ece43dcc31b2dd9fde697a33948f6d72fc15247..8a8640ea20e19fa9df3692a920a7cb05f0a4fb6a 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 d1a1f72a66fcdd11f29ca6d1e3deedc9d1bf342a..6da9b45547361bcea4bda0007ab7f7b1b0b95b1f 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 3455f8b7accad04dc7c09ace93b1c297cb633d49..528d24dd41e5da167008484c70f0802eb05b0353 100644 --- a/ExpressAPI/src/managers/GitlabManager.ts +++ b/ExpressAPI/src/managers/GitlabManager.ts @@ -16,6 +16,7 @@ import GitlabProfile from '../shared/types/Gitlab/GitlabProfile'; import GitlabRelease from '../shared/types/Gitlab/GitlabRelease'; import { CommitSchema, Gitlab } from '@gitbeaker/rest'; import logger from '../shared/logging/WinstonLogger'; +import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; class GitlabManager { @@ -97,11 +98,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 +113,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 +135,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> { @@ -169,16 +170,18 @@ class GitlabManager { return response.data; } - async checkTemplateAccess(projectIdOrNamespace: string, req: express.Request): Promise<StatusCodes> { + async checkTemplateAccess(projectIdOrNamespace: string, req: express.Request, res?: express.Response): Promise<boolean> { // Get the Gitlab project and check if it have public or internal visibility try { const project: GitlabRepository = await this.getRepository(projectIdOrNamespace); if ( [ GitlabVisibility.PUBLIC.valueOf(), GitlabVisibility.INTERNAL.valueOf() ].includes(project.visibility) ) { - return StatusCodes.OK; + req.session.sendResponse(res, StatusCodes.OK); + return true; } } catch ( e ) { - return StatusCodes.NOT_FOUND; + req.session.sendResponse(res, StatusCodes.NOT_FOUND, undefined, 'Template not found', DojoStatusCode.GITLAB_TEMPLATE_NOT_FOUND); + return false; } // Check if the user and dojo are members (with at least reporter access) of the project @@ -197,7 +200,13 @@ class GitlabManager { } }); - return isUsersAtLeastReporter.user && isUsersAtLeastReporter.dojo ? StatusCodes.OK : StatusCodes.UNAUTHORIZED; + if ( isUsersAtLeastReporter.user && isUsersAtLeastReporter.dojo ) { + req.session.sendResponse(res, StatusCodes.OK); + return true; + } else { + req.session.sendResponse(res, StatusCodes.UNAUTHORIZED, undefined, 'Template access unauthorized', DojoStatusCode.GITLAB_TEMPLATE_ACCESS_UNAUTHORIZED); + return false; + } } async protectBranch(repoId: number, branchName: string, allowForcePush: boolean, allowedToMerge: GitlabAccessLevel, allowedToPush: GitlabAccessLevel, allowedToUnprotect: GitlabAccessLevel): Promise<GitlabMember> { @@ -240,8 +249,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 +266,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 +285,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 075e72717a56923fe0d962b72ac14d11fad0d9e8..e67828d06b4b4289b74d241e1c0aa60b25d280be 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 37e6e4af13cb1905766ce82231ba7da531882049..6b4a0756419e4b5052988c6cf08cc5c527b2ba70 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 945f7551fa76a06c804be9e0d7ae477287c4d90f..fd061eb701901b88329da2fc94f3a2824ec62f92 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 a11d53c836f665652ed09ed12efb94976897e6a1..9f46f4973a57441ea2b043c117e343e77ccbed5a 100644 --- a/ExpressAPI/src/routes/AssignmentRoutes.ts +++ b/ExpressAPI/src/routes/AssignmentRoutes.ts @@ -98,14 +98,14 @@ 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' ) { - return res.status(StatusCodes.CONFLICT).send(); + if ( error.response?.data.message.name && error.response.data.message.name === 'has already been taken' ) { + return req.session.sendResponse(res, StatusCodes.CONFLICT, {}, `Repository name has already been take`, DojoStatusCode.ASSIGNMENT_NAME_CONFLICT); } - return res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); + return req.session.sendResponse(res, error.response?.status ?? HttpStatusCode.InternalServerError); } - return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); + return req.session.sendResponse(res, HttpStatusCode.InternalServerError); } await new Promise(resolve => setTimeout(resolve, Config.gitlab.repository.timeoutAfterCreation)); @@ -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; } } @@ -205,7 +206,7 @@ class AssignmentRoutes implements RoutesManager { 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 1317f5b40469d183c88323ee17cf2e5beab4176e..3d8c3e1769f82a9dc72501ee4268d654abfc9aee 100644 --- a/ExpressAPI/src/routes/ExerciseRoutes.ts +++ b/ExpressAPI/src/routes/ExerciseRoutes.ts @@ -78,62 +78,84 @@ 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 undefined; } } 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 undefined; } } } 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 undefined; + } + + 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 ) { + req.session.sendResponse(res, StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE, reachedLimitUsers, 'Max exercise per assignment reached', DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED); + return; + } + + + 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)); @@ -147,13 +169,15 @@ class ExerciseRoutes implements RoutesManager { await GitlabManager.addRepositoryBadge(repository.id, Config.gitlab.badges.pipeline.link, Config.gitlab.badges.pipeline.imageUrl, 'Pipeline Status'); } catch ( error ) { - return GlobalHelper.repositoryCreationError('Repo params error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); + GlobalHelper.repositoryCreationError('Repo params error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); + return; } try { await GitlabManager.updateFile(repository.id, '.gitlab-ci.yml', fs.readFileSync(path.join(__dirname, '../../assets/exercise_gitlab_ci.yml'), 'base64'), 'Add .gitlab-ci.yml (DO NOT MODIFY THIS FILE)'); } catch ( error ) { - return GlobalHelper.repositoryCreationError('CI file update error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); + GlobalHelper.repositoryCreationError('CI file update error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); + return; } try { @@ -194,9 +218,11 @@ class ExerciseRoutes implements RoutesManager { } }) as unknown as Exercise; - return req.session.sendResponse(res, StatusCodes.OK, exercise); + req.session.sendResponse(res, StatusCodes.OK, exercise); + return; } catch ( error ) { - return GlobalHelper.repositoryCreationError('DB error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); + await GlobalHelper.repositoryCreationError('DB error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); + return; } } @@ -214,12 +240,12 @@ class ExerciseRoutes implements RoutesManager { return file; })); - const dojoAssignmentFile: AssignmentFile = JSON5.parse(atob(assignmentHjsonFile.content)) as AssignmentFile; + const dojoAssignmentFile: AssignmentFile = JSON5.parse(atob(assignmentHjsonFile.content)); 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/routes/GitlabRoutes.ts b/ExpressAPI/src/routes/GitlabRoutes.ts index 2f4affb7a12eb72ab9c5bd927994f5aac7c60c16..2d8ced089e6321ee86e7493e6cc97ad04cb93e63 100644 --- a/ExpressAPI/src/routes/GitlabRoutes.ts +++ b/ExpressAPI/src/routes/GitlabRoutes.ts @@ -14,7 +14,7 @@ class GitlabRoutes implements RoutesManager { private async checkTemplateAccess(req: express.Request, res: express.Response) { const gitlabProjectIdOrNamespace: string = req.params.gitlabProjectIdOrNamespace; - return res.status(await GitlabManager.checkTemplateAccess(gitlabProjectIdOrNamespace, req)).send(); + await GitlabManager.checkTemplateAccess(gitlabProjectIdOrNamespace, req, res); } } diff --git a/ExpressAPI/src/shared b/ExpressAPI/src/shared index 9e3f29d2f313ef96944a199da0db39f1827c496a..75fedb26c47bb6f707725307a79a45a13e62496d 160000 --- a/ExpressAPI/src/shared +++ b/ExpressAPI/src/shared @@ -1 +1 @@ -Subproject commit 9e3f29d2f313ef96944a199da0db39f1827c496a +Subproject commit 75fedb26c47bb6f707725307a79a45a13e62496d diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000000000000000000000000000000000000..51242d1f89d9e1089c53b36720faff2a642a49df --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,3 @@ +sonar.projectKey=DojoBackendAPI +sonar.qualitygate.wait=true +sonar.exclusions=ExpressAPI/prisma/seed.ts \ No newline at end of file