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
  • add_route_assignments
  • ask-user-to-delete-exercises-on-duplicates
  • bedran_exercise-list
  • jw_sonar
  • jw_sonar_backup
  • main
  • update-dependencies
  • v6.0.0
  • 2.0.0
  • 2.1.0
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 3.1.0
  • 3.1.1
  • 3.1.2
  • 3.1.3
  • 3.2.0
  • 3.3.0
  • 3.4.0
  • 3.4.1
  • 3.4.2
  • 3.5.0
  • 3.5.1
  • 3.5.2
  • 3.5.3
  • 4.0.0
  • 4.1.0
  • 5.0.0
  • 5.0.1
  • 6.0.0-dev
  • v1.0.1
32 results

Target

Select target project
  • Dojo_Project_Nguyen/backend/dojobackendapi
  • dojo_project/projects/backend/dojobackendapi
2 results
Select Git revision
  • add_route_assignments
  • ask-user-to-delete-exercises-on-duplicates
  • bedran_exercise-list
  • jw_sonar
  • jw_sonar_backup
  • main
  • update-dependencies
  • v6.0.0
  • 2.0.0
  • 2.1.0
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 3.1.0
  • 3.1.1
  • 3.1.2
  • 3.1.3
  • 3.2.0
  • 3.3.0
  • 3.4.0
  • 3.4.1
  • 3.4.2
  • 3.5.0
  • 3.5.1
  • 3.5.2
  • 3.5.3
  • 4.0.0
  • 4.1.0
  • 5.0.0
  • 5.0.1
  • 6.0.0-dev
  • v1.0.1
