diff --git a/ExpressAPI/src/managers/ExerciceManager.ts b/ExpressAPI/src/managers/ExerciceManager.ts new file mode 100644 index 0000000000000000000000000000000000000000..4938f726894a17deca54f837fa080b51096e653d --- /dev/null +++ b/ExpressAPI/src/managers/ExerciceManager.ts @@ -0,0 +1,28 @@ +import Exercice from '../models/Exercice'; + + +class ExerciceManager { + private static _instance: ExerciceManager; + + private constructor() { } + + public static get instance(): ExerciceManager { + if ( !ExerciceManager._instance ) { + ExerciceManager._instance = new ExerciceManager(); + } + + return ExerciceManager._instance; + } + + createObjectFromRawSql(raw: any): Exercice { + const exercice = Exercice.createFromSql(raw); + + exercice.exerciceGitlabCreationInfo = raw.exerciceGitlabCreationInfo; + exercice.exerciceGitlabLastInfo = raw.exerciceGitlabLastInfo; + + return exercice; + } +} + + +export default ExerciceManager.instance; diff --git a/ExpressAPI/src/models/Exercice.ts b/ExpressAPI/src/models/Exercice.ts new file mode 100644 index 0000000000000000000000000000000000000000..d81b868538cbd6fe30c5164618f330d51218966a --- /dev/null +++ b/ExpressAPI/src/models/Exercice.ts @@ -0,0 +1,98 @@ +import Model from './Model'; +import db from '../helpers/DatabaseHelper'; +import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; + + +class Exercice extends Model { + static tableName: string = 'Exercice'; + + exerciceId: string = ''; + exerciceEnonceName: string = ''; + exerciceName: string = ''; + exerciceGitlabId: number = null; + exerciceGitlabLink: string = ''; + private _exerciceGitlabCreationInfo: string = '{}'; + private _exerciceGitlabLastInfo: string = '{}'; + exerciceGitlabLastInfoTs: number = null; + + get exerciceGitlabCreationInfo(): GitlabRepository { + return JSON.parse(this._exerciceGitlabCreationInfo); + } + + set exerciceGitlabCreationInfo(value: any) { + if ( typeof value === 'string' ) { + this._exerciceGitlabCreationInfo = value; + return; + } + + this._exerciceGitlabCreationInfo = JSON.stringify(value); + } + + get exerciceGitlabLastInfo(): GitlabRepository { + return JSON.parse(this._exerciceGitlabLastInfo); + } + + set exerciceGitlabLastInfo(value: any) { + if ( typeof value === 'string' ) { + this._exerciceGitlabLastInfo = value; + return; + } + + this._exerciceGitlabLastInfo = JSON.stringify(value); + } + + public async toJsonObject(): Promise<Object> { + const result = { + 'id' : this.exerciceId, + 'enonceName' : this.exerciceEnonceName, + 'name' : this.exerciceName, + 'gitlabId' : this.exerciceGitlabId, + 'gitlabLink' : this.exerciceGitlabLink, + 'gitlabCreationInfo': this.exerciceGitlabCreationInfo, + 'gitlabLastInfo' : this.exerciceGitlabLastInfo, + 'gitlabLastInfoTs' : this.exerciceGitlabLastInfoTs + }; + + return result; + }; + + public importFromJsonObject(jsonObject: any) { + this.exerciceId = jsonObject.id; + this.exerciceEnonceName = jsonObject.enonceName; + this.exerciceName = jsonObject.name; + this.exerciceGitlabId = jsonObject.gitlabId; + this.exerciceGitlabLink = jsonObject.gitlabLink; + this.exerciceGitlabCreationInfo = jsonObject.gitlabCreationInfo; + this.exerciceGitlabLastInfo = jsonObject.gitlabLastInfo; + this.exerciceGitlabLastInfoTs = jsonObject.gitlabLastInfoTs; + } + + public toDb(): any { + return { + exerciceId : this.exerciceId, + exerciceEnonceName : this.exerciceEnonceName, + exerciceName : this.exerciceName, + exerciceGitlabId : this.exerciceGitlabId, + exerciceGitlabLink : this.exerciceGitlabLink, + exerciceGitlabCreationInfo: this._exerciceGitlabCreationInfo, + exerciceGitlabLastInfo : this._exerciceGitlabLastInfo, + exerciceGitlabLastInfoTs : this.exerciceGitlabLastInfoTs + }; + } + + async create(): Promise<Exercice> { + await db(Exercice.tableName).insert(this.toDb()); + return this; + } + + update(): Promise<void> { + return db(Exercice.tableName).where('exerciceId', this.exerciceId).update(this.toDb()); + } + + del(): Promise<void> { + return db(Exercice.tableName).where('exerciceId', this.exerciceId).del(); + } +} + + +export default Exercice; diff --git a/ExpressAPI/src/models/ExerciceMember.ts b/ExpressAPI/src/models/ExerciceMember.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf2a2c8ee71d85c060b4e420e3b436917403e6b4 --- /dev/null +++ b/ExpressAPI/src/models/ExerciceMember.ts @@ -0,0 +1,43 @@ +import Model from './Model'; +import db from '../helpers/DatabaseHelper'; + + +class ExerciceMember extends Model { + static tableName: string = 'ExerciceMember'; + + exerciceId: string = ''; + userId: number = null; + + public async toJsonObject(): Promise<Object> { + const result = { + 'exerciceId': this.exerciceId, + 'userId' : this.userId + }; + + return result; + }; + + public importFromJsonObject(jsonObject: any) { + this.exerciceId = jsonObject.exerciceId; + this.userId = jsonObject.userId; + } + + public toDb(): any { + return { + exerciceId: this.exerciceId, + userId : this.userId + }; + } + + async create(): Promise<ExerciceMember> { + await db(ExerciceMember.tableName).insert(this.toDb()); + return this; + } + + del(): Promise<void> { + return db(ExerciceMember.tableName).where('exerciceId', this.exerciceId).andWhere('userId', this.userId).del(); + } +} + + +export default ExerciceMember; diff --git a/ExpressAPI/src/routes/ApiRoutesManager.ts b/ExpressAPI/src/routes/ApiRoutesManager.ts index 85ec024f873bfcf0b6946e90781fb45031fc60f0..f45b5cf3fd3f65a869098440d3ec85b71725f90f 100644 --- a/ExpressAPI/src/routes/ApiRoutesManager.ts +++ b/ExpressAPI/src/routes/ApiRoutesManager.ts @@ -1,9 +1,10 @@ -import { Express } from 'express-serve-static-core'; -import RoutesManager from '../express/RoutesManager'; -import BaseRoutes from './BaseRoutes'; -import SessionRoutes from './SessionRoutes'; -import EnonceRoutes from './EnonceRoutes'; -import GitlabRoutes from './GitlabRoutes'; +import { Express } from 'express-serve-static-core'; +import RoutesManager from '../express/RoutesManager'; +import BaseRoutes from './BaseRoutes'; +import SessionRoutes from './SessionRoutes'; +import EnonceRoutes from './EnonceRoutes'; +import GitlabRoutes from './GitlabRoutes'; +import ExerciceRoutes from './ExerciceRoutes'; class AdminRoutesManager implements RoutesManager { @@ -22,8 +23,9 @@ class AdminRoutesManager implements RoutesManager { registerOnBackend(backend: Express) { BaseRoutes.registerOnBackend(backend); SessionRoutes.registerOnBackend(backend); - EnonceRoutes.registerOnBackend(backend); GitlabRoutes.registerOnBackend(backend); + EnonceRoutes.registerOnBackend(backend); + ExerciceRoutes.registerOnBackend(backend); } } diff --git a/ExpressAPI/src/routes/ExerciceRoutes.ts b/ExpressAPI/src/routes/ExerciceRoutes.ts new file mode 100644 index 0000000000000000000000000000000000000000..418ec2fd10ba58c1e7a20632fd4f5333155564cb --- /dev/null +++ b/ExpressAPI/src/routes/ExerciceRoutes.ts @@ -0,0 +1,129 @@ +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 '../models/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 Enonce from '../models/Enonce'; +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 User from '../models/User'; +import UserManager from '../managers/UserManager'; +import Exercice from '../models/Exercice'; +import ExerciceMember from '../models/ExerciceMember'; +import ExerciceManager from '../managers/ExerciceManager'; + + +class ExerciceRoutes implements RoutesManager { + private static _instance: ExerciceRoutes; + + private constructor() { } + + public static get instance(): ExerciceRoutes { + if ( !ExerciceRoutes._instance ) { + ExerciceRoutes._instance = new ExerciceRoutes(); + } + + return ExerciceRoutes._instance; + } + + private readonly exerciceValidator: ExpressValidator.Schema = { + members: { + trim : true, + notEmpty : true, + customSanitizer: DojoValidators.jsonSanitizer + } + }; + + registerOnBackend(backend: Express) { + backend.post('/enonces/:enonceNameOrUrl/exercices', SecurityMiddleware.check(true), ParamsValidatorMiddleware.validate(this.exerciceValidator), this.createExercice.bind(this)); + } + + private getExerciceName(enonce: Enonce, members: Array<GitlabUser>, suffix: number): string { + return `DojoEx - ${ enonce.enonceName } - ${ members.map(member => member.username).join(' + ') }${ suffix > 0 ? ` - ${ suffix }` : '' }`; + } + + private getExercicePath(enonce: Enonce, exerciceId: string): string { + return `dojo-ex_${ enonce.enonceGitlabLastInfo.path }_${ exerciceId }`; + } + + private async createExercice(req: ApiRequest, res: express.Response) { + const params: { members: Array<GitlabUser> } = req.body; + params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ]; + + const exerciceId: string = uuidv4(); + + let repository: GitlabRepository; + let suffix: number = 0; + do { + try { + repository = await GitlabManager.forkRepository(req.boundParams.enonce.enonceGitlabCreationInfo.id, this.getExerciceName(req.boundParams.enonce, params.members, suffix), this.getExercicePath(req.boundParams.enonce, exerciceId), Config.exercice.default.description.replace('{{ENONCE_NAME}}', req.boundParams.enonce.enonceName), Config.exercice.default.visibility, Config.gitlab.group.exercices); + break; + } catch ( error ) { + if ( error instanceof AxiosError ) { + if ( error.response.data.message.name && error.response.data.message.name == 'has already been taken' ) { + suffix++; + } else { + return res.status(error.response.status).send(); + } + } else { + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); + } + } + } while ( suffix < Config.exercice.maxSameName ); + + if ( suffix >= Config.exercice.maxSameName ) { + return res.status(StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE).send(); + } + + try { + const members: Array<GitlabMember | false> = await Promise.all([ ...new Set([ ...(await req.boundParams.enonce.staff.value).map(member => member.userGitlabId), ...params.members.map(member => member.id) ]) ].map(async (memberId: number): Promise<GitlabMember | false> => { + try { + return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.Maintainer); + } catch ( e ) { + return false; + } + })); + + const exercice: Exercice = await ExerciceManager.createObjectFromRawSql({ + exerciceId : exerciceId, + exerciceEnonceName : req.boundParams.enonce.enonceName, + exerciceName : repository.name, + exerciceGitlabId : repository.id, + exerciceGitlabLink : repository.web_url, + exerciceGitlabCreationInfo: JSON.stringify(repository), + exerciceGitlabLastInfo : JSON.stringify(repository), + exerciceGitlabLastInfoTs : new Date().getTime() + }).create(); + + let dojoUsers: Array<User> = await UserManager.getFromGitlabUsers(params.members, true) as Array<User>; + dojoUsers = dojoUsers.reduce((unique, user) => (unique.findIndex(uniqueUser => uniqueUser.userId === user.userId) !== -1 ? unique : [ ...unique, user ]), Array<User>()); + await Promise.all(dojoUsers.map(dojoUser => ExerciceMember.createFromSql({ + exerciceId: exercice.exerciceId, + userId : dojoUser.userId + }).create())); + + return req.session.sendResponse(res, StatusCodes.OK, exercice.toJsonObject()); + } catch ( error ) { + if ( error instanceof AxiosError ) { + return res.status(error.response.status).send(); + } + + logger.error(error); + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); + } + } +} + + +export default ExerciceRoutes.instance;