15 files + 246 − 41 Side-by-side Compare changes Side-by-side Inline Show whitespace changes Files 15 ExpressAPI/package-lock.json +78 −1 Original line number Original line Diff line number Diff line Loading @@ -11,6 +11,7 @@ "@prisma/client": "^5.0.0", "@prisma/client": "^5.0.0", "axios": "^1.4.0", "axios": "^1.4.0", "bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3", "compression": "^1.7.4", "cors": "^2.8.5", "cors": "^2.8.5", "dotenv": "^16.0.3", "dotenv": "^16.0.3", "express": "^4.18.2", "express": "^4.18.2", Loading @@ -24,17 +25,21 @@ "multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", "mysql": "^2.18.1", "node": "^20.2.0", "node": "^20.2.0", "parse-link-header": "^2.0.0", "uuid": "^9.0.0", "uuid": "^9.0.0", "winston": "^3.8.2" "winston": "^3.8.2", "yaml": "^2.3.1" }, }, "devDependencies": { "devDependencies": { "@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2", "@types/compression": "^1.7.2", "@types/cors": "^2.8.13", "@types/cors": "^2.8.13", "@types/express": "^4.17.17", "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.2", "@types/jsonwebtoken": "^9.0.2", "@types/morgan": "^1.9.4", "@types/morgan": "^1.9.4", "@types/multer": "^1.4.7", "@types/multer": "^1.4.7", "@types/node": "^20.2.4", "@types/node": "^20.2.4", "@types/parse-link-header": "^2.0.1", "@types/uuid": "^9.0.2", "@types/uuid": "^9.0.2", "nodemon": "^2.0.22", "nodemon": "^2.0.22", "npm": "^9.6.7", "npm": "^9.6.7", Loading Loading @@ -170,6 +175,15 @@ "@types/node": "*" "@types/node": "*" } } }, }, "node_modules/@types/compression": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz", "integrity": "sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==", "dev": true, "dependencies": { "@types/express": "*" } }, "node_modules/@types/connect": { "node_modules/@types/connect": { "version": "3.4.35", "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", Loading Loading @@ -251,6 +265,12 @@ "integrity": "sha512-ni5f8Xlf4PwnT/Z3f0HURc3ZSw8UyrqMqmM3L5ysa7VjHu8c3FOmIo1nKCcLrV/OAmtf3N4kFna/aJqxsfEtnA==", "integrity": "sha512-ni5f8Xlf4PwnT/Z3f0HURc3ZSw8UyrqMqmM3L5ysa7VjHu8c3FOmIo1nKCcLrV/OAmtf3N4kFna/aJqxsfEtnA==", "dev": true "dev": true }, }, "node_modules/@types/parse-link-header": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/parse-link-header/-/parse-link-header-2.0.1.tgz", "integrity": "sha512-BrKNSrRTqn3UkMXvdVtr/znJch0PMBpEvEP8oBkxDx7eEGntuFLI+WpA5HGsNHK4SlqyhaMa+Ks0ViwyixQB5w==", "dev": true }, "node_modules/@types/qs": { "node_modules/@types/qs": { "version": "6.9.7", "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", Loading Loading @@ -603,6 +623,47 @@ "node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14" } } }, }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, "engines": { "node": ">= 0.6" } }, "node_modules/compression": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", "compressible": "~2.0.16", "debug": "2.6.9", "on-headers": "~1.0.2", "safe-buffer": "5.1.2", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/compression/node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "engines": { "node": ">= 0.8" } }, "node_modules/compression/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/concat-map": { "node_modules/concat-map": { "version": "0.0.1", "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", Loading Loading @@ -4930,6 +4991,14 @@ "fn.name": "1.x.x" "fn.name": "1.x.x" } } }, }, "node_modules/parse-link-header": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-link-header/-/parse-link-header-2.0.0.tgz", "integrity": "sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==", "dependencies": { "xtend": "~4.0.1" } }, "node_modules/parseurl": { "node_modules/parseurl": { "version": "1.3.3", "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", Loading Loading @@ -5575,6 +5644,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, }, "node_modules/yaml": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", "engines": { "node": ">= 14" } }, "node_modules/yn": { "node_modules/yn": { "version": "3.1.1", "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", Loading ExpressAPI/package.json +19 −14 Original line number Original line Diff line number Diff line Loading @@ -22,6 +22,7 @@ "@prisma/client" : "^5.0.0", "@prisma/client" : "^5.0.0", "axios" : "^1.4.0", "axios" : "^1.4.0", "bcryptjs" : "^2.4.3", "bcryptjs" : "^2.4.3", "compression" : "^1.7.4", "cors" : "^2.8.5", "cors" : "^2.8.5", "dotenv" : "^16.0.3", "dotenv" : "^16.0.3", "express" : "^4.18.2", "express" : "^4.18.2", Loading @@ -35,17 +36,21 @@ "multer" : "^1.4.5-lts.1", "multer" : "^1.4.5-lts.1", "mysql" : "^2.18.1", "mysql" : "^2.18.1", "node" : "^20.2.0", "node" : "^20.2.0", "parse-link-header": "^2.0.0", "uuid" : "^9.0.0", "uuid" : "^9.0.0", "winston" : "^3.8.2" "winston" : "^3.8.2", "yaml" : "^2.3.1" }, }, "devDependencies": { "devDependencies": { "@types/bcryptjs" : "^2.4.2", "@types/bcryptjs" : "^2.4.2", "@types/compression" : "^1.7.2", "@types/cors" : "^2.8.13", "@types/cors" : "^2.8.13", "@types/express" : "^4.17.17", "@types/express" : "^4.17.17", "@types/jsonwebtoken" : "^9.0.2", "@types/jsonwebtoken" : "^9.0.2", "@types/morgan" : "^1.9.4", "@types/morgan" : "^1.9.4", "@types/multer" : "^1.4.7", "@types/multer" : "^1.4.7", "@types/node" : "^20.2.4", "@types/node" : "^20.2.4", "@types/parse-link-header": "^2.0.1", "@types/uuid" : "^9.0.2", "@types/uuid" : "^9.0.2", "nodemon" : "^2.0.22", "nodemon" : "^2.0.22", "prisma" : "^5.0.0", "prisma" : "^5.0.0", Loading ExpressAPI/src/config/Config.ts +5 −3 Original line number Original line Diff line number Diff line Loading @@ -16,7 +16,7 @@ class Config { }; }; public enonce: { public enonce: { default: { description: string; initReadme: boolean; sharedRunnersEnabled: boolean; visibility: string; wikiEnabled: boolean; template: string }; default: { description: string; initReadme: boolean; sharedRunnersEnabled: boolean; visibility: string; wikiEnabled: boolean; template: string }; baseFiles: Array<string>; filename: string }; }; public exercice: { public exercice: { Loading Loading @@ -64,7 +64,9 @@ class Config { visibility : process.env.ENONCE_DEFAULT_VISIBILITY, visibility : process.env.ENONCE_DEFAULT_VISIBILITY, wikiEnabled : process.env.ENONCE_DEFAULT_WIKI_ENABLED.toBoolean(), wikiEnabled : process.env.ENONCE_DEFAULT_WIKI_ENABLED.toBoolean(), template : process.env.ENONCE_DEFAULT_TEMPLATE.replace('{{USERNAME}}', this.gitlab.account.username).replace('{{TOKEN}}', this.gitlab.account.token) template : process.env.ENONCE_DEFAULT_TEMPLATE.replace('{{USERNAME}}', this.gitlab.account.username).replace('{{TOKEN}}', this.gitlab.account.token) } }, baseFiles: JSON.parse(process.env.ENONCE_BASE_FILES || '[]'), filename : process.env.ENONCE_FILENAME || '' }; }; this.exercice = { this.exercice = { Loading ExpressAPI/src/controllers/Session.ts +14 −12 Original line number Original line Diff line number Diff line Loading @@ -5,7 +5,7 @@ import Config from '../config/Config'; import express from 'express'; import express from 'express'; import ApiRequest from '../types/ApiRequest'; import ApiRequest from '../types/ApiRequest'; import UserManager from '../managers/UserManager'; import UserManager from '../managers/UserManager'; import DojoResponse from '../shared/types/DojoResponse'; import DojoResponse from '../shared/types/Dojo/DojoResponse'; import { User } from '../types/DatabaseTypes'; import { User } from '../types/DatabaseTypes'; Loading @@ -26,6 +26,7 @@ class Session { async initSession(req: ApiRequest) { async initSession(req: ApiRequest) { const authorization = req.headers.authorization; const authorization = req.headers.authorization; if ( authorization ) { if ( authorization ) { if ( authorization.startsWith('Bearer ') ) { const jwtToken = authorization.replace('Bearer ', ''); const jwtToken = authorization.replace('Bearer ', ''); try { try { Loading @@ -38,9 +39,10 @@ class Session { } catch ( err ) { } } catch ( err ) { } } } } } } private static getToken(profileJson: any): string { private static getToken(profileJson: any): string { return profileJson.id === null ? null : jwt.sign({ profile: profileJson }, Config.jwtConfig.secret, Config.jwtConfig.expiresIn > 0 ? { expiresIn: Config.jwtConfig.expiresIn } : {}); return profileJson === null ? null : jwt.sign({ profile: profileJson }, Config.jwtConfig.secret, Config.jwtConfig.expiresIn > 0 ? { expiresIn: Config.jwtConfig.expiresIn } : {}); } } private async getResponse<T>(code: number, data: T, descriptionOverride?: string): Promise<DojoResponse<T>> { private async getResponse<T>(code: number, data: T, descriptionOverride?: string): Promise<DojoResponse<T>> { Loading ExpressAPI/src/express/API.ts +2 −0 Original line number Original line Diff line number Diff line Loading @@ -12,6 +12,7 @@ import Config from '../config/Config'; import logger from '../shared/logging/WinstonLogger'; import logger from '../shared/logging/WinstonLogger'; import ParamsCallbackManager from '../middlewares/ParamsCallbackManager'; import ParamsCallbackManager from '../middlewares/ParamsCallbackManager'; import ApiRoutesManager from '../routes/ApiRoutesManager'; import ApiRoutesManager from '../routes/ApiRoutesManager'; import compression from 'compression'; class API implements WorkerTask { class API implements WorkerTask { Loading @@ -27,6 +28,7 @@ class API implements WorkerTask { this.backend.use(morganMiddleware); //Log API accesses this.backend.use(morganMiddleware); //Log API accesses this.backend.use(helmet()); //Help to secure express, https://helmetjs.github.io/ this.backend.use(helmet()); //Help to secure express, https://helmetjs.github.io/ this.backend.use(cors()); //Allow CORS requests this.backend.use(cors()); //Allow CORS requests this.backend.use(compression()); //Compress responses ParamsCallbackManager.register(this.backend); ParamsCallbackManager.register(this.backend); Loading ExpressAPI/src/managers/ExerciceManager.ts 0 → 100644 +18 −0 Original line number Original line Diff line number Diff line import { Prisma } from '@prisma/client'; import { Enonce } from '../types/DatabaseTypes'; import db from '../helpers/DatabaseHelper'; class ExerciceManager { get(id: string, include: Prisma.ExerciceInclude | undefined = undefined): Promise<Enonce | undefined> { return db.exercice.findUnique({ where : { id: id }, include: include }); } } export default new ExerciceManager(); ExpressAPI/src/managers/GitlabManager.ts +42 −0 Original line number Original line Diff line number Diff line Loading @@ -8,6 +8,9 @@ import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; import ApiRequest from '../types/ApiRequest'; import ApiRequest from '../types/ApiRequest'; import GitlabUser from '../shared/types/Gitlab/GitlabUser'; import GitlabUser from '../shared/types/Gitlab/GitlabUser'; import GitlabRoutes from '../shared/types/Gitlab/GitlabRoutes'; import GitlabRoutes from '../shared/types/Gitlab/GitlabRoutes'; import GitlabTreeFile from '../shared/types/Gitlab/GitlabTreeFile'; import parseLinkHeader from 'parse-link-header'; import GitlabFile from '../shared/types/Gitlab/GitlabFile'; class GitlabManager { class GitlabManager { Loading Loading @@ -145,6 +148,45 @@ class GitlabManager { return response.data; return response.data; } } async getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<GitlabTreeFile>> { let address: string | undefined = this.getApiUrl(GitlabRoutes.REPOSITORY_TREE).replace('{{id}}', String(repoId)); let params: any = { pagination: 'keyset', recursive : recursive, per_page : 100, ref : branch }; let results: Array<GitlabTreeFile> = []; while ( params !== undefined ) { const response = await axios.get<Array<GitlabTreeFile>>(address, { params: params }); results.push(...response.data); if ( 'link' in response.headers ) { const link = parseLinkHeader(response.headers['link']); params = link.next; } else { params = undefined; } } return results; } async getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<GitlabFile> { const response = await axios.get<GitlabFile>(this.getApiUrl(GitlabRoutes.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), { params: { ref: branch } }); return response.data; } } } Loading ExpressAPI/src/middlewares/ParamsCallbackManager.ts +9 −1 Original line number Original line Diff line number Diff line Loading @@ -3,6 +3,7 @@ import ApiRequest from '../types/ApiRequest'; import express from 'express'; import express from 'express'; import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes'; import EnonceManager from '../managers/EnonceManager'; import EnonceManager from '../managers/EnonceManager'; import ExerciceManager from '../managers/ExerciceManager'; class ParamsCallbackManager { class ParamsCallbackManager { Loading @@ -24,7 +25,8 @@ class ParamsCallbackManager { initBoundParams(req: ApiRequest) { initBoundParams(req: ApiRequest) { if ( !req.boundParams ) { if ( !req.boundParams ) { req.boundParams = { req.boundParams = { enonce: null enonce : null, exercice: null }; }; } } } } Loading @@ -34,6 +36,12 @@ class ParamsCallbackManager { exercices: true, exercices: true, staff : true staff : true } ], 'enonce'); } ], 'enonce'); this.listenParam('exerciceId', backend, ExerciceManager.get.bind(ExerciceManager), [ { enonce : true, members: true, results: true } ], 'exercice'); } } } } Loading ExpressAPI/src/middlewares/SecurityMiddleware.ts +6 −4 Original line number Original line Diff line number Diff line Loading @@ -11,7 +11,7 @@ class SecurityMiddleware { check(checkIfConnected: boolean, ...checkTypes: Array<SecurityCheckType>): (req: ApiRequest, res: express.Response, next: express.NextFunction) => void { check(checkIfConnected: boolean, ...checkTypes: Array<SecurityCheckType>): (req: ApiRequest, res: express.Response, next: express.NextFunction) => void { return async (req: ApiRequest, res: express.Response, next: express.NextFunction) => { return async (req: ApiRequest, res: express.Response, next: express.NextFunction) => { if ( checkIfConnected ) { if ( checkIfConnected ) { if ( req.session.profile.id === null ) { if ( req.session.profile === null ) { return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED); return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED); } } } } Loading @@ -19,9 +19,9 @@ class SecurityMiddleware { let isAllowed = checkTypes.length === 0; let isAllowed = checkTypes.length === 0; if ( !isAllowed ) { if ( !isAllowed ) { for ( let checkType of checkTypes ) { for ( const checkType of checkTypes ) { try { try { switch ( checkType ) { switch ( String(checkType) ) { case SecurityCheckType.TEACHING_STAFF: case SecurityCheckType.TEACHING_STAFF: isAllowed = isAllowed || req.session.profile.isTeachingStaff; isAllowed = isAllowed || req.session.profile.isTeachingStaff; break; break; Loading @@ -31,8 +31,10 @@ class SecurityMiddleware { case SecurityCheckType.ENONCE_IS_PUBLISHED: case SecurityCheckType.ENONCE_IS_PUBLISHED: isAllowed = isAllowed || req.boundParams.enonce.published; isAllowed = isAllowed || req.boundParams.enonce.published; break; break; case SecurityCheckType.EXERCICE_SECRET: isAllowed = isAllowed || (req.headers.authorization && req.headers.authorization && req.headers.authorization.replace('ExerciceSecret ', '') === req.boundParams.exercice.secret); break; default: default: isAllowed = isAllowed || false; break; break; } } } catch ( e ) { } catch ( e ) { Loading ExpressAPI/src/routes/EnonceRoutes.ts +2 −0 Original line number Original line Diff line number Diff line Loading @@ -75,6 +75,8 @@ class EnonceRoutes implements RoutesManager { let repository: GitlabRepository; let repository: GitlabRepository; try { try { repository = await GitlabManager.createRepository(params.name, Config.enonce.default.description.replace('{{ENONCE_NAME}}', params.name), Config.enonce.default.visibility, Config.enonce.default.initReadme, Config.gitlab.group.enonces, Config.enonce.default.sharedRunnersEnabled, Config.enonce.default.wikiEnabled, params.template); repository = await GitlabManager.createRepository(params.name, Config.enonce.default.description.replace('{{ENONCE_NAME}}', params.name), Config.enonce.default.visibility, Config.enonce.default.initReadme, Config.gitlab.group.enonces, Config.enonce.default.sharedRunnersEnabled, Config.enonce.default.wikiEnabled, params.template); await GitlabManager.protectBranch(repository.id, '*', true, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.OWNER); } catch ( error ) { } catch ( error ) { if ( error instanceof AxiosError ) { 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' ) { Loading ExpressAPI/src/routes/ExerciceRoutes.ts +44 −1 Original line number Original line Diff line number Diff line Loading @@ -20,6 +20,11 @@ import { Prisma } from '@prisma/client'; import { Enonce, Exercice } from '../types/DatabaseTypes'; import { Enonce, Exercice } from '../types/DatabaseTypes'; import db from '../helpers/DatabaseHelper'; import db from '../helpers/DatabaseHelper'; import SecurityCheckType from '../types/SecurityCheckType'; import SecurityCheckType from '../types/SecurityCheckType'; import GitlabTreeFile from '../shared/types/Gitlab/GitlabTreeFile'; import GitlabFile from '../shared/types/Gitlab/GitlabFile'; import YAML from 'yaml'; import EnonceFile from '../shared/types/Dojo/EnonceFile'; import GitlabTreeFileType from '../shared/types/Gitlab/GitlabTreeFileType'; class ExerciceRoutes implements RoutesManager { class ExerciceRoutes implements RoutesManager { Loading @@ -33,6 +38,8 @@ class ExerciceRoutes implements RoutesManager { registerOnBackend(backend: Express) { registerOnBackend(backend: Express) { backend.post('/enonces/:enonceNameOrUrl/exercices', SecurityMiddleware.check(true, SecurityCheckType.ENONCE_IS_PUBLISHED), ParamsValidatorMiddleware.validate(this.exerciceValidator), this.createExercice.bind(this)); backend.post('/enonces/:enonceNameOrUrl/exercices', SecurityMiddleware.check(true, SecurityCheckType.ENONCE_IS_PUBLISHED), ParamsValidatorMiddleware.validate(this.exerciceValidator), this.createExercice.bind(this)); backend.get('/exercices/:exerciceId/enonce', SecurityMiddleware.check(false, SecurityCheckType.EXERCICE_SECRET), this.getEnonce.bind(this)); } } private getExerciceName(enonce: Enonce, members: Array<GitlabUser>, suffix: number): string { private getExerciceName(enonce: Enonce, members: Array<GitlabUser>, suffix: number): string { Loading @@ -58,7 +65,7 @@ class ExerciceRoutes implements RoutesManager { try { try { repository = await GitlabManager.forkRepository((enonce.gitlabCreationInfo as unknown as GitlabRepository).id, this.getExerciceName(enonce, params.members, suffix), this.getExercicePath(req.boundParams.enonce, exerciceId), Config.exercice.default.description.replace('{{ENONCE_NAME}}', enonce.name), Config.exercice.default.visibility, Config.gitlab.group.exercices); repository = await GitlabManager.forkRepository((enonce.gitlabCreationInfo as unknown as GitlabRepository).id, this.getExerciceName(enonce, params.members, suffix), this.getExercicePath(req.boundParams.enonce, exerciceId), Config.exercice.default.description.replace('{{ENONCE_NAME}}', enonce.name), Config.exercice.default.visibility, Config.gitlab.group.exercices); await GitlabManager.protectBranch(repository.id, '*', false, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER); await GitlabManager.protectBranch(repository.id, '*', false, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.OWNER); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_EXERCICE_ID', exerciceId, false, true); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_EXERCICE_ID', exerciceId, false, true); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, false, true); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, false, true); Loading Loading @@ -127,6 +134,42 @@ class ExerciceRoutes implements RoutesManager { return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); } } } } private async getEnonce(req: ApiRequest, res: express.Response) { const repoTree: Array<GitlabTreeFile> = await GitlabManager.getRepositoryTree(req.boundParams.exercice.enonce.gitlabId); let enonceYamlFile: GitlabFile; let immutableFiles: Array<GitlabFile> = await Promise.all(Config.enonce.baseFiles.map(async (baseFile: string) => { let file = await GitlabManager.getFile(req.boundParams.exercice.enonce.gitlabId, baseFile); if ( baseFile === Config.enonce.filename ) { enonceYamlFile = file; } return file; })); const dojoEnonceFile: EnonceFile = YAML.parse(atob(enonceYamlFile.content)); const immutablePaths = dojoEnonceFile.immutable.map(fileDescriptor => fileDescriptor.path); await Promise.all(repoTree.map(async gitlabTreeFile => { if ( gitlabTreeFile.type == GitlabTreeFileType.BLOB ) { for ( const immutablePath of immutablePaths ) { if ( gitlabTreeFile.path.startsWith(immutablePath) ) { immutableFiles.push(await GitlabManager.getFile(req.boundParams.exercice.enonce.gitlabId, gitlabTreeFile.path)); break; } } } })); return req.session.sendResponse(res, StatusCodes.OK, { enonce : (req.boundParams.exercice as Exercice).enonce, enonceFile: dojoEnonceFile, immutable : immutableFiles }); } } } Loading shared @ bfca2c40 Compare c9154d42 to bfca2c40 Original line number Original line Diff line number Diff line Subproject commit c9154d42dac81311cf1957f0d75f806737849b40 Subproject commit bfca2c401e4b5ff69b0a515fd9dcab49d36ee212 ExpressAPI/src/types/ApiRequest.ts +4 −4 Original line number Original line Diff line number Diff line import express from 'express'; import express from 'express'; import Session from '../controllers/Session'; import Session from '../controllers/Session'; import { Enonce } from './DatabaseTypes'; import { Enonce, Exercice } from './DatabaseTypes'; type ApiRequest = express.Request & { type ApiRequest = express.Request & { session: Session, boundParams: { session: Session, boundParams: { enonce: Enonce enonce: Enonce, exercice: Exercice } } } } Loading ExpressAPI/src/types/DatabaseTypes.ts +1 −0 Original line number Original line Diff line number Diff line Loading @@ -14,6 +14,7 @@ const enonceBase = Prisma.validator<Prisma.EnonceArgs>()({ }); }); const exerciceBase = Prisma.validator<Prisma.ExerciceArgs>()({ const exerciceBase = Prisma.validator<Prisma.ExerciceArgs>()({ include: { include: { enonce : true, members: true, members: true, results: true results: true } } Loading ExpressAPI/src/types/SecurityCheckType.ts +1 −0 Original line number Original line Diff line number Diff line Loading @@ -2,6 +2,7 @@ enum SecurityCheckType { TEACHING_STAFF = 'teachingStaff', TEACHING_STAFF = 'teachingStaff', ENONCE_STAFF = 'enonceStaff', ENONCE_STAFF = 'enonceStaff', ENONCE_IS_PUBLISHED = 'enonceIsPublished', ENONCE_IS_PUBLISHED = 'enonceIsPublished', EXERCICE_SECRET = 'exerciceSecret', } } Loading
ExpressAPI/package-lock.json +78 −1 Original line number Original line Diff line number Diff line Loading @@ -11,6 +11,7 @@ "@prisma/client": "^5.0.0", "@prisma/client": "^5.0.0", "axios": "^1.4.0", "axios": "^1.4.0", "bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3", "compression": "^1.7.4", "cors": "^2.8.5", "cors": "^2.8.5", "dotenv": "^16.0.3", "dotenv": "^16.0.3", "express": "^4.18.2", "express": "^4.18.2", Loading @@ -24,17 +25,21 @@ "multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", "mysql": "^2.18.1", "node": "^20.2.0", "node": "^20.2.0", "parse-link-header": "^2.0.0", "uuid": "^9.0.0", "uuid": "^9.0.0", "winston": "^3.8.2" "winston": "^3.8.2", "yaml": "^2.3.1" }, }, "devDependencies": { "devDependencies": { "@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2", "@types/compression": "^1.7.2", "@types/cors": "^2.8.13", "@types/cors": "^2.8.13", "@types/express": "^4.17.17", "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.2", "@types/jsonwebtoken": "^9.0.2", "@types/morgan": "^1.9.4", "@types/morgan": "^1.9.4", "@types/multer": "^1.4.7", "@types/multer": "^1.4.7", "@types/node": "^20.2.4", "@types/node": "^20.2.4", "@types/parse-link-header": "^2.0.1", "@types/uuid": "^9.0.2", "@types/uuid": "^9.0.2", "nodemon": "^2.0.22", "nodemon": "^2.0.22", "npm": "^9.6.7", "npm": "^9.6.7", Loading Loading @@ -170,6 +175,15 @@ "@types/node": "*" "@types/node": "*" } } }, }, "node_modules/@types/compression": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz", "integrity": "sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==", "dev": true, "dependencies": { "@types/express": "*" } }, "node_modules/@types/connect": { "node_modules/@types/connect": { "version": "3.4.35", "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", Loading Loading @@ -251,6 +265,12 @@ "integrity": "sha512-ni5f8Xlf4PwnT/Z3f0HURc3ZSw8UyrqMqmM3L5ysa7VjHu8c3FOmIo1nKCcLrV/OAmtf3N4kFna/aJqxsfEtnA==", "integrity": "sha512-ni5f8Xlf4PwnT/Z3f0HURc3ZSw8UyrqMqmM3L5ysa7VjHu8c3FOmIo1nKCcLrV/OAmtf3N4kFna/aJqxsfEtnA==", "dev": true "dev": true }, }, "node_modules/@types/parse-link-header": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/parse-link-header/-/parse-link-header-2.0.1.tgz", "integrity": "sha512-BrKNSrRTqn3UkMXvdVtr/znJch0PMBpEvEP8oBkxDx7eEGntuFLI+WpA5HGsNHK4SlqyhaMa+Ks0ViwyixQB5w==", "dev": true }, "node_modules/@types/qs": { "node_modules/@types/qs": { "version": "6.9.7", "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", Loading Loading @@ -603,6 +623,47 @@ "node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14" } } }, }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, "engines": { "node": ">= 0.6" } }, "node_modules/compression": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", "compressible": "~2.0.16", "debug": "2.6.9", "on-headers": "~1.0.2", "safe-buffer": "5.1.2", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/compression/node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "engines": { "node": ">= 0.8" } }, "node_modules/compression/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/concat-map": { "node_modules/concat-map": { "version": "0.0.1", "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", Loading Loading @@ -4930,6 +4991,14 @@ "fn.name": "1.x.x" "fn.name": "1.x.x" } } }, }, "node_modules/parse-link-header": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-link-header/-/parse-link-header-2.0.0.tgz", "integrity": "sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==", "dependencies": { "xtend": "~4.0.1" } }, "node_modules/parseurl": { "node_modules/parseurl": { "version": "1.3.3", "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", Loading Loading @@ -5575,6 +5644,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, }, "node_modules/yaml": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", "engines": { "node": ">= 14" } }, "node_modules/yn": { "node_modules/yn": { "version": "3.1.1", "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", Loading
ExpressAPI/package.json +19 −14 Original line number Original line Diff line number Diff line Loading @@ -22,6 +22,7 @@ "@prisma/client" : "^5.0.0", "@prisma/client" : "^5.0.0", "axios" : "^1.4.0", "axios" : "^1.4.0", "bcryptjs" : "^2.4.3", "bcryptjs" : "^2.4.3", "compression" : "^1.7.4", "cors" : "^2.8.5", "cors" : "^2.8.5", "dotenv" : "^16.0.3", "dotenv" : "^16.0.3", "express" : "^4.18.2", "express" : "^4.18.2", Loading @@ -35,17 +36,21 @@ "multer" : "^1.4.5-lts.1", "multer" : "^1.4.5-lts.1", "mysql" : "^2.18.1", "mysql" : "^2.18.1", "node" : "^20.2.0", "node" : "^20.2.0", "parse-link-header": "^2.0.0", "uuid" : "^9.0.0", "uuid" : "^9.0.0", "winston" : "^3.8.2" "winston" : "^3.8.2", "yaml" : "^2.3.1" }, }, "devDependencies": { "devDependencies": { "@types/bcryptjs" : "^2.4.2", "@types/bcryptjs" : "^2.4.2", "@types/compression" : "^1.7.2", "@types/cors" : "^2.8.13", "@types/cors" : "^2.8.13", "@types/express" : "^4.17.17", "@types/express" : "^4.17.17", "@types/jsonwebtoken" : "^9.0.2", "@types/jsonwebtoken" : "^9.0.2", "@types/morgan" : "^1.9.4", "@types/morgan" : "^1.9.4", "@types/multer" : "^1.4.7", "@types/multer" : "^1.4.7", "@types/node" : "^20.2.4", "@types/node" : "^20.2.4", "@types/parse-link-header": "^2.0.1", "@types/uuid" : "^9.0.2", "@types/uuid" : "^9.0.2", "nodemon" : "^2.0.22", "nodemon" : "^2.0.22", "prisma" : "^5.0.0", "prisma" : "^5.0.0", Loading
ExpressAPI/src/config/Config.ts +5 −3 Original line number Original line Diff line number Diff line Loading @@ -16,7 +16,7 @@ class Config { }; }; public enonce: { public enonce: { default: { description: string; initReadme: boolean; sharedRunnersEnabled: boolean; visibility: string; wikiEnabled: boolean; template: string }; default: { description: string; initReadme: boolean; sharedRunnersEnabled: boolean; visibility: string; wikiEnabled: boolean; template: string }; baseFiles: Array<string>; filename: string }; }; public exercice: { public exercice: { Loading Loading @@ -64,7 +64,9 @@ class Config { visibility : process.env.ENONCE_DEFAULT_VISIBILITY, visibility : process.env.ENONCE_DEFAULT_VISIBILITY, wikiEnabled : process.env.ENONCE_DEFAULT_WIKI_ENABLED.toBoolean(), wikiEnabled : process.env.ENONCE_DEFAULT_WIKI_ENABLED.toBoolean(), template : process.env.ENONCE_DEFAULT_TEMPLATE.replace('{{USERNAME}}', this.gitlab.account.username).replace('{{TOKEN}}', this.gitlab.account.token) template : process.env.ENONCE_DEFAULT_TEMPLATE.replace('{{USERNAME}}', this.gitlab.account.username).replace('{{TOKEN}}', this.gitlab.account.token) } }, baseFiles: JSON.parse(process.env.ENONCE_BASE_FILES || '[]'), filename : process.env.ENONCE_FILENAME || '' }; }; this.exercice = { this.exercice = { Loading
ExpressAPI/src/controllers/Session.ts +14 −12 Original line number Original line Diff line number Diff line Loading @@ -5,7 +5,7 @@ import Config from '../config/Config'; import express from 'express'; import express from 'express'; import ApiRequest from '../types/ApiRequest'; import ApiRequest from '../types/ApiRequest'; import UserManager from '../managers/UserManager'; import UserManager from '../managers/UserManager'; import DojoResponse from '../shared/types/DojoResponse'; import DojoResponse from '../shared/types/Dojo/DojoResponse'; import { User } from '../types/DatabaseTypes'; import { User } from '../types/DatabaseTypes'; Loading @@ -26,6 +26,7 @@ class Session { async initSession(req: ApiRequest) { async initSession(req: ApiRequest) { const authorization = req.headers.authorization; const authorization = req.headers.authorization; if ( authorization ) { if ( authorization ) { if ( authorization.startsWith('Bearer ') ) { const jwtToken = authorization.replace('Bearer ', ''); const jwtToken = authorization.replace('Bearer ', ''); try { try { Loading @@ -38,9 +39,10 @@ class Session { } catch ( err ) { } } catch ( err ) { } } } } } } private static getToken(profileJson: any): string { private static getToken(profileJson: any): string { return profileJson.id === null ? null : jwt.sign({ profile: profileJson }, Config.jwtConfig.secret, Config.jwtConfig.expiresIn > 0 ? { expiresIn: Config.jwtConfig.expiresIn } : {}); return profileJson === null ? null : jwt.sign({ profile: profileJson }, Config.jwtConfig.secret, Config.jwtConfig.expiresIn > 0 ? { expiresIn: Config.jwtConfig.expiresIn } : {}); } } private async getResponse<T>(code: number, data: T, descriptionOverride?: string): Promise<DojoResponse<T>> { private async getResponse<T>(code: number, data: T, descriptionOverride?: string): Promise<DojoResponse<T>> { Loading
ExpressAPI/src/express/API.ts +2 −0 Original line number Original line Diff line number Diff line Loading @@ -12,6 +12,7 @@ import Config from '../config/Config'; import logger from '../shared/logging/WinstonLogger'; import logger from '../shared/logging/WinstonLogger'; import ParamsCallbackManager from '../middlewares/ParamsCallbackManager'; import ParamsCallbackManager from '../middlewares/ParamsCallbackManager'; import ApiRoutesManager from '../routes/ApiRoutesManager'; import ApiRoutesManager from '../routes/ApiRoutesManager'; import compression from 'compression'; class API implements WorkerTask { class API implements WorkerTask { Loading @@ -27,6 +28,7 @@ class API implements WorkerTask { this.backend.use(morganMiddleware); //Log API accesses this.backend.use(morganMiddleware); //Log API accesses this.backend.use(helmet()); //Help to secure express, https://helmetjs.github.io/ this.backend.use(helmet()); //Help to secure express, https://helmetjs.github.io/ this.backend.use(cors()); //Allow CORS requests this.backend.use(cors()); //Allow CORS requests this.backend.use(compression()); //Compress responses ParamsCallbackManager.register(this.backend); ParamsCallbackManager.register(this.backend); Loading
ExpressAPI/src/managers/ExerciceManager.ts 0 → 100644 +18 −0 Original line number Original line Diff line number Diff line import { Prisma } from '@prisma/client'; import { Enonce } from '../types/DatabaseTypes'; import db from '../helpers/DatabaseHelper'; class ExerciceManager { get(id: string, include: Prisma.ExerciceInclude | undefined = undefined): Promise<Enonce | undefined> { return db.exercice.findUnique({ where : { id: id }, include: include }); } } export default new ExerciceManager();
ExpressAPI/src/managers/GitlabManager.ts +42 −0 Original line number Original line Diff line number Diff line Loading @@ -8,6 +8,9 @@ import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; import ApiRequest from '../types/ApiRequest'; import ApiRequest from '../types/ApiRequest'; import GitlabUser from '../shared/types/Gitlab/GitlabUser'; import GitlabUser from '../shared/types/Gitlab/GitlabUser'; import GitlabRoutes from '../shared/types/Gitlab/GitlabRoutes'; import GitlabRoutes from '../shared/types/Gitlab/GitlabRoutes'; import GitlabTreeFile from '../shared/types/Gitlab/GitlabTreeFile'; import parseLinkHeader from 'parse-link-header'; import GitlabFile from '../shared/types/Gitlab/GitlabFile'; class GitlabManager { class GitlabManager { Loading Loading @@ -145,6 +148,45 @@ class GitlabManager { return response.data; return response.data; } } async getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<GitlabTreeFile>> { let address: string | undefined = this.getApiUrl(GitlabRoutes.REPOSITORY_TREE).replace('{{id}}', String(repoId)); let params: any = { pagination: 'keyset', recursive : recursive, per_page : 100, ref : branch }; let results: Array<GitlabTreeFile> = []; while ( params !== undefined ) { const response = await axios.get<Array<GitlabTreeFile>>(address, { params: params }); results.push(...response.data); if ( 'link' in response.headers ) { const link = parseLinkHeader(response.headers['link']); params = link.next; } else { params = undefined; } } return results; } async getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<GitlabFile> { const response = await axios.get<GitlabFile>(this.getApiUrl(GitlabRoutes.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), { params: { ref: branch } }); return response.data; } } } Loading
ExpressAPI/src/middlewares/ParamsCallbackManager.ts +9 −1 Original line number Original line Diff line number Diff line Loading @@ -3,6 +3,7 @@ import ApiRequest from '../types/ApiRequest'; import express from 'express'; import express from 'express'; import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes'; import EnonceManager from '../managers/EnonceManager'; import EnonceManager from '../managers/EnonceManager'; import ExerciceManager from '../managers/ExerciceManager'; class ParamsCallbackManager { class ParamsCallbackManager { Loading @@ -24,7 +25,8 @@ class ParamsCallbackManager { initBoundParams(req: ApiRequest) { initBoundParams(req: ApiRequest) { if ( !req.boundParams ) { if ( !req.boundParams ) { req.boundParams = { req.boundParams = { enonce: null enonce : null, exercice: null }; }; } } } } Loading @@ -34,6 +36,12 @@ class ParamsCallbackManager { exercices: true, exercices: true, staff : true staff : true } ], 'enonce'); } ], 'enonce'); this.listenParam('exerciceId', backend, ExerciceManager.get.bind(ExerciceManager), [ { enonce : true, members: true, results: true } ], 'exercice'); } } } } Loading
ExpressAPI/src/middlewares/SecurityMiddleware.ts +6 −4 Original line number Original line Diff line number Diff line Loading @@ -11,7 +11,7 @@ class SecurityMiddleware { check(checkIfConnected: boolean, ...checkTypes: Array<SecurityCheckType>): (req: ApiRequest, res: express.Response, next: express.NextFunction) => void { check(checkIfConnected: boolean, ...checkTypes: Array<SecurityCheckType>): (req: ApiRequest, res: express.Response, next: express.NextFunction) => void { return async (req: ApiRequest, res: express.Response, next: express.NextFunction) => { return async (req: ApiRequest, res: express.Response, next: express.NextFunction) => { if ( checkIfConnected ) { if ( checkIfConnected ) { if ( req.session.profile.id === null ) { if ( req.session.profile === null ) { return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED); return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED); } } } } Loading @@ -19,9 +19,9 @@ class SecurityMiddleware { let isAllowed = checkTypes.length === 0; let isAllowed = checkTypes.length === 0; if ( !isAllowed ) { if ( !isAllowed ) { for ( let checkType of checkTypes ) { for ( const checkType of checkTypes ) { try { try { switch ( checkType ) { switch ( String(checkType) ) { case SecurityCheckType.TEACHING_STAFF: case SecurityCheckType.TEACHING_STAFF: isAllowed = isAllowed || req.session.profile.isTeachingStaff; isAllowed = isAllowed || req.session.profile.isTeachingStaff; break; break; Loading @@ -31,8 +31,10 @@ class SecurityMiddleware { case SecurityCheckType.ENONCE_IS_PUBLISHED: case SecurityCheckType.ENONCE_IS_PUBLISHED: isAllowed = isAllowed || req.boundParams.enonce.published; isAllowed = isAllowed || req.boundParams.enonce.published; break; break; case SecurityCheckType.EXERCICE_SECRET: isAllowed = isAllowed || (req.headers.authorization && req.headers.authorization && req.headers.authorization.replace('ExerciceSecret ', '') === req.boundParams.exercice.secret); break; default: default: isAllowed = isAllowed || false; break; break; } } } catch ( e ) { } catch ( e ) { Loading
ExpressAPI/src/routes/EnonceRoutes.ts +2 −0 Original line number Original line Diff line number Diff line Loading @@ -75,6 +75,8 @@ class EnonceRoutes implements RoutesManager { let repository: GitlabRepository; let repository: GitlabRepository; try { try { repository = await GitlabManager.createRepository(params.name, Config.enonce.default.description.replace('{{ENONCE_NAME}}', params.name), Config.enonce.default.visibility, Config.enonce.default.initReadme, Config.gitlab.group.enonces, Config.enonce.default.sharedRunnersEnabled, Config.enonce.default.wikiEnabled, params.template); repository = await GitlabManager.createRepository(params.name, Config.enonce.default.description.replace('{{ENONCE_NAME}}', params.name), Config.enonce.default.visibility, Config.enonce.default.initReadme, Config.gitlab.group.enonces, Config.enonce.default.sharedRunnersEnabled, Config.enonce.default.wikiEnabled, params.template); await GitlabManager.protectBranch(repository.id, '*', true, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.OWNER); } catch ( error ) { } catch ( error ) { if ( error instanceof AxiosError ) { 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' ) { Loading
ExpressAPI/src/routes/ExerciceRoutes.ts +44 −1 Original line number Original line Diff line number Diff line Loading @@ -20,6 +20,11 @@ import { Prisma } from '@prisma/client'; import { Enonce, Exercice } from '../types/DatabaseTypes'; import { Enonce, Exercice } from '../types/DatabaseTypes'; import db from '../helpers/DatabaseHelper'; import db from '../helpers/DatabaseHelper'; import SecurityCheckType from '../types/SecurityCheckType'; import SecurityCheckType from '../types/SecurityCheckType'; import GitlabTreeFile from '../shared/types/Gitlab/GitlabTreeFile'; import GitlabFile from '../shared/types/Gitlab/GitlabFile'; import YAML from 'yaml'; import EnonceFile from '../shared/types/Dojo/EnonceFile'; import GitlabTreeFileType from '../shared/types/Gitlab/GitlabTreeFileType'; class ExerciceRoutes implements RoutesManager { class ExerciceRoutes implements RoutesManager { Loading @@ -33,6 +38,8 @@ class ExerciceRoutes implements RoutesManager { registerOnBackend(backend: Express) { registerOnBackend(backend: Express) { backend.post('/enonces/:enonceNameOrUrl/exercices', SecurityMiddleware.check(true, SecurityCheckType.ENONCE_IS_PUBLISHED), ParamsValidatorMiddleware.validate(this.exerciceValidator), this.createExercice.bind(this)); backend.post('/enonces/:enonceNameOrUrl/exercices', SecurityMiddleware.check(true, SecurityCheckType.ENONCE_IS_PUBLISHED), ParamsValidatorMiddleware.validate(this.exerciceValidator), this.createExercice.bind(this)); backend.get('/exercices/:exerciceId/enonce', SecurityMiddleware.check(false, SecurityCheckType.EXERCICE_SECRET), this.getEnonce.bind(this)); } } private getExerciceName(enonce: Enonce, members: Array<GitlabUser>, suffix: number): string { private getExerciceName(enonce: Enonce, members: Array<GitlabUser>, suffix: number): string { Loading @@ -58,7 +65,7 @@ class ExerciceRoutes implements RoutesManager { try { try { repository = await GitlabManager.forkRepository((enonce.gitlabCreationInfo as unknown as GitlabRepository).id, this.getExerciceName(enonce, params.members, suffix), this.getExercicePath(req.boundParams.enonce, exerciceId), Config.exercice.default.description.replace('{{ENONCE_NAME}}', enonce.name), Config.exercice.default.visibility, Config.gitlab.group.exercices); repository = await GitlabManager.forkRepository((enonce.gitlabCreationInfo as unknown as GitlabRepository).id, this.getExerciceName(enonce, params.members, suffix), this.getExercicePath(req.boundParams.enonce, exerciceId), Config.exercice.default.description.replace('{{ENONCE_NAME}}', enonce.name), Config.exercice.default.visibility, Config.gitlab.group.exercices); await GitlabManager.protectBranch(repository.id, '*', false, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER); await GitlabManager.protectBranch(repository.id, '*', false, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.OWNER); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_EXERCICE_ID', exerciceId, false, true); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_EXERCICE_ID', exerciceId, false, true); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, false, true); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, false, true); Loading Loading @@ -127,6 +134,42 @@ class ExerciceRoutes implements RoutesManager { return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); } } } } private async getEnonce(req: ApiRequest, res: express.Response) { const repoTree: Array<GitlabTreeFile> = await GitlabManager.getRepositoryTree(req.boundParams.exercice.enonce.gitlabId); let enonceYamlFile: GitlabFile; let immutableFiles: Array<GitlabFile> = await Promise.all(Config.enonce.baseFiles.map(async (baseFile: string) => { let file = await GitlabManager.getFile(req.boundParams.exercice.enonce.gitlabId, baseFile); if ( baseFile === Config.enonce.filename ) { enonceYamlFile = file; } return file; })); const dojoEnonceFile: EnonceFile = YAML.parse(atob(enonceYamlFile.content)); const immutablePaths = dojoEnonceFile.immutable.map(fileDescriptor => fileDescriptor.path); await Promise.all(repoTree.map(async gitlabTreeFile => { if ( gitlabTreeFile.type == GitlabTreeFileType.BLOB ) { for ( const immutablePath of immutablePaths ) { if ( gitlabTreeFile.path.startsWith(immutablePath) ) { immutableFiles.push(await GitlabManager.getFile(req.boundParams.exercice.enonce.gitlabId, gitlabTreeFile.path)); break; } } } })); return req.session.sendResponse(res, StatusCodes.OK, { enonce : (req.boundParams.exercice as Exercice).enonce, enonceFile: dojoEnonceFile, immutable : immutableFiles }); } } } Loading
shared @ bfca2c40 Compare c9154d42 to bfca2c40 Original line number Original line Diff line number Diff line Subproject commit c9154d42dac81311cf1957f0d75f806737849b40 Subproject commit bfca2c401e4b5ff69b0a515fd9dcab49d36ee212
ExpressAPI/src/types/ApiRequest.ts +4 −4 Original line number Original line Diff line number Diff line import express from 'express'; import express from 'express'; import Session from '../controllers/Session'; import Session from '../controllers/Session'; import { Enonce } from './DatabaseTypes'; import { Enonce, Exercice } from './DatabaseTypes'; type ApiRequest = express.Request & { type ApiRequest = express.Request & { session: Session, boundParams: { session: Session, boundParams: { enonce: Enonce enonce: Enonce, exercice: Exercice } } } } Loading
ExpressAPI/src/types/DatabaseTypes.ts +1 −0 Original line number Original line Diff line number Diff line Loading @@ -14,6 +14,7 @@ const enonceBase = Prisma.validator<Prisma.EnonceArgs>()({ }); }); const exerciceBase = Prisma.validator<Prisma.ExerciceArgs>()({ const exerciceBase = Prisma.validator<Prisma.ExerciceArgs>()({ include: { include: { enonce : true, members: true, members: true, results: true results: true } } Loading
ExpressAPI/src/types/SecurityCheckType.ts +1 −0 Original line number Original line Diff line number Diff line Loading @@ -2,6 +2,7 @@ enum SecurityCheckType { TEACHING_STAFF = 'teachingStaff', TEACHING_STAFF = 'teachingStaff', ENONCE_STAFF = 'enonceStaff', ENONCE_STAFF = 'enonceStaff', ENONCE_IS_PUBLISHED = 'enonceIsPublished', ENONCE_IS_PUBLISHED = 'enonceIsPublished', EXERCICE_SECRET = 'exerciceSecret', } } Loading