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'; class AssignmentRoutes implements RoutesManager { private readonly assignmentValidator: ExpressValidator.Schema = { name : { trim : true, notEmpty: true }, members : { trim : true, notEmpty : true, customSanitizer: DojoValidators.jsonSanitizer }, template: { trim : true, custom : DojoValidators.templateUrlValidator, customSanitizer: DojoValidators.templateUrlSanitizer } }; registerOnBackend(backend: Express) { backend.get('/assignments/:assignmentNameOrUrl', SecurityMiddleware.check(true), this.getAssignment); backend.post('/assignments', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), ParamsValidatorMiddleware.validate(this.assignmentValidator), this.createAssignment); backend.patch('/assignments/:assignmentNameOrUrl/publish', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.changeAssignmentPublishedStatus(true)); backend.patch('/assignments/:assignmentNameOrUrl/unpublish', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.changeAssignmentPublishedStatus(false)); } // Get an assignment by its name or gitlab url private async getAssignment(req: express.Request, res: express.Response) { const assignment: Assignment | undefined = req.boundParams.assignment; if ( assignment && !assignment.published && !await AssignmentManager.isUserAllowedToAccessAssignment(assignment, req.session.profile) ) { // @ts-ignore delete assignment.gitlabId; // @ts-ignore delete assignment.gitlabLink; // @ts-ignore delete assignment.gitlabCreationInfo; // @ts-ignore delete assignment.gitlabLastInfo; // @ts-ignore delete assignment.gitlabLastInfoDate; // @ts-ignore delete assignment.staff; // @ts-ignore delete assignment.exercises; } return assignment ? req.session.sendResponse(res, StatusCodes.OK, assignment) : res.status(StatusCodes.NOT_FOUND).send(); } private async createAssignment(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); let repository: GitlabRepository; 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); 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 ) { if ( error instanceof AxiosError ) { if ( error.response?.data.message.name && error.response.data.message.name == 'has already been taken' ) { return res.status(StatusCodes.CONFLICT).send(); } return res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); } logger.error(error); return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); } 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)'); } catch ( error ) { logger.error(error); if ( error instanceof AxiosError ) { return res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); } return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); } 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 ( e ) { 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 ) { if ( error instanceof AxiosError ) { return res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); } logger.error(error); return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); } } private changeAssignmentPublishedStatus(publish: boolean): (req: express.Request, res: express.Response) => Promise<void> { return async (req: express.Request, res: express.Response): Promise<void> => { if ( publish ) { const isPublishable = await SharedAssignmentHelper.isPublishable(req.boundParams.assignment!.gitlabId); if ( !isPublishable.isPublishable ) { return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, { lastPipeline: isPublishable.lastPipeline }, isPublishable.status?.message, isPublishable.status?.code); } } try { await GitlabManager.changeRepositoryVisibility(req.boundParams.assignment!.gitlabId, publish ? GitlabVisibility.INTERNAL : GitlabVisibility.PRIVATE); await db.assignment.update({ where: { name: req.boundParams.assignment!.name }, data : { published: publish } }); req.session.sendResponse(res, StatusCodes.OK); } catch ( error ) { if ( error instanceof AxiosError ) { res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); return; } logger.error(error); res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); } }; } } export default new AssignmentRoutes();