Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • Dojo_Project_Nguyen/backend/dojobackendapi
  • dojo_project/projects/backend/dojobackendapi
2 results
Select Git revision
Show changes
Showing
with 1162 additions and 575 deletions
import axios from 'axios'; import Config from '../config/Config.js';
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 { StatusCodes } from 'http-status-codes';
import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility.js';
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 express from 'express';
import GitlabRoute from '../shared/types/Gitlab/GitlabRoute'; import { CommitSchema, ExpandedUserSchema, Gitlab, MemberSchema, ProjectBadgeSchema, ProjectSchema, ReleaseSchema, RepositoryFileExpandedSchema, RepositoryFileSchema, RepositoryTreeSchema } from '@gitbeaker/rest';
import SharedConfig from '../shared/config/SharedConfig'; import logger from '../shared/logging/WinstonLogger.js';
import GitlabProfile from '../shared/types/Gitlab/GitlabProfile'; import { AccessLevel, EditProjectOptions, ProjectVariableSchema, ProtectedBranchAccessLevel, ProtectedBranchSchema } from '@gitbeaker/core';
import GitlabRelease from '../shared/types/Gitlab/GitlabRelease'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import { CommitSchema, Gitlab } from '@gitbeaker/rest'; import SharedGitlabManager from '../shared/managers/SharedGitlabManager.js';
import logger from '../shared/logging/WinstonLogger';
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> { class GitlabManager extends SharedGitlabManager {
try { constructor() {
return (await axios.get<GitlabProfile>(this.getApiUrl(GitlabRoute.PROFILE_GET), { super(Config.gitlab.url, Config.gitlab.account.token);
headers: {
DojoOverrideAuthorization: true,
DojoAuthorizationHeader : 'Authorization',
DojoAuthorizationValue : `Bearer ${ token }`
}
})).data;
} catch ( e ) {
return undefined;
}
} }
public async getUserById(id: number): Promise<GitlabUser | undefined> { getUserProfile(token: string): Promise<ExpandedUserSchema> | undefined {
try { try {
const user = (await axios.get<GitlabUser>(`${ this.getApiUrl(GitlabRoute.USERS_GET) }/${ String(id) }`)).data; const profileApi = new Gitlab({
host : Config.gitlab.url,
oauthToken: token
});
return user.id === id ? user : undefined; return profileApi.Users.showCurrentUser();
} catch ( e ) { } catch ( e ) {
logger.error(JSON.stringify(e));
return undefined; return undefined;
} }
} }
public async getUserByUsername(username: string): Promise<GitlabUser | undefined> { async getRepositoryMembers(idOrNamespace: string, includeInherited: boolean = true): Promise<Array<MemberSchema>> {
try { try {
const params: Record<string, string> = {}; return await this.api.ProjectMembers.all(idOrNamespace, { includeInherited: includeInherited });
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 ) { } catch ( e ) {
return undefined; logger.error(JSON.stringify(e));
return Promise.reject(e);
} }
} }
async getRepository(projectIdOrNamespace: string): Promise<GitlabRepository> { async getRepositoryReleases(repoId: number): Promise<Array<ReleaseSchema>> {
const response = await axios.get<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', encodeURIComponent(projectIdOrNamespace))); try {
return await this.api.ProjectReleases.all(repoId);
return response.data; } catch ( e ) {
} logger.error(JSON.stringify(e));
return Promise.reject(e);
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;
} }
async getRepositoryLastCommit(repoId: number, branch: string = 'main'): Promise<CommitSchema | undefined> { async getRepositoryLastCommit(repoId: number, branch: string = 'main'): Promise<CommitSchema | undefined> {
...@@ -92,93 +56,152 @@ class GitlabManager { ...@@ -92,93 +56,152 @@ class GitlabManager {
return commits.length > 0 ? commits[0] : undefined; return commits.length > 0 ? commits[0] : undefined;
} catch ( e ) { } catch ( e ) {
logger.error(e); logger.error(JSON.stringify(e));
return undefined;
}
}
async getRepositoryCommit(repoId: number, commitSha: string): Promise<CommitSchema | undefined> {
try {
return await this.api.Commits.show(repoId, commitSha);
} catch ( e ) {
logger.error(JSON.stringify(e));
return undefined; return undefined;
} }
} }
async createRepository(name: string, description: string, visibility: string, initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, import_url: string): Promise<GitlabRepository> { async createRepository(name: string, description: string, visibility: 'public' | 'internal' | 'private', initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, importUrl: string): Promise<ProjectSchema> {
const response = await axios.post<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_CREATE), { try {
return await this.api.Projects.create({
name : name, name : name,
description : description, description : description,
import_url : import_url, importUrl : importUrl,
initialize_with_readme: initializeWithReadme, initializeWithReadme: initializeWithReadme,
namespace_id : namespace, namespaceId : namespace,
shared_runners_enabled: sharedRunnersEnabled, sharedRunnersEnabled: sharedRunnersEnabled,
visibility : visibility, visibility : visibility,
wiki_enabled : wikiEnabled wikiAccessLevel : wikiEnabled ? 'enabled' : 'disabled'
}); });
} catch ( e ) {
return response.data; logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async deleteRepository(repoId: number): Promise<void> { async deleteRepository(repoId: number): Promise<void> {
return await axios.delete(this.getApiUrl(GitlabRoute.REPOSITORY_DELETE).replace('{{id}}', String(repoId))); try {
return await this.api.Projects.remove(repoId);
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async forkRepository(forkId: number, name: string, path: string, description: string, visibility: string, namespace: number): Promise<GitlabRepository> { async forkRepository(forkId: number, name: string, path: string, description: string, visibility: 'public' | 'internal' | 'private', namespace: number): Promise<ProjectSchema> {
const response = await axios.post<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_FORK).replace('{{id}}', String(forkId)), { try {
return await this.api.Projects.fork(forkId, {
name : name, name : name,
path : path, path : path,
description: description, description: description,
namespace_id: namespace, namespaceId: namespace,
visibility : visibility visibility : visibility
}); });
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
}
return response.data; async editRepository(repoId: number, newAttributes: EditProjectOptions): Promise<ProjectSchema> {
try {
return await this.api.Projects.edit(repoId, newAttributes);
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async editRepository(repoId: number, newAttributes: Partial<GitlabRepository>): Promise<GitlabRepository> { async deleteRepositoryMember(repoId: number, userId: number, skipSubresources: boolean = false, unassignIssuables: boolean = false): Promise<void> {
const response = await axios.put<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_EDIT).replace('{{id}}', String(repoId)), newAttributes); try {
return await this.api.ProjectMembers.remove(repoId, userId, {
skipSubresourceS: skipSubresources,
unassignIssuables
});
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
}
return response.data; async renameRepository(repoId: number, newName: string): Promise<ProjectSchema> {
try {
return await this.api.Projects.edit(repoId, {
name: newName
});
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<GitlabRepository> { async moveRepository(repoId: number, newRepoId: number): Promise<ProjectSchema> {
return await this.editRepository(repoId, { visibility: visibility.toString() }); try {
return await this.api.Projects.transfer(repoId, newRepoId);
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async addRepositoryMember(repoId: number, userId: number, accessLevel: GitlabAccessLevel): Promise<GitlabMember> { changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<ProjectSchema> {
const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_MEMBER_ADD).replace('{{id}}', String(repoId)), { return this.editRepository(repoId, { visibility: visibility });
user_id : userId, }
access_level: accessLevel
});
return response.data; async addRepositoryMember(repoId: number, userId: number, accessLevel: Exclude<AccessLevel, AccessLevel.ADMIN>): Promise<MemberSchema> {
try {
return await this.api.ProjectMembers.add(repoId, accessLevel, { userId });
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<GitlabMember> { async addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<ProjectVariableSchema> {
const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_VARIABLES_ADD).replace('{{id}}', String(repoId)), { try {
key : key, return await this.api.ProjectVariables.create(repoId, key, value, {
variable_type: 'env_var', variableType: 'env_var',
value : value,
protected : isProtected, protected : isProtected,
masked : isMasked masked : isMasked
}); });
} catch ( e ) {
return response.data; logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): Promise<GitlabMember> { async addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): Promise<ProjectBadgeSchema> {
const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_BADGES_ADD).replace('{{id}}', String(repoId)), { try {
link_url : linkUrl, return await this.api.ProjectBadges.add(repoId, linkUrl, imageUrl, {
image_url: imageUrl,
name: name name: name
}); });
} catch ( e ) {
return response.data; logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async checkTemplateAccess(projectIdOrNamespace: string, req: express.Request): Promise<StatusCodes> { 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 // Get the Gitlab project and check if it have public or internal visibility
try { 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) ) {
return StatusCodes.OK; req.session.sendResponse(res, StatusCodes.OK);
return true;
} }
} catch ( e ) { } catch ( e ) {
return StatusCodes.NOT_FOUND; req.session.sendResponse(res, StatusCodes.NOT_FOUND, undefined, 'Template not found', DojoStatusCode.GITLAB_TEMPLATE_NOT_FOUND);
return false;
} }
// Check if the user and dojo are members (with at least reporter access) of the project // Check if the user and dojo are members (with at least reporter access) of the project
...@@ -188,7 +211,7 @@ class GitlabManager { ...@@ -188,7 +211,7 @@ class GitlabManager {
dojo: false dojo: false
}; };
members.forEach(member => { members.forEach(member => {
if ( member.access_level >= GitlabAccessLevel.REPORTER ) { if ( member.access_level >= AccessLevel.REPORTER ) {
if ( member.id === req.session.profile.id ) { if ( member.id === req.session.profile.id ) {
isUsersAtLeastReporter.user = true; isUsersAtLeastReporter.user = true;
} else if ( member.id === Config.gitlab.account.id ) { } else if ( member.id === Config.gitlab.account.id ) {
...@@ -197,90 +220,83 @@ class GitlabManager { ...@@ -197,90 +220,83 @@ class GitlabManager {
} }
}); });
return isUsersAtLeastReporter.user && isUsersAtLeastReporter.dojo ? StatusCodes.OK : StatusCodes.UNAUTHORIZED; if ( isUsersAtLeastReporter.user && isUsersAtLeastReporter.dojo ) {
req.session.sendResponse(res, StatusCodes.OK);
return true;
} else {
req.session.sendResponse(res, StatusCodes.UNAUTHORIZED, undefined, 'Template access unauthorized', DojoStatusCode.GITLAB_TEMPLATE_ACCESS_UNAUTHORIZED);
return false;
}
} }
async protectBranch(repoId: number, branchName: string, allowForcePush: boolean, allowedToMerge: GitlabAccessLevel, allowedToPush: GitlabAccessLevel, allowedToUnprotect: GitlabAccessLevel): Promise<GitlabMember> { async protectBranch(repoId: number, branchName: string, allowForcePush: boolean, allowedToMerge: ProtectedBranchAccessLevel, allowedToPush: ProtectedBranchAccessLevel, allowedToUnprotect: ProtectedBranchAccessLevel): Promise<ProtectedBranchSchema> {
const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_BRANCHES_PROTECT).replace('{{id}}', String(repoId)), { try {
name : branchName, return await this.api.ProtectedBranches.protect(repoId, branchName, {
allow_force_push : allowForcePush, allowForcePush : allowForcePush,
merge_access_level : allowedToMerge.valueOf(), mergeAccessLevel : allowedToMerge,
push_access_level : allowedToPush.valueOf(), pushAccessLevel : allowedToPush,
unprotect_access_level: allowedToUnprotect.valueOf() unprotectAccessLevel: allowedToUnprotect
}); });
} catch ( e ) {
return response.data; logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<GitlabTreeFile>> { async getRepositoryTree(repoId: number, recursive: boolean = true, branch: string = 'main'): Promise<Array<RepositoryTreeSchema>> {
const address: string | undefined = this.getApiUrl(GitlabRoute.REPOSITORY_TREE).replace('{{id}}', String(repoId)); try {
let params: Partial<parseLinkHeader.Link | { recursive: boolean, per_page: number }> | undefined = { return await this.api.Repositories.allRepositoryTrees(repoId, {
pagination: 'keyset',
recursive: recursive, recursive: recursive,
per_page : 100,
ref : branch ref : branch
};
const results: Array<GitlabTreeFile> = [];
while ( params !== undefined ) {
const response = await axios.get<Array<GitlabTreeFile>>(address, {
params: params
}); });
} catch ( e ) {
results.push(...response.data); logger.error(JSON.stringify(e));
return Promise.reject(e);
if ( 'link' in response.headers ) {
params = parseLinkHeader(response.headers['link'])?.next ?? undefined;
} else {
params = undefined;
}
} }
return results;
} }
async getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<GitlabFile> { async getFile(repoId: number, filePath: string, branch: string = 'main'): Promise<RepositoryFileExpandedSchema> {
const response = await axios.get<GitlabFile>(this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), { try {
params: { return await this.api.RepositoryFiles.show(repoId, filePath, branch);
ref: branch } catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
} }
});
return response.data;
} }
private async createUpdateFile(create: boolean, repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined) { private async createUpdateFile(create: boolean, repoId: number, filePath: string, fileBase64: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<RepositoryFileSchema> {
const axiosFunction = create ? axios.post : axios.put; try {
const gitFunction = create ? this.api.RepositoryFiles.create.bind(this.api) : this.api.RepositoryFiles.edit.bind(this.api);
await axiosFunction(this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), { return await gitFunction(repoId, filePath, branch, fileBase64, commitMessage, {
encoding : 'base64', encoding : 'base64',
branch : branch, authorName : authorName,
commit_message: commitMessage, authorEmail: authorMail
content : fileBase64,
author_name : authorName,
author_email : authorMail
}); });
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
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); 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); 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) { async deleteFile(repoId: number, filePath: string, commitMessage: string, branch: string = 'main', authorName: string = 'Dojo', authorMail: string | undefined = undefined): Promise<void> {
await axios.delete(this.getApiUrl(GitlabRoute.REPOSITORY_FILE).replace('{{id}}', String(repoId)).replace('{{filePath}}', encodeURIComponent(filePath)), { try {
data: { return await this.api.RepositoryFiles.remove(repoId, filePath, branch, commitMessage, {
branch : branch, authorName : authorName,
commit_message: commitMessage, authorEmail: authorMail
author_name : authorName,
author_email : authorMail
}
}); });
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
} }
......
import axios, { AxiosError, AxiosRequestHeaders } from 'axios'; import axios, { AxiosError, AxiosRequestHeaders } from 'axios';
import Config from '../config/Config';
import FormData from 'form-data'; import FormData from 'form-data';
import logger from '../shared/logging/WinstonLogger'; import logger from '../shared/logging/WinstonLogger.js';
import SharedConfig from '../shared/config/SharedConfig';
class HttpManager { class HttpManager {
...@@ -12,33 +10,16 @@ class HttpManager { ...@@ -12,33 +10,16 @@ class HttpManager {
} }
private registerRequestInterceptor() { private registerRequestInterceptor() {
axios.interceptors.request.use((config) => { axios.interceptors.request.use(config => {
if ( config.data instanceof FormData ) { if ( config.data instanceof FormData ) {
config.headers = { ...config.headers, ...(config.data as FormData).getHeaders() } as AxiosRequestHeaders; config.headers = { ...config.headers, ...config.data.getHeaders() } as AxiosRequestHeaders;
} }
if ( config.url && config.url.indexOf(SharedConfig.gitlab.apiURL) !== -1 ) {
if ( !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; return config;
}); });
} }
private registerResponseInterceptor() { private registerResponseInterceptor() {
axios.interceptors.response.use((response) => { axios.interceptors.response.use(response => response, error => {
return response;
}, (error) => {
if ( error instanceof AxiosError ) { if ( error instanceof AxiosError ) {
logger.error(`${ JSON.stringify(error.response?.data) }`); logger.error(`${ JSON.stringify(error.response?.data) }`);
} else { } else {
......
import SharedConfig from '../shared/config/SharedConfig';
import SonarRoute from '../shared/types/Sonar/SonarRoute';
import axios, { AxiosInstance } from 'axios';
import Config from '../config/Config';
import SonarProjectCreation from '../shared/types/Sonar/SonarProjectCreation';
import https from 'https';
import GlobalHelper from '../helpers/GlobalHelper';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode';
import express from 'express';
import GitlabRepository from '../shared/types/Gitlab/GitlabRepository';
class SonarManager {
private instance: AxiosInstance = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
});
private getApiUrl(route: SonarRoute): string {
return `${ SharedConfig.sonar.url }${ route }`;
}
/**
* Assign a Gitlab Personal Access Token to a Sonar account (needed for any other request linked to gitlab)
* @private
*/
private async setPAT() {
const formData = new FormData();
formData.append('almSetting', 'dojo');
formData.append('pat', Config.gitlab.account.token);
await this.instance.post(this.getApiUrl(SonarRoute.SET_PAT), formData, {
headers: {
Authorization: `Basic ${ btoa(SharedConfig.sonar.token + ":") }`
}
});
}
private async executePostRequest<T>(url: string, data?: FormData) {
await this.setPAT(); // Always set PAT to be sure it has been set
return (await this.instance.post<T>(url, data, {
headers: {
Authorization: `Basic ${ btoa(SharedConfig.sonar.token + ":") }`
}
})).data;
}
private async executeGetRequest<T>(url: string, data?: unknown) {
return (await this.instance.get<T>(url, {
headers: {
Authorization: `Basic ${ btoa(SharedConfig.sonar.token + ":") }`
},
params: data
})).data;
}
async createProjectFromGitlab(projectId: number) {
const formData = new FormData();
formData.append('almSetting', 'dojo');
formData.append('gitlabProjectId', projectId.toString());
return await this.executePostRequest<SonarProjectCreation>(this.getApiUrl(SonarRoute.PROJECT_CREATE_GITLAB), formData)
}
async addQualityGate(projectKey: string, qualityGate: string) {
const formData = new FormData();
formData.append('projectKey', projectKey);
formData.append('gateName', qualityGate);
return await this.executePostRequest<undefined>(this.getApiUrl(SonarRoute.PROJECT_ADD_GATE), formData);
}
async addQualityProfile(projectKey: string, qualityProfile: string, language: string) {
const formData = new FormData();
formData.append('project', projectKey);
formData.append('qualityProfile', qualityProfile);
formData.append('language', language);
return await this.executePostRequest<unknown>(this.getApiUrl(SonarRoute.PROJECT_ADD_PROFILE), formData);
}
async createProjectWithQualities(gitlabRepository: GitlabRepository, qualityGate: string | null, qualityProfiles: string[] | null, req: express.Request, res: express.Response) {
let sonarProject: SonarProjectCreation | undefined = undefined;
try {
sonarProject = await this.createProjectFromGitlab(gitlabRepository.id);
if (sonarProject == undefined) {
return await GlobalHelper.repositoryCreationError('Sonar error', undefined, req, res, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_ERROR, gitlabRepository);
}
} catch ( error ) {
return await GlobalHelper.repositoryCreationError('Sonar project creation error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, gitlabRepository, sonarProject);
}
// Add gate and profiles to sonar project
if ( qualityGate != undefined && qualityGate != "" ) {
try {
await this.addQualityGate(sonarProject.project.key, qualityGate);
} catch ( error ) {
return await GlobalHelper.repositoryCreationError('Sonar gate error', error, req, res, DojoStatusCode.ASSIGNMENT_SONAR_GATE_NOT_FOUND, DojoStatusCode.ASSIGNMENT_SONAR_GATE_NOT_FOUND, gitlabRepository, sonarProject);
}
}
if ( qualityProfiles != undefined && qualityProfiles.length > 0 ) {
for ( const profile of qualityProfiles ) {
try {
const [ lang, name ] = profile.split('/');
if (lang.trim() != '' && name.trim() != '') {
await this.addQualityProfile(sonarProject.project.key, name.trim(), lang.trim());
} else {
return await GlobalHelper.repositoryCreationError('Sonar profile invalid', undefined, req, res, DojoStatusCode.ASSIGNMENT_SONAR_PROFILE_NOT_FOUND, DojoStatusCode.ASSIGNMENT_SONAR_PROFILE_NOT_FOUND, gitlabRepository, sonarProject);
}
} catch ( error ) {
return await GlobalHelper.repositoryCreationError('Sonar profile not found', error, req, res, DojoStatusCode.ASSIGNMENT_SONAR_PROFILE_NOT_FOUND, DojoStatusCode.ASSIGNMENT_SONAR_PROFILE_NOT_FOUND, gitlabRepository, sonarProject);
}
}
}
return sonarProject;
}
async deleteProject(projectKey: string) {
const formData = new FormData();
formData.append('project', projectKey);
return await this.executePostRequest<SonarProjectCreation>(this.getApiUrl(SonarRoute.PROJECT_DELETE), formData)
}
async getLanguages() {
const resp = await this.executeGetRequest<{ languages: { key: string, name: string }[]}>(this.getApiUrl(SonarRoute.GET_LANGUAGES))
return resp.languages.map(l => l.key)
}
async testQualityGate(gateName: string) {
try {
await this.executeGetRequest(this.getApiUrl(SonarRoute.TEST_GATE), { name: gateName });
return true;
} catch ( e ) {
return false;
}
}
async testQualityProfile(profileName: string, language: string) {
try {
const formData = new FormData();
formData.append('language', language);
formData.append('qualityProfile', profileName);
const resp = await this.executeGetRequest<{ profiles: { key: string, name: string, language: string }[] }>(
this.getApiUrl(SonarRoute.TEST_PROFILE), formData
);
return (resp.profiles.length > 0 && resp.profiles.some(p => p.name === profileName && p.language === language))
} catch ( e ) {
return false;
}
}
/**
* Return the sonar user login name associated with the gitlab username
* @param gitlabUsername Username to look up
*/
async findUserFromGitlabUser(gitlabUsername: string) {
const formData = new FormData();
formData.append('q', gitlabUsername);
const resp = await this.executeGetRequest<{ users: { login: string, name: string }[] }>(this.getApiUrl(SonarRoute.SEARCH_USER), formData);
for (const u of resp.users) {
if ( u.name == gitlabUsername ) {
return u.login;
}
}
return undefined;
}
async addUserToProject(username: string, projectKey: string, privileged: boolean) {
const permissions = ['user', 'codeviewer'];
if (privileged) {
permissions.push('issueadmin');
}
for (const perm of permissions) {
const formData = new FormData();
formData.append('projectKey', projectKey);
formData.append('permission', perm);
formData.append('login', username);
await this.executePostRequest(this.getApiUrl(SonarRoute.PROJECT_ADD_USER), formData)
}
}
async addGitlabUserToProject(gitlabUsername: string, projectKey: string, privileged: boolean) {
const username = await this.findUserFromGitlabUser(gitlabUsername);
if (username == undefined) {
return false;
}
await this.addUserToProject(username, projectKey, privileged);
return true;
}
}
export default new SonarManager();
\ No newline at end of file
import { Prisma, Tag } from '@prisma/client';
import db from '../helpers/DatabaseHelper';
class TagManager {
async get(name: string, include: Prisma.TagInclude | undefined = undefined): Promise<Tag | undefined> {
return await db.tag.findUnique({
where : {
name: name
},
include: include
}) as unknown as Tag ?? undefined;
}
}
export default new TagManager();
import { TagProposal } from '@prisma/client';
import db from '../helpers/DatabaseHelper';
class TagProposalManager {
async get(name: string | undefined = undefined): Promise<TagProposal | undefined> {
return await db.tagProposal.findUnique({
where: {
name: name
}
}) as unknown as TagProposal ?? undefined;
}
}
export default new TagProposalManager();
import GitlabUser from '../shared/types/Gitlab/GitlabUser';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import db from '../helpers/DatabaseHelper'; import db from '../helpers/DatabaseHelper.js';
import GitlabProfile from '../shared/types/Gitlab/GitlabProfile'; import { User } from '../types/DatabaseTypes.js';
import { User } from '../types/DatabaseTypes'; import * as Gitlab from '@gitbeaker/rest';
class UserManager { class UserManager {
async getFiltered(filters: Prisma.UserWhereInput | undefined, include: Prisma.UserInclude | undefined = undefined): Promise<Array<User> | undefined> {
return await db.user.findMany({
where : filters,
include: include
}) as unknown as Array<User> ?? undefined;
}
async getByMail(mail: string, include: Prisma.UserInclude | undefined = undefined): Promise<User | undefined> { async getByMail(mail: string, include: Prisma.UserInclude | undefined = undefined): Promise<User | undefined> {
return await db.user.findUnique({ return await db.user.findUnique({
where : { where : {
...@@ -15,16 +21,16 @@ class UserManager { ...@@ -15,16 +21,16 @@ class UserManager {
}) as unknown as User ?? undefined; }) as unknown as User ?? undefined;
} }
async getById(id: number, include: Prisma.UserInclude | undefined = undefined): Promise<User | undefined> { async getById(id: string | number, include: Prisma.UserInclude | undefined = undefined): Promise<User | undefined> {
return await db.user.findUnique({ return await db.user.findUnique({
where : { where : {
id: id id: Number(id)
}, },
include: include include: include
}) as unknown as User ?? undefined; }) as unknown as User ?? undefined;
} }
async getUpdateFromGitlabProfile(gitlabProfile: GitlabProfile): Promise<User> { async getUpdateFromGitlabProfile(gitlabProfile: Gitlab.ExpandedUserSchema): Promise<User> {
await db.user.upsert({ await db.user.upsert({
where : { where : {
id: gitlabProfile.id id: gitlabProfile.id
...@@ -46,7 +52,7 @@ class UserManager { ...@@ -46,7 +52,7 @@ class UserManager {
return (await this.getById(gitlabProfile.id))!; 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; let user = await this.getById(gitlabUser.id, include) ?? gitlabUser.id;
if ( typeof user === 'number' && createIfNotExist ) { if ( typeof user === 'number' && createIfNotExist ) {
...@@ -61,7 +67,7 @@ class UserManager { ...@@ -61,7 +67,7 @@ class UserManager {
return user; 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))); return Promise.all(gitlabUsers.map(gitlabUser => this.getFromGitlabUser(gitlabUser, createIfNotExist, include)));
} }
} }
......
import express from 'express'; import express from 'express';
import Config from '../config/Config'; import Config from '../config/Config.js';
import semver from 'semver/preload'; import semver from 'semver/preload';
import Session from '../controllers/Session'; import Session from '../controllers/Session.js';
import { HttpStatusCode } from 'axios'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; import { StatusCodes } from 'http-status-codes';
class ClientVersionCheckerMiddleware { class ClientVersionCheckerMiddleware {
register(): (req: express.Request, res: express.Response, next: express.NextFunction) => void { register(): (req: express.Request, res: express.Response, next: express.NextFunction) => void {
return async (req: express.Request, res: express.Response, next: express.NextFunction) => { return (req: express.Request, res: express.Response, next: express.NextFunction) => {
if ( req.headers['client'] && req.headers['client-version'] ) { if ( req.headers['client'] && req.headers['client-version'] ) {
const requestClient = req.headers['client'] as string; const requestClient = req.headers['client'] as string;
const requestClientVersion = req.headers['client-version'] as string; const requestClientVersion = req.headers['client-version'] as string;
...@@ -19,13 +19,15 @@ class ClientVersionCheckerMiddleware { ...@@ -19,13 +19,15 @@ class ClientVersionCheckerMiddleware {
next(); next();
return; return;
} else { } 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; 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();
} }
}; };
} }
......
...@@ -3,6 +3,9 @@ import express from 'express'; ...@@ -3,6 +3,9 @@ import express from 'express';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import ExerciseManager from '../managers/ExerciseManager'; import ExerciseManager from '../managers/ExerciseManager';
import AssignmentManager from '../managers/AssignmentManager'; import AssignmentManager from '../managers/AssignmentManager';
import TagManager from '../managers/TagManager';
import TagProposalManager from '../managers/TagProposalManager';
import UserManager from '../managers/UserManager';
type GetFunction = (id: string | number, ...args: Array<unknown>) => Promise<unknown> type GetFunction = (id: string | number, ...args: Array<unknown>) => Promise<unknown>
...@@ -27,23 +30,50 @@ class ParamsCallbackManager { ...@@ -27,23 +30,50 @@ class ParamsCallbackManager {
initBoundParams(req: express.Request) { initBoundParams(req: express.Request) {
if ( !req.boundParams ) { if ( !req.boundParams ) {
req.boundParams = { req.boundParams = {
user : undefined,
assignment : undefined, assignment : undefined,
exercise : undefined exercise : undefined,
tag : undefined,
tagProposal: undefined
}; };
} }
} }
registerOnBackend(backend: Express) { registerOnBackend(backend: Express) {
this.listenParam('userId', backend, (UserManager.getById as GetFunction).bind(UserManager), [ {
assignments: true,
exercises : {
include: {
members : true,
assignment: {
include: {
staff: true
}
}
}
}
} ], 'user');
this.listenParam('assignmentNameOrUrl', backend, (AssignmentManager.get as GetFunction).bind(AssignmentManager), [ { this.listenParam('assignmentNameOrUrl', backend, (AssignmentManager.get as GetFunction).bind(AssignmentManager), [ {
exercises: true, exercises: true,
staff : true staff : true
} ], 'assignment'); } ], 'assignment');
this.listenParam('exerciseIdOrUrl', backend, (ExerciseManager.get as GetFunction).bind(ExerciseManager), [ { this.listenParam('exerciseIdOrUrl', backend, (ExerciseManager.get as GetFunction).bind(ExerciseManager), [ {
assignment: true, assignment: {
include: {
staff: true
}
},
members : true, members : true,
results : true results : true
} ], 'exercise'); } ], 'exercise');
this.listenParam('tagName', backend, (TagManager.get as GetFunction).bind(TagManager), [ {
assignments: true
} ], 'tag');
this.listenParam('tagProposalName', backend, (TagProposalManager.get as GetFunction).bind(TagProposalManager), [ {} ], 'tagProposal');
} }
} }
......
...@@ -5,20 +5,20 @@ import { StatusCodes } from 'http-status-codes'; ...@@ -5,20 +5,20 @@ import { StatusCodes } from 'http-status-codes';
class ParamsValidatorMiddleware { class ParamsValidatorMiddleware {
validate(validations: Array<ExpressValidator.ValidationChain> | ExpressValidator.Schema): (req: express.Request, res: express.Response, next: express.NextFunction) => void { 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) => { return (req: express.Request, res: express.Response, next: express.NextFunction) => {
if ( !(validations instanceof Array) ) { if ( !(validations instanceof Array) ) {
validations = ExpressValidator.checkSchema(validations); validations = ExpressValidator.checkSchema(validations);
} }
await Promise.all(validations.map(validation => validation.run(req))); Promise.all(validations.map(validation => validation.run(req))).then(() => {
const errors = ExpressValidator.validationResult(req); const errors = ExpressValidator.validationResult(req);
if ( !errors.isEmpty() ) { if ( !errors.isEmpty() ) {
return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, { errors: errors.array() }); req.session.sendResponse(res, StatusCodes.BAD_REQUEST, { errors: errors.array() });
return;
} }
return next(); next();
});
}; };
} }
} }
......
import express from 'express'; import express from 'express';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import SecurityCheckType from '../types/SecurityCheckType'; import SecurityCheckType from '../types/SecurityCheckType.js';
import logger from '../shared/logging/WinstonLogger'; import logger from '../shared/logging/WinstonLogger.js';
import AssignmentManager from '../managers/AssignmentManager'; import AssignmentManager from '../managers/AssignmentManager.js';
import ExerciseManager from '../managers/ExerciseManager';
class SecurityMiddleware { 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. private checkIfConnected(checkIfConnected: boolean, req: express.Request): boolean {
check(checkIfConnected: boolean, ...checkTypes: Array<SecurityCheckType>): (req: express.Request, res: express.Response, next: express.NextFunction) => void { return !checkIfConnected || (req.session.profile !== null && req.session.profile !== undefined);
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if ( checkIfConnected ) {
if ( req.session.profile === null || req.session.profile === undefined ) {
return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED);
} }
}
let isAllowed = checkTypes.length === 0;
if ( !isAllowed ) { private async checkType(checkType: SecurityCheckType, req: express.Request): Promise<boolean> {
for ( const checkType of checkTypes ) {
try { try {
switch ( String(checkType) ) { switch ( String(checkType) ) {
case SecurityCheckType.TEACHING_STAFF: case SecurityCheckType.USER.valueOf():
isAllowed = isAllowed || req.session.profile.isTeachingStaff; return this.checkIfConnected(true, req);
break; case SecurityCheckType.ADMIN.valueOf():
case SecurityCheckType.ASSIGNMENT_STAFF: return req.session.profile.isAdmin;
isAllowed = isAllowed || await AssignmentManager.isUserAllowedToAccessAssignment(req.boundParams.assignment!, req.session.profile); case SecurityCheckType.TEACHING_STAFF.valueOf():
break; return req.session.profile.isTeachingStaff;
case SecurityCheckType.ASSIGNMENT_IS_PUBLISHED: case SecurityCheckType.EXERCISE_MEMBERS.valueOf():
isAllowed = isAllowed || (req.boundParams.assignment?.published ?? false); return await ExerciseManager.isUserAllowedToAccessExercise(req.boundParams.exercise!, req.session.profile);
break; case SecurityCheckType.ASSIGNMENT_STAFF.valueOf():
case SecurityCheckType.EXERCISE_SECRET: return await AssignmentManager.isUserAllowedToAccessAssignment(req.boundParams.assignment!, req.session.profile);
isAllowed = isAllowed || (req.headers.exercisesecret as string | undefined) === req.boundParams.exercise!.secret; case SecurityCheckType.ASSIGNMENT_IS_PUBLISHED.valueOf():
break; return req.boundParams.assignment?.published ?? false;
case SecurityCheckType.EXERCISE_SECRET.valueOf():
return (req.headers.exercisesecret as string | undefined) === req.boundParams.exercise!.secret;
case SecurityCheckType.ASSIGNMENT_SECRET:
return (req.headers.assignmentsecret as string | undefined) === req.boundParams.assignment!.secret;
default: default:
break; return false;
} }
} catch ( e ) { } catch ( e ) {
logger.error('Security check failed !!! => ' + e); logger.error('Security check failed !!! => ' + JSON.stringify(e));
isAllowed = isAllowed || false; 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 (req: express.Request, res: express.Response, next: express.NextFunction) => {
if ( !this.checkIfConnected(checkIfConnected, req) ) {
return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED);
} }
const isAllowed: boolean = checkTypes.length === 0 ? true : checkTypes.find(async checkType => this.checkType(checkType, req)) !== undefined;
if ( !isAllowed ) { if ( !isAllowed ) {
return req.session.sendResponse(res, StatusCodes.FORBIDDEN); return req.session.sendResponse(res, StatusCodes.FORBIDDEN);
} }
......
import express from 'express'; import express from 'express';
import Session from '../controllers/Session'; import Session from '../controllers/Session.js';
import { Express } from 'express-serve-static-core'; import { Express } from 'express-serve-static-core';
class SessionMiddleware { class SessionMiddleware {
registerOnBackend(backend: Express) { registerOnBackend(backend: Express) {
backend.use(async (req: express.Request, res: express.Response, next: express.NextFunction) => { backend.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
req.session = new Session(); req.session = new Session();
await req.session.initSession(req, res); req.session.initSession(req, res).then(() => {
next();
return next(); });
}); });
} }
} }
......
import cluster, { Worker } from 'node:cluster'; import cluster, { Worker } from 'node:cluster';
import WorkerRole from './WorkerRole'; import WorkerRole from './WorkerRole.js';
import os from 'os'; import os from 'os';
import ClusterStrategy from './ClusterStrategy'; import ClusterStrategy from './ClusterStrategy.js';
import WorkerPool from './WorkerPool'; import WorkerPool from './WorkerPool.js';
import logger from '../shared/logging/WinstonLogger'; import logger from '../shared/logging/WinstonLogger.js';
/* /*
...@@ -11,10 +11,13 @@ import logger from '../shared/logging/WinstonLogger'; ...@@ -11,10 +11,13 @@ import logger from '../shared/logging/WinstonLogger';
*/ */
class ClusterManager { class ClusterManager {
public static readonly CORES = os.cpus().length; public static readonly CORES = os.cpus().length;
private readonly strategy: ClusterStrategy;
private workers: { [pid: number]: WorkerRole; } = []; private workers: { [pid: number]: WorkerRole; } = [];
constructor(private strategy: ClusterStrategy) {} constructor(strategy: ClusterStrategy) {
this.strategy = strategy;
}
private getWorkerPool(role: WorkerRole): WorkerPool | undefined { private getWorkerPool(role: WorkerRole): WorkerPool | undefined {
return this.strategy.find(elem => elem.role === role); return this.strategy.find(elem => elem.role === role);
......
import WorkerPool from './WorkerPool'; import WorkerPool from './WorkerPool.js';
type ClusterStrategy = Array<WorkerPool> type ClusterStrategy = Array<WorkerPool>
......
import WorkerRole from './WorkerRole'; import WorkerRole from './WorkerRole.js';
import WorkerTask from './WorkerTask'; import WorkerTask from './WorkerTask.js';
/* /*
......
import { Express } from 'express-serve-static-core'; import { Express } from 'express-serve-static-core';
import RoutesManager from '../express/RoutesManager'; import RoutesManager from '../express/RoutesManager.js';
import BaseRoutes from './BaseRoutes'; import BaseRoutes from './BaseRoutes.js';
import SessionRoutes from './SessionRoutes'; import SessionRoutes from './SessionRoutes.js';
import AssignmentRoutes from './AssignmentRoutes'; import AssignmentRoutes from './AssignmentRoutes.js';
import GitlabRoutes from './GitlabRoutes'; import GitlabRoutes from './GitlabRoutes.js';
import ExerciseRoutes from './ExerciseRoutes'; import ExerciseRoutes from './ExerciseRoutes.js';
import TagsRoutes from './TagRoutes';
import UserRoutes from './UserRoutes';
import SonarRoutes from './SonarRoutes';
class AdminRoutesManager implements RoutesManager { class AdminRoutesManager implements RoutesManager {
...@@ -14,6 +17,9 @@ class AdminRoutesManager implements RoutesManager { ...@@ -14,6 +17,9 @@ class AdminRoutesManager implements RoutesManager {
GitlabRoutes.registerOnBackend(backend); GitlabRoutes.registerOnBackend(backend);
AssignmentRoutes.registerOnBackend(backend); AssignmentRoutes.registerOnBackend(backend);
ExerciseRoutes.registerOnBackend(backend); ExerciseRoutes.registerOnBackend(backend);
TagsRoutes.registerOnBackend(backend);
UserRoutes.registerOnBackend(backend);
SonarRoutes.registerOnBackend(backend);
} }
} }
......
import { Express } from 'express-serve-static-core'; import { Express } from 'express-serve-static-core';
import express from 'express'; import express, { RequestHandler } from 'express';
import * as ExpressValidator from 'express-validator'; import * as ExpressValidator from 'express-validator';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import RoutesManager from '../express/RoutesManager'; import RoutesManager from '../express/RoutesManager.js';
import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware'; import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware.js';
import SecurityMiddleware from '../middlewares/SecurityMiddleware'; import SecurityMiddleware from '../middlewares/SecurityMiddleware.js';
import SecurityCheckType from '../types/SecurityCheckType'; import SecurityCheckType from '../types/SecurityCheckType.js';
import GitlabUser from '../shared/types/Gitlab/GitlabUser'; import GitlabManager from '../managers/GitlabManager.js';
import GitlabManager from '../managers/GitlabManager'; import Config from '../config/Config.js';
import Config from '../config/Config'; import logger from '../shared/logging/WinstonLogger.js';
import GitlabMember from '../shared/types/Gitlab/GitlabMember'; import DojoValidators from '../helpers/DojoValidators.js';
import GitlabAccessLevel from '../shared/types/Gitlab/GitlabAccessLevel'; import { Language, Prisma } from '@prisma/client';
import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; import db from '../helpers/DatabaseHelper.js';
import { AxiosError, HttpStatusCode } from 'axios'; import { Assignment, Exercise } from '../types/DatabaseTypes.js';
import logger from '../shared/logging/WinstonLogger'; import AssignmentManager from '../managers/AssignmentManager.js';
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 fs from 'fs';
import path from 'path'; import path from 'path';
import SharedAssignmentHelper from '../shared/helpers/Dojo/SharedAssignmentHelper'; import SharedAssignmentHelper from '../shared/helpers/Dojo/SharedAssignmentHelper.js';
import GlobalHelper from '../helpers/GlobalHelper'; import GlobalHelper from '../helpers/GlobalHelper.js';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import DojoModelsHelper from '../helpers/DojoModelsHelper'; import DojoModelsHelper from '../helpers/DojoModelsHelper.js';
import * as Gitlab from '@gitbeaker/rest';
import { GitbeakerRequestError } from '@gitbeaker/requester-utils';
import SharedConfig from '../shared/config/SharedConfig.js';
import SharedSonarManager from '../shared/managers/SharedSonarManager';
import SonarProjectCreation from '../shared/types/Sonar/SonarProjectCreation';
import SonarManager from '../managers/SonarManager';
import { v4 as uuidv4 } from 'uuid';
class AssignmentRoutes implements RoutesManager { class AssignmentRoutes implements RoutesManager {
...@@ -43,6 +44,21 @@ class AssignmentRoutes implements RoutesManager { ...@@ -43,6 +44,21 @@ class AssignmentRoutes implements RoutesManager {
trim : true, trim : true,
custom : DojoValidators.templateUrlValidator, custom : DojoValidators.templateUrlValidator,
customSanitizer: DojoValidators.templateUrlSanitizer customSanitizer: DojoValidators.templateUrlSanitizer
},
useSonar: {
trim : true,
notEmpty : true,
isBoolean: true
},
allowSonarFailure: {
trim : true,
notEmpty : false,
isBoolean: true
},
language: {
trim : true,
notEmpty: true,
custom : DojoValidators.supportedLanguageValidator
} }
}; };
...@@ -51,25 +67,48 @@ class AssignmentRoutes implements RoutesManager { ...@@ -51,25 +67,48 @@ class AssignmentRoutes implements RoutesManager {
trim : true, trim : true,
notEmpty: true, notEmpty: true,
custom : DojoValidators.exerciseIdOrUrlValidator custom : DojoValidators.exerciseIdOrUrlValidator
},
commit : {
trim : true,
notEmpty: false
},
description : {
trim : true,
notEmpty: false
}
};
private readonly assignmentUpdateCorrigeValidator: ExpressValidator.Schema = {
commit : {
trim : true,
notEmpty: false
},
description: {
trim : true,
notEmpty: false
} }
}; };
registerOnBackend(backend: Express) { registerOnBackend(backend: Express) {
backend.get('/assignments/:assignmentNameOrUrl', SecurityMiddleware.check(true), this.getAssignment.bind(this)); backend.get('/assignments/:assignmentNameOrUrl', SecurityMiddleware.check(false, SecurityCheckType.ASSIGNMENT_SECRET, SecurityCheckType.USER), this.getAssignment.bind(this) as RequestHandler);
backend.post('/assignments', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), ParamsValidatorMiddleware.validate(this.assignmentValidator), this.createAssignment.bind(this)); backend.post('/assignments', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), ParamsValidatorMiddleware.validate(this.assignmentValidator), this.createAssignment.bind(this) as RequestHandler);
backend.get('/assignments/languages', this.getLanguages.bind(this) as RequestHandler);
backend.patch('/assignments/:assignmentNameOrUrl/publish', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.changeAssignmentPublishedStatus(true).bind(this)); backend.patch('/assignments/:assignmentNameOrUrl/publish', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.changeAssignmentPublishedStatus(true).bind(this) as RequestHandler);
backend.patch('/assignments/:assignmentNameOrUrl/unpublish', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.changeAssignmentPublishedStatus(false).bind(this)); backend.patch('/assignments/:assignmentNameOrUrl/unpublish', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.changeAssignmentPublishedStatus(false).bind(this) as RequestHandler);
backend.post('/assignments/:assignmentNameOrUrl/corrections', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), ParamsValidatorMiddleware.validate(this.assignmentAddCorrigeValidator), this.linkUpdateAssignmentCorrection(false).bind(this)); backend.post('/assignments/:assignmentNameOrUrl/corrections', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), ParamsValidatorMiddleware.validate(this.assignmentAddCorrigeValidator), this.linkUpdateAssignmentCorrection(false).bind(this) as RequestHandler);
backend.patch('/assignments/:assignmentNameOrUrl/corrections/:exerciseIdOrUrl', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.linkUpdateAssignmentCorrection(true).bind(this)); backend.patch('/assignments/:assignmentNameOrUrl/corrections/:exerciseIdOrUrl', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), ParamsValidatorMiddleware.validate(this.assignmentUpdateCorrigeValidator), this.linkUpdateAssignmentCorrection(true).bind(this) as RequestHandler);
backend.delete('/assignments/:assignmentNameOrUrl/corrections/:exerciseIdOrUrl', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.unlinkAssignmentCorrection.bind(this) as RequestHandler);
} }
// Get an assignment by its name or gitlab url // Get an assignment by its name or gitlab url
private async getAssignment(req: express.Request, res: express.Response) { private async getAssignment(req: express.Request, res: express.Response) {
const assignment: Partial<Assignment> | undefined = req.boundParams.assignment; const assignment: Partial<Assignment> | undefined = req.boundParams.assignment;
if ( assignment && !assignment.published && !await AssignmentManager.isUserAllowedToAccessAssignment(assignment as Assignment, req.session.profile) ) { if ( assignment ) {
if ( !assignment.published && !await AssignmentManager.isUserAllowedToAccessAssignment(assignment as Assignment, req.session.profile) ) {
delete assignment.gitlabId; delete assignment.gitlabId;
delete assignment.gitlabLink; delete assignment.gitlabLink;
delete assignment.gitlabCreationInfo; delete assignment.gitlabCreationInfo;
...@@ -79,74 +118,122 @@ class AssignmentRoutes implements RoutesManager { ...@@ -79,74 +118,122 @@ class AssignmentRoutes implements RoutesManager {
delete assignment.exercises; delete assignment.exercises;
} }
return assignment ? req.session.sendResponse(res, StatusCodes.OK, DojoModelsHelper.getFullSerializableObject(assignment)) : res.status(StatusCodes.NOT_FOUND).send(); const getExercises = req.query.getMyExercises;
let exercises: Array<Omit<Exercise, 'assignment'>> = [];
if ( getExercises ) {
exercises = await db.exercise.findMany({
where : {
assignmentName: assignment.name,
members : {
some: {
id: req.session.profile.id
}
},
deleted : false
},
include: {
assignment: false,
members : true,
results : true,
tags : true
}
});
}
return req.session.sendResponse(res, StatusCodes.OK, DojoModelsHelper.getFullSerializableObject(Object.assign(assignment, { myExercises: exercises })));
} else {
return res.status(StatusCodes.NOT_FOUND).send();
}
} }
private async createAssignment(req: express.Request, res: express.Response) { private async createAssignment(req: express.Request, res: express.Response) {
const params: { const params: {
name: string, members: Array<GitlabUser>, template: string name: string, members: Array<Gitlab.UserSchema>, template: string, useSonar: string, sonarGate: string, sonarProfiles: string, language: string, allowSonarFailure: string | undefined
} = req.body; } = req.body;
const useSonar = params.useSonar === 'true';
const allowSonarFailure = params.allowSonarFailure === 'true';
params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ]; params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ];
params.members = params.members.removeObjectDuplicates(gitlabUser => gitlabUser.id); params.members = params.members.removeObjectDuplicates(gitlabUser => gitlabUser.id);
if ( useSonar && !(await SharedSonarManager.isSonarSupported()) ) {
return req.session.sendResponse(res, StatusCodes.UNPROCESSABLE_ENTITY, {}, `Sonar integration is not supported`, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_ERROR);
}
let repository: Gitlab.ProjectSchema;
const secret: string = uuidv4();
let repository: GitlabRepository;
try { 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); 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 ) { } catch ( error ) {
logger.error('Repo creation error'); logger.error('Repo creation error');
logger.error(error); logger.error(JSON.stringify(error));
if ( error instanceof AxiosError ) { if ( error instanceof GitbeakerRequestError ) {
if ( error.response?.data.message.name && error.response.data.message.name == 'has already been taken' ) { if ( error.cause?.description ) {
return res.status(StatusCodes.CONFLICT).send(); 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 res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send(); req.session.sendResponse(res, error.cause?.response.status ?? StatusCodes.INTERNAL_SERVER_ERROR);
return;
} }
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(); req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR);
return;
} }
await new Promise(resolve => setTimeout(resolve, Config.gitlab.repository.timeoutAfterCreation)); await new Promise(resolve => setTimeout(resolve, Config.gitlab.repository.timeoutAfterCreation));
try { const repoCreationFnExec = GlobalHelper.repoCreationFnExecCreator(req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository);
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 */ }
try { 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');
} catch ( error ) { await repoCreationFnExec(() => GitlabManager.addRepositoryVariable(repository.id, 'DOJO_ASSIGNMENT_NAME', repository.name, false, false), 'Add repo variable "DOJO_ASSIGNMENT_NAME" error');
return GlobalHelper.repositoryCreationError('CI file error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository); await repoCreationFnExec(() => GitlabManager.addRepositoryVariable(repository.id, 'DOJO_ASSIGNMENT_SECRET', secret, false, true), 'Add repo variable "DOJO_ASSIGNMENT_SECRET" 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', Buffer.from(fs.readFileSync(path.join(__dirname, '../../assets/assignment_gitlab_ci.yml'), 'utf8').replace('{{DOCKERHUB_REPO_ASSIGNMENT_CHECKER}}', Config.dockerhub.repositories.assignmentChecker)).toString('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');
// Create Sonar project
let sonarProject: SonarProjectCreation | undefined = undefined;
if ( useSonar ) {
const profiles: string[] = JSON.parse(params.sonarProfiles);
sonarProject = await GlobalHelper.repoCreationFnExecCreator(req, res, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository)(() => SonarManager.createProjectWithQualities(repository, params.sonarGate, profiles, req, res), 'Sonar project creation error') as SonarProjectCreation;
if ( sonarProject == undefined ) {
return;
} }
try { await GlobalHelper.repoCreationFnExecCreator(req, res, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository, sonarProject)(async () => {
await Promise.all(params.members.map(member => member.id).map(async (memberId: number): Promise<GitlabMember | false> => { if ( !(await Promise.all(params.members.map(member => SonarManager.addGitlabUserToProject(member.username, sonarProject!.project.key, true)))).every(Boolean) ) {
try { throw new Error();
return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.DEVELOPER); }
} catch ( error ) { }, 'Sonar add member error');
logger.error('Add member error');
logger.error(error);
return false;
} }
}));
const assignment: Assignment = await db.assignment.create({ const assignment: Assignment = await repoCreationFnExec(() => db.assignment.create({
data: { data: {
name : repository.name, name : repository.name,
gitlabId : repository.id, gitlabId : repository.id,
gitlabLink : repository.web_url, gitlabLink : repository.web_url,
gitlabCreationInfo: repository as unknown as Prisma.JsonObject, gitlabCreationInfo: repository as unknown as Prisma.JsonObject,
gitlabCreationDate: new Date(),
gitlabLastInfo : repository as unknown as Prisma.JsonObject, gitlabLastInfo : repository as unknown as Prisma.JsonObject,
gitlabLastInfoDate: new Date(), gitlabLastInfoDate: new Date(),
useSonar : useSonar,
allowSonarFailure : allowSonarFailure,
sonarKey : sonarProject?.project.key,
sonarCreationInfo : sonarProject?.project,
sonarGate : params.sonarGate,
sonarProfiles : params.sonarProfiles,
language : Language[params.language as keyof typeof Language],
secret : secret,
staff : { staff : {
connectOrCreate: [ ...params.members.map(gitlabUser => { connectOrCreate: [ ...params.members.map(gitlabUser => {
return { return {
...@@ -161,11 +248,12 @@ class AssignmentRoutes implements RoutesManager { ...@@ -161,11 +248,12 @@ class AssignmentRoutes implements RoutesManager {
}) ] }) ]
} }
} }
}) as unknown as Assignment; }), 'Database error') as Assignment;
return req.session.sendResponse(res, StatusCodes.OK, assignment); req.session.sendResponse(res, StatusCodes.OK, assignment);
} catch ( error ) { } catch ( error ) {
return GlobalHelper.repositoryCreationError('DB error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository); /* Empty */
} }
} }
...@@ -174,12 +262,13 @@ class AssignmentRoutes implements RoutesManager { ...@@ -174,12 +262,13 @@ class AssignmentRoutes implements RoutesManager {
if ( publish ) { if ( publish ) {
const isPublishable = await SharedAssignmentHelper.isPublishable(req.boundParams.assignment!.gitlabId); const isPublishable = await SharedAssignmentHelper.isPublishable(req.boundParams.assignment!.gitlabId);
if ( !isPublishable.isPublishable ) { if ( !isPublishable.isPublishable ) {
return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, { lastPipeline: isPublishable.lastPipeline }, isPublishable.status?.message, isPublishable.status?.code); req.session.sendResponse(res, StatusCodes.BAD_REQUEST, { lastPipeline: isPublishable.lastPipeline }, isPublishable.status?.message, isPublishable.status?.code);
return;
} }
} }
try { 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({ await db.assignment.update({
where: { where: {
...@@ -192,20 +281,21 @@ class AssignmentRoutes implements RoutesManager { ...@@ -192,20 +281,21 @@ class AssignmentRoutes implements RoutesManager {
req.session.sendResponse(res, StatusCodes.OK); req.session.sendResponse(res, StatusCodes.OK);
} catch ( error ) { } catch ( error ) {
if ( error instanceof AxiosError ) { logger.error(JSON.stringify(error));
res.status(error.response?.status ?? HttpStatusCode.InternalServerError).send();
if ( error instanceof GitbeakerRequestError ) {
req.session.sendResponse(res, error.cause?.response.status ?? StatusCodes.INTERNAL_SERVER_ERROR, undefined, 'Error while updating the assignment state');
return; return;
} }
logger.error(error); req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, undefined, 'Error while updating the assignment state');
res.status(StatusCodes.INTERNAL_SERVER_ERROR).send();
} }
}; };
} }
private linkUpdateAssignmentCorrection(isUpdate: boolean): (req: express.Request, res: express.Response) => Promise<void> { private linkUpdateAssignmentCorrection(isUpdate: boolean): (req: express.Request, res: express.Response) => Promise<void> {
return async (req: express.Request, res: express.Response): Promise<void> => { return async (req: express.Request, res: express.Response): Promise<void> => {
if ( req.boundParams.exercise?.assignmentName != req.boundParams.assignment?.name ) { if ( req.boundParams.exercise?.assignmentName !== req.boundParams.assignment?.name ) {
return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'The exercise does not belong to the assignment', DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED); return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'The exercise does not belong to the assignment', DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED);
} }
...@@ -219,27 +309,60 @@ class AssignmentRoutes implements RoutesManager { ...@@ -219,27 +309,60 @@ class AssignmentRoutes implements RoutesManager {
return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'This exercise is not a correction', DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST); return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'This exercise is not a correction', DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST);
} }
const lastCommit = await GitlabManager.getRepositoryLastCommit(req.boundParams.exercise!.gitlabId); const commit: Gitlab.CommitSchema | undefined = req.body.commit ? await GitlabManager.getRepositoryCommit(req.boundParams.exercise!.gitlabId, req.body.commit as string) : await GitlabManager.getRepositoryLastCommit(req.boundParams.exercise!.gitlabId);
if ( lastCommit ) {
if ( !isUpdate ) { if ( commit ) {
await GitlabManager.changeRepositoryVisibility(req.boundParams.exercise!.gitlabId, GitlabVisibility.INTERNAL); if ( !isUpdate && SharedConfig.production ) { //Disable in dev env because gitlab dev group is private and we can't change visibility of sub projects
await GitlabManager.changeRepositoryVisibility(req.boundParams.exercise!.gitlabId, 'internal');
} }
await db.exercise.update({ await db.exercise.update({
where: { where: {
id: req.boundParams.exercise!.id id: req.boundParams.exercise!.id
}, },
data : { data : Object.assign({
correctionCommit: lastCommit correctionCommit: commit
} }, isUpdate && req.body.description === undefined ? {} : {
correctionDescription: req.body.description
})
}); });
return req.session.sendResponse(res, StatusCodes.OK); return req.session.sendResponse(res, StatusCodes.OK);
} else { } else {
return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, undefined, 'No last commit found'); return req.session.sendResponse(res, StatusCodes.NOT_FOUND, undefined, 'Commit not found');
} }
}; };
} }
private async unlinkAssignmentCorrection(req: express.Request, res: express.Response) {
if ( req.boundParams.exercise?.assignmentName !== req.boundParams.assignment?.name ) {
return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'The exercise does not belong to the assignment', DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED);
}
if ( !req.boundParams.exercise?.isCorrection ) {
return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, undefined, 'This exercise is not a correction', DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST);
}
if ( SharedConfig.production ) { //Disable in dev env because gitlab dev group is private and we can't change visibility of sub projects
await GitlabManager.changeRepositoryVisibility(req.boundParams.exercise.gitlabId, 'private');
}
await db.exercise.update({
where: {
id: req.boundParams.exercise.id
},
data : {
correctionCommit : Prisma.DbNull,
correctionDescription: null
}
});
return req.session.sendResponse(res, StatusCodes.OK);
}
private async getLanguages(req: express.Request, res: express.Response) {
req.session.sendResponse(res, StatusCodes.OK, Object.values(Language));
}
} }
......
import { Express } from 'express-serve-static-core'; import { Express } from 'express-serve-static-core';
import express from 'express'; import express, { RequestHandler } from 'express';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import RoutesManager from '../express/RoutesManager'; import RoutesManager from '../express/RoutesManager.js';
import Config from '../config/Config';
import SharedConfig from '../shared/config/SharedConfig';
import GlobalHelper from '../helpers/GlobalHelper';
import SharedSonarManager from '../shared/managers/SharedSonarManager';
import SonarManager from '../managers/SonarManager';
class BaseRoutes implements RoutesManager { class BaseRoutes implements RoutesManager {
registerOnBackend(backend: Express) { registerOnBackend(backend: Express) {
backend.get('/', this.homepage.bind(this)); backend.get('/', this.homepage.bind(this) as RequestHandler);
backend.get('/health_check', this.healthCheck.bind(this)); backend.get('/health_check', this.healthCheck.bind(this) as RequestHandler);
backend.get('/sonar', this.sonar.bind(this));
backend.get('/clients_config', this.clientsConfig.bind(this) as RequestHandler);
} }
private async homepage(req: express.Request, res: express.Response) { private async homepage(req: express.Request, res: express.Response) {
...@@ -17,6 +26,24 @@ class BaseRoutes implements RoutesManager { ...@@ -17,6 +26,24 @@ class BaseRoutes implements RoutesManager {
private async healthCheck(req: express.Request, res: express.Response) { private async healthCheck(req: express.Request, res: express.Response) {
return req.session.sendResponse(res, StatusCodes.OK); return req.session.sendResponse(res, StatusCodes.OK);
} }
private async clientsConfig(req: express.Request, res: express.Response) {
return req.session.sendResponse(res, StatusCodes.OK, {
gitlabUrl : Config.gitlab.url,
gitlabAccountId : Config.gitlab.account.id,
gitlabAccountUsername : Config.gitlab.account.username,
loginGitlabClientId : Config.login.gitlab.client.id,
exerciseMaxPerAssignment: Config.exercise.maxPerAssignment
});
}
private async sonar(req: express.Request, res: express.Response) {
const data = {
sonarEnabled: await SharedSonarManager.isSonarSupported(),
languages: await SonarManager.getLanguages()
};
return req.session.sendResponse(res, StatusCodes.OK, data);
}
} }
......
import { Express } from 'express-serve-static-core'; import { Express } from 'express-serve-static-core';
import express from 'express'; import express, { RequestHandler } from 'express';
import * as ExpressValidator from 'express-validator'; import * as ExpressValidator from 'express-validator';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import RoutesManager from '../express/RoutesManager'; import RoutesManager from '../express/RoutesManager.js';
import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware'; import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware.js';
import SecurityMiddleware from '../middlewares/SecurityMiddleware'; import SecurityMiddleware from '../middlewares/SecurityMiddleware.js';
import GitlabUser from '../shared/types/Gitlab/GitlabUser'; import GitlabManager from '../managers/GitlabManager.js';
import GitlabManager from '../managers/GitlabManager'; import Config from '../config/Config.js';
import Config from '../config/Config'; import logger from '../shared/logging/WinstonLogger.js';
import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; import DojoValidators from '../helpers/DojoValidators.js';
import { AxiosError } from 'axios';
import logger from '../shared/logging/WinstonLogger';
import DojoValidators from '../helpers/DojoValidators';
import { v4 as uuidv4 } from 'uuid'; 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 { Prisma } from '@prisma/client';
import { Assignment, Exercise } from '../types/DatabaseTypes'; import { Assignment, Exercise } from '../types/DatabaseTypes.js';
import db from '../helpers/DatabaseHelper'; import db from '../helpers/DatabaseHelper.js';
import SecurityCheckType from '../types/SecurityCheckType'; import SecurityCheckType from '../types/SecurityCheckType.js';
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 JSON5 from 'json5';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import AssignmentFile from '../shared/types/Dojo/AssignmentFile'; import AssignmentFile from '../shared/types/Dojo/AssignmentFile.js';
import ExerciseResultsFile from '../shared/types/Dojo/ExerciseResultsFile'; import ExerciseResultsFile from '../shared/types/Dojo/ExerciseResultsFile.js';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import GlobalHelper from '../helpers/GlobalHelper'; import GlobalHelper from '../helpers/GlobalHelper.js';
import { IFileDirStat } from '../shared/helpers/recursiveFilesStats/RecursiveFilesStats'; import { IFileDirStat } from '../shared/helpers/recursiveFilesStats/RecursiveFilesStats.js';
import ExerciseManager from '../managers/ExerciseManager'; import ExerciseManager from '../managers/ExerciseManager.js';
import * as Gitlab from '@gitbeaker/rest';
import { ProjectSchema } from '@gitbeaker/rest';
import GitlabTreeFileType from '../shared/types/Gitlab/GitlabTreeFileType.js';
import { GitbeakerRequestError } from '@gitbeaker/requester-utils';
import SonarProjectCreation from '../shared/types/Sonar/SonarProjectCreation';
import SonarManager from '../managers/SonarManager';
class ExerciseRoutes implements RoutesManager { class ExerciseRoutes implements RoutesManager {
...@@ -66,108 +64,247 @@ class ExerciseRoutes implements RoutesManager { ...@@ -66,108 +64,247 @@ class ExerciseRoutes implements RoutesManager {
archiveBase64: { archiveBase64: {
isBase64: true, isBase64: true,
notEmpty: true notEmpty: true
},
sonarGatePass: {
trim : true,
notEmpty : false,
isBoolean: true
} }
}; };
registerOnBackend(backend: Express) { registerOnBackend(backend: Express) {
backend.post('/assignments/:assignmentNameOrUrl/exercises', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_IS_PUBLISHED), ParamsValidatorMiddleware.validate(this.exerciseValidator), this.createExercise.bind(this)); backend.post('/assignments/:assignmentNameOrUrl/exercises', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_IS_PUBLISHED), ParamsValidatorMiddleware.validate(this.exerciseValidator), this.createExercise.bind(this) as RequestHandler);
backend.get('/exercises', SecurityMiddleware.check(true, SecurityCheckType.ADMIN), this.getAllExercises.bind(this) as RequestHandler);
backend.get('/exercises/:exerciseIdOrUrl/assignment', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), this.getAssignment.bind(this) as RequestHandler);
backend.get('/exercises/:exerciseIdOrUrl', SecurityMiddleware.check(false, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS, SecurityCheckType.EXERCISE_SECRET), this.getExercise.bind(this) as RequestHandler);
backend.get('/exercises/:exerciseIdOrUrl/members', SecurityMiddleware.check(true, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS), this.getExerciseMembers.bind(this) as RequestHandler);
backend.get('/exercises/:exerciseIdOrUrl/results', SecurityMiddleware.check(true, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS), this.getExerciseResults.bind(this) as RequestHandler);
backend.get('/exercises/:exerciseIdOrUrl/assignment', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), this.getAssignment.bind(this)); backend.delete('/exercises/:exerciseIdOrUrl', SecurityMiddleware.check(true, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS), this.deleteExercise.bind(this) as RequestHandler);
backend.post('/exercises/:exerciseIdOrUrl/results', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), ParamsValidatorMiddleware.validate(this.resultValidator), this.createResult.bind(this)); backend.get('/users/:userId/exercises', SecurityMiddleware.check(true), this.getUserExercises.bind(this) as RequestHandler);
backend.get('/exercises/:exerciseIdOrLink/results', SecurityMiddleware.check(true), this.getExerciseResultsByIdOrLink.bind(this) as RequestHandler);
backend.post('/exercises/:exerciseIdOrUrl/results', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), ParamsValidatorMiddleware.validate(this.resultValidator), this.createResult.bind(this) as RequestHandler);
} }
private getExerciseName(assignment: Assignment, members: Array<GitlabUser>, suffix: number): string { private async getExerciseResultsByIdOrLink(req: express.Request, res: express.Response) {
return `DojoEx - ${ assignment.name } - ${ members.map(member => member.username).sort((a, b) => a.localeCompare(b)).join(' + ') }${ suffix > 0 ? ` - ${ suffix }` : '' }`; const exerciseIdOrLink = req.params.exerciseIdOrLink;
const exercise = await db.exercise.findFirst({
where: {
OR: [ { id: exerciseIdOrLink }, { gitlabLink: exerciseIdOrLink } ]
} }
});
private getExercisePath(assignment: Assignment, exerciseId: string): string { if ( !exercise ) {
return `dojo-ex_${ (assignment.gitlabLastInfo as unknown as GitlabRepository).path }_${ exerciseId }`; return res.status(StatusCodes.NOT_FOUND).send('Exercise not found');
} }
private async createExercise(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 assignment: Assignment = req.boundParams.assignment!;
const results = await db.result.findMany({
where: { exerciseId: exercise.id }
});
return res.status(StatusCodes.OK).json(results);
}
private async getAllExercises(req: express.Request, res: express.Response) {
const exos = await db.exercise.findMany();
return req.session.sendResponse(res, StatusCodes.OK, exos);
}
private async getUserExercises(req: express.Request, res: express.Response) {
if ( req.boundParams.user ) {
if ( req.session.profile.isAdmin || req.session.profile.id === req.boundParams.user.id ) {
return req.session.sendResponse(res, StatusCodes.OK, req.boundParams.user.exercises.filter(exercise => !exercise.deleted));
} else {
return req.session.sendResponse(res, StatusCodes.FORBIDDEN);
}
} else {
return req.session.sendResponse(res, StatusCodes.NOT_FOUND);
}
}
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 async getExercise(req: express.Request, res: express.Response) {
return req.session.sendResponse(res, StatusCodes.OK, req.boundParams.exercise!);
}
private async getExerciseMembers(req: express.Request, res: express.Response) {
const repoId = req.boundParams.exercise!.gitlabId;
const members = await GitlabManager.getRepositoryMembers(String(repoId));
return req.session.sendResponse(res, StatusCodes.OK, members);
}
private async getExerciseResults(req: express.Request, res: express.Response) {
const results = await db.result.findMany({
where : { exerciseId: req.boundParams.exercise!.id },
orderBy: { dateTime: 'desc' }
});
return req.session.sendResponse(res, StatusCodes.OK, results);
}
private async deleteExercise(req: express.Request, res: express.Response) {
const repoId = req.boundParams.exercise!.gitlabId;
const members = await GitlabManager.getRepositoryMembers(String(repoId), false);
for ( const member of members ) {
if ( member.id !== Config.gitlab.account.id ) {
await GitlabManager.deleteRepositoryMember(repoId, member.id);
}
}
// We rename (with unique str added) the repository before moving it because of potential name conflicts
const newName: string = `${ req.boundParams.exercise!.name }_${ uuidv4() }`;
await GitlabManager.renameRepository(repoId, newName);
const repository: ProjectSchema = await GitlabManager.moveRepository(repoId, Config.gitlab.group.deletedExercises);
await db.exercise.update({
where: { id: req.boundParams.exercise!.id },
data : {
name : newName,
gitlabLastInfo : repository as unknown as Prisma.JsonObject,
gitlabLastInfoDate: new Date(),
deleted : true
}
});
const exercises: Array<Exercise> | undefined = await ExerciseManager.getFromAssignment(assignment.name, { members: true }); return req.session.sendResponse(res, StatusCodes.OK);
const reachedLimitUsers: Array<GitlabUser> = []; }
if ( exercises ) {
for ( const member of params.members ) { private getExercisePath(assignment: Assignment, exerciseId: string): string {
return `dojo-ex_${ (assignment.gitlabLastInfo as unknown as Gitlab.ProjectSchema).path }_${ exerciseId }`;
}
private async checkExerciseLimit(assignment: Assignment, members: Array<Gitlab.UserSchema>): Promise<Array<Gitlab.UserSchema>> {
const exercises: Array<Exercise> | undefined = await ExerciseManager.getFromAssignment(assignment.name, false, { 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; const exerciseCount: number = exercises.filter(exercise => exercise.members.findIndex(exerciseMember => exerciseMember.id === member.id) !== -1).length;
if ( exerciseCount >= Config.exercise.maxPerAssignment ) { if ( exerciseCount >= Config.exercise.maxPerAssignment ) {
reachedLimitUsers.push(member); reachedLimitUsers.push(member);
} }
} }
} }
if ( reachedLimitUsers.length > 0 ) {
return req.session.sendResponse(res, StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE, reachedLimitUsers, 'Max exercise per assignment reached', DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED);
}
return reachedLimitUsers;
}
const exerciseId: string = uuidv4(); private async createExerciseRepository(assignment: Assignment, members: Array<Gitlab.UserSchema>, exerciseId: string, req: express.Request, res: express.Response): Promise<Gitlab.ProjectSchema | undefined> {
const secret: string = uuidv4(); let repository!: Gitlab.ProjectSchema;
let repository!: GitlabRepository;
let suffix: number = 0; let suffix: number = 0;
do { do {
try { try {
repository = await GitlabManager.forkRepository((assignment.gitlabCreationInfo as unknown as GitlabRepository).id, this.getExerciseName(assignment, params.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; break;
} catch ( error ) { } catch ( error ) {
logger.error('Repo creation error'); logger.error('Repo creation error');
logger.error(error); logger.error(JSON.stringify(error));
if ( error instanceof AxiosError ) { if ( error instanceof GitbeakerRequestError && error.cause?.description ) {
if ( error.response?.data.message.name && error.response.data.message.name == 'has already been taken' ) { const description = error.cause.description as unknown;
if ( GlobalHelper.isRepoNameAlreadyTaken(description) ) {
suffix++; suffix++;
} else { } else {
return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown gitlab error while forking repository', DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR); req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown gitlab error while forking repository', DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR);
return undefined;
} }
} else { } else {
return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown error while forking repository', DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR); req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, 'Unknown error while forking repository', DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR);
return undefined;
} }
} }
} while ( suffix < Config.exercise.maxSameName ); } while ( suffix < Config.exercise.maxSameName );
if ( suffix >= Config.exercise.maxSameName ) { return repository;
logger.error('Max exercise with same name reached'); }
return res.status(StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE).send();
private async createExercise(req: express.Request, res: express.Response) {
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<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;
}
const exerciseId: string = uuidv4();
const secret: string = uuidv4();
const repository: Gitlab.ProjectSchema | undefined = await this.createExerciseRepository(assignment, params.members, exerciseId, req, res);
let sonarProject: SonarProjectCreation | undefined = undefined;
if ( !repository ) {
return;
} }
await new Promise(resolve => setTimeout(resolve, Config.gitlab.repository.timeoutAfterCreation)); await new Promise(resolve => setTimeout(resolve, Config.gitlab.repository.timeoutAfterCreation));
const repoCreationFnExec = GlobalHelper.repoCreationFnExecCreator(req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository);
try { try {
await GitlabManager.protectBranch(repository.id, '*', false, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.DEVELOPER, GitlabAccessLevel.OWNER); 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_EXERCISE_ID', exerciseId, false, true);
await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, 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.addRepositoryVariable(repository.id, 'DOJO_RESULTS_FOLDER', Config.exercise.pipelineResultsFolder, false, false);
}, 'Pipeline variables addition error');
await GitlabManager.addRepositoryBadge(repository.id, Config.gitlab.badges.pipeline.link, Config.gitlab.badges.pipeline.imageUrl, 'Pipeline Status'); await repoCreationFnExec(() => GitlabManager.updateFile(repository.id, '.gitlab-ci.yml', Buffer.from(fs.readFileSync(path.join(__dirname, '../../assets/exercise_gitlab_ci.yml'), 'utf8').replace('{{DOCKERHUB_REPO_EXERCISE_CHECKER}}', Config.dockerhub.repositories.exerciseChecker)).toString('base64'), 'Add .gitlab-ci.yml (DO NOT MODIFY THIS FILE)'), 'CI/CD file update error');
} catch ( error ) {
return GlobalHelper.repositoryCreationError('Repo params error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository);
}
try { await repoCreationFnExec(async () => Promise.all([ ...new Set([ ...assignment.staff, ...params.members ].map(member => member.id)) ].map(GlobalHelper.addRepoMember(repository.id))), 'Add repository members error');
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 ) { // Create Sonar project
return GlobalHelper.repositoryCreationError('CI file update error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); if ( assignment.useSonar && assignment.sonarProfiles != null ) {
const profiles: string[] = JSON.parse(assignment.sonarProfiles as string);
sonarProject = await GlobalHelper.repoCreationFnExecCreator(req, res, DojoStatusCode.EXERCISE_CREATION_SONAR_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository)(() => SonarManager.createProjectWithQualities(repository, assignment.sonarGate, profiles, req, res), 'Sonar project creation error') as SonarProjectCreation;
if ( sonarProject == undefined ) {
return;
} }
try { 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> => { for ( const u of assignment.staff ) {
try { const success = await SonarManager.addGitlabUserToProject(u.gitlabUsername, sonarProject.project.key, true);
return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.DEVELOPER); if (!success) {
return GlobalHelper.repositoryCreationError('Sonar add member error', undefined, req, res, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_MEMBER, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_MEMBER, repository, sonarProject);
}
}
for ( const u of params.members ) {
const success = await SonarManager.addGitlabUserToProject(u.username, sonarProject.project.key, false);
if (!success) {
return GlobalHelper.repositoryCreationError('Sonar add member error', undefined, req, res, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_MEMBER, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_MEMBER, repository, sonarProject);
}
}
} catch ( error ) { } catch ( error ) {
logger.error('Add member error'); logger.error('Sonar add member error');
logger.error(error); logger.error(error);
return false; return GlobalHelper.repositoryCreationError('Sonar add member error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_SONAR_ERROR, repository, sonarProject);
} }
})); }
const exercise: Exercise = await db.exercise.create({ let exercise: Exercise = await repoCreationFnExec(() => db.exercise.create({
data: { data: {
id : exerciseId, id : exerciseId,
assignmentName : assignment.name, assignmentName : assignment.name,
...@@ -176,8 +313,11 @@ class ExerciseRoutes implements RoutesManager { ...@@ -176,8 +313,11 @@ class ExerciseRoutes implements RoutesManager {
gitlabId : repository.id, gitlabId : repository.id,
gitlabLink : repository.web_url, gitlabLink : repository.web_url,
gitlabCreationInfo: repository as unknown as Prisma.JsonObject, gitlabCreationInfo: repository as unknown as Prisma.JsonObject,
gitlabCreationDate: new Date(),
gitlabLastInfo : repository as unknown as Prisma.JsonObject, gitlabLastInfo : repository as unknown as Prisma.JsonObject,
gitlabLastInfoDate: new Date(), gitlabLastInfoDate: new Date(),
sonarKey : sonarProject?.project.key,
sonarCreationInfo : sonarProject?.project,
members : { members : {
connectOrCreate: [ ...params.members.map(gitlabUser => { connectOrCreate: [ ...params.members.map(gitlabUser => {
return { return {
...@@ -192,19 +332,29 @@ class ExerciseRoutes implements RoutesManager { ...@@ -192,19 +332,29 @@ class ExerciseRoutes implements RoutesManager {
}) ] }) ]
} }
} }
}) as unknown as Exercise; })) as Exercise;
return req.session.sendResponse(res, StatusCodes.OK, exercise); exercise = await ExerciseManager.get(exercise.id, {
members : true,
assignment: {
include: {
staff: true
}
}
}) as Exercise;
req.session.sendResponse(res, StatusCodes.OK, exercise);
return;
} catch ( error ) { } catch ( error ) {
return GlobalHelper.repositoryCreationError('DB error', error, req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); /* Empty */
} }
} }
private async getAssignment(req: express.Request, res: express.Response) { 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; let assignmentHjsonFile!: Gitlab.RepositoryFileExpandedSchema;
const immutableFiles: Array<GitlabFile> = await Promise.all(Config.assignment.baseFiles.map(async (baseFile: string) => { 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); const file = await GitlabManager.getFile(req.boundParams.exercise!.assignment.gitlabId, baseFile);
if ( baseFile === Config.assignment.filename ) { if ( baseFile === Config.assignment.filename ) {
...@@ -214,12 +364,12 @@ class ExerciseRoutes implements RoutesManager { ...@@ -214,12 +364,12 @@ class ExerciseRoutes implements RoutesManager {
return file; return file;
})); }));
const dojoAssignmentFile: AssignmentFile = JSON5.parse(atob(assignmentHjsonFile.content)) as AssignmentFile; const dojoAssignmentFile: AssignmentFile = JSON5.parse(atob(assignmentHjsonFile.content));
const immutablePaths = dojoAssignmentFile.immutable.map(fileDescriptor => fileDescriptor.path); const immutablePaths = dojoAssignmentFile.immutable.map(fileDescriptor => fileDescriptor.path);
await Promise.all(repoTree.map(async gitlabTreeFile => { await Promise.all(repoTree.map(async gitlabTreeFile => {
if ( gitlabTreeFile.type == GitlabTreeFileType.BLOB ) { if ( gitlabTreeFile.type === GitlabTreeFileType.BLOB.valueOf() ) {
for ( const immutablePath of immutablePaths ) { for ( const immutablePath of immutablePaths ) {
if ( gitlabTreeFile.path.startsWith(immutablePath) ) { if ( gitlabTreeFile.path.startsWith(immutablePath) ) {
immutableFiles.push(await GitlabManager.getFile(req.boundParams.exercise!.assignment.gitlabId, gitlabTreeFile.path)); immutableFiles.push(await GitlabManager.getFile(req.boundParams.exercise!.assignment.gitlabId, gitlabTreeFile.path));
...@@ -237,14 +387,16 @@ class ExerciseRoutes implements RoutesManager { ...@@ -237,14 +387,16 @@ class ExerciseRoutes implements RoutesManager {
} }
private async createResult(req: express.Request, res: express.Response) { private async createResult(req: express.Request, res: express.Response) {
const params: { exitCode: number, commit: Record<string, string>, results: ExerciseResultsFile, files: Array<IFileDirStat>, archiveBase64: string } = req.body; const params: { exitCode: number, commit: Record<string, string>, results: ExerciseResultsFile, files: Array<IFileDirStat>, archiveBase64: string, sonarGatePass: string | undefined } = req.body;
const exercise: Exercise = req.boundParams.exercise!; const exercise: Exercise = req.boundParams.exercise!;
const sonarGate = params.sonarGatePass === "true";
const result = await db.result.create({ const result = await db.result.create({
data: { data: {
exerciseId : exercise.id, exerciseId : exercise.id,
exitCode : params.exitCode, exitCode : params.exitCode,
success : params.results.success!, success : params.results.success!,
sonarGatePass: sonarGate,
commit : params.commit, commit : params.commit,
results : params.results as unknown as Prisma.JsonObject, results : params.results as unknown as Prisma.JsonObject,
files : params.files files : params.files
...@@ -259,3 +411,5 @@ class ExerciseRoutes implements RoutesManager { ...@@ -259,3 +411,5 @@ class ExerciseRoutes implements RoutesManager {
export default new ExerciseRoutes(); export default new ExerciseRoutes();
import { Express } from 'express-serve-static-core'; import { Express } from 'express-serve-static-core';
import express from 'express'; import express, { RequestHandler } from 'express';
import RoutesManager from '../express/RoutesManager'; import RoutesManager from '../express/RoutesManager.js';
import SecurityMiddleware from '../middlewares/SecurityMiddleware'; import SecurityMiddleware from '../middlewares/SecurityMiddleware.js';
import SecurityCheckType from '../types/SecurityCheckType'; import SecurityCheckType from '../types/SecurityCheckType.js';
import GitlabManager from '../managers/GitlabManager'; import GitlabManager from '../managers/GitlabManager.js';
class GitlabRoutes implements RoutesManager { class GitlabRoutes implements RoutesManager {
registerOnBackend(backend: Express) { registerOnBackend(backend: Express) {
backend.get('/gitlab/project/:gitlabProjectIdOrNamespace/checkTemplateAccess', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), this.checkTemplateAccess.bind(this)); backend.get('/gitlab/project/:gitlabProjectIdOrNamespace/checkTemplateAccess', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), this.checkTemplateAccess.bind(this) as RequestHandler);
} }
private async checkTemplateAccess(req: express.Request, res: express.Response) { private async checkTemplateAccess(req: express.Request, res: express.Response) {
const gitlabProjectIdOrNamespace: string = req.params.gitlabProjectIdOrNamespace; const gitlabProjectIdOrNamespace: string = req.params.gitlabProjectIdOrNamespace;
return res.status(await GitlabManager.checkTemplateAccess(gitlabProjectIdOrNamespace, req)).send(); await GitlabManager.checkTemplateAccess(gitlabProjectIdOrNamespace, req, res);
} }
} }
......
import { Express } from 'express-serve-static-core'; import { Express } from 'express-serve-static-core';
import express from 'express'; import express, { RequestHandler } from 'express';
import * as ExpressValidator from 'express-validator'; import * as ExpressValidator from 'express-validator';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import RoutesManager from '../express/RoutesManager'; import RoutesManager from '../express/RoutesManager.js';
import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware'; import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware.js';
import SecurityMiddleware from '../middlewares/SecurityMiddleware'; import SecurityMiddleware from '../middlewares/SecurityMiddleware.js';
import GitlabManager from '../managers/GitlabManager'; import GitlabManager from '../managers/GitlabManager.js';
import UserManager from '../managers/UserManager'; import UserManager from '../managers/UserManager.js';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import SharedGitlabManager from '../shared/managers/SharedGitlabManager'; import Config from '../config/Config.js';
import Config from '../config/Config';
class SessionRoutes implements RoutesManager { class SessionRoutes implements RoutesManager {
...@@ -32,9 +31,9 @@ class SessionRoutes implements RoutesManager { ...@@ -32,9 +31,9 @@ class SessionRoutes implements RoutesManager {
}; };
registerOnBackend(backend: Express) { registerOnBackend(backend: Express) {
backend.post('/login', ParamsValidatorMiddleware.validate(this.loginValidator), this.login.bind(this)); backend.post('/login', ParamsValidatorMiddleware.validate(this.loginValidator), this.login.bind(this) as RequestHandler);
backend.post('/refresh_tokens', ParamsValidatorMiddleware.validate(this.refreshTokensValidator), this.refreshTokens.bind(this)); backend.post('/refresh_tokens', ParamsValidatorMiddleware.validate(this.refreshTokensValidator), this.refreshTokens.bind(this) as RequestHandler);
backend.get('/test_session', SecurityMiddleware.check(true), this.testSession.bind(this)); backend.get('/test_session', SecurityMiddleware.check(true), this.testSession.bind(this) as RequestHandler);
} }
private async login(req: express.Request, res: express.Response) { private async login(req: express.Request, res: express.Response) {
...@@ -64,7 +63,7 @@ class SessionRoutes implements RoutesManager { ...@@ -64,7 +63,7 @@ class SessionRoutes implements RoutesManager {
refreshToken: string refreshToken: string
} = req.body; } = 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); req.session.sendResponse(res, StatusCodes.OK, gitlabTokens);
} catch ( error ) { } catch ( error ) {
......