32 results
Show changes
Showing
with 819 additions and 199 deletions
-- AlterTable
ALTER TABLE `Assignment` ADD COLUMN `allowSonarFailure` BOOLEAN NOT NULL DEFAULT true;
/*
Warnings:
- Added the required column `gitlabCreationDate` to the `Assignment` table without a default value. This is not possible if the table is not empty.
- Added the required column `gitlabCreationDate` to the `Exercise` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE `Assignment` ADD COLUMN `gitlabCreationDate` DATETIME(3) NULL AFTER `gitlabCreationInfo`;
UPDATE `Assignment` SET `gitlabCreationDate` = `gitlabLastInfoDate`;
ALTER TABLE `Assignment` MODIFY COLUMN `gitlabCreationDate` DATETIME(3) NOT NULL AFTER `gitlabCreationInfo`;
-- AlterTable
ALTER TABLE `Exercise` ADD COLUMN `gitlabCreationDate` DATETIME(3) NULL DEFAULT NULL AFTER `gitlabCreationInfo`;
UPDATE `Exercise` SET `gitlabCreationDate` = `gitlabLastInfoDate`;
ALTER TABLE `Exercise` MODIFY COLUMN `gitlabCreationDate` DATETIME(3) NOT NULL AFTER `gitlabCreationInfo`;
......@@ -13,6 +13,13 @@ enum UserRole {
ADMIN
}
enum TagType {
LANGUAGE
FRAMEWORK
THEME
USERDEFINED
}
model User {
id Int @id /// The user's id is the same as their gitlab id
name String?
......@@ -28,15 +35,26 @@ model User {
model Assignment {
name String @id
secret String @unique @db.Char(36)
gitlabId Int
gitlabLink String
gitlabCreationInfo Json @db.Json
gitlabCreationDate DateTime
gitlabLastInfo Json @db.Json
gitlabLastInfoDate DateTime
published Boolean @default(false)
language Language @default(other)
useSonar Boolean @default(false)
allowSonarFailure Boolean @default(true)
sonarKey String?
sonarCreationInfo Json? @db.Json
sonarGate String?
sonarProfiles Json? @db.Json
exercises Exercise[]
staff User[]
tags Tag[]
}
model Exercise {
......@@ -47,27 +65,119 @@ model Exercise {
gitlabId Int
gitlabLink String
gitlabCreationInfo Json @db.Json
gitlabCreationDate DateTime
gitlabLastInfo Json @db.Json
gitlabLastInfoDate DateTime
deleted Boolean @default(false)
correctionCommit Json? @db.Json
sonarKey String?
sonarCreationInfo Json? @db.Json
correctionCommit Json? @db.Json
correctionDescription String? @db.VarChar(80)
assignment Assignment @relation(fields: [assignmentName], references: [name], onDelete: NoAction, onUpdate: Cascade)
members User[]
results Result[]
tags Tag[]
}
model Result {
exerciseId String @db.Char(36)
dateTime DateTime @default(now())
success Boolean
exitCode Int
commit Json @db.Json
results Json @db.Json
files Json @db.Json
exerciseId String @db.Char(36)
dateTime DateTime @default(now())
success Boolean
sonarGatePass Boolean?
exitCode Int
commit Json @db.Json
results Json @db.Json
files Json @db.Json
exercise Exercise @relation(fields: [exerciseId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@id([exerciseId, dateTime])
}
model Tag {
name String @id @db.Char(36)
type TagType
assignments Assignment[]
exercises Exercise[]
}
model TagProposal {
name String @id @db.Char(36)
type TagType
state String @default("PendingApproval")
details String?
}
enum Language {
abap
ada
asm
bash
bqn
c
caml
cloudformation
cpp
csharp
css
cuda
dart
delphi
docker
erlang
f
fsharp
flex
fortran
futhark
go
groovy
haskell
hepial
json
jsp
java
js
julia
kotlin
kubernetes
latex
lisp
lua
matlab
objc
ocaml
pascal
pearl
perl
php
postscript
powershell
prolog
promela
python
r
ruby
rust
scala
sql
smalltalk
swift
terraform
text
ts
tsql
typst
vba
vbnet
web
xml
yaml
other
}
require('../src/InitialImports'); // ATTENTION : These lines MUST be the first of this file
// ATTENTION : This line MUST be the first of this file
import '../src/init.js';
import * as process from 'process';
import SharedConfig from '../src/shared/config/SharedConfig';
import { UserRole } from '@prisma/client';
import logger from '../src/shared/logging/WinstonLogger';
import db from '../src/helpers/DatabaseHelper';
import * as process from 'process';
import SharedConfig from '../src/shared/config/SharedConfig.js';
import { TagType, UserRole } from '@prisma/client';
import logger from '../src/shared/logging/WinstonLogger.js';
import db from '../src/helpers/DatabaseHelper.js';
async function main() {
......@@ -12,12 +13,13 @@ async function main() {
await assignments();
await exercises();
await results();
await tag();
}
main().then(async () => {
await db.$disconnect();
}).catch(async (e) => {
logger.error(e);
}).catch(async e => {
logger.error(JSON.stringify(e));
await db.$disconnect();
process.exit(1);
});
......@@ -215,6 +217,7 @@ async function assignments() {
'issue_branch_template' : null,
'autoclose_referenced_issues' : true
},
gitlabCreationDate: new Date('2021-10-14T14:20:15.239Z'),
gitlabLastInfo : {
'id' : 13862,
'description' : 'Dojo assignment repository.\n\nName of the assignment: c_hello_world',
......@@ -338,7 +341,8 @@ async function assignments() {
'autoclose_referenced_issues' : true
},
gitlabLastInfoDate: new Date('2021-10-14T14:20:15.239Z'),
published : false
published : false,
secret : '77B9BB29-82F8-4D1F-B6D2-A201ABFB8509'
}
});
await db.assignment.upsert({
......@@ -470,6 +474,7 @@ async function assignments() {
'issue_branch_template' : null,
'autoclose_referenced_issues' : true
},
gitlabCreationDate: new Date('2023-11-07T20:35:54.692Z'),
gitlabLastInfo : {
'id' : 13893,
'description' : 'Dojo assignment repository.\n\nName of the assignment: Technique de compilation - TP',
......@@ -593,7 +598,8 @@ async function assignments() {
'autoclose_referenced_issues' : true
},
gitlabLastInfoDate: new Date('2023-11-07T20:35:54.692Z'),
published : true
published : true,
secret : '1127A3E7-4461-43B9-85DE-13C2317617AA'
}
});
}
......@@ -763,6 +769,7 @@ async function exercises() {
'issue_branch_template' : null,
'autoclose_referenced_issues' : true
},
gitlabCreationDate: new Date('2023-12-14T14:54:35.692Z'),
gitlabLastInfo : {
'id' : 14232,
'description' : 'Dojo exercise repository based on the the assignment: Technique de compilation - TP',
......@@ -1579,4 +1586,383 @@ async function results() {
}
});
}
}
\ No newline at end of file
}
async function tag() {
await db.tag.upsert({
where : { name: 'C' },
update: {},
create: {
name: 'C',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Java' },
update: {},
create: {
name: 'Java',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Scala' },
update: {},
create: {
name: 'Scala',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Kotlin' },
update: {},
create: {
name: 'Kotlin',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Rust' },
update: {},
create: {
name: 'Rust',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'JavaScript' },
update: {},
create: {
name: 'JavaScript',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'TypeScript' },
update: {},
create: {
name: 'TypeScript',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Python' },
update: {},
create: {
name: 'Python',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'HTML' },
update: {},
create: {
name: 'HTML',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'CSS' },
update: {},
create: {
name: 'CSS',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'C++' },
update: {},
create: {
name: 'C++',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Go' },
update: {},
create: {
name: 'Go',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'PHP' },
update: {},
create: {
name: 'PHP',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'C#' },
update: {},
create: {
name: 'C#',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Swift' },
update: {},
create: {
name: 'Swift',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Matlab' },
update: {},
create: {
name: 'Matlab',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'SQL' },
update: {},
create: {
name: 'SQL',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Assembly' },
update: {},
create: {
name: 'Assembly',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Ruby' },
update: {},
create: {
name: 'Ruby',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Fortran' },
update: {},
create: {
name: 'Fortran',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Pascal' },
update: {},
create: {
name: 'Pascal',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Visual Basic' },
update: {},
create: {
name: 'Visual Basic',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'R' },
update: {},
create: {
name: 'R',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Objective-C' },
update: {},
create: {
name: 'Objective-C',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Lua' },
update: {},
create: {
name: 'Lua',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Ada' },
update: {},
create: {
name: 'Ada',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Haskell' },
update: {},
create: {
name: 'Haskell',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Shell/PowerShell' },
update: {},
create: {
name: 'Shell/PowerShell',
type: TagType.LANGUAGE
}
});
await db.tag.upsert({
where : { name: 'Express' },
update: {},
create: {
name: 'Express',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Django' },
update: {},
create: {
name: 'Django',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Ruby on Rails' },
update: {},
create: {
name: 'Ruby on Rails',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Angular' },
update: {},
create: {
name: 'Angular',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'React' },
update: {},
create: {
name: 'React',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Flutter' },
update: {},
create: {
name: 'Flutter',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Ionic' },
update: {},
create: {
name: 'Ionic',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Flask' },
update: {},
create: {
name: 'Flask',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'React Native' },
update: {},
create: {
name: 'React Native',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Xamarin' },
update: {},
create: {
name: 'Xamarin',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Laravel' },
update: {},
create: {
name: 'Laravel',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Spring' },
update: {},
create: {
name: 'Spring',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Play' },
update: {},
create: {
name: 'Play',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Symfony' },
update: {},
create: {
name: 'Symfony',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'ASP.NET' },
update: {},
create: {
name: 'ASP.NET',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Meteor' },
update: {},
create: {
name: 'Meteor',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Vue.js' },
update: {},
create: {
name: 'Vue.js',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Svelte' },
update: {},
create: {
name: 'Svelte',
type: TagType.FRAMEWORK
}
});
await db.tag.upsert({
where : { name: 'Express.js' },
update: {},
create: {
name: 'Express.js',
type: TagType.FRAMEWORK
}
});
}
import path from 'node:path';
import cluster from 'node:cluster';
import myEnv = require('dotenv');
import dotenvExpand = require('dotenv-expand');
if ( cluster.isPrimary ) {
if ( process.env.NODE_ENV && process.env.NODE_ENV === 'production' ) {
dotenvExpand.expand(myEnv.config());
} else {
myEnv.config({ path: path.join(__dirname, '../.env.keys') });
dotenvExpand.expand(myEnv.config({ DOTENV_KEY: process.env.DOTENV_KEY_DEVELOPMENT }));
}
}
require('./shared/helpers/TypeScriptExtensions'); // ATTENTION : This line MUST be after the dotenv.config() calls
require('./InitialImports'); // ATTENTION : These lines MUST be the first of this file
// ATTENTION : This line MUST be the first of this file
import './init.js';
import WorkerRole from './process/WorkerRole';
import ClusterManager from './process/ClusterManager';
import API from './express/API';
import HttpManager from './managers/HttpManager';
import SharedConfig from './shared/config/SharedConfig.js';
import WorkerRole from './process/WorkerRole.js';
import ClusterManager from './process/ClusterManager.js';
import API from './express/API.js';
import HttpManager from './managers/HttpManager.js';
HttpManager.registerAxiosInterceptor();
(new ClusterManager([ {
role : WorkerRole.API,
quantity : ClusterManager.CORES,
restartOnFail: true,
loadTask : () => {
return new API();
}
} ])).run();
if ( SharedConfig.production ) {
(new ClusterManager([ {
role : WorkerRole.API,
quantity : ClusterManager.CORES,
restartOnFail: true,
loadTask : () => new API()
} ])).run();
} else {
(new API()).run();
}
import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility';
import path from 'path';
import fs from 'fs';
import { Exercise } from '../types/DatabaseTypes';
import { Exercise } from '../types/DatabaseTypes.js';
import JSON5 from 'json5';
import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility.js';
type ConfigGitlabBadge = {
......@@ -19,7 +19,7 @@ class Config {
version: {
[client: string]: string
}
}; // { version: { CLIENT: CONDITION } }
};
public readonly dojoCLI: {
versionUpdatePeriodMs: number
......@@ -33,18 +33,26 @@ class Config {
public readonly login: {
gitlab: {
client: {
secret: string
id: string, secret: string
}, url: {
redirect: string, token: string
}
}
};
public readonly dockerhub: {
repositories: {
assignmentChecker: string, exerciseChecker: string
}
};
public readonly gitlab: {
urls: Array<string>; repository: {
url: string; urls: Array<string>; repository: {
timeoutAfterCreation: number;
}; account: {
id: number; username: string; token: string;
}; group: {
root: number; templates: number; assignments: number; exercises: number;
root: number; templates: number; assignments: number; exercises: number; deletedAssignments: number; deletedExercises: number;
}, badges: {
pipeline: ConfigGitlabBadge
}
......@@ -52,19 +60,19 @@ class Config {
public readonly assignment: {
default: {
description: string; initReadme: boolean; sharedRunnersEnabled: boolean; visibility: string; wikiEnabled: boolean; template: string
description: string; initReadme: boolean; sharedRunnersEnabled: boolean; visibility: GitlabVisibility; wikiEnabled: boolean; template: string
}; baseFiles: Array<string>; filename: string
};
public readonly exercise: {
maxSameName: number; maxPerAssignment: number; resultsFolder: string, pipelineResultsFolder: string; default: {
description: string; visibility: string;
description: string; visibility: GitlabVisibility;
};
};
constructor() {
this.api = {
port: Number(process.env.API_PORT || 30992)
port: Number(process.env.API_PORT || 0)
};
this.requestClientValidation = JSON5.parse(process.env.REQUEST_CLIENT_VALIDATION || '{"version": {}}');
......@@ -82,12 +90,25 @@ class Config {
this.login = {
gitlab: {
client: {
id : process.env.LOGIN_GITLAB_CLIENT_ID || '',
secret: process.env.LOGIN_GITLAB_CLIENT_SECRET || ''
},
url : {
redirect: process.env.LOGIN_GITLAB_URL_REDIRECT ?? '',
token : process.env.LOGIN_GITLAB_URL_TOKEN ?? ''
}
}
};
this.dockerhub = {
repositories: {
assignmentChecker: process.env.DOCKERHUB_REPO_ASSIGNMENT_CHECKER ?? '',
exerciseChecker : process.env.DOCKERHUB_REPO_EXERCISE_CHECKER ?? ''
}
};
this.gitlab = {
url : process.env.GITLAB_URL || '',
urls : JSON5.parse(process.env.GITLAB_URLS || '[]'),
account : {
id : Number(process.env.GITLAB_DOJO_ACCOUNT_ID || 0),
......@@ -98,10 +119,12 @@ class Config {
timeoutAfterCreation: Number(process.env.GITLAB_REPOSITORY_CREATION_TIMEOUT || 5000)
},
group : {
root : Number(process.env.GITLAB_GROUP_ROOT_ID || 0),
templates : Number(process.env.GITLAB_GROUP_TEMPLATES_ID || 0),
assignments: Number(process.env.GITLAB_GROUP_ASSIGNMENTS_ID || 0),
exercises : Number(process.env.GITLAB_GROUP_EXERCISES_ID || 0)
root : Number(process.env.GITLAB_GROUP_ROOT_ID || 0),
templates : Number(process.env.GITLAB_GROUP_TEMPLATES_ID || 0),
assignments : Number(process.env.GITLAB_GROUP_ASSIGNMENTS_ID || 0),
exercises : Number(process.env.GITLAB_GROUP_EXERCISES_ID || 0),
deletedAssignments: Number(process.env.GITLAB_GROUP_DELETED_ASSIGNMENTS_ID || 0),
deletedExercises : Number(process.env.GITLAB_GROUP_DELETED_EXERCISES_ID || 0)
},
badges : {
pipeline: {
......@@ -116,7 +139,7 @@ class Config {
description : process.env.ASSIGNMENT_DEFAULT_DESCRIPTION?.convertWithEnvVars() ?? '',
initReadme : process.env.ASSIGNMENT_DEFAULT_INIT_README?.toBoolean() ?? false,
sharedRunnersEnabled: process.env.ASSIGNMENT_DEFAULT_SHARED_RUNNERS_ENABLED?.toBoolean() ?? true,
visibility : process.env.ASSIGNMENT_DEFAULT_VISIBILITY || GitlabVisibility.PRIVATE,
visibility : process.env.ASSIGNMENT_DEFAULT_VISIBILITY as GitlabVisibility || 'private',
wikiEnabled : process.env.ASSIGNMENT_DEFAULT_WIKI_ENABLED?.toBoolean() ?? false,
template : process.env.ASSIGNMENT_DEFAULT_TEMPLATE?.replace('{{USERNAME}}', this.gitlab.account.username).replace('{{TOKEN}}', this.gitlab.account.token) ?? ''
},
......@@ -131,7 +154,7 @@ class Config {
pipelineResultsFolder: process.env.EXERCISE_PIPELINE_RESULTS_FOLDER ?? '', //Do not use convertWithEnvVars() because it is used in the exercise creation and muste be interpreted at exercise runtime
default : {
description: process.env.EXERCISE_DEFAULT_DESCRIPTION?.convertWithEnvVars() ?? '',
visibility : process.env.EXERCISE_DEFAULT_VISIBILITY || GitlabVisibility.PRIVATE
visibility : process.env.EXERCISE_DEFAULT_VISIBILITY as GitlabVisibility || 'private'
}
};
}
......
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
import * as jwt from 'jsonwebtoken';
import { JwtPayload } from 'jsonwebtoken';
import Config from '../config/Config';
import Config from '../config/Config.js';
import express from 'express';
import UserManager from '../managers/UserManager';
import { User } from '../types/DatabaseTypes';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse';
import UserManager from '../managers/UserManager.js';
import { User } from '../types/DatabaseTypes.js';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse.js';
class Session {
......@@ -19,30 +19,27 @@ class Session {
this._profile = newProfile;
}
constructor() { }
async initSession(req: express.Request, res: express.Response) {
const authorization = req.headers.authorization;
if ( authorization ) {
if ( authorization.startsWith('Bearer ') ) {
const jwtToken = authorization.replace('Bearer ', '');
if ( authorization && authorization.startsWith('Bearer ') ) {
const jwtToken = authorization.replace('Bearer ', '');
try {
const jwtData = jwt.verify(jwtToken, Config.jwtConfig.secret) as JwtPayload;
try {
const jwtData = jwt.verify(jwtToken, Config.jwtConfig.secret) as JwtPayload;
if ( jwtData.profile ) {
this.profile = jwtData.profile;
this.profile = await UserManager.getById(this.profile.id!) ?? this.profile;
}
} catch ( err ) {
res.sendStatus(StatusCodes.UNAUTHORIZED).end();
if ( jwtData.profile ) {
this.profile = jwtData.profile;
this.profile = await UserManager.getById(this.profile.id) ?? this.profile;
}
} catch ( err ) {
res.sendStatus(StatusCodes.UNAUTHORIZED).end();
}
}
}
private static getToken(profileJson: unknown): string | null {
return profileJson === null ? null : jwt.sign({ profile: profileJson }, Config.jwtConfig.secret, Config.jwtConfig.expiresIn > 0 ? { expiresIn: Config.jwtConfig.expiresIn } : {});
const options = Config.jwtConfig.expiresIn > 0 ? { expiresIn: Config.jwtConfig.expiresIn } : {};
return profileJson === null ? null : jwt.sign({ profile: profileJson }, Config.jwtConfig.secret, options);
}
private async getResponse<T>(code: number, data: T, descriptionOverride?: string): Promise<DojoBackendResponse<T>> {
......@@ -67,12 +64,14 @@ class Session {
Send a response to the client
Information: Data could be a promise or an object. If it's a promise, we wait on the data to be resolved before sending the response
*/
sendResponse(res: express.Response, code: number, data?: unknown, descriptionOverride?: string, internalCode?: number) {
Promise.resolve(data).then((toReturn: unknown) => {
this.getResponse(internalCode ?? code, toReturn, descriptionOverride).then(response => {
res.status(code).json(response);
sendResponse(res: express.Response | undefined, code: number, data?: unknown, descriptionOverride?: string, internalCode?: number) {
if ( res ) {
Promise.resolve(data).then((toReturn: unknown) => {
this.getResponse(internalCode ?? code, toReturn, descriptionOverride).then(response => {
res.status(code).json(response);
});
});
});
}
}
}
......
import { Express } from 'express-serve-static-core';
import cors from 'cors';
import morganMiddleware from '../logging/MorganMiddleware';
import morganMiddleware from '../logging/MorganMiddleware.js';
import { AddressInfo } from 'net';
import http from 'http';
import helmet from 'helmet';
import express from 'express';
import WorkerTask from '../process/WorkerTask';
import WorkerTask from '../process/WorkerTask.js';
import multer from 'multer';
import SessionMiddleware from '../middlewares/SessionMiddleware';
import Config from '../config/Config';
import logger from '../shared/logging/WinstonLogger';
import ParamsCallbackManager from '../middlewares/ParamsCallbackManager';
import ApiRoutesManager from '../routes/ApiRoutesManager';
import SessionMiddleware from '../middlewares/SessionMiddleware.js';
import Config from '../config/Config.js';
import logger from '../shared/logging/WinstonLogger.js';
import ParamsCallbackManager from '../middlewares/ParamsCallbackManager.js';
import ApiRoutesManager from '../routes/ApiRoutesManager.js';
import compression from 'compression';
import ClientVersionCheckerMiddleware from '../middlewares/ClientVersionCheckerMiddleware';
import ClientVersionCheckerMiddleware from '../middlewares/ClientVersionCheckerMiddleware.js';
import swaggerUi from 'swagger-ui-express';
import path from 'path';
import DojoCliVersionHelper from '../helpers/DojoCliVersionHelper';
class API implements WorkerTask {
private readonly backend: Express;
private server: http.Server | undefined;
private server!: http.Server;
constructor() {
this.backend = express();
......@@ -45,10 +44,15 @@ class API implements WorkerTask {
this.backend.use(cors()); //Allow CORS requests
this.backend.use(compression()); //Compress responses
this.backend.use(async (req, res, next) => {
res.header('dojocli-latest-version', await DojoCliVersionHelper.getLatestVersion());
next();
/*
this.backend.use((_req, res, next) => {
DojoCliVersionHelper.getLatestVersion().then(latestVersion => {
res.header('dojocli-latest-version', latestVersion);
next();
});
});
*/
}
private initOpenAPI() {
......@@ -59,9 +63,9 @@ class API implements WorkerTask {
url: '../OpenAPI.yaml'
}
};
this.backend.get('/docs/OpenAPI.yaml', (req, res) => res.sendFile(path.resolve(__dirname + '/../../assets/OpenAPI/OpenAPI.yaml')));
this.backend.get('/docs/OpenAPI.yaml', (_req, res) => res.sendFile(path.resolve(__dirname + '/../../assets/OpenAPI/OpenAPI.yaml')));
this.backend.use('/docs/swagger', swaggerUi.serveFiles(undefined, options), swaggerUi.setup(undefined, options));
this.backend.get('/docs/redoc.html', (req, res) => res.sendFile(path.resolve(__dirname + '/../../assets/OpenAPI/redoc.html')));
this.backend.get('/docs/redoc.html', (_req, res) => res.sendFile(path.resolve(__dirname + '/../../assets/OpenAPI/redoc.html')));
this.backend.get('/docs/', (req, res) => {
const prefix = req.url.slice(-1) === '/' ? '' : 'docs/';
......@@ -89,7 +93,7 @@ class API implements WorkerTask {
const {
port,
address
} = this.server!.address() as AddressInfo;
} = this.server.address() as AddressInfo;
logger.info(`Server started on http://${ address }:${ port }`);
});
}
......
import { PrismaClient } from '@prisma/client';
import logger from '../shared/logging/WinstonLogger';
import UserQueryExtension from './Prisma/Extensions/UserQueryExtension';
import UserResultExtension from './Prisma/Extensions/UserResultExtension';
import AssignmentResultExtension from './Prisma/Extensions/AssignmentResultExtension';
import ExerciseResultExtension from './Prisma/Extensions/ExerciseResultExtension';
import logger from '../shared/logging/WinstonLogger.js';
import UserQueryExtension from './Prisma/Extensions/UserQueryExtension.js';
import UserResultExtension from './Prisma/Extensions/UserResultExtension.js';
import AssignmentResultExtension from './Prisma/Extensions/AssignmentResultExtension.js';
import ExerciseResultExtension from './Prisma/Extensions/ExerciseResultExtension.js';
const prisma = new PrismaClient({
......@@ -31,7 +31,7 @@ prisma.$on('warn', e => logger.warn(`Prisma => ${ e.message }`));
prisma.$on('error', e => logger.error(`Prisma => ${ e.message }`));
const db = prisma.$extends(UserQueryExtension).$extends(UserResultExtension).$extends(AssignmentResultExtension).$extends(ExerciseResultExtension);
const DatabaseHelper = prisma.$extends(UserQueryExtension).$extends(UserResultExtension).$extends(AssignmentResultExtension).$extends(ExerciseResultExtension);
export default db;
\ No newline at end of file
export default DatabaseHelper;
\ No newline at end of file
import Config from '../config/Config';
import GitlabRelease from '../shared/types/Gitlab/GitlabRelease';
import GitlabManager from '../managers/GitlabManager';
import Config from '../config/Config.js';
import GitlabManager from '../managers/GitlabManager.js';
import * as Gitlab from '@gitbeaker/rest';
class DojoCliVersionHelper {
private latestUpdate: Date | undefined;
private latestVersion: string | undefined;
constructor() { }
private async updateVersion(): Promise<void> {
const releases: Array<GitlabRelease> = await GitlabManager.getRepositoryReleases(Config.dojoCLI.repositoryId);
const releases: Array<Gitlab.ReleaseSchema> = await GitlabManager.getRepositoryReleases(Config.dojoCLI.repositoryId);
for ( const release of releases ) {
if ( !isNaN(+release.tag_name.replace('.', '')) ) {
this.latestVersion = release.tag_name;
......
import LazyVal from '../shared/helpers/LazyVal';
import LazyVal from '../shared/helpers/LazyVal.js';
class DojoModelsHelper {
......@@ -9,8 +9,7 @@ class DojoModelsHelper {
* @param depth The depth of the search for LazyVal instances
*/
async getFullSerializableObject<T extends NonNullable<unknown>>(obj: T, depth: number = 0): Promise<unknown> {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const result: any = {};
const result: { [key: string]: unknown } = {};
for ( const key in obj ) {
let value: unknown = obj[key];
......
import Config from '../config/Config';
import Config from '../config/Config.js';
import GitlabManager from '../managers/GitlabManager.js';
import express from 'express';
import logger from '../shared/logging/WinstonLogger.js';
import Json5FileValidator from '../shared/helpers/Json5FileValidator.js';
import ExerciseResultsFile from '../shared/types/Dojo/ExerciseResultsFile.js';
import ParamsCallbackManager from '../middlewares/ParamsCallbackManager.js';
import ExerciseManager from '../managers/ExerciseManager.js';
import Toolbox from '../shared/helpers/Toolbox.js';
import { CustomValidator, FieldMessageFactory, Meta, ValidationChain, ErrorMessage } from 'express-validator/lib/index.js';
import { ErrorMessage } from 'express-validator/lib/base.js';
import { StatusCodes } from 'http-status-codes';
import { CustomValidator, ErrorMessage, FieldMessageFactory, Meta } from 'express-validator/src/base';
import { BailOptions, ValidationChain } from 'express-validator/src/chain';
import GitlabManager from '../managers/GitlabManager';
import express from 'express';
import logger from '../shared/logging/WinstonLogger';
import Json5FileValidator from '../shared/helpers/Json5FileValidator';
import ExerciseResultsFile from '../shared/types/Dojo/ExerciseResultsFile';
import ParamsCallbackManager from '../middlewares/ParamsCallbackManager';
import ExerciseManager from '../managers/ExerciseManager';
import { Language } from '@prisma/client';
declare type DojoMeta = Meta & {
......@@ -31,11 +34,11 @@ class DojoValidators {
}
readonly nullSanitizer = this.toValidatorSchemaOptions({
options: (value) => {
options: value => {
try {
return value == 'null' || value == 'undefined' || value == '' ? null : value;
return value === 'null' || value === 'undefined' || value === '' ? null : value;
} catch ( error ) {
logger.error(`null sanitizer error: ${ error }`);
logger.error(`null sanitizer error: ${ JSON.stringify(error) }`);
return value;
}
......@@ -43,9 +46,9 @@ class DojoValidators {
});
readonly jsonSanitizer = this.toValidatorSchemaOptions({
options: (value) => {
options: value => {
try {
return JSON.parse(value as string);
return JSON.parse(value as string) as unknown;
} catch ( e ) {
return value;
}
......@@ -62,8 +65,8 @@ class DojoValidators {
return new Promise((resolve, reject) => {
const template = this.getParamValue(req, path) as string;
if ( template ) {
GitlabManager.checkTemplateAccess(template, req).then((templateAccess) => {
templateAccess !== StatusCodes.OK ? reject() : resolve(true);
GitlabManager.checkTemplateAccess(template, req).then(templateAccess => {
templateAccess ? resolve(true) : reject();
});
}
resolve(true);
......@@ -78,13 +81,14 @@ class DojoValidators {
}) => {
try {
const template = this.getParamValue(req, path);
if ( template ) {
return `${ Config.gitlab.urls[0].replace(/^([a-z]{3,5}:\/{2})?(.*)/, `$1${ Config.gitlab.account.username }:${ Config.gitlab.account.token }@$2`) }${ template }.git`;
if ( template && Toolbox.isString(template) ) {
const gitlabUrlWithCredentials = Config.gitlab.urls[0].replace(/^([a-z]{3,5}:\/{2})?(.*)/, `$1${ Config.gitlab.account.username }:${ Config.gitlab.account.token }@$2`);
return `${ gitlabUrlWithCredentials }${ template }.git`;
} else {
return Config.assignment.default.template;
}
} catch ( error ) {
logger.error(`Template url sanitizer error: ${ error }`);
logger.error(`Template url sanitizer error: ${ JSON.stringify(error) }`);
return value;
}
......@@ -121,7 +125,7 @@ class DojoValidators {
if ( exerciseIdOrUrl ) {
ParamsCallbackManager.initBoundParams(req);
ExerciseManager.get(exerciseIdOrUrl).then((exercise) => {
ExerciseManager.get(exerciseIdOrUrl).then(exercise => {
req.boundParams.exercise = exercise;
exercise !== undefined ? resolve(true) : reject();
......@@ -134,6 +138,23 @@ class DojoValidators {
});
}
});
readonly supportedLanguageValidator = this.toValidatorSchemaOptions({
bail : true,
errorMessage: 'Unsupported language',
options : (_value, {
req,
path
}) => {
return new Promise((resolve, reject) => {
const language = this.getParamValue(req, path) as string;
if ( language ) {
(Object.values(Language).includes(language as keyof typeof Language) ? resolve(true) : reject());
}
reject();
});
}
});
}
......
import express from 'express';
import GitlabRepository from '../shared/types/Gitlab/GitlabRepository';
import logger from '../shared/logging/WinstonLogger';
import GitlabManager from '../managers/GitlabManager';
import { AxiosError } from 'axios';
import { StatusCodes } from 'http-status-codes';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode';
import express from 'express';
import logger from '../shared/logging/WinstonLogger.js';
import GitlabManager from '../managers/GitlabManager.js';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import { StatusCodes } from 'http-status-codes';
import { GitbeakerRequestError } from '@gitbeaker/requester-utils';
import * as Gitlab from '@gitbeaker/rest';
import SonarProjectCreation from '../shared/types/Sonar/SonarProjectCreation';
import SonarManager from '../managers/SonarManager';
class GlobalHelper {
async repositoryCreationError(message: string, error: unknown, req: express.Request, res: express.Response, gitlabError: DojoStatusCode, internalError: DojoStatusCode, repositoryToRemove?: GitlabRepository): Promise<void> {
logger.error(message);
logger.error(error);
repoCreationFnExecCreator(req: express.Request, res: express.Response, gitlabError: DojoStatusCode, internalError: DojoStatusCode, repositoryToRemove?: Gitlab.ProjectSchema, sonarToRemove?: SonarProjectCreation) {
return async (toExec: () => Promise<unknown>, errorMessage?: string) => {
try {
return await toExec();
} catch ( error ) {
if ( errorMessage ) {
logger.error(errorMessage);
logger.error(JSON.stringify(error));
try {
if ( repositoryToRemove ) {
await GitlabManager.deleteRepository(repositoryToRemove.id);
try {
if ( repositoryToRemove ) {
await GitlabManager.deleteRepository(repositoryToRemove.id);
}
if ( sonarToRemove ) {
await SonarManager.deleteProject(sonarToRemove.project.key);
}
} catch ( deleteError ) {
logger.error('Repository deletion error');
logger.error(JSON.stringify(deleteError));
}
if ( error instanceof GitbeakerRequestError ) {
req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, `Unknown gitlab error: ${ errorMessage }`, gitlabError);
throw error;
}
req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, `Unknown error: ${ errorMessage }`, internalError);
throw error;
}
}
} catch ( error ) {
logger.error('Repository deletion error');
logger.error(error);
}
if ( error instanceof AxiosError ) {
return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, `Unknown gitlab error: ${ message }`, gitlabError);
}
return undefined;
};
}
return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, `Unknown error: ${ message }`, internalError);
isRepoNameAlreadyTaken(errorDescription: unknown) {
return errorDescription instanceof Array && errorDescription.length > 0 && (errorDescription[0] as string).includes('has already been taken');
}
addRepoMember(repositoryId: number) {
return async (memberId: number): Promise<Gitlab.MemberSchema | false> => {
try {
return await GitlabManager.addRepositoryMember(repositoryId, memberId, Gitlab.AccessLevel.DEVELOPER);
} catch ( error ) {
logger.error('Add member error');
logger.error(JSON.stringify(error));
return false;
}
};
}
}
......
import { Prisma } from '@prisma/client';
import { Exercise } from '../../../types/DatabaseTypes';
import db from '../../DatabaseHelper';
import LazyVal from '../../../shared/helpers/LazyVal';
import { Exercise } from '../../../types/DatabaseTypes.js';
import db from '../../DatabaseHelper.js';
import LazyVal from '../../../shared/helpers/LazyVal.js';
async function getCorrections(assignment: { name: string }): Promise<Array<Partial<Exercise>> | undefined> {
......@@ -30,9 +30,7 @@ export default Prisma.defineExtension(client => {
assignment: {
corrections: {
compute(assignment) {
return new LazyVal<Array<Partial<Exercise>> | undefined>(() => {
return getCorrections(assignment);
});
return new LazyVal<Array<Partial<Exercise>> | undefined>(() => assignment.published ? getCorrections(assignment) : []);
}
}
}
......
import { Prisma, UserRole } from '@prisma/client';
import LazyVal from '../../../shared/helpers/LazyVal';
import GitlabUser from '../../../shared/types/Gitlab/GitlabUser';
import GitlabManager from '../../../managers/GitlabManager';
import LazyVal from '../../../shared/helpers/LazyVal.js';
import * as Gitlab from '@gitbeaker/rest';
import GitlabManager from '../../../managers/GitlabManager.js';
export default Prisma.defineExtension(client => {
......@@ -13,7 +13,7 @@ export default Prisma.defineExtension(client => {
role: true
},
compute(user) {
return user.role == UserRole.TEACHING_STAFF || user.role == UserRole.ADMIN;
return user.role === UserRole.TEACHING_STAFF || user.role === UserRole.ADMIN;
}
},
isAdmin : {
......@@ -21,14 +21,12 @@ export default Prisma.defineExtension(client => {
role: true
},
compute(user) {
return user.role == UserRole.ADMIN;
return user.role === UserRole.ADMIN;
}
},
gitlabProfile : {
compute(user) {
return new LazyVal<GitlabUser | undefined>(() => {
return GitlabManager.getUserById(user.id);
});
return new LazyVal<Gitlab.UserSchema | undefined>(() => GitlabManager.getUserById(user.id));
}
}
}
......
import path from 'node:path';
import cluster from 'node:cluster';
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
import './shared/helpers/TypeScriptExtensions.js';
if ( cluster.isPrimary ) {
if ( process.env.NODE_ENV && process.env.NODE_ENV === 'production' ) {
dotenvExpand.expand(dotenv.config());
} else {
dotenv.config({ path: path.join(__dirname, '../.env.keys') });
dotenvExpand.expand(dotenv.config({ DOTENV_KEY: process.env.DOTENV_KEY_DEVELOPMENT }));
dotenvExpand.expand(dotenv.config({ path: path.join(__dirname, '../config.env') }));
}
}
import morgan, { StreamOptions } from 'morgan';
import logger from '../shared/logging/WinstonLogger';
import logger from '../shared/logging/WinstonLogger.js';
const stream: StreamOptions = {
write: (message) => logger.http(message)
write: message => logger.http(message)
};
const skip = () => {
return false; //SharedConfig.production;
};
const skip = () => false;
const morganMiddleware = morgan(':method :url :status :res[content-length] - :response-time ms', {
stream,
......
import { Prisma } from '@prisma/client';
import { Assignment, User } from '../types/DatabaseTypes';
import db from '../helpers/DatabaseHelper';
import { Assignment, User } from '../types/DatabaseTypes.js';
import db from '../helpers/DatabaseHelper.js';
class AssignmentManager {
async isUserAllowedToAccessAssignment(assignment: Assignment, user: User): Promise<boolean> {
if (user === null || user === undefined) {
return false;
}
if ( !assignment.staff ) {
assignment.staff = await db.assignment.findUnique({
where: {
......@@ -17,21 +20,32 @@ class AssignmentManager {
async getByName(name: string, include: Prisma.AssignmentInclude | undefined = undefined): Promise<Assignment | undefined> {
return await db.assignment.findUnique({
where : {
where : {
name: name
}, include: include
},
include: include
}) as unknown as Assignment ?? undefined;
}
getByGitlabLink(gitlabLink: string, include: Prisma.AssignmentInclude | undefined = undefined): Promise<Assignment | undefined> {
const name = gitlabLink.replace('.git', '').split('/').pop()!;
return this.getByName(name, include);
async getByGitlabLink(gitlabLink: string, include: Prisma.AssignmentInclude | undefined = undefined): Promise<Assignment | undefined> {
const nameInUrl = gitlabLink.replace('.git', '').split('/').pop()!;
const result = await db.assignment.findMany({
where : {
gitlabLink: {
endsWith: `/${ nameInUrl }`
}
},
include: include
}) as Array<Assignment>;
return result.length > 0 ? result[0] : undefined;
}
get(nameOrUrl: string, include: Prisma.AssignmentInclude | undefined = undefined): Promise<Assignment | undefined> {
// We can use the same function for both name and url because the name is the last part of the url and the name extraction from the url doesn't corrupt the name
return this.getByGitlabLink(nameOrUrl, include);
if ( nameOrUrl.includes('://') ) {
return this.getByGitlabLink(nameOrUrl, include);
} else {
return this.getByName(nameOrUrl, include);
}
}
}
......
import { Prisma } from '@prisma/client';
import { Exercise } from '../types/DatabaseTypes';
import db from '../helpers/DatabaseHelper';
import { Prisma } from '@prisma/client';
import { Exercise, User } from '../types/DatabaseTypes.js';
import db from '../helpers/DatabaseHelper.js';
class ExerciseManager {
......@@ -15,13 +15,30 @@ class ExerciseManager {
}) as unknown as Exercise ?? undefined;
}
async getFromAssignment(assignmentName: string, include: Prisma.ExerciseInclude | undefined = undefined): Promise<Array<Exercise> | undefined> {
return await db.exercise.findMany({
where : {
assignmentName: assignmentName
},
include: include
}) as Array<Exercise> ?? undefined;
// deleteFilter is a boolean that is true to get only deleted exercises, false to get only non-deleted exercises, and undefined to get all exercises
getFromAssignment(assignmentName: string, deleteFilter: boolean | undefined = undefined, include: Prisma.ExerciseInclude | undefined = undefined): Promise<Array<Exercise>> {
return db.exercise.findMany({
where : { assignmentName: assignmentName, ...(deleteFilter !== undefined ? { deleted: deleteFilter } : {}) },
include: include
}) as Promise<Array<Exercise>>;
}
async isUserAllowedToAccessExercise(exercise: Exercise, user: User): Promise<boolean> {
if ( !exercise.members ) {
exercise.members = await db.exercise.findUnique({
where: {
id: exercise.id
}
}).members() ?? [];
}
const assignmentStaff = (await db.assignment.findUnique({
where: {
name: exercise.assignmentName
}
}).staff()) ?? [];
return exercise.members.findIndex(member => member.id === user.id) !== -1 || assignmentStaff.findIndex(staff => staff.id === user.id) !== -1;
}
}
......