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 { StatusCodes } from 'http-status-codes';
import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility.js';
import GitlabAccessLevel from '../shared/types/Gitlab/GitlabAccessLevel'; import express from 'express';
import GitlabMember from '../shared/types/Gitlab/GitlabMember'; import { CommitSchema, ExpandedUserSchema, Gitlab, MemberSchema, ProjectBadgeSchema, ProjectSchema, ReleaseSchema, RepositoryFileExpandedSchema, RepositoryFileSchema, RepositoryTreeSchema } from '@gitbeaker/rest';
import { StatusCodes } from 'http-status-codes'; import logger from '../shared/logging/WinstonLogger.js';
import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; import { AccessLevel, EditProjectOptions, ProjectVariableSchema, ProtectedBranchAccessLevel, ProtectedBranchSchema } from '@gitbeaker/core';
import GitlabUser from '../shared/types/Gitlab/GitlabUser'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import GitlabTreeFile from '../shared/types/Gitlab/GitlabTreeFile'; import SharedGitlabManager from '../shared/managers/SharedGitlabManager.js';
import parseLinkHeader from 'parse-link-header';
import GitlabFile from '../shared/types/Gitlab/GitlabFile';
import express from 'express'; class GitlabManager extends SharedGitlabManager {
import GitlabRoute from '../shared/types/Gitlab/GitlabRoute'; constructor() {
import SharedConfig from '../shared/config/SharedConfig'; super(Config.gitlab.url, Config.gitlab.account.token);
import GitlabProfile from '../shared/types/Gitlab/GitlabProfile';
import GitlabRelease from '../shared/types/Gitlab/GitlabRelease';
import { CommitSchema, Gitlab } from '@gitbeaker/rest';
import logger from '../shared/logging/WinstonLogger';
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> { getUserProfile(token: string): Promise<ExpandedUserSchema> | undefined {
try { try {
return (await axios.get<GitlabProfile>(this.getApiUrl(GitlabRoute.PROFILE_GET), { const profileApi = new Gitlab({
headers: { host : Config.gitlab.url,
DojoOverrideAuthorization: true, oauthToken: token
DojoAuthorizationHeader : 'Authorization', });
DojoAuthorizationValue : `Bearer ${ token }`
} return profileApi.Users.showCurrentUser();
})).data;
} catch ( e ) { } catch ( e ) {
logger.error(JSON.stringify(e));
return undefined; return undefined;
} }
} }
public async getUserById(id: number): Promise<GitlabUser | undefined> { async getRepositoryMembers(idOrNamespace: string, includeInherited: boolean = true): Promise<Array<MemberSchema>> {
try { try {
const user = (await axios.get<GitlabUser>(`${ this.getApiUrl(GitlabRoute.USERS_GET) }/${ String(id) }`)).data; return await this.api.ProjectMembers.all(idOrNamespace, { includeInherited: includeInherited });
return user.id === id ? user : undefined;
} catch ( e ) { } catch ( e ) {
return undefined; logger.error(JSON.stringify(e));
return Promise.reject(e);
} }
} }
public async getUserByUsername(username: string): Promise<GitlabUser | undefined> { async getRepositoryReleases(repoId: number): Promise<Array<ReleaseSchema>> {
try { try {
const params: Record<string, string> = {}; return await this.api.ProjectReleases.all(repoId);
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> {
const response = await axios.get<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', encodeURIComponent(projectIdOrNamespace)));
return response.data;
}
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> {
try { try {
const commits = await this.api.Commits.all(repoId, { const commits = await this.api.Commits.all(repoId, {
...@@ -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; return undefined;
} }
} }
async createRepository(name: string, description: string, visibility: string, initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, import_url: string): Promise<GitlabRepository> { async getRepositoryCommit(repoId: number, commitSha: string): Promise<CommitSchema | undefined> {
const response = await axios.post<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_CREATE), { try {
name : name, return await this.api.Commits.show(repoId, commitSha);
description : description, } catch ( e ) {
import_url : import_url, logger.error(JSON.stringify(e));
initialize_with_readme: initializeWithReadme, return undefined;
namespace_id : namespace, }
shared_runners_enabled: sharedRunnersEnabled, }
visibility : visibility,
wiki_enabled : wikiEnabled
});
return response.data; async createRepository(name: string, description: string, visibility: 'public' | 'internal' | 'private', initializeWithReadme: boolean, namespace: number, sharedRunnersEnabled: boolean, wikiEnabled: boolean, importUrl: string): Promise<ProjectSchema> {
try {
return await this.api.Projects.create({
name : name,
description : description,
importUrl : importUrl,
initializeWithReadme: initializeWithReadme,
namespaceId : namespace,
sharedRunnersEnabled: sharedRunnersEnabled,
visibility : visibility,
wikiAccessLevel : wikiEnabled ? 'enabled' : 'disabled'
});
} catch ( e ) {
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 {
name : name, return await this.api.Projects.fork(forkId, {
path : path, name : name,
description : description, path : path,
namespace_id: namespace, description: description,
visibility : visibility namespaceId: namespace,
}); visibility : visibility
});
return response.data; } catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async editRepository(repoId: number, newAttributes: Partial<GitlabRepository>): Promise<GitlabRepository> { async editRepository(repoId: number, newAttributes: EditProjectOptions): Promise<ProjectSchema> {
const response = await axios.put<GitlabRepository>(this.getApiUrl(GitlabRoute.REPOSITORY_EDIT).replace('{{id}}', String(repoId)), newAttributes); try {
return await this.api.Projects.edit(repoId, newAttributes);
return response.data; } catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<GitlabRepository> { async deleteRepositoryMember(repoId: number, userId: number, skipSubresources: boolean = false, unassignIssuables: boolean = false): Promise<void> {
return await this.editRepository(repoId, { visibility: visibility.toString() }); try {
return await this.api.ProjectMembers.remove(repoId, userId, {
skipSubresourceS: skipSubresources,
unassignIssuables
});
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
} }
async addRepositoryMember(repoId: number, userId: number, accessLevel: GitlabAccessLevel): Promise<GitlabMember> { async renameRepository(repoId: number, newName: string): Promise<ProjectSchema> {
const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_MEMBER_ADD).replace('{{id}}', String(repoId)), { try {
user_id : userId, return await this.api.Projects.edit(repoId, {
access_level: accessLevel name: newName
}); });
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
}
return response.data; async moveRepository(repoId: number, newRepoId: number): Promise<ProjectSchema> {
try {
return await this.api.Projects.transfer(repoId, newRepoId);
} 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> { changeRepositoryVisibility(repoId: number, visibility: GitlabVisibility): Promise<ProjectSchema> {
const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_VARIABLES_ADD).replace('{{id}}', String(repoId)), { return this.editRepository(repoId, { visibility: visibility });
key : key, }
variable_type: 'env_var',
value : value,
protected : isProtected,
masked : isMasked
});
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 addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): 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_BADGES_ADD).replace('{{id}}', String(repoId)), { try {
link_url : linkUrl, return await this.api.ProjectVariables.create(repoId, key, value, {
image_url: imageUrl, variableType: 'env_var',
name : name protected : isProtected,
}); masked : isMasked
});
} catch ( e ) {
logger.error(JSON.stringify(e));
return Promise.reject(e);
}
}
return response.data; async addRepositoryBadge(repoId: number, linkUrl: string, imageUrl: string, name: string): Promise<ProjectBadgeSchema> {
try {
return await this.api.ProjectBadges.add(repoId, linkUrl, imageUrl, {
name: name
});
} catch ( e ) {
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, ref : branch
per_page : 100,
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, } catch ( e ) {
author_email : authorMail 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.js';
import db from '../helpers/DatabaseHelper'; import { User } from '../types/DatabaseTypes.js';
import GitlabProfile from '../shared/types/Gitlab/GitlabProfile'; import * as Gitlab from '@gitbeaker/rest';
import { User } from '../types/DatabaseTypes';
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();
} }
}; };
} }
......
import { Express } from 'express-serve-static-core'; import { Express } from 'express-serve-static-core';
import express from 'express'; 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 = {
assignment: undefined, user : undefined,
exercise : undefined assignment : 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() ) { req.session.sendResponse(res, StatusCodes.BAD_REQUEST, { errors: errors.array() });
return 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 {
private checkIfConnected(checkIfConnected: boolean, req: express.Request): boolean {
return !checkIfConnected || (req.session.profile !== null && req.session.profile !== undefined);
}
private async checkType(checkType: SecurityCheckType, req: express.Request): Promise<boolean> {
try {
switch ( String(checkType) ) {
case SecurityCheckType.USER.valueOf():
return this.checkIfConnected(true, req);
case SecurityCheckType.ADMIN.valueOf():
return req.session.profile.isAdmin;
case SecurityCheckType.TEACHING_STAFF.valueOf():
return req.session.profile.isTeachingStaff;
case SecurityCheckType.EXERCISE_MEMBERS.valueOf():
return await ExerciseManager.isUserAllowedToAccessExercise(req.boundParams.exercise!, req.session.profile);
case SecurityCheckType.ASSIGNMENT_STAFF.valueOf():
return await AssignmentManager.isUserAllowedToAccessAssignment(req.boundParams.assignment!, req.session.profile);
case SecurityCheckType.ASSIGNMENT_IS_PUBLISHED.valueOf():
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:
return false;
}
} catch ( e ) {
logger.error('Security check failed !!! => ' + JSON.stringify(e));
return false;
}
}
// First check if connected then check if at least ONE rule match. It's NOT an AND but it's a OR function. // 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 { check(checkIfConnected: boolean, ...checkTypes: Array<SecurityCheckType>): (req: express.Request, res: express.Response, next: express.NextFunction) => void {
return async (req: express.Request, res: express.Response, next: express.NextFunction) => { return (req: express.Request, res: express.Response, next: express.NextFunction) => {
if ( checkIfConnected ) { if ( !this.checkIfConnected(checkIfConnected, req) ) {
if ( req.session.profile === null || req.session.profile === undefined ) { return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED);
return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED);
}
} }
let isAllowed = checkTypes.length === 0; const isAllowed: boolean = checkTypes.length === 0 ? true : checkTypes.find(async checkType => this.checkType(checkType, req)) !== undefined;
if ( !isAllowed ) {
for ( const checkType of checkTypes ) {
try {
switch ( String(checkType) ) {
case SecurityCheckType.TEACHING_STAFF:
isAllowed = isAllowed || req.session.profile.isTeachingStaff;
break;
case SecurityCheckType.ASSIGNMENT_STAFF:
isAllowed = isAllowed || await AssignmentManager.isUserAllowedToAccessAssignment(req.boundParams.assignment!, req.session.profile);
break;
case SecurityCheckType.ASSIGNMENT_IS_PUBLISHED:
isAllowed = isAllowed || (req.boundParams.assignment?.published ?? false);
break;
case SecurityCheckType.EXERCISE_SECRET:
isAllowed = isAllowed || (req.headers.exercisesecret as string | undefined) === req.boundParams.exercise!.secret;
break;
default:
break;
}
} catch ( e ) {
logger.error('Security check failed !!! => ' + e);
isAllowed = isAllowed || false;
}
}
}
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 fs from 'fs';
import { Prisma } from '@prisma/client'; import path from 'path';
import db from '../helpers/DatabaseHelper'; import SharedAssignmentHelper from '../shared/helpers/Dojo/SharedAssignmentHelper.js';
import { Assignment } from '../types/DatabaseTypes'; import GlobalHelper from '../helpers/GlobalHelper.js';
import AssignmentManager from '../managers/AssignmentManager'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility'; import DojoModelsHelper from '../helpers/DojoModelsHelper.js';
import fs from 'fs'; import * as Gitlab from '@gitbeaker/rest';
import path from 'path'; import { GitbeakerRequestError } from '@gitbeaker/requester-utils';
import SharedAssignmentHelper from '../shared/helpers/Dojo/SharedAssignmentHelper'; import SharedConfig from '../shared/config/SharedConfig.js';
import GlobalHelper from '../helpers/GlobalHelper'; import SharedSonarManager from '../shared/managers/SharedSonarManager';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; import SonarProjectCreation from '../shared/types/Sonar/SonarProjectCreation';
import DojoModelsHelper from '../helpers/DojoModelsHelper'; 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,121 +67,193 @@ class AssignmentRoutes implements RoutesManager { ...@@ -51,121 +67,193 @@ 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 ) {
delete assignment.gitlabId; if ( !assignment.published && !await AssignmentManager.isUserAllowedToAccessAssignment(assignment as Assignment, req.session.profile) ) {
delete assignment.gitlabLink; delete assignment.gitlabId;
delete assignment.gitlabCreationInfo; delete assignment.gitlabLink;
delete assignment.gitlabLastInfo; delete assignment.gitlabCreationInfo;
delete assignment.gitlabLastInfoDate; delete assignment.gitlabLastInfo;
delete assignment.staff; delete assignment.gitlabLastInfoDate;
delete assignment.exercises; delete assignment.staff;
} delete assignment.exercises;
}
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 assignment ? req.session.sendResponse(res, StatusCodes.OK, DojoModelsHelper.getFullSerializableObject(assignment)) : res.status(StatusCodes.NOT_FOUND).send(); 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 { try {
await GitlabManager.deleteFile(repository.id, '.gitlab-ci.yml', 'Remove .gitlab-ci.yml'); await repoCreationFnExec(() => GitlabManager.protectBranch(repository.id, '*', true, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.ADMIN), 'Branch protection modification error');
} catch ( error ) { /* empty */ } await repoCreationFnExec(() => GitlabManager.addRepositoryVariable(repository.id, 'DOJO_ASSIGNMENT_NAME', repository.name, false, false), 'Add repo variable "DOJO_ASSIGNMENT_NAME" error');
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 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)'); if ( !(await Promise.all(params.members.map(member => SonarManager.addGitlabUserToProject(member.username, sonarProject!.project.key, true)))).every(Boolean) ) {
} catch ( error ) { throw new Error();
return GlobalHelper.repositoryCreationError('CI file error', error, req, res, DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR, DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR, repository); }
} }, 'Sonar add member error');
}
try { const assignment: Assignment = await repoCreationFnExec(() => db.assignment.create({
await Promise.all(params.members.map(member => member.id).map(async (memberId: number): Promise<GitlabMember | false> => { data: {
try { name : repository.name,
return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.DEVELOPER); gitlabId : repository.id,
} catch ( error ) { gitlabLink : repository.web_url,
logger.error('Add member error'); gitlabCreationInfo: repository as unknown as Prisma.JsonObject,
logger.error(error); gitlabCreationDate: new Date(),
return false; gitlabLastInfo : repository as unknown as Prisma.JsonObject,
} gitlabLastInfoDate: new Date(),
})); useSonar : useSonar,
allowSonarFailure : allowSonarFailure,
const assignment: Assignment = await db.assignment.create({ sonarKey : sonarProject?.project.key,
data: { sonarCreationInfo : sonarProject?.project,
name : repository.name, sonarGate : params.sonarGate,
gitlabId : repository.id, sonarProfiles : params.sonarProfiles,
gitlabLink : repository.web_url, language : Language[params.language as keyof typeof Language],
gitlabCreationInfo: repository as unknown as Prisma.JsonObject, secret : secret,
gitlabLastInfo : repository as unknown as Prisma.JsonObject, staff : {
gitlabLastInfoDate: new Date(), connectOrCreate: [ ...params.members.map(gitlabUser => {
staff : { return {
connectOrCreate: [ ...params.members.map(gitlabUser => { create: {
return { id : gitlabUser.id,
create: { gitlabUsername: gitlabUser.name
id : gitlabUser.id, },
gitlabUsername: gitlabUser.name where : {
}, id: gitlabUser.id
where : { }
id: gitlabUser.id };
} }) ]
}; }
}) ] }
} }), 'Database error') as Assignment;
}
}) as unknown as Assignment;
req.session.sendResponse(res, StatusCodes.OK, assignment);
return 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 { v4 as uuidv4 } from 'uuid';
import logger from '../shared/logging/WinstonLogger'; import { Prisma } from '@prisma/client';
import DojoValidators from '../helpers/DojoValidators'; import { Assignment, Exercise } from '../types/DatabaseTypes.js';
import { v4 as uuidv4 } from 'uuid'; import db from '../helpers/DatabaseHelper.js';
import GitlabMember from '../shared/types/Gitlab/GitlabMember'; import SecurityCheckType from '../types/SecurityCheckType.js';
import GitlabAccessLevel from '../shared/types/Gitlab/GitlabAccessLevel'; import JSON5 from 'json5';
import { Prisma } from '@prisma/client'; import fs from 'fs';
import { Assignment, Exercise } from '../types/DatabaseTypes'; import path from 'path';
import db from '../helpers/DatabaseHelper'; import AssignmentFile from '../shared/types/Dojo/AssignmentFile.js';
import SecurityCheckType from '../types/SecurityCheckType'; import ExerciseResultsFile from '../shared/types/Dojo/ExerciseResultsFile.js';
import GitlabTreeFile from '../shared/types/Gitlab/GitlabTreeFile'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import GitlabFile from '../shared/types/Gitlab/GitlabFile'; import GlobalHelper from '../helpers/GlobalHelper.js';
import GitlabTreeFileType from '../shared/types/Gitlab/GitlabTreeFileType'; import { IFileDirStat } from '../shared/helpers/recursiveFilesStats/RecursiveFilesStats.js';
import JSON5 from 'json5'; import ExerciseManager from '../managers/ExerciseManager.js';
import fs from 'fs'; import * as Gitlab from '@gitbeaker/rest';
import path from 'path'; import { ProjectSchema } from '@gitbeaker/rest';
import AssignmentFile from '../shared/types/Dojo/AssignmentFile'; import GitlabTreeFileType from '../shared/types/Gitlab/GitlabTreeFileType.js';
import ExerciseResultsFile from '../shared/types/Dojo/ExerciseResultsFile'; import { GitbeakerRequestError } from '@gitbeaker/requester-utils';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; import SonarProjectCreation from '../shared/types/Sonar/SonarProjectCreation';
import GlobalHelper from '../helpers/GlobalHelper'; import SonarManager from '../managers/SonarManager';
import { IFileDirStat } from '../shared/helpers/recursiveFilesStats/RecursiveFilesStats';
import ExerciseManager from '../managers/ExerciseManager';
class ExerciseRoutes implements RoutesManager { class ExerciseRoutes implements RoutesManager {
...@@ -66,145 +64,297 @@ class ExerciseRoutes implements RoutesManager { ...@@ -66,145 +64,297 @@ 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.delete('/exercises/:exerciseIdOrUrl', SecurityMiddleware.check(true, SecurityCheckType.ADMIN, SecurityCheckType.EXERCISE_MEMBERS), this.deleteExercise.bind(this) as RequestHandler);
backend.get('/exercises/:exerciseIdOrUrl/assignment', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), this.getAssignment.bind(this)); backend.get('/users/:userId/exercises', SecurityMiddleware.check(true), this.getUserExercises.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('/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 } ]
}
});
if ( !exercise ) {
return res.status(StatusCodes.NOT_FOUND).send('Exercise not found');
}
const results = await db.result.findMany({
where: { exerciseId: exercise.id }
});
return res.status(StatusCodes.OK).json(results);
} }
private getExercisePath(assignment: Assignment, exerciseId: string): string { private async getAllExercises(req: express.Request, res: express.Response) {
return `dojo-ex_${ (assignment.gitlabLastInfo as unknown as GitlabRepository).path }_${ exerciseId }`; const exos = await db.exercise.findMany();
return req.session.sendResponse(res, StatusCodes.OK, exos);
} }
private async createExercise(req: express.Request, res: express.Response) { private async getUserExercises(req: express.Request, res: express.Response) {
const params: { members: Array<GitlabUser> } = req.body; if ( req.boundParams.user ) {
params.members = [ await req.session.profile.gitlabProfile!.value, ...params.members ].removeObjectDuplicates(gitlabUser => gitlabUser.id); if ( req.session.profile.isAdmin || req.session.profile.id === req.boundParams.user.id ) {
const assignment: Assignment = req.boundParams.assignment!; 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);
}
const exercises: Array<Exercise> | undefined = await ExerciseManager.getFromAssignment(assignment.name, { members: true }); private async getExerciseResults(req: express.Request, res: express.Response) {
const reachedLimitUsers: Array<GitlabUser> = []; const results = await db.result.findMany({
if ( exercises ) { where : { exerciseId: req.boundParams.exercise!.id },
for ( const member of params.members ) { 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
}
});
return req.session.sendResponse(res, StatusCodes.OK);
}
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 GitlabManager.addRepositoryVariable(repository.id, 'DOJO_EXERCISE_ID', exerciseId, false, true); await repoCreationFnExec(async () => {
await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, false, true); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_EXERCISE_ID', exerciseId, false, true);
await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_RESULTS_FOLDER', Config.exercise.pipelineResultsFolder, false, false); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, false, true);
await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_RESULTS_FOLDER', Config.exercise.pipelineResultsFolder, false, false);
}, 'Pipeline variables addition error');
await 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 {
await Promise.all([ ...new Set([ ...assignment.staff.map(user => user.id), ...params.members.map(member => member.id) ]) ].map(async (memberId: number): Promise<GitlabMember | false> => {
try { try {
return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.DEVELOPER); for ( const u of assignment.staff ) {
const success = await SonarManager.addGitlabUserToProject(u.gitlabUsername, sonarProject.project.key, true);
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({
data: { let exercise: Exercise = await repoCreationFnExec(() => db.exercise.create({
id : exerciseId, data: {
assignmentName : assignment.name, id : exerciseId,
name : repository.name, assignmentName : assignment.name,
secret : secret, name : repository.name,
gitlabId : repository.id, secret : secret,
gitlabLink : repository.web_url, gitlabId : repository.id,
gitlabCreationInfo: repository as unknown as Prisma.JsonObject, gitlabLink : repository.web_url,
gitlabLastInfo : repository as unknown as Prisma.JsonObject, gitlabCreationInfo: repository as unknown as Prisma.JsonObject,
gitlabLastInfoDate: new Date(), gitlabCreationDate: new Date(),
members : { gitlabLastInfo : repository as unknown as Prisma.JsonObject,
connectOrCreate: [ ...params.members.map(gitlabUser => { gitlabLastInfoDate: new Date(),
return { sonarKey : sonarProject?.project.key,
create: { sonarCreationInfo : sonarProject?.project,
id : gitlabUser.id, members : {
gitlabUsername: gitlabUser.name connectOrCreate: [ ...params.members.map(gitlabUser => {
}, return {
where : { create: {
id: gitlabUser.id id : gitlabUser.id,
} gitlabUsername: gitlabUser.name
}; },
}) ] where : {
} id: gitlabUser.id
} }
}) as unknown as Exercise; };
}) ]
return req.session.sendResponse(res, StatusCodes.OK, exercise); }
}
})) as 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,17 +387,19 @@ class ExerciseRoutes implements RoutesManager { ...@@ -237,17 +387,19 @@ 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!,
commit : params.commit, sonarGatePass: sonarGate,
results : params.results as unknown as Prisma.JsonObject, commit : params.commit,
files : params.files results : params.results as unknown as Prisma.JsonObject,
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 ) {
......