diff --git a/ExpressAPI/.idea/jetbrainsConfiguration b/ExpressAPI/.idea/jetbrainsConfiguration index ffc5d65f9f0f0e825688177425e526131aa84631..f572bf3afa0a98675247df85c599b5d1e5a62d0d 160000 --- a/ExpressAPI/.idea/jetbrainsConfiguration +++ b/ExpressAPI/.idea/jetbrainsConfiguration @@ -1 +1 @@ -Subproject commit ffc5d65f9f0f0e825688177425e526131aa84631 +Subproject commit f572bf3afa0a98675247df85c599b5d1e5a62d0d diff --git a/ExpressAPI/package-lock.json b/ExpressAPI/package-lock.json index 760ee8e43a64e8471e6752843ac3858a7a0acfad..7f414466df56364dbc435a8393f8fcc30c9bda47 100644 --- a/ExpressAPI/package-lock.json +++ b/ExpressAPI/package-lock.json @@ -9,7 +9,7 @@ "version": "3.6.0", "license": "AGPLv3", "dependencies": { - "@gitbeaker/rest": "^39.34.2", + "@gitbeaker/rest": "^40.0.1", "@prisma/client": "^5.9.1", "axios": "^1.6.7", "compression": "^1.7.4", @@ -224,11 +224,11 @@ "dev": true }, "node_modules/@gitbeaker/core": { - "version": "39.34.2", - "resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-39.34.2.tgz", - "integrity": "sha512-Vs1BKnEMnHltq1nMuBKxust1E+JUroDVKLy87ElLgvjAkH726mEVJCFnNC2/o2Ru7Et2qqhFN+PlUeYzzAbU2w==", + "version": "40.0.1", + "resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-40.0.1.tgz", + "integrity": "sha512-Zh2eVUgy2kYVnp7Db4gWoFqFbjgsnm2FvBEERbH3UM3cOA/iMqM+tw/of+Qk4yO+gv6tGZ9f4nF7+vK0tQFmDA==", "dependencies": { - "@gitbeaker/requester-utils": "^39.34.2", + "@gitbeaker/requester-utils": "^40.0.1", "qs": "^6.11.2", "xcase": "^2.0.1" }, @@ -237,11 +237,11 @@ } }, "node_modules/@gitbeaker/core/node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -251,9 +251,9 @@ } }, "node_modules/@gitbeaker/requester-utils": { - "version": "39.34.2", - "resolved": "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-39.34.2.tgz", - "integrity": "sha512-ToCwNKQe/+uHjB2kPTXY72SvbAyjsPABb9T1EiMGuVahk6rWdhtVZIM659rGuqdJGTqQ4y18wk0A+w6D3Z2lCQ==", + "version": "40.0.1", + "resolved": "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-40.0.1.tgz", + "integrity": "sha512-cn6fltKuQ3TbthoMTg+JsKQfozqGcRcz1jT9Nqzr4gpHWgjdQ/nr5JpjwzKABQNVL2JH3UJWr6Eji60CFZDZ6Q==", "dependencies": { "picomatch-browser": "^2.2.6", "qs": "^6.11.2", @@ -265,11 +265,11 @@ } }, "node_modules/@gitbeaker/requester-utils/node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -279,12 +279,12 @@ } }, "node_modules/@gitbeaker/rest": { - "version": "39.34.2", - "resolved": "https://registry.npmjs.org/@gitbeaker/rest/-/rest-39.34.2.tgz", - "integrity": "sha512-MT4Vue1ltvsR7Nug18A6DIk+u+gu64+b0Un/R2XIsLB7eSAX8Pm/sQnYxsjHksroZJVlyGHiGsaxbllX75Pntg==", + "version": "40.0.1", + "resolved": "https://registry.npmjs.org/@gitbeaker/rest/-/rest-40.0.1.tgz", + "integrity": "sha512-JEd9WNuzgur7gLiJPMWPYKaWe5uX1ic8CGKR1fMtBityFZ2xyZkTZ+LG0nqWTV1MyiowYnJ1swTh8Yff+kLsKA==", "dependencies": { - "@gitbeaker/core": "^39.34.2", - "@gitbeaker/requester-utils": "^39.34.2" + "@gitbeaker/core": "^40.0.1", + "@gitbeaker/requester-utils": "^40.0.1" }, "engines": { "node": ">=18.0.0" @@ -1801,14 +1801,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", - "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "set-function-length": "^1.2.0" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -2326,17 +2327,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz", - "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { + "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.2", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/delayed-stream": { @@ -2535,6 +2538,17 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -3437,20 +3451,20 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -3470,9 +3484,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -8410,16 +8424,16 @@ } }, "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8512,11 +8526,11 @@ "dev": true }, "node_modules/side-channel": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" diff --git a/ExpressAPI/package.json b/ExpressAPI/package.json index b2da7463ee002bf102ec7288d0d8c6c337b9abab..119426c43cc4492ba747bcb8a3f22958a2e55ed5 100644 --- a/ExpressAPI/package.json +++ b/ExpressAPI/package.json @@ -28,7 +28,7 @@ "seed": "node dist/prisma/seed" }, "dependencies" : { - "@gitbeaker/rest" : "^39.34.2", + "@gitbeaker/rest" : "^40.0.1", "@prisma/client" : "^5.9.1", "axios" : "^1.6.7", "compression" : "^1.7.4", diff --git a/ExpressAPI/prisma/seed.ts b/ExpressAPI/prisma/seed.ts index fc8dce5183e470dafbfd99caa6eca2de6faec393..74d814d9f269ff4ca5c3651ab743f979799758f5 100644 --- a/ExpressAPI/prisma/seed.ts +++ b/ExpressAPI/prisma/seed.ts @@ -17,7 +17,7 @@ async function main() { main().then(async () => { await db.$disconnect(); }).catch(async e => { - logger.error(e); + logger.error(JSON.stringify(e)); await db.$disconnect(); process.exit(1); }); diff --git a/ExpressAPI/src/config/Config.ts b/ExpressAPI/src/config/Config.ts index a768fded7ca22aae497741693e813a785157a61f..2eaa5ec692b2c84a82c917564ac27634c3433639 100644 --- a/ExpressAPI/src/config/Config.ts +++ b/ExpressAPI/src/config/Config.ts @@ -1,8 +1,8 @@ -import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; import path from 'path'; import fs from 'fs'; import { Exercise } from '../types/DatabaseTypes'; import JSON5 from 'json5'; +import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; type ConfigGitlabBadge = { @@ -52,13 +52,13 @@ class Config { public readonly assignment: { default: { - description: string; initReadme: boolean; sharedRunnersEnabled: boolean; visibility: string; wikiEnabled: boolean; template: string + description: string; initReadme: boolean; sharedRunnersEnabled: boolean; visibility: GitlabVisibility; wikiEnabled: boolean; template: string }; baseFiles: Array<string>; filename: string }; public readonly exercise: { maxSameName: number; maxPerAssignment: number; resultsFolder: string, pipelineResultsFolder: string; default: { - description: string; visibility: string; + description: string; visibility: GitlabVisibility; }; }; @@ -116,7 +116,7 @@ class Config { description : process.env.ASSIGNMENT_DEFAULT_DESCRIPTION?.convertWithEnvVars() ?? '', initReadme : process.env.ASSIGNMENT_DEFAULT_INIT_README?.toBoolean() ?? false, sharedRunnersEnabled: process.env.ASSIGNMENT_DEFAULT_SHARED_RUNNERS_ENABLED?.toBoolean() ?? true, - visibility : process.env.ASSIGNMENT_DEFAULT_VISIBILITY || GitlabVisibility.PRIVATE, + visibility : process.env.ASSIGNMENT_DEFAULT_VISIBILITY as GitlabVisibility || 'private', wikiEnabled : process.env.ASSIGNMENT_DEFAULT_WIKI_ENABLED?.toBoolean() ?? false, template : process.env.ASSIGNMENT_DEFAULT_TEMPLATE?.replace('{{USERNAME}}', this.gitlab.account.username).replace('{{TOKEN}}', this.gitlab.account.token) ?? '' }, @@ -131,7 +131,7 @@ class Config { pipelineResultsFolder: process.env.EXERCISE_PIPELINE_RESULTS_FOLDER ?? '', //Do not use convertWithEnvVars() because it is used in the exercise creation and muste be interpreted at exercise runtime default : { description: process.env.EXERCISE_DEFAULT_DESCRIPTION?.convertWithEnvVars() ?? '', - visibility : process.env.EXERCISE_DEFAULT_VISIBILITY || GitlabVisibility.PRIVATE + visibility : process.env.EXERCISE_DEFAULT_VISIBILITY as GitlabVisibility || 'private' } }; } diff --git a/ExpressAPI/src/express/API.ts b/ExpressAPI/src/express/API.ts index 0f885ecf1ca1297c12abbc42b0c384a20521ccf7..519918ef7da595f36394abd87f6d58c2aa6ea273 100644 --- a/ExpressAPI/src/express/API.ts +++ b/ExpressAPI/src/express/API.ts @@ -21,7 +21,7 @@ import DojoCliVersionHelper from '../helpers/DojoCliVersionHelper'; class API implements WorkerTask { private readonly backend: Express; - private server: http.Server | undefined; + private server!: http.Server; constructor() { this.backend = express(); @@ -38,9 +38,7 @@ class API implements WorkerTask { private initBaseMiddlewares() { this.backend.use(multer({ - limits: { - fieldSize: 15728640 // 15MB - } + limits: { fieldSize: 100 * 1024 * 1024 } }).none()); //Used for extract params from body with format "form-data", The none is for say that we do not wait a file in params this.backend.use(morganMiddleware); //Log API accesses this.backend.use(helmet()); //Help to secure express, https://helmetjs.github.io/ @@ -91,7 +89,7 @@ class API implements WorkerTask { const { port, address - } = this.server!.address() as AddressInfo; + } = this.server.address() as AddressInfo; logger.info(`Server started on http://${ address }:${ port }`); }); } diff --git a/ExpressAPI/src/helpers/DojoCliVersionHelper.ts b/ExpressAPI/src/helpers/DojoCliVersionHelper.ts index 962bf1c1d41ed2d9da7c8dfbac9fb9c51be8396e..4fd8d2505f55d38cd7f0925c94a6b23eb154d9aa 100644 --- a/ExpressAPI/src/helpers/DojoCliVersionHelper.ts +++ b/ExpressAPI/src/helpers/DojoCliVersionHelper.ts @@ -1,6 +1,6 @@ import Config from '../config/Config'; -import GitlabRelease from '../shared/types/Gitlab/GitlabRelease'; import GitlabManager from '../managers/GitlabManager'; +import * as Gitlab from '@gitbeaker/rest'; class DojoCliVersionHelper { @@ -8,7 +8,7 @@ class DojoCliVersionHelper { private latestVersion: string | undefined; private async updateVersion(): Promise<void> { - const releases: Array<GitlabRelease> = await GitlabManager.getRepositoryReleases(Config.dojoCLI.repositoryId); + const releases: Array<Gitlab.ReleaseSchema> = await GitlabManager.getRepositoryReleases(Config.dojoCLI.repositoryId); for ( const release of releases ) { if ( !isNaN(+release.tag_name.replace('.', '')) ) { this.latestVersion = release.tag_name; diff --git a/ExpressAPI/src/helpers/DojoValidators.ts b/ExpressAPI/src/helpers/DojoValidators.ts index 442f1608730d62f94772846d00fdfc28235d4406..26f60f4ce97d986c2ba2867ad268d79a43ce34c6 100644 --- a/ExpressAPI/src/helpers/DojoValidators.ts +++ b/ExpressAPI/src/helpers/DojoValidators.ts @@ -34,7 +34,7 @@ class DojoValidators { try { return value === 'null' || value === 'undefined' || value === '' ? null : value; } catch ( error ) { - logger.error(`null sanitizer error: ${ error }`); + logger.error(`null sanitizer error: ${ JSON.stringify(error) }`); return value; } @@ -84,7 +84,7 @@ class DojoValidators { return Config.assignment.default.template; } } catch ( error ) { - logger.error(`Template url sanitizer error: ${ error }`); + logger.error(`Template url sanitizer error: ${ JSON.stringify(error) }`); return value; } diff --git a/ExpressAPI/src/helpers/GlobalHelper.ts b/ExpressAPI/src/helpers/GlobalHelper.ts index e7e6e344f29769d48a8517d77ec7eb0b7493aa2d..6bd7b8890dd7b08c7f3188ac5fc0100d0da57dcc 100644 --- a/ExpressAPI/src/helpers/GlobalHelper.ts +++ b/ExpressAPI/src/helpers/GlobalHelper.ts @@ -1,31 +1,59 @@ -import express from 'express'; -import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; -import logger from '../shared/logging/WinstonLogger'; -import GitlabManager from '../managers/GitlabManager'; -import { AxiosError } from 'axios'; -import { StatusCodes } from 'http-status-codes'; -import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; +import express from 'express'; +import logger from '../shared/logging/WinstonLogger'; +import GitlabManager from '../managers/GitlabManager'; +import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; +import { StatusCodes } from 'http-status-codes'; +import { GitbeakerRequestError } from '@gitbeaker/requester-utils'; +import * as Gitlab from '@gitbeaker/rest'; class GlobalHelper { - async repositoryCreationError(message: string, error: unknown, req: express.Request, res: express.Response, gitlabError: DojoStatusCode, internalError: DojoStatusCode, repositoryToRemove?: GitlabRepository): Promise<void> { - logger.error(message); - logger.error(error); + repoCreationFnExecCreator(req: express.Request, res: express.Response, gitlabError: DojoStatusCode, internalError: DojoStatusCode, repositoryToRemove?: Gitlab.ProjectSchema) { + return async (toExec: () => Promise<unknown>, errorMessage?: string) => { + try { + return await toExec(); + } catch ( error ) { + if ( errorMessage ) { + logger.error(errorMessage); + logger.error(JSON.stringify(error)); - try { - if ( repositoryToRemove ) { - await GitlabManager.deleteRepository(repositoryToRemove.id); + try { + if ( repositoryToRemove ) { + await GitlabManager.deleteRepository(repositoryToRemove.id); + } + } catch ( deleteError ) { + logger.error('Repository deletion error'); + logger.error(JSON.stringify(deleteError)); + } + + if ( error instanceof GitbeakerRequestError ) { + req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, `Unknown gitlab error: ${ errorMessage }`, gitlabError); + throw error; + } + + req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, `Unknown error: ${ errorMessage }`, internalError); + throw error; + } } - } catch ( deleteError ) { - logger.error('Repository deletion error'); - logger.error(deleteError); - } - if ( error instanceof AxiosError ) { - return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, `Unknown gitlab error: ${ message }`, gitlabError); - } + return undefined; + }; + } + + isRepoNameAlreadyTaken(errorDescription: unknown) { + return errorDescription instanceof Object && 'name' in errorDescription && errorDescription.name instanceof Array && errorDescription.name.length > 0 && errorDescription.name[0] === 'has already been taken'; + } - return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, `Unknown error: ${ message }`, internalError); + addRepoMember(repositoryId: number) { + return async (memberId: number): Promise<Gitlab.MemberSchema | false> => { + try { + return await GitlabManager.addRepositoryMember(repositoryId, memberId, Gitlab.AccessLevel.DEVELOPER); + } catch ( error ) { + logger.error('Add member error'); + logger.error(JSON.stringify(error)); + return false; + } + }; } } diff --git a/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts b/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts index 8a8640ea20e19fa9df3692a920a7cb05f0a4fb6a..af63f05ed484cf5de6bd45421f484627a6845e63 100644 --- a/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts +++ b/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts @@ -1,6 +1,6 @@ import { Prisma, UserRole } from '@prisma/client'; import LazyVal from '../../../shared/helpers/LazyVal'; -import GitlabUser from '../../../shared/types/Gitlab/GitlabUser'; +import * as Gitlab from '@gitbeaker/rest'; import GitlabManager from '../../../managers/GitlabManager'; @@ -26,7 +26,7 @@ export default Prisma.defineExtension(client => { }, gitlabProfile : { compute(user) { - return new LazyVal<GitlabUser | undefined>(() => GitlabManager.getUserById(user.id)); + return new LazyVal<Gitlab.UserSchema | undefined>(() => GitlabManager.getUserById(user.id)); } } } diff --git a/ExpressAPI/src/managers/AssignmentManager.ts b/ExpressAPI/src/managers/AssignmentManager.ts index ad51e6b80209bfd4b9b623720d7968672e2a9ad0..9053b8e5ef05469e31c55be50476d98bbb70308d 100644 --- a/ExpressAPI/src/managers/AssignmentManager.ts +++ b/ExpressAPI/src/managers/AssignmentManager.ts @@ -45,7 +45,6 @@ class AssignmentManager { } else { return this.getByName(nameOrUrl, include); } - } } diff --git a/ExpressAPI/src/managers/GitlabManager.ts b/ExpressAPI/src/managers/GitlabManager.ts index 528d24dd41e5da167008484c70f0802eb05b0353..f48e9391cad444bf03d0660a3392ef1b08be9f5b 100644 --- a/ExpressAPI/src/managers/GitlabManager.ts +++ b/ExpressAPI/src/managers/GitlabManager.ts @@ -1,86 +1,39 @@ -import axios from 'axios'; -import Config from '../config/Config'; -import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; -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 GitlabUser from '../shared/types/Gitlab/GitlabUser'; -import GitlabTreeFile from '../shared/types/Gitlab/GitlabTreeFile'; -import parseLinkHeader from 'parse-link-header'; -import GitlabFile from '../shared/types/Gitlab/GitlabFile'; -import express from 'express'; -import GitlabRoute from '../shared/types/Gitlab/GitlabRoute'; -import SharedConfig from '../shared/config/SharedConfig'; -import GitlabProfile from '../shared/types/Gitlab/GitlabProfile'; -import GitlabRelease from '../shared/types/Gitlab/GitlabRelease'; -import { CommitSchema, Gitlab } from '@gitbeaker/rest'; -import logger from '../shared/logging/WinstonLogger'; -import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; +import Config from '../config/Config'; +import { StatusCodes } from 'http-status-codes'; +import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; +import express from 'express'; +import SharedConfig from '../shared/config/SharedConfig'; +import { CommitSchema, ExpandedUserSchema, Gitlab, MemberSchema, ProjectBadgeSchema, ProjectSchema, ReleaseSchema, RepositoryFileExpandedSchema, RepositoryFileSchema, RepositoryTreeSchema } from '@gitbeaker/rest'; +import logger from '../shared/logging/WinstonLogger'; +import { AccessLevel, EditProjectOptions, ProjectVariableSchema, ProtectedBranchAccessLevel, ProtectedBranchSchema } from '@gitbeaker/core'; +import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; +import SharedGitlabManager from '../shared/managers/SharedGitlabManager'; -class GitlabManager { - readonly api = new Gitlab({ - host : SharedConfig.gitlab.URL, - token: Config.gitlab.account.token - }); - - private getApiUrl(route: GitlabRoute): string { - return `${ SharedConfig.gitlab.apiURL }${ route }`; - } - - public async getUserProfile(token: string): Promise<GitlabProfile | undefined> { - try { - return (await axios.get<GitlabProfile>(this.getApiUrl(GitlabRoute.PROFILE_GET), { - headers: { - DojoOverrideAuthorization: true, - DojoAuthorizationHeader : 'Authorization', - DojoAuthorizationValue : `Bearer ${ token }` - } - })).data; - } catch ( e ) { - return undefined; - } +class GitlabManager extends SharedGitlabManager { + constructor() { + super(Config.gitlab.account.token); } - public async getUserById(id: number): Promise<GitlabUser | undefined> { + getUserProfile(token: string): Promise<ExpandedUserSchema> | undefined { try { - const user = (await axios.get<GitlabUser>(`${ this.getApiUrl(GitlabRoute.USERS_GET) }/${ String(id) }`)).data; + const profileApi = new Gitlab({ + host : SharedConfig.gitlab.URL, + oauthToken: token + }); - return user.id === id ? user : undefined; + return profileApi.Users.showCurrentUser(); } catch ( e ) { return undefined; } } - public async getUserByUsername(username: string): Promise<GitlabUser | undefined> { - try { - const params: Record<string, string> = {}; - params['search'] = username; - const user = (await axios.get<Array<GitlabUser>>(this.getApiUrl(GitlabRoute.USERS_GET), { params: params })).data[0]; - - return user.username === username ? user : undefined; - } catch ( e ) { - return undefined; - } - } - - async getRepository(projectIdOrNamespace: string): Promise<GitlabRepository> { - const response = await axios.get<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', encodeURIComponent(projectIdOrNamespace))); - - return response.data; + getRepositoryMembers(idOrNamespace: string): Promise<Array<MemberSchema>> { + return this.api.ProjectMembers.all(idOrNamespace, { includeInherited: true }); } - async getRepositoryMembers(idOrNamespace: string): Promise<Array<GitlabMember>> { - const response = await axios.get<Array<GitlabMember>>(this.getApiUrl(GitlabRoute.REPOSITORY_MEMBERS_GET).replace('{{id}}', encodeURIComponent(idOrNamespace))); - - return response.data; - } - - async getRepositoryReleases(repoId: number): Promise<Array<GitlabRelease>> { - const response = await axios.get<Array<GitlabRelease>>(this.getApiUrl(GitlabRoute.REPOSITORY_RELEASES_GET).replace('{{id}}', String(repoId))); - - return response.data; + getRepositoryReleases(repoId: number): Promise<Array<ReleaseSchema>> { + return this.api.ProjectReleases.all(repoId); } async getRepositoryLastCommit(repoId: number, branch: string = 'main'): Promise<CommitSchema | undefined> { @@ -93,89 +46,70 @@ class GitlabManager { return commits.length > 0 ? commits[0] : undefined; } catch ( e ) { - logger.error(e); + logger.error(JSON.stringify(e)); return undefined; } } - async createRepository(name: string, description: string, visibility: string, initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, importUrl: string): Promise<GitlabRepository> { - const response = await axios.post<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_CREATE), { - name : name, - description : description, - import_url : importUrl, - initialize_with_readme: initializeWithReadme, - namespace_id : namespace, - shared_runners_enabled: sharedRunnersEnabled, - visibility : visibility, - wiki_enabled : wikiEnabled - }); - - return response.data; + createRepository(name: string, description: string, visibility: 'public' | 'internal' | 'private', initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, importUrl: string): Promise<ProjectSchema> { + return this.api.Projects.create({ + name : name, + description : description, + importUrl : importUrl, + initializeWithReadme: initializeWithReadme, + namespaceId : namespace, + sharedRunnersEnabled: sharedRunnersEnabled, + visibility : visibility, + wikiAccessLevel : wikiEnabled ? 'enabled' : 'disabled' + }); } deleteRepository(repoId: number): Promise<void> { - return axios.delete(this.getApiUrl(GitlabRoute.REPOSITORY_DELETE).replace('{{id}}', String(repoId))); + return this.api.Projects.remove(repoId); } - async forkRepository(forkId: number, name: string, path: string, description: string, visibility: string, namespace: number): Promise<GitlabRepository> { - const response = await axios.post<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_FORK).replace('{{id}}', String(forkId)), { - name : name, - path : path, - description : description, - namespace_id: namespace, - visibility : visibility + forkRepository(forkId: number, name: string, path: string, description: string, visibility: 'public' | 'internal' | 'private', namespace: number): Promise<ProjectSchema> { + return this.api.Projects.fork(forkId, { + name : name, + path : path, + description: description, + namespaceId: namespace, + visibility : visibility }); - - return response.data; } - async editRepository(repoId: number, newAttributes: Partial<GitlabRepository>): Promise<GitlabRepository> { - const response = await axios.put<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_EDIT).replace('{{id}}', String(repoId)), newAttributes); - - return response.data; + editRepository(repoId: number, newAttributes: EditProjectOptions): Promise<ProjectSchema> { + return this.api.Projects.edit(repoId, newAttributes); } - changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<GitlabRepository> { - return this.editRepository(repoId, { visibility: visibility.toString() }); + changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<ProjectSchema> { + return this.editRepository(repoId, { visibility: visibility }); } - async addRepositoryMember(repoId: number, userId: number, accessLevel: GitlabAccessLevel): Promise<GitlabMember> { - const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_MEMBER_ADD).replace('{{id}}', String(repoId)), { - user_id : userId, - access_level: accessLevel - }); - - return response.data; + addRepositoryMember(repoId: number, userId: number, accessLevel: Exclude<AccessLevel, AccessLevel.ADMIN>): Promise<MemberSchema> { + return this.api.ProjectMembers.add(repoId, userId, accessLevel); } - async addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<GitlabMember> { - const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_VARIABLES_ADD).replace('{{id}}', String(repoId)), { - key : key, - variable_type: 'env_var', - value : value, - protected : isProtected, - masked : isMasked + addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<ProjectVariableSchema> { + return this.api.ProjectVariables.create(repoId, key, value, { + variableType: 'env_var', + protected : isProtected, + masked : isMasked }); - - return response.data; } - async addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): Promise<GitlabMember> { - const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_BADGES_ADD).replace('{{id}}', String(repoId)), { - link_url : linkUrl, - image_url: imageUrl, - name : name + addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): Promise<ProjectBadgeSchema> { + return this.api.ProjectBadges.add(repoId, linkUrl, imageUrl, { + name: name }); - - return response.data; } async checkTemplateAccess(projectIdOrNamespace: string, req: express.Request, res?: express.Response): Promise<boolean> { // Get the Gitlab project and check if it have public or internal visibility try { - const project: GitlabRepository = await this.getRepository(projectIdOrNamespace); + const project: ProjectSchema = await this.getRepository(projectIdOrNamespace); - if ( [ GitlabVisibility.PUBLIC.valueOf(), GitlabVisibility.INTERNAL.valueOf() ].includes(project.visibility) ) { + if ( [ 'public', 'internal' ].includes(project.visibility) ) { req.session.sendResponse(res, StatusCodes.OK); return true; } @@ -191,7 +125,7 @@ class GitlabManager { dojo: false }; members.forEach(member => { - if ( member.access_level >= GitlabAccessLevel.REPORTER ) { + if ( member.access_level >= AccessLevel.REPORTER ) { if ( member.id === req.session.profile.id ) { isUsersAtLeastReporter.user = true; } else if ( member.id === Config.gitlab.account.id ) { @@ -209,91 +143,49 @@ 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(GitlabRoute.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() + protectBranch(repoId: number, branchName: string, allowForcePush: boolean, allowedToMerge: ProtectedBranchAccessLevel, allowedToPush: ProtectedBranchAccessLevel, allowedToUnprotect: ProtectedBranchAccessLevel): Promise<ProtectedBranchSchema> { + return this.api.ProtectedBranches.protect(repoId, branchName, { + allowForcePush : allowForcePush, + mergeAccessLevel : allowedToMerge, + pushAccessLevel : allowedToPush, + unprotectAccessLevel: allowedToUnprotect }); - - return response.data; } - async getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<GitlabTreeFile>> { - const address: string | undefined = this.getApiUrl(GitlabRoute.REPOSITORY_TREE).replace('{{id}}', String(repoId)); - let params: Partial<parseLinkHeader.Link | { recursive: boolean, per_page: number }> | undefined = { - pagination: 'keyset', - recursive : recursive, - per_page : 100, - ref : branch - }; - - const 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 ) { - params = parseLinkHeader(response.headers['link'])?.next ?? undefined; - } else { - params = undefined; - } - } - - return results; - } - - private getRepositoryFileUrl(repoId: number, filePath: string): string { - return this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)); - } - - async getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<GitlabFile> { - const response = await axios.get<GitlabFile>(this.getRepositoryFileUrl(repoId, filePath), { - params: { - ref: branch - } + getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<RepositoryTreeSchema>> { + return this.api.Repositories.allRepositoryTrees(repoId, { + recursive: recursive, + ref : branch }); + } - return response.data; + getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<RepositoryFileExpandedSchema> { + return this.api.RepositoryFiles.show(repoId, filePath, branch); } - private async createUpdateFile(create: boolean, repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) { - const axiosFunction = create ? axios.post : axios.put; + private createUpdateFile(create: boolean, repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<RepositoryFileSchema> { + const gitFunction = create ? this.api.RepositoryFiles.create.bind(this.api) : this.api.RepositoryFiles.edit.bind(this.api); - await axiosFunction(this.getRepositoryFileUrl(repoId, filePath), { - encoding : 'base64', - branch : branch, - commit_message: commitMessage, - content : fileBase64, - author_name : authorName, - author_email : authorMail + return gitFunction(repoId, filePath, branch, fileBase64, commitMessage, { + encoding : 'base64', + authorName : authorName, + authorEmail: authorMail }); } - async createFile(repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) { + createFile(repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<RepositoryFileSchema> { return this.createUpdateFile(true, repoId, filePath, fileBase64, commitMessage, branch, authorName, authorMail); } - async updateFile(repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) { + updateFile(repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<RepositoryFileSchema> { return this.createUpdateFile(false, repoId, filePath, fileBase64, commitMessage, branch, authorName, authorMail); } - async deleteFile(repoId: number, filePath: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) { - await axios.delete(this.getRepositoryFileUrl(repoId, filePath), { - data: { - branch : branch, - commit_message: commitMessage, - author_name : authorName, - author_email : authorMail - } + deleteFile(repoId: number, filePath: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<void> { + return this.api.RepositoryFiles.remove(repoId, filePath, branch, commitMessage, { + authorName : authorName, + authorEmail: authorMail }); - } } diff --git a/ExpressAPI/src/managers/HttpManager.ts b/ExpressAPI/src/managers/HttpManager.ts index e67828d06b4b4289b74d241e1c0aa60b25d280be..408adc592d9119e511e1eff457cfde149937c68f 100644 --- a/ExpressAPI/src/managers/HttpManager.ts +++ b/ExpressAPI/src/managers/HttpManager.ts @@ -1,8 +1,6 @@ import axios, { AxiosError, AxiosRequestHeaders } from 'axios'; -import Config from '../config/Config'; import FormData from 'form-data'; import logger from '../shared/logging/WinstonLogger'; -import SharedConfig from '../shared/config/SharedConfig'; class HttpManager { @@ -16,19 +14,6 @@ class HttpManager { if ( config.data instanceof FormData ) { config.headers = { ...config.headers, ...(config.data as FormData).getHeaders() } as AxiosRequestHeaders; } - - if ( config.url && config.url.indexOf(SharedConfig.gitlab.apiURL) !== -1 && !config.headers.DojoOverrideAuthorization ) { - config.headers['PRIVATE-TOKEN'] = Config.gitlab.account.token; - } - - if ( config.headers.DojoOverrideAuthorization && 'DojoAuthorizationHeader' in config.headers && 'DojoAuthorizationValue' in config.headers ) { - config.headers[config.headers.DojoAuthorizationHeader] = config.headers.DojoAuthorizationValue; - - delete config.headers.DojoOverrideAuthorization; - delete config.headers.DojoAuthorizationHeader; - delete config.headers.DojoAuthorizationValue; - } - return config; }); } diff --git a/ExpressAPI/src/managers/UserManager.ts b/ExpressAPI/src/managers/UserManager.ts index 686ee267551004d22b1745e93f343c30c21aec02..6008cf5d7d301e02fbd83919f076d29193cf5bac 100644 --- a/ExpressAPI/src/managers/UserManager.ts +++ b/ExpressAPI/src/managers/UserManager.ts @@ -1,8 +1,7 @@ -import GitlabUser from '../shared/types/Gitlab/GitlabUser'; -import { Prisma } from '@prisma/client'; -import db from '../helpers/DatabaseHelper'; -import GitlabProfile from '../shared/types/Gitlab/GitlabProfile'; -import { User } from '../types/DatabaseTypes'; +import { Prisma } from '@prisma/client'; +import db from '../helpers/DatabaseHelper'; +import { User } from '../types/DatabaseTypes'; +import * as Gitlab from '@gitbeaker/rest'; class UserManager { @@ -24,7 +23,7 @@ class UserManager { }) as unknown as User ?? undefined; } - async getUpdateFromGitlabProfile(gitlabProfile: GitlabProfile): Promise<User> { + async getUpdateFromGitlabProfile(gitlabProfile: Gitlab.ExpandedUserSchema): Promise<User> { await db.user.upsert({ where : { id: gitlabProfile.id @@ -46,7 +45,7 @@ class UserManager { return (await this.getById(gitlabProfile.id))!; } - async getFromGitlabUser(gitlabUser: GitlabUser, createIfNotExist: boolean = false, include: Prisma.UserInclude | undefined = undefined): Promise<User | number | undefined> { + async getFromGitlabUser(gitlabUser: Gitlab.UserSchema, createIfNotExist: boolean = false, include: Prisma.UserInclude | undefined = undefined): Promise<User | number | undefined> { let user = await this.getById(gitlabUser.id, include) ?? gitlabUser.id; if ( typeof user === 'number' && createIfNotExist ) { @@ -61,7 +60,7 @@ class UserManager { return user; } - async getFromGitlabUsers(gitlabUsers: Array<GitlabUser>, createIfNotExist: boolean = false, include: Prisma.UserInclude | undefined = undefined): Promise<Array<User | number | undefined>> { + async getFromGitlabUsers(gitlabUsers: Array<Gitlab.UserSchema>, 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/ClientVersionCheckerMiddleware.ts b/ExpressAPI/src/middlewares/ClientVersionCheckerMiddleware.ts index 952fd955aea5dc836081f48a0db33d8a7ac20b4e..f58b2b761a13d06482652eab64594234ac906f92 100644 --- a/ExpressAPI/src/middlewares/ClientVersionCheckerMiddleware.ts +++ b/ExpressAPI/src/middlewares/ClientVersionCheckerMiddleware.ts @@ -1,9 +1,9 @@ -import express from 'express'; -import Config from '../config/Config'; -import semver from 'semver/preload'; -import Session from '../controllers/Session'; -import { HttpStatusCode } from 'axios'; -import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; +import express from 'express'; +import Config from '../config/Config'; +import semver from 'semver/preload'; +import Session from '../controllers/Session'; +import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; +import { StatusCodes } from 'http-status-codes'; class ClientVersionCheckerMiddleware { @@ -19,13 +19,13 @@ class ClientVersionCheckerMiddleware { next(); return; } else { - new Session().sendResponse(res, HttpStatusCode.MethodNotAllowed, {}, `Client version ${ requestClientVersion } is not supported. Please update your client.`, DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED); + new Session().sendResponse(res, StatusCodes.METHOD_NOT_ALLOWED, {}, `Client version ${ requestClientVersion } is not supported. Please update your client.`, DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED); return; } } } - new Session().sendResponse(res, HttpStatusCode.MethodNotAllowed, {}, `Unsupported client.`, DojoStatusCode.CLIENT_NOT_SUPPORTED); + new Session().sendResponse(res, StatusCodes.METHOD_NOT_ALLOWED, {}, `Unsupported client.`, DojoStatusCode.CLIENT_NOT_SUPPORTED); } else { next(); } diff --git a/ExpressAPI/src/middlewares/SecurityMiddleware.ts b/ExpressAPI/src/middlewares/SecurityMiddleware.ts index 6b4a0756419e4b5052988c6cf08cc5c527b2ba70..a3a505902c4ef17af352f5023e3198fd1bbdf506 100644 --- a/ExpressAPI/src/middlewares/SecurityMiddleware.ts +++ b/ExpressAPI/src/middlewares/SecurityMiddleware.ts @@ -6,40 +6,38 @@ import AssignmentManager from '../managers/AssignmentManager'; class SecurityMiddleware { + private isConnected(checkIfConnected: boolean, req: express.Request): boolean { + return checkIfConnected && req.session.profile !== null && req.session.profile !== undefined; + } + + private async checkType(checkType: SecurityCheckType, req: express.Request): Promise<boolean> { + try { + switch ( String(checkType) ) { + case SecurityCheckType.TEACHING_STAFF: + return req.session.profile.isTeachingStaff; + case SecurityCheckType.ASSIGNMENT_STAFF: + return await AssignmentManager.isUserAllowedToAccessAssignment(req.boundParams.assignment!, req.session.profile); + case SecurityCheckType.ASSIGNMENT_IS_PUBLISHED: + return req.boundParams.assignment?.published ?? false; + case SecurityCheckType.EXERCISE_SECRET: + return (req.headers.exercisesecret as string | undefined) === req.boundParams.exercise!.secret; + default: + return false; + } + } catch ( e ) { + logger.error('Security check failed !!! => ' + JSON.stringify(e)); + return false; + } + } + // First check if connected then check if at least ONE rule match. It's NOT an AND but it's a OR function. check(checkIfConnected: boolean, ...checkTypes: Array<SecurityCheckType>): (req: express.Request, res: express.Response, next: express.NextFunction) => void { return async (req: express.Request, res: express.Response, next: express.NextFunction) => { - if ( checkIfConnected && (req.session.profile === null || req.session.profile === undefined) ) { + if ( !this.isConnected(checkIfConnected, req) ) { return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED); } - let isAllowed = checkTypes.length === 0; - - if ( !isAllowed ) { - for ( const checkType of checkTypes ) { - try { - switch ( String(checkType) ) { - case SecurityCheckType.TEACHING_STAFF: - isAllowed = isAllowed || req.session.profile.isTeachingStaff; - break; - case SecurityCheckType.ASSIGNMENT_STAFF: - isAllowed = isAllowed || await AssignmentManager.isUserAllowedToAccessAssignment(req.boundParams.assignment!, req.session.profile); - break; - case SecurityCheckType.ASSIGNMENT_IS_PUBLISHED: - isAllowed = isAllowed || (req.boundParams.assignment?.published ?? false); - break; - case SecurityCheckType.EXERCISE_SECRET: - isAllowed = isAllowed || (req.headers.exercisesecret as string | undefined) === req.boundParams.exercise!.secret; - break; - default: - break; - } - } catch ( e ) { - logger.error('Security check failed !!! => ' + e); - isAllowed = isAllowed || false; - } - } - } + const isAllowed: boolean = checkTypes.length === 0 ? true : checkTypes.find(async checkType => this.checkType(checkType, req)) !== undefined; if ( !isAllowed ) { return req.session.sendResponse(res, StatusCodes.FORBIDDEN); diff --git a/ExpressAPI/src/routes/AssignmentRoutes.ts b/ExpressAPI/src/routes/AssignmentRoutes.ts index 9f46f4973a57441ea2b043c117e343e77ccbed5a..121c4c4fe75ecfc3e69d649043eacd64c0d5702e 100644 --- a/ExpressAPI/src/routes/AssignmentRoutes.ts +++ b/ExpressAPI/src/routes/AssignmentRoutes.ts @@ -1,31 +1,27 @@ -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 { Assignment } from '../types/DatabaseTypes'; -import AssignmentManager from '../managers/AssignmentManager'; -import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; -import fs from 'fs'; -import path from 'path'; -import SharedAssignmentHelper from '../shared/helpers/Dojo/SharedAssignmentHelper'; -import GlobalHelper from '../helpers/GlobalHelper'; -import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; -import DojoModelsHelper from '../helpers/DojoModelsHelper'; +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 GitlabManager from '../managers/GitlabManager'; +import Config from '../config/Config'; +import logger from '../shared/logging/WinstonLogger'; +import DojoValidators from '../helpers/DojoValidators'; +import { Prisma } from '@prisma/client'; +import db from '../helpers/DatabaseHelper'; +import { Assignment } from '../types/DatabaseTypes'; +import AssignmentManager from '../managers/AssignmentManager'; +import fs from 'fs'; +import path from 'path'; +import SharedAssignmentHelper from '../shared/helpers/Dojo/SharedAssignmentHelper'; +import GlobalHelper from '../helpers/GlobalHelper'; +import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; +import DojoModelsHelper from '../helpers/DojoModelsHelper'; +import * as Gitlab from '@gitbeaker/rest'; +import { GitbeakerRequestError } from '@gitbeaker/requester-utils'; class AssignmentRoutes implements RoutesManager { @@ -84,88 +80,75 @@ class AssignmentRoutes implements RoutesManager { private async createAssignment(req: express.Request, res: express.Response) { const params: { - name: string, members: Array<GitlabUser>, template: string + name: string, members: Array<Gitlab.UserSchema>, template: string } = req.body; params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ]; params.members = params.members.removeObjectDuplicates(gitlabUser => gitlabUser.id); - let repository: GitlabRepository; + let repository: Gitlab.ProjectSchema; try { repository = await GitlabManager.createRepository(params.name, Config.assignment.default.description.replace('{{ASSIGNMENT_NAME}}', params.name), Config.assignment.default.visibility, Config.assignment.default.initReadme, Config.gitlab.group.assignments, Config.assignment.default.sharedRunnersEnabled, Config.assignment.default.wikiEnabled, params.template); } catch ( error ) { logger.error('Repo creation error'); - logger.error(error); - - if ( error instanceof AxiosError ) { - if ( error.response?.data.message.name && error.response.data.message.name === 'has already been taken' ) { - return req.session.sendResponse(res, StatusCodes.CONFLICT, {}, `Repository name has already been take`, DojoStatusCode.ASSIGNMENT_NAME_CONFLICT); + logger.error(JSON.stringify(error)); + + if ( error instanceof GitbeakerRequestError ) { + if ( error.cause?.description ) { + const description = error.cause.description as unknown; + if ( GlobalHelper.isRepoNameAlreadyTaken(description) ) { + req.session.sendResponse(res, StatusCodes.CONFLICT, {}, `Repository name has already been taken`, DojoStatusCode.ASSIGNMENT_NAME_CONFLICT); + return; + } } - return req.session.sendResponse(res, error.response?.status ?? HttpStatusCode.InternalServerError); + req.session.sendResponse(res, error.cause?.response.status ?? StatusCodes.INTERNAL_SERVER_ERROR); + return; } - return req.session.sendResponse(res, HttpStatusCode.InternalServerError); + req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR); + return; } await new Promise(resolve => setTimeout(resolve, Config.gitlab.repository.timeoutAfterCreation)); - try { - await GitlabManager.protectBranch(repository.id, '*', true, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.OWNER); - - await GitlabManager.addRepositoryBadge(repository.id, Config.gitlab.badges.pipeline.link, Config.gitlab.badges.pipeline.imageUrl, 'Pipeline Status'); - } catch ( error ) { - return GlobalHelper.repositoryCreationError('Repo params error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository); - } - - try { - await GitlabManager.deleteFile(repository.id, '.gitlab-ci.yml', 'Remove .gitlab-ci.yml'); - } catch ( error ) { /* empty */ } + const repoCreationFnExec = GlobalHelper.repoCreationFnExecCreator(req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository); try { - await GitlabManager.createFile(repository.id, '.gitlab-ci.yml', fs.readFileSync(path.join(__dirname, '../../assets/assignment_gitlab_ci.yml'), 'base64'), 'Add .gitlab-ci.yml (DO NOT MODIFY THIS FILE)'); + await repoCreationFnExec(() => GitlabManager.protectBranch(repository.id, '*', true, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.ADMIN), 'Branch protection modification error'); + await repoCreationFnExec(() => GitlabManager.addRepositoryBadge(repository.id, Config.gitlab.badges.pipeline.link, Config.gitlab.badges.pipeline.imageUrl, 'Pipeline Status'), 'Pipeline badge addition error'); + await repoCreationFnExec(() => GitlabManager.deleteFile(repository.id, '.gitlab-ci.yml', 'Remove .gitlab-ci.yml')); + await repoCreationFnExec(() => GitlabManager.createFile(repository.id, '.gitlab-ci.yml', fs.readFileSync(path.join(__dirname, '../../assets/assignment_gitlab_ci.yml'), 'base64'), 'Add .gitlab-ci.yml (DO NOT MODIFY THIS FILE)'), 'CI/CD file creation error'); + + await repoCreationFnExec(() => Promise.all(params.members.map(member => member.id).map(GlobalHelper.addRepoMember(repository.id))), 'Add repository members error'); + + const assignment: Assignment = await repoCreationFnExec(() => db.assignment.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 : { + connectOrCreate: [ ...params.members.map(gitlabUser => { + return { + create: { + id : gitlabUser.id, + gitlabUsername: gitlabUser.name + }, + where : { + id: gitlabUser.id + } + }; + }) ] + } + } + }), 'Database error') as Assignment; + + req.session.sendResponse(res, StatusCodes.OK, assignment); } catch ( error ) { - return GlobalHelper.repositoryCreationError('CI file error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository); - } - - try { - await Promise.all(params.members.map(member => member.id).map(async (memberId: number): Promise<GitlabMember | false> => { - try { - return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.DEVELOPER); - } catch ( error ) { - logger.error('Add member error'); - logger.error(error); - return false; - } - })); - - const assignment: Assignment = await db.assignment.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 : { - connectOrCreate: [ ...params.members.map(gitlabUser => { - return { - create: { - id : gitlabUser.id, - gitlabUsername: gitlabUser.name - }, - where : { - id: gitlabUser.id - } - }; - }) ] - } - } - }) as unknown as Assignment; - - return req.session.sendResponse(res, StatusCodes.OK, assignment); - } catch ( error ) { - return GlobalHelper.repositoryCreationError('DB error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository); + /* Empty */ } } @@ -180,7 +163,7 @@ class AssignmentRoutes implements RoutesManager { } try { - await GitlabManager.changeRepositoryVisibility(req.boundParams.assignment!.gitlabId, publish ? GitlabVisibility.INTERNAL : GitlabVisibility.PRIVATE); + await GitlabManager.changeRepositoryVisibility(req.boundParams.assignment!.gitlabId, publish ? 'internal' : 'private'); await db.assignment.update({ where: { @@ -193,13 +176,14 @@ class AssignmentRoutes implements RoutesManager { req.session.sendResponse(res, StatusCodes.OK); } catch ( error ) { - if ( error instanceof AxiosError ) { - res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); + logger.error(JSON.stringify(error)); + + if ( error instanceof GitbeakerRequestError ) { + req.session.sendResponse(res, error.cause?.response.status ?? StatusCodes.INTERNAL_SERVER_ERROR, undefined, 'Error while updating the assignment state'); return; } - logger.error(error); - res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); + req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, undefined, 'Error while updating the assignment state'); } }; } @@ -223,7 +207,7 @@ class AssignmentRoutes implements RoutesManager { const lastCommit = await GitlabManager.getRepositoryLastCommit(req.boundParams.exercise!.gitlabId); if ( lastCommit ) { if ( !isUpdate ) { - await GitlabManager.changeRepositoryVisibility(req.boundParams.exercise!.gitlabId, GitlabVisibility.INTERNAL); + await GitlabManager.changeRepositoryVisibility(req.boundParams.exercise!.gitlabId, 'internal'); } await db.exercise.update({ diff --git a/ExpressAPI/src/routes/ExerciseRoutes.ts b/ExpressAPI/src/routes/ExerciseRoutes.ts index d5cb5b68c164dec82a2bf3c4c4750c0477f73225..34607a0484e811c48956ec67a76b911bd0aff8d2 100644 --- a/ExpressAPI/src/routes/ExerciseRoutes.ts +++ b/ExpressAPI/src/routes/ExerciseRoutes.ts @@ -5,23 +5,15 @@ 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 } 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 { Assignment, Exercise } 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 GitlabTreeFileType from '../shared/types/Gitlab/GitlabTreeFileType'; import JSON5 from 'json5'; import fs from 'fs'; import path from 'path'; @@ -31,6 +23,9 @@ import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; import GlobalHelper from '../helpers/GlobalHelper'; import { IFileDirStat } from '../shared/helpers/recursiveFilesStats/RecursiveFilesStats'; import ExerciseManager from '../managers/ExerciseManager'; +import * as Gitlab from '@gitbeaker/rest'; +import GitlabTreeFileType from '../shared/types/Gitlab/GitlabTreeFileType'; +import { GitbeakerRequestError } from '@gitbeaker/requester-utils'; class ExerciseRoutes implements RoutesManager { @@ -77,19 +72,19 @@ class ExerciseRoutes implements RoutesManager { backend.post('/exercises/:exerciseIdOrUrl/results', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), ParamsValidatorMiddleware.validate(this.resultValidator), this.createResult.bind(this)); } - private getExerciseName(assignment: Assignment, members: Array<GitlabUser>, suffix: number): string { + private getExerciseName(assignment: Assignment, members: Array<Gitlab.UserSchema>, suffix: number): string { const memberNames: string = members.map(member => member.username).sort((a, b) => a.localeCompare(b)).join(' + '); const suffixString: string = suffix > 0 ? ` - ${ suffix }` : ''; return `DojoEx - ${ assignment.name } - ${ memberNames }${ suffixString }`; } private getExercisePath(assignment: Assignment, exerciseId: string): string { - return `dojo-ex_${ (assignment.gitlabLastInfo as unknown as GitlabRepository).path }_${ exerciseId }`; + return `dojo-ex_${ (assignment.gitlabLastInfo as unknown as Gitlab.ProjectSchema).path }_${ exerciseId }`; } - private async checkExerciseLimit(assignment: Assignment, members: Array<GitlabUser>): Promise<Array<GitlabUser>> { - const exercises: Array<Exercise> = await ExerciseManager.getFromAssignment(assignment.name, { members: true }); - const reachedLimitUsers: Array<GitlabUser> = []; + private async checkExerciseLimit(assignment: Assignment, members: Array<Gitlab.UserSchema>): Promise<Array<Gitlab.UserSchema>> { + const exercises: Array<Exercise> | undefined = await ExerciseManager.getFromAssignment(assignment.name, { members: true }); + const reachedLimitUsers: Array<Gitlab.UserSchema> = []; if ( exercises.length > 0 ) { for ( const member of members ) { const exerciseCount: number = exercises.filter(exercise => exercise.members.findIndex(exerciseMember => exerciseMember.id === member.id) !== -1).length; @@ -102,20 +97,21 @@ class ExerciseRoutes implements RoutesManager { return reachedLimitUsers; } - private async createExerciseRepository(assignment: Assignment, members: Array<GitlabUser>, exerciseId: string, req: express.Request, res: express.Response): Promise<GitlabRepository | undefined> { - let repository!: GitlabRepository; + private async createExerciseRepository(assignment: Assignment, members: Array<Gitlab.UserSchema>, exerciseId: string, req: express.Request, res: express.Response): Promise<Gitlab.ProjectSchema | undefined> { + let repository!: Gitlab.ProjectSchema; let suffix: number = 0; do { try { - repository = await GitlabManager.forkRepository((assignment.gitlabCreationInfo as unknown as GitlabRepository).id, this.getExerciseName(assignment, members, suffix), this.getExercisePath(req.boundParams.assignment!, exerciseId), Config.exercise.default.description.replace('{{ASSIGNMENT_NAME}}', assignment.name), Config.exercise.default.visibility, Config.gitlab.group.exercises); + repository = await GitlabManager.forkRepository((assignment.gitlabCreationInfo as unknown as Gitlab.ProjectSchema).id, this.getExerciseName(assignment, members, suffix), this.getExercisePath(req.boundParams.assignment!, exerciseId), Config.exercise.default.description.replace('{{ASSIGNMENT_NAME}}', assignment.name), Config.exercise.default.visibility, Config.gitlab.group.exercises); break; } catch ( error ) { logger.error('Repo creation error'); - logger.error(error); + logger.error(JSON.stringify(error)); - if ( error instanceof AxiosError ) { - if ( error.response?.data.message.name && error.response.data.message.name === 'has already been taken' ) { + if ( error instanceof GitbeakerRequestError && error.cause?.description ) { + const description = error.cause.description as unknown; + if ( GlobalHelper.isRepoNameAlreadyTaken(description) ) { suffix++; } else { req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown gitlab error while forking repository', DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR); @@ -128,22 +124,16 @@ class ExerciseRoutes implements RoutesManager { } } while ( suffix < Config.exercise.maxSameName ); - if ( suffix >= Config.exercise.maxSameName ) { - logger.error('Max exercise with same name reached'); - req.session.sendResponse(res, StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE, undefined, 'Max exercise per assignment reached', DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED); - return undefined; - } - return repository; } private async createExercise(req: express.Request, res: express.Response) { - const params: { members: Array<GitlabUser> } = req.body; + const params: { members: Array<Gitlab.UserSchema> } = req.body; params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ].removeObjectDuplicates(gitlabUser => gitlabUser.id); const assignment: Assignment = req.boundParams.assignment!; - const reachedLimitUsers: Array<GitlabUser> = await this.checkExerciseLimit(assignment, params.members); + const reachedLimitUsers: Array<Gitlab.UserSchema> = await this.checkExerciseLimit(assignment, params.members); if ( reachedLimitUsers.length > 0 ) { req.session.sendResponse(res, StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE, reachedLimitUsers, 'Max exercise per assignment reached', DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED); return; @@ -152,7 +142,7 @@ class ExerciseRoutes implements RoutesManager { const exerciseId: string = uuidv4(); const secret: string = uuidv4(); - const repository: GitlabRepository | undefined = await this.createExerciseRepository(assignment, params.members, exerciseId, req, res); + const repository: Gitlab.ProjectSchema | undefined = await this.createExerciseRepository(assignment, params.members, exerciseId, req, res); if ( !repository ) { return; @@ -160,77 +150,61 @@ class ExerciseRoutes implements RoutesManager { await new Promise(resolve => setTimeout(resolve, Config.gitlab.repository.timeoutAfterCreation)); - try { - await GitlabManager.protectBranch(repository.id, '*', false, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.OWNER); - - await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_EXERCISE_ID', exerciseId, false, true); - await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, false, true); - await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_RESULTS_FOLDER', Config.exercise.pipelineResultsFolder, false, false); - - await GitlabManager.addRepositoryBadge(repository.id, Config.gitlab.badges.pipeline.link, Config.gitlab.badges.pipeline.imageUrl, 'Pipeline Status'); - } catch ( error ) { - GlobalHelper.repositoryCreationError('Repo params error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); - return; - } - - try { - await GitlabManager.updateFile(repository.id, '.gitlab-ci.yml', fs.readFileSync(path.join(__dirname, '../../assets/exercise_gitlab_ci.yml'), 'base64'), 'Add .gitlab-ci.yml (DO NOT MODIFY THIS FILE)'); - } catch ( error ) { - GlobalHelper.repositoryCreationError('CI file update error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); - return; - } + const repoCreationFnExec = GlobalHelper.repoCreationFnExecCreator(req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); try { - await Promise.all([ ...new Set([ ...assignment.staff.map(user => user.id), ...params.members.map(member => member.id) ]) ].map(async (memberId: number): Promise<GitlabMember | false> => { - try { - return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.DEVELOPER); - } catch ( error ) { - logger.error('Add member error'); - logger.error(error); - return false; - } - })); - - const exercise: Exercise = await db.exercise.create({ - data: { - id : exerciseId, - assignmentName : assignment.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: { - id : gitlabUser.id, - gitlabUsername: gitlabUser.name - }, - where : { - id: gitlabUser.id - } - }; - }) ] - } - } - }) as unknown as Exercise; + await repoCreationFnExec(() => GitlabManager.protectBranch(repository.id, '*', false, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.ADMIN), 'Branch protection modification error'); + await repoCreationFnExec(() => GitlabManager.addRepositoryBadge(repository.id, Config.gitlab.badges.pipeline.link, Config.gitlab.badges.pipeline.imageUrl, 'Pipeline Status'), 'Pipeline badge addition error'); + + await repoCreationFnExec(async () => { + await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_EXERCISE_ID', exerciseId, false, true); + await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, false, true); + await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_RESULTS_FOLDER', Config.exercise.pipelineResultsFolder, false, false); + }, 'Pipeline variables addition error'); + + await repoCreationFnExec(() => GitlabManager.updateFile(repository.id, '.gitlab-ci.yml', fs.readFileSync(path.join(__dirname, '../../assets/exercise_gitlab_ci.yml'), 'base64'), 'Add .gitlab-ci.yml (DO NOT MODIFY THIS FILE)'), 'CI/CD file update error'); + + await repoCreationFnExec(async () => Promise.all([ ...new Set([ ...assignment.staff, ...params.members ].map(member => member.id)) ].map(GlobalHelper.addRepoMember(repository.id))), 'Add repository members error'); + + const exercise: Exercise = await repoCreationFnExec(() => db.exercise.create({ + data: { + id : exerciseId, + assignmentName : assignment.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: { + id : gitlabUser.id, + gitlabUsername: gitlabUser.name + }, + where : { + id: gitlabUser.id + } + }; + }) ] + } + } + })) as Exercise; req.session.sendResponse(res, StatusCodes.OK, exercise); return; } catch ( error ) { - await GlobalHelper.repositoryCreationError('DB error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); - return; + /* Empty */ } } private async getAssignment(req: express.Request, res: express.Response) { - const repoTree: Array<GitlabTreeFile> = await GitlabManager.getRepositoryTree(req.boundParams.exercise!.assignment.gitlabId); + const repoTree: Array<Gitlab.RepositoryTreeSchema> = await GitlabManager.getRepositoryTree(req.boundParams.exercise!.assignment.gitlabId); - let assignmentHjsonFile!: GitlabFile; - const immutableFiles: Array<GitlabFile> = await Promise.all(Config.assignment.baseFiles.map(async (baseFile: string) => { + let assignmentHjsonFile!: Gitlab.RepositoryFileExpandedSchema; + const immutableFiles: Array<Gitlab.RepositoryFileExpandedSchema> = await Promise.all(Config.assignment.baseFiles.map(async (baseFile: string) => { const file = await GitlabManager.getFile(req.boundParams.exercise!.assignment.gitlabId, baseFile); if ( baseFile === Config.assignment.filename ) { diff --git a/ExpressAPI/src/routes/SessionRoutes.ts b/ExpressAPI/src/routes/SessionRoutes.ts index 60b630d9fcf37254f4f068acc0272265ca149d1c..b4f223e664ed675c40f2648a96ca4e42daf8d610 100644 --- a/ExpressAPI/src/routes/SessionRoutes.ts +++ b/ExpressAPI/src/routes/SessionRoutes.ts @@ -8,7 +8,6 @@ import SecurityMiddleware from '../middlewares/SecurityMiddleware'; import GitlabManager from '../managers/GitlabManager'; import UserManager from '../managers/UserManager'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; -import SharedGitlabManager from '../shared/managers/SharedGitlabManager'; import Config from '../config/Config'; @@ -64,7 +63,7 @@ class SessionRoutes implements RoutesManager { refreshToken: string } = req.body; - const gitlabTokens = await SharedGitlabManager.getTokens(params.refreshToken, true, Config.login.gitlab.client.secret); + const gitlabTokens = await GitlabManager.getTokens(params.refreshToken, true, Config.login.gitlab.client.secret); req.session.sendResponse(res, StatusCodes.OK, gitlabTokens); } catch ( error ) { diff --git a/ExpressAPI/src/shared b/ExpressAPI/src/shared index 985c0b6a5dcb640404e5cf5fc91978215f961e3b..6e78095b3fe73f2c2987de1a3d3b55511335a2bf 160000 --- a/ExpressAPI/src/shared +++ b/ExpressAPI/src/shared @@ -1 +1 @@ -Subproject commit 985c0b6a5dcb640404e5cf5fc91978215f961e3b +Subproject commit 6e78095b3fe73f2c2987de1a3d3b55511335a2bf diff --git a/ExpressAPI/src/types/DatabaseTypes.ts b/ExpressAPI/src/types/DatabaseTypes.ts index 14e9b00a7f35a210302854e69c94dcc2f4dc63b4..2a9726980de2a1bcc8187be1df7d3837c0fd4def 100644 --- a/ExpressAPI/src/types/DatabaseTypes.ts +++ b/ExpressAPI/src/types/DatabaseTypes.ts @@ -1,6 +1,6 @@ -import { Prisma } from '@prisma/client'; -import LazyVal from '../shared/helpers/LazyVal'; -import GitlabUser from '../shared/types/Gitlab/GitlabUser'; +import { Prisma } from '@prisma/client'; +import LazyVal from '../shared/helpers/LazyVal'; +import * as Gitlab from '@gitbeaker/rest'; const userBase = Prisma.validator<Prisma.UserDefaultArgs>()({ @@ -29,7 +29,7 @@ const resultBase = Prisma.validator<Prisma.ResultDefaultArgs>()({ export type User = Prisma.UserGetPayload<typeof userBase> & { isTeachingStaff: boolean isAdmin: boolean - gitlabProfile: LazyVal<GitlabUser> + gitlabProfile: LazyVal<Gitlab.UserSchema> } export type Exercise = Prisma.ExerciseGetPayload<typeof exerciseBase> & { isCorrection: boolean