diff --git a/ExpressAPI/nodemon.json b/ExpressAPI/nodemon.json new file mode 100644 index 0000000000000000000000000000000000000000..94244c21c327ceda987f6f0c7353f0c61f141ecf --- /dev/null +++ b/ExpressAPI/nodemon.json @@ -0,0 +1,13 @@ +{ + "watch" : [ + "nodemon.json", + "node_modules", + "prisma", + "src", + ".env" + ], + "verbose": true, + "ext" : ".ts,.js", + "ignore" : [], + "exec" : "ts-node --files ./src/app.ts" +} diff --git a/ExpressAPI/package-lock.json b/ExpressAPI/package-lock.json index 8b700dbfd85a086e49623cd8aacbb9b106bdebae..30c87dabc67c4ad49a99b25c88483fd49e895e8a 100644 --- a/ExpressAPI/package-lock.json +++ b/ExpressAPI/package-lock.json @@ -8,7 +8,7 @@ "name": "dojo_backend_api", "version": "1.0.0", "dependencies": { - "@prisma/client": "^5.0.0", + "@prisma/client": "^5.1.1", "ajv": "^8.12.0", "axios": "^1.4.0", "bcryptjs": "^2.4.3", @@ -40,14 +40,14 @@ "@types/jsonwebtoken": "^9.0.2", "@types/morgan": "^1.9.4", "@types/multer": "^1.4.7", - "@types/node": "^20.2.4", + "@types/node": "^20.4.6", "@types/parse-link-header": "^2.0.1", "@types/uuid": "^9.0.2", "nodemon": "^2.0.22", "npm": "^9.6.7", - "prisma": "^5.0.0", + "prisma": "^5.1.1", "ts-node": "^10.9.1", - "typescript": "^5.0.4" + "typescript": "^5.1.6" } }, "node_modules/@colors/colors": { @@ -106,12 +106,12 @@ } }, "node_modules/@prisma/client": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.0.0.tgz", - "integrity": "sha512-XlO5ELNAQ7rV4cXIDJUNBEgdLwX3pjtt9Q/RHqDpGf43szpNJx2hJnggfFs7TKNx0cOFsl6KJCSfqr5duEU/bQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.1.1.tgz", + "integrity": "sha512-fxcCeK5pMQGcgCqCrWsi+I2rpIbk0rAhdrN+ke7f34tIrgPwA68ensrpin+9+fZvuV2OtzHmuipwduSY6HswdA==", "hasInstallScript": true, "dependencies": { - "@prisma/engines-version": "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584" + "@prisma/engines-version": "5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e" }, "engines": { "node": ">=16.13" @@ -126,16 +126,16 @@ } }, "node_modules/@prisma/engines": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.0.0.tgz", - "integrity": "sha512-kyT/8fd0OpWmhAU5YnY7eP31brW1q1YrTGoblWrhQJDiN/1K+Z8S1kylcmtjqx5wsUGcP1HBWutayA/jtyt+sg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.1.1.tgz", + "integrity": "sha512-NV/4nVNWFZSJCCIA3HIFJbbDKO/NARc9ej0tX5S9k2EVbkrFJC4Xt9b0u4rNZWL4V+F5LAjvta8vzEUw0rw+HA==", "devOptional": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { - "version": "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584.tgz", - "integrity": "sha512-HHiUF6NixsldsP3JROq07TYBLEjXFKr6PdH8H4gK/XAoTmIplOJBCgrIUMrsRAnEuGyRoRLXKXWUb943+PFoKQ==" + "version": "5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e.tgz", + "integrity": "sha512-owZqbY/wucbr65bXJ/ljrHPgQU5xXTSkmcE/JcbqE1kusuAXV/TLN3/exmz21SZ5rJ7WDkyk70J2G/n68iogbQ==" }, "node_modules/@tsconfig/node10": { "version": "1.0.9", @@ -268,9 +268,9 @@ } }, "node_modules/@types/node": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.4.tgz", - "integrity": "sha512-ni5f8Xlf4PwnT/Z3f0HURc3ZSw8UyrqMqmM3L5ysa7VjHu8c3FOmIo1nKCcLrV/OAmtf3N4kFna/aJqxsfEtnA==", + "version": "20.4.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.6.tgz", + "integrity": "sha512-q0RkvNgMweWWIvSMDiXhflGUKMdIxBo2M2tYM/0kEGDueQByFzK4KZAgu5YHGFNxziTlppNpTIBcqHQAxlfHdA==", "dev": true }, "node_modules/@types/parse-link-header": { @@ -1592,9 +1592,9 @@ } }, "node_modules/node": { - "version": "20.2.0", - "resolved": "https://registry.npmjs.org/node/-/node-20.2.0.tgz", - "integrity": "sha512-oF1+u42FT/nFtRFdcsUNdexVCWQKAHB7LtKHOk/9o0cQBq19leB/0awrB3ZdzHU/Pz9tC9RHwgd52PK9bON30A==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/node/-/node-20.5.0.tgz", + "integrity": "sha512-+mVyKGaCscnINaMNIa+plgDoN9XXb5ksw7FQB78pUmodmftxomwlaU6z6ze1aCbHVB69BGqo84KkyD6NJC9Ifg==", "hasInstallScript": true, "dependencies": { "node-bin-setup": "^1.0.0" @@ -5076,13 +5076,13 @@ } }, "node_modules/prisma": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.0.0.tgz", - "integrity": "sha512-KYWk83Fhi1FH59jSpavAYTt2eoMVW9YKgu8ci0kuUnt6Dup5Qy47pcB4/TLmiPAbhGrxxSz7gsSnJcCmkyPANA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.1.1.tgz", + "integrity": "sha512-WJFG/U7sMmcc6TjJTTifTfpI6Wjoh55xl4AzopVwAdyK68L9/ogNo8QQ2cxuUjJf/Wa82z/uhyh3wMzvRIBphg==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.0.0" + "@prisma/engines": "5.1.1" }, "bin": { "prisma": "build/index.js" @@ -5559,16 +5559,16 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/undefsafe": { diff --git a/ExpressAPI/package.json b/ExpressAPI/package.json index f51f44645e30753dc9163334811daafa12058a03..60207b678d0c7619644fa7422edf7a85d7913d76 100644 --- a/ExpressAPI/package.json +++ b/ExpressAPI/package.json @@ -19,7 +19,7 @@ "seed": "node dist/prisma/seed" }, "dependencies" : { - "@prisma/client" : "^5.0.0", + "@prisma/client" : "^5.1.1", "ajv" : "^8.12.0", "axios" : "^1.4.0", "bcryptjs" : "^2.4.3", @@ -51,13 +51,13 @@ "@types/jsonwebtoken" : "^9.0.2", "@types/morgan" : "^1.9.4", "@types/multer" : "^1.4.7", - "@types/node" : "^20.2.4", + "@types/node" : "^20.4.6", "@types/parse-link-header": "^2.0.1", "@types/uuid" : "^9.0.2", "nodemon" : "^2.0.22", - "prisma" : "^5.0.0", + "prisma" : "^5.1.1", "ts-node" : "^10.9.1", - "typescript" : "^5.0.4", + "typescript" : "^5.1.6", "npm" : "^9.6.7" } } diff --git a/ExpressAPI/src/controllers/Session.ts b/ExpressAPI/src/controllers/Session.ts index b8f9f1893da43e97f82b92a54868fa5e0a5f65e2..18d02a63fc5486fa200dfe1ddcc419ce7b95e939 100644 --- a/ExpressAPI/src/controllers/Session.ts +++ b/ExpressAPI/src/controllers/Session.ts @@ -3,14 +3,13 @@ import * as jwt from 'jsonwebtoken'; import { JwtPayload } from 'jsonwebtoken'; import Config from '../config/Config'; import express from 'express'; -import ApiRequest from '../types/ApiRequest'; import UserManager from '../managers/UserManager'; import DojoResponse from '../shared/types/Dojo/DojoResponse'; import { User } from '../types/DatabaseTypes'; class Session { - private _profile: User; + private _profile!: User; get profile(): User { return this._profile; @@ -23,7 +22,7 @@ class Session { constructor() { } - async initSession(req: ApiRequest) { + async initSession(req: express.Request) { const authorization = req.headers.authorization; if ( authorization ) { if ( authorization.startsWith('Bearer ') ) { @@ -34,14 +33,14 @@ class Session { if ( jwtData.profile ) { this.profile = jwtData.profile; - this.profile = await UserManager.getById(this.profile.id); + this.profile = await UserManager.getById(this.profile.id!) ?? this.profile; } } catch ( err ) { } } } } - private static getToken(profileJson: any): string { + private static getToken(profileJson: any): string | null { return profileJson === null ? null : jwt.sign({ profile: profileJson }, Config.jwtConfig.secret, Config.jwtConfig.expiresIn > 0 ? { expiresIn: Config.jwtConfig.expiresIn } : {}); } @@ -55,11 +54,7 @@ class Session { } catch {} return { - timestamp : (new Date()).toISOString(), - code : code, - description : descriptionOverride ? descriptionOverride : reasonPhrase, - sessionToken: Session.getToken(profileJson), - data : data + timestamp: (new Date()).toISOString(), code: code, description: descriptionOverride ? descriptionOverride : reasonPhrase, sessionToken: Session.getToken(profileJson), data: data }; } diff --git a/ExpressAPI/src/express/API.ts b/ExpressAPI/src/express/API.ts index b705aa666f8bbad996baa5670369a469f5725fb7..f7cfe2a622c0d342194ab42bee4b807d2b57b1ee 100644 --- a/ExpressAPI/src/express/API.ts +++ b/ExpressAPI/src/express/API.ts @@ -17,7 +17,7 @@ import compression from 'compression'; class API implements WorkerTask { private readonly backend: Express; - private server: http.Server; + private server: http.Server | undefined; constructor() { this.backend = express(); @@ -40,9 +40,8 @@ class API implements WorkerTask { run() { this.server = this.backend.listen(Config.api.port, '0.0.0.0', () => { const { - port, - address - } = this.server.address() as AddressInfo; + port, address + } = this.server!.address() as AddressInfo; logger.info(`Server started on http://${ address }:${ port }`); }); } diff --git a/ExpressAPI/src/helpers/DojoValidators.ts b/ExpressAPI/src/helpers/DojoValidators.ts index d86a8f711e6dad056a5a80f16c5d7a4ba9cb4daa..2648d0aeab27a070c615e6e8fb8cc9e318daec2e 100644 --- a/ExpressAPI/src/helpers/DojoValidators.ts +++ b/ExpressAPI/src/helpers/DojoValidators.ts @@ -1,25 +1,27 @@ -import ApiRequest from '../types/ApiRequest'; 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'; +import express from 'express'; declare type DojoMeta = Meta & { - req: ApiRequest + req: express.Request }; declare type DojoCustomValidator = (input: any, meta: DojoMeta) => any; +declare type DojoCustomValidatorSchemaOptions = { errorMessage?: FieldMessageFactory | ErrorMessage, negated?: boolean, bail?: boolean | BailOptions, if?: CustomValidator | ValidationChain, options?: CustomValidator } + class DojoValidators { private toValidatorSchemaOptions(arg: { errorMessage?: FieldMessageFactory | ErrorMessage, negated?: boolean, bail?: boolean | BailOptions, if?: DojoCustomValidator | ValidationChain, options?: DojoCustomValidator }) { - // This is a hack to make the types work with req arg as ApiRequest instead of Request - return arg as unknown as { errorMessage?: FieldMessageFactory | ErrorMessage, negated?: boolean, bail?: boolean | BailOptions, if?: CustomValidator | ValidationChain, options?: CustomValidator }; + // This is a hack to make the types work with req arg as express.Request instead of Request defined by express-validator + return arg as unknown as DojoCustomValidatorSchemaOptions; } - private getParamValue(req: ApiRequest, path: string): any { + private getParamValue(req: express.Request, path: string): any { return 'body' in req && path in req.body ? req.body[path] : req.query[path]; } @@ -44,28 +46,24 @@ class DojoValidators { }); readonly templateUrlValidator = this.toValidatorSchemaOptions({ - bail : true, - errorMessage: 'Template doesn\'t exist or you don\'t have access to it', - options : (value, { - req, - path - }) => { - return new Promise((resolve, reject) => { - const template = this.getParamValue(req, path); - if ( template ) { - GitlabManager.checkTemplateAccess(template, req).then((templateAccess) => { - templateAccess !== StatusCodes.OK ? reject() : resolve(true); - }); - } - resolve(true); - }); - } + bail: true, errorMessage: 'Template doesn\'t exist or you don\'t have access to it', options: (value, { + req, path + }) => { + return new Promise((resolve, reject) => { + const template = this.getParamValue(req, path); + if ( template ) { + GitlabManager.checkTemplateAccess(template, req).then((templateAccess) => { + templateAccess !== StatusCodes.OK ? reject() : resolve(true); + }); + } + resolve(true); + }); + } }); readonly templateUrlSanitizer = this.toValidatorSchemaOptions({ options: (value, { - req, - path + req, path }) => { try { const template = this.getParamValue(req, path); diff --git a/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts b/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts index 604b6de6a9a8dd619928c9fdd6ee5030b2ee574e..84325a559cf1c816d23b36295c9ce4a5d4be5251 100644 --- a/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts +++ b/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts @@ -9,20 +9,17 @@ export default Prisma.defineExtension(client => { return client.$extends({ result: { user: { - isTeachingStaff: { + isTeachingStaff : { needs: { role: true - }, - compute(user) { - return Config.permissions.teachingStaff.includes(user.role); + }, compute(user) { + return Config.permissions.teachingStaff.includes(user.role!); } - }, - gitlabProfile : { + }, gitlabProfile: { needs: { gitlabId: true - }, - compute(user) { - return new LazyVal<GitlabUser>(() => { + }, compute(user) { + return new LazyVal<GitlabUser | undefined>(() => { return GitlabManager.getUserById(user.gitlabId); }); } diff --git a/ExpressAPI/src/managers/EnonceManager.ts b/ExpressAPI/src/managers/EnonceManager.ts index 985368b7f867679e336e1f4afe271ef0e11fc12b..0a6606beb5cae99804a3636fc605d93ee67e5ad2 100644 --- a/ExpressAPI/src/managers/EnonceManager.ts +++ b/ExpressAPI/src/managers/EnonceManager.ts @@ -10,22 +10,21 @@ class EnonceManager { where: { name: enonce.name } - }).staff(); + }).staff() ?? []; } return enonce.staff.findIndex(staff => staff.id === user.id) !== -1; } async getByName(name: string, include: Prisma.EnonceInclude | undefined = undefined): Promise<Enonce | undefined> { - return db.enonce.findUnique({ - where : { - name: name - }, - include: include - }); + return await db.enonce.findUnique({ + where : { + name: name + }, include: include + }) as unknown as Enonce ?? undefined; } getByGitlabLink(gitlabLink: string, include: Prisma.EnonceInclude | undefined = undefined): Promise<Enonce | undefined> { - const name = gitlabLink.replace('.git', '').split('/').pop(); + const name = gitlabLink.replace('.git', '').split('/').pop()!; return this.getByName(name, include); } diff --git a/ExpressAPI/src/managers/ExerciceManager.ts b/ExpressAPI/src/managers/ExerciceManager.ts index 489a6e6bb58efcae5dd0d4d24367b4c5401f2cc1..bcf0990c5b0f6e8223dfc9ca1496e0db504296f1 100644 --- a/ExpressAPI/src/managers/ExerciceManager.ts +++ b/ExpressAPI/src/managers/ExerciceManager.ts @@ -1,16 +1,15 @@ -import { Prisma } from '@prisma/client'; -import { Enonce } from '../types/DatabaseTypes'; -import db from '../helpers/DatabaseHelper'; +import { Prisma } from '@prisma/client'; +import { Exercice } 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 - }); + async get(id: string, include: Prisma.ExerciceInclude | undefined = undefined): Promise<Exercice | undefined> { + return await db.exercice.findUnique({ + where : { + id: id + }, include: include + }) as unknown as Exercice ?? undefined; } } diff --git a/ExpressAPI/src/managers/GitlabManager.ts b/ExpressAPI/src/managers/GitlabManager.ts index 01fc48f8474ca9be96df8a67a78851ae928d4f32..b1639775454dccf1c105290ab8c63d9aa9dbe63c 100644 --- a/ExpressAPI/src/managers/GitlabManager.ts +++ b/ExpressAPI/src/managers/GitlabManager.ts @@ -5,12 +5,12 @@ import GitlabAccessLevel from '../shared/types/Gitlab/GitlabAccessLevel'; import GitlabMember from '../shared/types/Gitlab/GitlabMember'; import { StatusCodes } from 'http-status-codes'; import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; -import ApiRequest from '../types/ApiRequest'; import GitlabUser from '../shared/types/Gitlab/GitlabUser'; 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'; +import express from 'express'; class GitlabManager { @@ -50,14 +50,7 @@ class GitlabManager { async createRepository(name: string, description: string, visibility: string, initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, import_url: string): Promise<GitlabRepository> { const response = await axios.post<GitlabRepository>(this.getApiUrl(GitlabRoutes.REPOSITORY_CREATE), { - name : name, - description : description, - import_url : import_url, - initialize_with_readme: initializeWithReadme, - namespace_id : namespace, - shared_runners_enabled: sharedRunnersEnabled, - visibility : visibility, - wiki_enabled : wikiEnabled + name: name, description: description, import_url: import_url, initialize_with_readme: initializeWithReadme, namespace_id: namespace, shared_runners_enabled: sharedRunnersEnabled, visibility: visibility, wiki_enabled: wikiEnabled }); return response.data; @@ -65,11 +58,7 @@ class GitlabManager { async forkRepository(forkId: number, name: string, path: string, description: string, visibility: string, namespace: number): Promise<GitlabRepository> { const response = await axios.post<GitlabRepository>(this.getApiUrl(GitlabRoutes.REPOSITORY_FORK).replace('{{id}}', String(forkId)), { - name : name, - path : path, - description : description, - namespace_id: namespace, - visibility : visibility + name: name, path: path, description: description, namespace_id: namespace, visibility: visibility }); return response.data; @@ -87,8 +76,7 @@ class GitlabManager { async addRepositoryMember(repoId: number, userId: number, accessLevel: GitlabAccessLevel): Promise<GitlabMember> { const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoutes.REPOSITORY_MEMBER_ADD).replace('{{id}}', String(repoId)), { - user_id : userId, - access_level: accessLevel + user_id: userId, access_level: accessLevel }); return response.data; @@ -96,17 +84,13 @@ class GitlabManager { async addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<GitlabMember> { const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoutes.REPOSITORY_VARIABLES_ADD).replace('{{id}}', String(repoId)), { - key : key, - variable_type: 'env_var', - value : value, - protected : isProtected, - masked : isMasked + key: key, variable_type: 'env_var', value: value, protected: isProtected, masked: isMasked }); return response.data; } - async checkTemplateAccess(idOrNamespace: string, req: ApiRequest): Promise<StatusCodes> { + async checkTemplateAccess(idOrNamespace: string, req: express.Request): Promise<StatusCodes> { // Get the Gitlab project and check if it have public or internal visibility try { const project: GitlabRepository = await this.getRepository(idOrNamespace); @@ -121,8 +105,7 @@ class GitlabManager { // Check if the user and dojo are members (with at least reporter access) of the project const members = await this.getRepositoryMembers(idOrNamespace); const isUsersAtLeastReporter = { - user: false, - dojo: false + user: false, dojo: false }; members.forEach(member => { if ( member.access_level >= GitlabAccessLevel.REPORTER ) { @@ -139,11 +122,7 @@ class GitlabManager { async protectBranch(repoId: number, branchName: string, allowForcePush: boolean, allowedToMerge: GitlabAccessLevel, allowedToPush: GitlabAccessLevel, allowedToUnprotect: GitlabAccessLevel): Promise<GitlabMember> { const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoutes.REPOSITORY_BRANCHES_PROTECT).replace('{{id}}', String(repoId)), { - name : branchName, - allow_force_push : allowForcePush, - merge_access_level : allowedToMerge.valueOf(), - push_access_level : allowedToPush.valueOf(), - unprotect_access_level: allowedToUnprotect.valueOf() + name: branchName, allow_force_push: allowForcePush, merge_access_level: allowedToMerge.valueOf(), push_access_level: allowedToPush.valueOf(), unprotect_access_level: allowedToUnprotect.valueOf() }); return response.data; @@ -152,10 +131,7 @@ class GitlabManager { 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 + pagination: 'keyset', recursive: recursive, per_page: 100, ref: branch }; let results: Array<GitlabTreeFile> = []; @@ -168,8 +144,7 @@ class GitlabManager { results.push(...response.data); if ( 'link' in response.headers ) { - const link = parseLinkHeader(response.headers['link']); - params = link.next; + params = parseLinkHeader(response.headers['link'])?.next ?? undefined; } else { params = undefined; } diff --git a/ExpressAPI/src/managers/HttpManager.ts b/ExpressAPI/src/managers/HttpManager.ts index 958f8b921b8fddf66ae254818a01fb4c15aab235..80da4d4ec8abf644fa67a3bc6503f462e7625a07 100644 --- a/ExpressAPI/src/managers/HttpManager.ts +++ b/ExpressAPI/src/managers/HttpManager.ts @@ -29,7 +29,7 @@ class HttpManager { return response; }, (error) => { if ( error instanceof AxiosError ) { - logger.error(`${ JSON.stringify(error.response.data) }`); + logger.error(`${ JSON.stringify(error.response?.data) }`); } else { logger.error(`${ JSON.stringify(error) }`); } diff --git a/ExpressAPI/src/managers/UserManager.ts b/ExpressAPI/src/managers/UserManager.ts index 35f63d19c25a7e903d892e40abd3f11162249618..bf2e7a5212d091a9a1db6f2b5239bcae9246b7b2 100644 --- a/ExpressAPI/src/managers/UserManager.ts +++ b/ExpressAPI/src/managers/UserManager.ts @@ -6,40 +6,36 @@ import { User } from '../types/DatabaseTypes'; class UserManager { async getByMail(mail: string, include: Prisma.UserInclude | undefined = undefined): Promise<User | undefined> { - return db.user.findUnique({ - where : { - mail: mail - }, - include: include - }); + return await db.user.findUnique({ + where : { + mail: mail + }, include: include + }) as unknown as User ?? undefined; } async getById(id: number, include: Prisma.UserInclude | undefined = undefined): Promise<User | undefined> { return await db.user.findUnique({ - where : { + where : { id: id - }, - include: include - }); + }, include: include + }) as unknown as User ?? undefined; } async getByGitlabId(gitlabId: number, returnIdIfUndefined: boolean = true, include: Prisma.UserInclude | undefined = undefined): Promise<User | number | undefined> { - return (await db.user.findUnique({ - where : { - gitlabId: gitlabId - }, - include: include - })) ?? (returnIdIfUndefined ? gitlabId : undefined); + return await db.user.findUnique({ + where : { + gitlabId: gitlabId + }, include: include + }) as unknown as User ?? (returnIdIfUndefined ? gitlabId : undefined); } - async getFromGitlabUser(gitlabUser: GitlabUser, createIfNotExist: boolean = false, include: Prisma.UserInclude | undefined = undefined): Promise<User | number> { + async getFromGitlabUser(gitlabUser: GitlabUser, createIfNotExist: boolean = false, include: Prisma.UserInclude | undefined = undefined): Promise<User | number | undefined> { let user = await this.getByGitlabId(gitlabUser.id, true, include); if ( typeof user === 'number' && createIfNotExist ) { user = (await db.user.create({ data: { - firstname: gitlabUser.name, - gitlabId : gitlabUser.id + firstname: gitlabUser.name, gitlabId: gitlabUser.id } })).id; } @@ -47,7 +43,7 @@ class UserManager { return user; } - async getFromGitlabUsers(gitlabUsers: Array<GitlabUser>, createIfNotExist: boolean = false, include: Prisma.UserInclude | undefined = undefined): Promise<Array<User | number>> { + async getFromGitlabUsers(gitlabUsers: Array<GitlabUser>, createIfNotExist: boolean = false, include: Prisma.UserInclude | undefined = undefined): Promise<Array<User | number | undefined>> { return Promise.all(gitlabUsers.map(gitlabUser => this.getFromGitlabUser(gitlabUser, createIfNotExist, include))); } } diff --git a/ExpressAPI/src/middlewares/ParamsCallbackManager.ts b/ExpressAPI/src/middlewares/ParamsCallbackManager.ts index 358da8a97cd1807a6509c077883bd8a054a3dfd7..c7893d6f85fcfe4ba88fc7d67cbd0bf827466b6b 100644 --- a/ExpressAPI/src/middlewares/ParamsCallbackManager.ts +++ b/ExpressAPI/src/middlewares/ParamsCallbackManager.ts @@ -1,14 +1,16 @@ import { Express } from 'express-serve-static-core'; -import ApiRequest from '../types/ApiRequest'; import express from 'express'; import { StatusCodes } from 'http-status-codes'; import EnonceManager from '../managers/EnonceManager'; import ExerciceManager from '../managers/ExerciceManager'; +type GetFunction = (id: string | number, ...args: Array<any>) => Promise<any> + + class ParamsCallbackManager { - protected listenParam(paramName: string, backend: Express, getFunction: (id: string | number, ...args: Array<any>) => Promise<any>, args: Array<any>, indexName: string) { - backend.param(paramName, (req: ApiRequest, res: express.Response, next: express.NextFunction, id: string | number) => { + protected listenParam(paramName: string, backend: Express, getFunction: GetFunction, args: Array<any>, indexName: string) { + backend.param(paramName, (req: express.Request, res: express.Response, next: express.NextFunction, id: string | number) => { getFunction(id, ...args).then(result => { if ( result ) { this.initBoundParams(req); @@ -22,25 +24,21 @@ class ParamsCallbackManager { }); } - initBoundParams(req: ApiRequest) { + initBoundParams(req: express.Request) { if ( !req.boundParams ) { req.boundParams = { - enonce : null, - exercice: null + enonce: undefined, exercice: undefined }; } } register(backend: Express) { - this.listenParam('enonceNameOrUrl', backend, EnonceManager.get.bind(EnonceManager), [ { - exercices: true, - staff : true + this.listenParam('enonceNameOrUrl', backend, (EnonceManager.get as GetFunction).bind(EnonceManager), [ { + exercices: true, staff: true } ], 'enonce'); - this.listenParam('exerciceId', backend, ExerciceManager.get.bind(ExerciceManager), [ { - enonce : true, - members: true, - results: true + this.listenParam('exerciceId', backend, (ExerciceManager.get as GetFunction).bind(ExerciceManager), [ { + enonce: true, members: true, results: true } ], 'exercice'); } } diff --git a/ExpressAPI/src/middlewares/ParamsValidatorMiddleware.ts b/ExpressAPI/src/middlewares/ParamsValidatorMiddleware.ts index 9d70f6cc44e676c98da81496b3efc30bec2a5e52..6729055cdb2bf16f6668f027f0e54c087022a7a3 100644 --- a/ExpressAPI/src/middlewares/ParamsValidatorMiddleware.ts +++ b/ExpressAPI/src/middlewares/ParamsValidatorMiddleware.ts @@ -1,12 +1,11 @@ import * as ExpressValidator from 'express-validator'; import express from 'express'; import { StatusCodes } from 'http-status-codes'; -import ApiRequest from '../types/ApiRequest'; class ParamsValidatorMiddleware { - validate(validations: Array<ExpressValidator.ValidationChain> | ExpressValidator.Schema): (req: ApiRequest, res: express.Response, next: express.NextFunction) => void { - return async (req: ApiRequest, res: express.Response, next: express.NextFunction) => { + validate(validations: Array<ExpressValidator.ValidationChain> | ExpressValidator.Schema): (req: express.Request, res: express.Response, next: express.NextFunction) => void { + return async (req: express.Request, res: express.Response, next: express.NextFunction) => { if ( !(validations instanceof Array) ) { validations = ExpressValidator.checkSchema(validations); diff --git a/ExpressAPI/src/middlewares/SecurityMiddleware.ts b/ExpressAPI/src/middlewares/SecurityMiddleware.ts index 97a2299be28a358cbba948564a577b52e364e378..9ba9c2f41de95600760aac0dde6549236b5018b7 100644 --- a/ExpressAPI/src/middlewares/SecurityMiddleware.ts +++ b/ExpressAPI/src/middlewares/SecurityMiddleware.ts @@ -2,14 +2,13 @@ import express from 'express'; import { StatusCodes } from 'http-status-codes'; import SecurityCheckType from '../types/SecurityCheckType'; import logger from '../shared/logging/WinstonLogger'; -import ApiRequest from '../types/ApiRequest'; import EnonceManager from '../managers/EnonceManager'; 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: ApiRequest, res: express.Response, next: express.NextFunction) => void { - return async (req: ApiRequest, res: express.Response, next: express.NextFunction) => { + 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 ) { return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED); @@ -26,13 +25,13 @@ class SecurityMiddleware { isAllowed = isAllowed || req.session.profile.isTeachingStaff; break; case SecurityCheckType.ENONCE_STAFF: - isAllowed = isAllowed || await EnonceManager.isUserAllowedToAccessEnonce(req.boundParams.enonce, req.session.profile); + isAllowed = isAllowed || await EnonceManager.isUserAllowedToAccessEnonce(req.boundParams.enonce!, req.session.profile); break; case SecurityCheckType.ENONCE_IS_PUBLISHED: - isAllowed = isAllowed || req.boundParams.enonce.published; + isAllowed = isAllowed || (req.boundParams.enonce?.published ?? false); break; case SecurityCheckType.EXERCICE_SECRET: - isAllowed = isAllowed || (req.headers.authorization && req.headers.authorization && req.headers.authorization.replace('ExerciceSecret ', '') === req.boundParams.exercice.secret); + isAllowed = isAllowed || req.headers.authorization?.replace('ExerciceSecret ', '') === req.boundParams.exercice!.secret; break; default: break; diff --git a/ExpressAPI/src/middlewares/SessionMiddleware.ts b/ExpressAPI/src/middlewares/SessionMiddleware.ts index 120409163750ae01debdf5bbc13b3aeed7036a64..bd246225ab6be1e5ac67c74d7d5040c676709afb 100644 --- a/ExpressAPI/src/middlewares/SessionMiddleware.ts +++ b/ExpressAPI/src/middlewares/SessionMiddleware.ts @@ -1,11 +1,10 @@ -import express from 'express'; -import ApiRequest from '../types/ApiRequest'; -import Session from '../controllers/Session'; +import express from 'express'; +import Session from '../controllers/Session'; class SessionMiddleware { - register(): (req: ApiRequest, res: express.Response, next: express.NextFunction) => void { - return async (req: ApiRequest, res: express.Response, next: express.NextFunction) => { + register(): (req: express.Request, res: express.Response, next: express.NextFunction) => void { + return async (req: express.Request, res: express.Response, next: express.NextFunction) => { req.session = new Session(); await req.session.initSession(req); diff --git a/ExpressAPI/src/process/ClusterManager.ts b/ExpressAPI/src/process/ClusterManager.ts index d49f88429a27268c795ea6b6454ccb62951774d5..945f7551fa76a06c804be9e0d7ae477287c4d90f 100644 --- a/ExpressAPI/src/process/ClusterManager.ts +++ b/ExpressAPI/src/process/ClusterManager.ts @@ -27,7 +27,9 @@ class ClusterManager { this.strategy.forEach(workerPool => { for ( let i = 0 ; i < workerPool.quantity ; i += 1 ) { const worker = cluster.fork({ role: workerPool.role }); - this.workers[worker.process.pid] = workerPool.role; + if ( worker.process.pid ) { + this.workers[worker.process.pid] = workerPool.role; + } } }); @@ -35,15 +37,20 @@ class ClusterManager { cluster.on('exit', (worker: Worker, code: number) => { logger.info(`Worker ${ worker.process.pid } exited with code ${ code }`); - const workerRole = this.workers[worker.process.pid]; + if ( worker.process.pid ) { + const workerRole = this.workers[worker.process.pid]; - const workerPool = this.getWorkerPool(workerRole); - if ( workerPool && workerPool.restartOnFail ) { - const newWorker = cluster.fork({ role: workerRole }); - this.workers[newWorker.process.pid] = workerPool.role; - } + const workerPool = this.getWorkerPool(workerRole); + if ( workerPool && workerPool.restartOnFail ) { + const newWorker = cluster.fork({ role: workerRole }); + + if ( newWorker.process.pid ) { + this.workers[newWorker.process.pid] = workerPool.role; + } + } - delete this.workers[worker.process.pid]; + delete this.workers[worker.process.pid]; + } }); } diff --git a/ExpressAPI/src/routes/BaseRoutes.ts b/ExpressAPI/src/routes/BaseRoutes.ts index d8cbce5ede3c65953e5fa904181a9d2bf91b2bad..0ef1eb0d6af2aab2e6c9fbd7f471c5aa670ebc93 100644 --- a/ExpressAPI/src/routes/BaseRoutes.ts +++ b/ExpressAPI/src/routes/BaseRoutes.ts @@ -1,5 +1,4 @@ import { Express } from 'express-serve-static-core'; -import ApiRequest from '../types/ApiRequest'; import express from 'express'; import { StatusCodes } from 'http-status-codes'; import RoutesManager from '../express/RoutesManager'; @@ -7,8 +6,8 @@ import RoutesManager from '../express/RoutesManager'; class BaseRoutes implements RoutesManager { registerOnBackend(backend: Express) { - backend.get('/', (req: ApiRequest, res: express.Response) => { res.status(StatusCodes.OK).end(); }); - backend.get('/health_check', (req: ApiRequest, res: express.Response) => { res.status(StatusCodes.OK).end(); }); + backend.get('/', (req: express.Request, res: express.Response) => { res.status(StatusCodes.OK).end(); }); + backend.get('/health_check', (req: express.Request, res: express.Response) => { res.status(StatusCodes.OK).end(); }); } } diff --git a/ExpressAPI/src/routes/EnonceRoutes.ts b/ExpressAPI/src/routes/EnonceRoutes.ts index dbe094ebabe6a32672d46d948575dec5bf01b219..6d371861c6845c1d5bbbcca7d28d918ac92a3b35 100644 --- a/ExpressAPI/src/routes/EnonceRoutes.ts +++ b/ExpressAPI/src/routes/EnonceRoutes.ts @@ -1,43 +1,35 @@ -import { Express } from 'express-serve-static-core'; -import express from 'express'; -import * as ExpressValidator from 'express-validator'; -import { StatusCodes } from 'http-status-codes'; -import RoutesManager from '../express/RoutesManager'; -import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware'; -import ApiRequest from '../types/ApiRequest'; -import SecurityMiddleware from '../middlewares/SecurityMiddleware'; -import SecurityCheckType from '../types/SecurityCheckType'; -import GitlabUser from '../shared/types/Gitlab/GitlabUser'; -import GitlabManager from '../managers/GitlabManager'; -import Config from '../config/Config'; -import GitlabMember from '../shared/types/Gitlab/GitlabMember'; -import GitlabAccessLevel from '../shared/types/Gitlab/GitlabAccessLevel'; -import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; -import { AxiosError } from 'axios'; -import logger from '../shared/logging/WinstonLogger'; -import DojoValidators from '../helpers/DojoValidators'; -import { Prisma } from '@prisma/client'; -import db from '../helpers/DatabaseHelper'; -import { Enonce } from '../types/DatabaseTypes'; -import EnonceManager from '../managers/EnonceManager'; -import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; +import { Express } from 'express-serve-static-core'; +import express from 'express'; +import * as ExpressValidator from 'express-validator'; +import { StatusCodes } from 'http-status-codes'; +import RoutesManager from '../express/RoutesManager'; +import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware'; +import SecurityMiddleware from '../middlewares/SecurityMiddleware'; +import SecurityCheckType from '../types/SecurityCheckType'; +import GitlabUser from '../shared/types/Gitlab/GitlabUser'; +import GitlabManager from '../managers/GitlabManager'; +import Config from '../config/Config'; +import GitlabMember from '../shared/types/Gitlab/GitlabMember'; +import GitlabAccessLevel from '../shared/types/Gitlab/GitlabAccessLevel'; +import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; +import { AxiosError, HttpStatusCode } from 'axios'; +import logger from '../shared/logging/WinstonLogger'; +import DojoValidators from '../helpers/DojoValidators'; +import { Prisma } from '@prisma/client'; +import db from '../helpers/DatabaseHelper'; +import { Enonce } from '../types/DatabaseTypes'; +import EnonceManager from '../managers/EnonceManager'; +import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; class EnonceRoutes implements RoutesManager { private readonly enonceValidator: ExpressValidator.Schema = { - name : { - trim : true, - notEmpty: true - }, - members : { - trim : true, - notEmpty : true, - customSanitizer: DojoValidators.jsonSanitizer - }, - template: { - trim : true, - custom : DojoValidators.templateUrlValidator, - customSanitizer: DojoValidators.templateUrlSanitizer + name : { + trim: true, notEmpty: true + }, members : { + trim: true, notEmpty: true, customSanitizer: DojoValidators.jsonSanitizer + }, template: { + trim: true, custom: DojoValidators.templateUrlValidator, customSanitizer: DojoValidators.templateUrlSanitizer } }; @@ -50,24 +42,33 @@ class EnonceRoutes implements RoutesManager { } // Get an enonce by its name or gitlab url - private async getEnonce(req: ApiRequest, res: express.Response) { - const enonce: Enonce = req.boundParams.enonce; + private async getEnonce(req: express.Request, res: express.Response) { + const enonce: Enonce | undefined = req.boundParams.enonce; if ( enonce && !enonce.published && !await EnonceManager.isUserAllowedToAccessEnonce(enonce, req.session.profile) ) { + // @ts-ignore delete enonce.gitlabId; + // @ts-ignore delete enonce.gitlabLink; + // @ts-ignore delete enonce.gitlabCreationInfo; + // @ts-ignore delete enonce.gitlabLastInfo; + // @ts-ignore delete enonce.gitlabLastInfoDate; + // @ts-ignore delete enonce.staff; + // @ts-ignore delete enonce.exercices; } return enonce ? req.session.sendResponse(res, StatusCodes.OK, enonce) : res.status(StatusCodes.NOT_FOUND).send(); } - private async createEnonce(req: ApiRequest, res: express.Response) { - const params: { name: string, members: Array<GitlabUser>, template: string } = req.body; + private async createEnonce(req: express.Request, res: express.Response) { + const params: { + name: string, members: Array<GitlabUser>, template: string + } = req.body; params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ]; params.members = params.members.removeObjectDuplicates(gitlabUser => gitlabUser.id); @@ -79,11 +80,11 @@ class EnonceRoutes implements RoutesManager { await GitlabManager.protectBranch(repository.id, '*', true, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.OWNER); } catch ( 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(); } - return res.status(error.response.status).send(); + return res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); } logger.error(error); @@ -101,32 +102,24 @@ class EnonceRoutes implements RoutesManager { const enonce: Enonce = await db.enonce.create({ data: { - name : repository.name, - gitlabId : repository.id, - gitlabLink : repository.web_url, - gitlabCreationInfo: repository as unknown as Prisma.JsonObject, - gitlabLastInfo : repository as unknown as Prisma.JsonObject, - gitlabLastInfoDate: new Date(), - staff : { + name: repository.name, gitlabId: repository.id, gitlabLink: repository.web_url, gitlabCreationInfo: repository as unknown as Prisma.JsonObject, gitlabLastInfo: repository as unknown as Prisma.JsonObject, gitlabLastInfoDate: new Date(), staff: { connectOrCreate: [ ...params.members.map(gitlabUser => { return { - create: { - gitlabId : gitlabUser.id, - firstname: gitlabUser.name - }, - where : { + create : { + gitlabId: gitlabUser.id, firstname: gitlabUser.name + }, where: { gitlabId: gitlabUser.id } }; }) ] } } - }); + }) as unknown as Enonce; return req.session.sendResponse(res, StatusCodes.OK, enonce); } catch ( error ) { if ( error instanceof AxiosError ) { - return res.status(error.response.status).send(); + return res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); } logger.error(error); @@ -134,24 +127,23 @@ class EnonceRoutes implements RoutesManager { } } - private changeEnoncePublishedStatus(publish: boolean): (req: ApiRequest, res: express.Response) => Promise<void> { - return async (req: ApiRequest, res: express.Response): Promise<void> => { + private changeEnoncePublishedStatus(publish: boolean): (req: express.Request, res: express.Response) => Promise<void> { + return async (req: express.Request, res: express.Response): Promise<void> => { try { - await GitlabManager.changeRepositoryVisibility(req.boundParams.enonce.gitlabId, publish ? GitlabVisibility.INTERNAL : GitlabVisibility.PRIVATE); + await GitlabManager.changeRepositoryVisibility(req.boundParams.enonce!.gitlabId, publish ? GitlabVisibility.INTERNAL : GitlabVisibility.PRIVATE); await db.enonce.update({ - where: { - name: req.boundParams.enonce.name - }, - data : { - published: publish - } + where : { + name: req.boundParams.enonce!.name + }, data: { + published: publish + } }); req.session.sendResponse(res, StatusCodes.OK); } catch ( error ) { if ( error instanceof AxiosError ) { - res.status(error.response.status).send(); + res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); return; } diff --git a/ExpressAPI/src/routes/ExerciceRoutes.ts b/ExpressAPI/src/routes/ExerciceRoutes.ts index ec657f81bf1b1ed6f18381175896b4f90cdd7794..bb736cd4971a2aba5623ef547a5dabecf88603da 100644 --- a/ExpressAPI/src/routes/ExerciceRoutes.ts +++ b/ExpressAPI/src/routes/ExerciceRoutes.ts @@ -1,38 +1,35 @@ -import { Express } from 'express-serve-static-core'; -import express from 'express'; -import * as ExpressValidator from 'express-validator'; -import { StatusCodes } from 'http-status-codes'; -import RoutesManager from '../express/RoutesManager'; -import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware'; -import ApiRequest from '../types/ApiRequest'; -import SecurityMiddleware from '../middlewares/SecurityMiddleware'; -import GitlabUser from '../shared/types/Gitlab/GitlabUser'; -import GitlabManager from '../managers/GitlabManager'; -import Config from '../config/Config'; -import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; -import { AxiosError } from 'axios'; -import logger from '../shared/logging/WinstonLogger'; -import DojoValidators from '../helpers/DojoValidators'; -import { v4 as uuidv4 } from 'uuid'; -import GitlabMember from '../shared/types/Gitlab/GitlabMember'; -import GitlabAccessLevel from '../shared/types/Gitlab/GitlabAccessLevel'; -import { Prisma } from '@prisma/client'; -import { Enonce, Exercice } from '../types/DatabaseTypes'; -import db from '../helpers/DatabaseHelper'; -import SecurityCheckType from '../types/SecurityCheckType'; -import GitlabTreeFile from '../shared/types/Gitlab/GitlabTreeFile'; -import GitlabFile from '../shared/types/Gitlab/GitlabFile'; -import EnonceFile from '../shared/types/Dojo/EnonceFile'; -import GitlabTreeFileType from '../shared/types/Gitlab/GitlabTreeFileType'; -import hjson from 'hjson'; +import { Express } from 'express-serve-static-core'; +import express from 'express'; +import * as ExpressValidator from 'express-validator'; +import { StatusCodes } from 'http-status-codes'; +import RoutesManager from '../express/RoutesManager'; +import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware'; +import SecurityMiddleware from '../middlewares/SecurityMiddleware'; +import GitlabUser from '../shared/types/Gitlab/GitlabUser'; +import GitlabManager from '../managers/GitlabManager'; +import Config from '../config/Config'; +import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; +import { AxiosError, HttpStatusCode } from 'axios'; +import logger from '../shared/logging/WinstonLogger'; +import DojoValidators from '../helpers/DojoValidators'; +import { v4 as uuidv4 } from 'uuid'; +import GitlabMember from '../shared/types/Gitlab/GitlabMember'; +import GitlabAccessLevel from '../shared/types/Gitlab/GitlabAccessLevel'; +import { Prisma } from '@prisma/client'; +import { Enonce, Exercice } from '../types/DatabaseTypes'; +import db from '../helpers/DatabaseHelper'; +import SecurityCheckType from '../types/SecurityCheckType'; +import GitlabTreeFile from '../shared/types/Gitlab/GitlabTreeFile'; +import GitlabFile from '../shared/types/Gitlab/GitlabFile'; +import EnonceFile from '../shared/types/Dojo/EnonceFile'; +import GitlabTreeFileType from '../shared/types/Gitlab/GitlabTreeFileType'; +import hjson from 'hjson'; class ExerciceRoutes implements RoutesManager { private readonly exerciceValidator: ExpressValidator.Schema = { members: { - trim : true, - notEmpty : true, - customSanitizer: DojoValidators.jsonSanitizer + trim: true, notEmpty: true, customSanitizer: DojoValidators.jsonSanitizer } }; @@ -50,20 +47,20 @@ class ExerciceRoutes implements RoutesManager { return `dojo-ex_${ (enonce.gitlabLastInfo as unknown as GitlabRepository).path }_${ exerciceId }`; } - private async createExercice(req: ApiRequest, res: express.Response) { + private async createExercice(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 enonce: Enonce = req.boundParams.enonce; + params.members = [ await req.session.profile.gitlabProfile!.value, ...params.members ].removeObjectDuplicates(gitlabUser => gitlabUser.id); + const enonce: Enonce = req.boundParams.enonce!; const exerciceId: string = uuidv4(); const secret: string = uuidv4(); - let repository: GitlabRepository; + let repository!: GitlabRepository; let suffix: number = 0; do { 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.OWNER); @@ -74,10 +71,10 @@ class ExerciceRoutes implements RoutesManager { break; } catch ( 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 res.status(error.response.status).send(); + return res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); } } else { return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); @@ -100,35 +97,24 @@ class ExerciceRoutes implements RoutesManager { const exercice: Exercice = await db.exercice.create({ data: { - id : exerciceId, - enonceName : enonce.name, - name : repository.name, - secret : secret, - gitlabId : repository.id, - gitlabLink : repository.web_url, - gitlabCreationInfo: repository as unknown as Prisma.JsonObject, - gitlabLastInfo : repository as unknown as Prisma.JsonObject, - gitlabLastInfoDate: new Date(), - members : { + id: exerciceId, enonceName: enonce.name, name: repository.name, secret: secret, gitlabId: repository.id, gitlabLink: repository.web_url, gitlabCreationInfo: repository as unknown as Prisma.JsonObject, gitlabLastInfo: repository as unknown as Prisma.JsonObject, gitlabLastInfoDate: new Date(), members: { connectOrCreate: [ ...params.members.map(gitlabUser => { return { - create: { - gitlabId : gitlabUser.id, - firstname: gitlabUser.name - }, - where : { + create : { + gitlabId: gitlabUser.id, firstname: gitlabUser.name + }, where: { gitlabId: gitlabUser.id } }; }) ] } } - }); + }) as unknown as Exercice; return req.session.sendResponse(res, StatusCodes.OK, exercice); } catch ( error ) { if ( error instanceof AxiosError ) { - return res.status(error.response.status).send(); + return res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); } logger.error(error); @@ -136,21 +122,21 @@ class ExerciceRoutes implements RoutesManager { } } - private async getEnonce(req: ApiRequest, res: express.Response) { - const repoTree: Array<GitlabTreeFile> = await GitlabManager.getRepositoryTree(req.boundParams.exercice.enonce.gitlabId); + private async getEnonce(req: express.Request, res: express.Response) { + const repoTree: Array<GitlabTreeFile> = await GitlabManager.getRepositoryTree(req.boundParams.exercice!.enonce.gitlabId); - let enonceYamlFile: GitlabFile; + let enonceHjsonFile!: 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); + let file = await GitlabManager.getFile(req.boundParams.exercice!.enonce.gitlabId, baseFile); if ( baseFile === Config.enonce.filename ) { - enonceYamlFile = file; + enonceHjsonFile = file; } return file; })); - const dojoEnonceFile: EnonceFile = hjson.parse(atob(enonceYamlFile.content)) as EnonceFile; + const dojoEnonceFile: EnonceFile = hjson.parse(atob(enonceHjsonFile.content)) as EnonceFile; const immutablePaths = dojoEnonceFile.immutable.map(fileDescriptor => fileDescriptor.path); @@ -158,7 +144,7 @@ class ExerciceRoutes implements RoutesManager { 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)); + immutableFiles.push(await GitlabManager.getFile(req.boundParams.exercice!.enonce.gitlabId, gitlabTreeFile.path)); break; } } @@ -166,9 +152,7 @@ class ExerciceRoutes implements RoutesManager { })); return req.session.sendResponse(res, StatusCodes.OK, { - enonce : (req.boundParams.exercice as Exercice).enonce, - enonceFile: dojoEnonceFile, - immutable : immutableFiles + enonce: (req.boundParams.exercice as Exercice).enonce, enonceFile: dojoEnonceFile, immutable: immutableFiles }); } } diff --git a/ExpressAPI/src/routes/GitlabRoutes.ts b/ExpressAPI/src/routes/GitlabRoutes.ts index 6804d9467e72dc598964d16df4c378c82df22d63..ad03cfca87eb268d9cac241ec744de8df7779d81 100644 --- a/ExpressAPI/src/routes/GitlabRoutes.ts +++ b/ExpressAPI/src/routes/GitlabRoutes.ts @@ -1,7 +1,6 @@ import { Express } from 'express-serve-static-core'; import express from 'express'; import RoutesManager from '../express/RoutesManager'; -import ApiRequest from '../types/ApiRequest'; import SecurityMiddleware from '../middlewares/SecurityMiddleware'; import SecurityCheckType from '../types/SecurityCheckType'; import GitlabManager from '../managers/GitlabManager'; @@ -12,7 +11,7 @@ class EnonceRoutes implements RoutesManager { backend.get('/gitlab/project/:idOrNamespace/checkTemplateAccess', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), this.checkTemplateAccess); } - private async checkTemplateAccess(req: ApiRequest, res: express.Response) { + private async checkTemplateAccess(req: express.Request, res: express.Response) { const idOrNamespace: string = req.params.idOrNamespace; return res.status(await GitlabManager.checkTemplateAccess(idOrNamespace, req)).send(); diff --git a/ExpressAPI/src/routes/SessionRoutes.ts b/ExpressAPI/src/routes/SessionRoutes.ts index 839d091e23075b03886e6df03089c08c2347a3fc..e3569e3e06d12ec8f22b9c32815124f4558bb50b 100644 --- a/ExpressAPI/src/routes/SessionRoutes.ts +++ b/ExpressAPI/src/routes/SessionRoutes.ts @@ -5,7 +5,6 @@ import { StatusCodes } from 'http-status-codes'; import * as bcrypt from 'bcryptjs'; import RoutesManager from '../express/RoutesManager'; import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware'; -import ApiRequest from '../types/ApiRequest'; import UserManager from '../managers/UserManager'; import SecurityMiddleware from '../middlewares/SecurityMiddleware'; import { User } from '../types/DatabaseTypes'; @@ -13,28 +12,27 @@ import { User } from '../types/DatabaseTypes'; class SessionRoutes implements RoutesManager { private readonly loginValidator: ExpressValidator.Schema = { - user : { - trim : true, - notEmpty: true - }, - password: { - trim : true, - notEmpty: true + user : { + trim: true, notEmpty: true + }, password: { + trim: true, notEmpty: true } }; registerOnBackend(backend: Express) { backend.post('/login', ParamsValidatorMiddleware.validate(this.loginValidator), this.login); - backend.get('/test_session', SecurityMiddleware.check(true), (req: ApiRequest, res: express.Response) => req.session.sendResponse(res, StatusCodes.OK)); + backend.get('/test_session', SecurityMiddleware.check(true), (req: express.Request, res: express.Response) => req.session.sendResponse(res, StatusCodes.OK)); } - private async login(req: ApiRequest, res: express.Response) { - const params: { user: string, password: string } = req.body; + private async login(req: express.Request, res: express.Response) { + const params: { + user: string, password: string + } = req.body; const user: User | undefined = await UserManager.getByMail(params.user); if ( user ) { - if ( bcrypt.compareSync(params.password, user.password) ) { + if ( bcrypt.compareSync(params.password, user.password ?? '') ) { req.session.profile = user; req.session.sendResponse(res, StatusCodes.OK); diff --git a/ExpressAPI/src/shared b/ExpressAPI/src/shared index bdcfaffc04421d131cc63636a7c2b84a20cfadc5..7d7227e748ce8d5475fee4a5a5fc2ced0c0ef5c3 160000 --- a/ExpressAPI/src/shared +++ b/ExpressAPI/src/shared @@ -1 +1 @@ -Subproject commit bdcfaffc04421d131cc63636a7c2b84a20cfadc5 +Subproject commit 7d7227e748ce8d5475fee4a5a5fc2ced0c0ef5c3 diff --git a/ExpressAPI/src/types/ApiRequest.ts b/ExpressAPI/src/types/ApiRequest.ts deleted file mode 100644 index 9a6c06d0016e1017e757ac9796bad4522eccac0a..0000000000000000000000000000000000000000 --- a/ExpressAPI/src/types/ApiRequest.ts +++ /dev/null @@ -1,13 +0,0 @@ -import express from 'express'; -import Session from '../controllers/Session'; -import { Enonce, Exercice } from './DatabaseTypes'; - - -type ApiRequest = express.Request & { - session: Session, boundParams: { - enonce: Enonce, exercice: Exercice - } -} - - -export default ApiRequest; diff --git a/ExpressAPI/src/types/DatabaseTypes.ts b/ExpressAPI/src/types/DatabaseTypes.ts index a4ac8b5d7883458ebb47ec1a5e45a2344ba0ff6a..50443639229d30eee65150b3f98ee5a130fb7ece 100644 --- a/ExpressAPI/src/types/DatabaseTypes.ts +++ b/ExpressAPI/src/types/DatabaseTypes.ts @@ -8,15 +8,12 @@ const userBase = Prisma.validator<Prisma.UserArgs>()({ }); const enonceBase = Prisma.validator<Prisma.EnonceArgs>()({ include: { - exercices: true, - staff : true + exercices: true, staff: true } }); const exerciceBase = Prisma.validator<Prisma.ExerciceArgs>()({ include: { - enonce : true, - members: true, - results: true + enonce: true, members: true, results: true } }); const resultBase = Prisma.validator<Prisma.ResultArgs>()({ @@ -26,10 +23,11 @@ const resultBase = Prisma.validator<Prisma.ResultArgs>()({ }); -export type User = Partial<Prisma.UserGetPayload<typeof userBase> & { +export type User = Omit<Prisma.UserGetPayload<typeof userBase>, 'password'> & { + password?: string isTeachingStaff: boolean gitlabProfile: LazyVal<GitlabUser> -}> -export type Enonce = Partial<Prisma.EnonceGetPayload<typeof enonceBase>> -export type Exercice = Partial<Prisma.ExerciceGetPayload<typeof exerciceBase>> -export type Result = Partial<Prisma.ResultGetPayload<typeof resultBase>> \ No newline at end of file +} +export type Enonce = Prisma.EnonceGetPayload<typeof enonceBase> +export type Exercice = Prisma.ExerciceGetPayload<typeof exerciceBase> +export type Result = Prisma.ResultGetPayload<typeof resultBase> \ No newline at end of file diff --git a/ExpressAPI/src/types/express/index.d.ts b/ExpressAPI/src/types/express/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..c33801d8513fa8dde55cd0cd685a3c3918c3e22b --- /dev/null +++ b/ExpressAPI/src/types/express/index.d.ts @@ -0,0 +1,16 @@ +import Session from '../../controllers/Session'; +import { Enonce, Exercice } from '../DatabaseTypes'; + +// to make the file a module and avoid the TypeScript error +export {}; + +declare global { + namespace Express { + export interface Request { + session: Session, + boundParams: { + enonce: Enonce | undefined, exercice: Exercice | undefined + } + } + } +} diff --git a/ExpressAPI/tsconfig.json b/ExpressAPI/tsconfig.json index 61c095536832314b507e82b3f425e04ef8b93205..b5e155a00c59808e300cd53394790b1f7baad9b4 100644 --- a/ExpressAPI/tsconfig.json +++ b/ExpressAPI/tsconfig.json @@ -1,23 +1,24 @@ { "compilerOptions": { - "module": "commonjs", - "esModuleInterop": true, - "target": "es6", - "noImplicitAny": true, + "outDir" : "dist", + "strict" : true, + "target" : "es6", + "module" : "commonjs", + "sourceMap" : true, + "esModuleInterop" : true, "moduleResolution": "node", - "sourceMap": true, - "outDir": "dist", - "baseUrl": ".", - "paths": { + "noImplicitAny" : true, + "baseUrl" : ".", + "paths" : { "*": [ "node_modules/*" ] } }, - "include": [ + "include" : [ "src/**/*", "prisma/seed.ts" ], - "exclude": [ + "exclude" : [ ] }