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 { ...@@ -13,6 +13,13 @@ enum UserRole {
ADMIN ADMIN
} }
enum TagType {
LANGUAGE
FRAMEWORK
THEME
USERDEFINED
}
model User { model User {
id Int @id /// The user's id is the same as their gitlab id id Int @id /// The user's id is the same as their gitlab id
name String? name String?
...@@ -28,15 +35,26 @@ model User { ...@@ -28,15 +35,26 @@ model User {
model Assignment { model Assignment {
name String @id name String @id
secret String @unique @db.Char(36)
gitlabId Int gitlabId Int
gitlabLink String gitlabLink String
gitlabCreationInfo Json @db.Json gitlabCreationInfo Json @db.Json
gitlabCreationDate DateTime
gitlabLastInfo Json @db.Json gitlabLastInfo Json @db.Json
gitlabLastInfoDate DateTime gitlabLastInfoDate DateTime
published Boolean @default(false) 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[] exercises Exercise[]
staff User[] staff User[]
tags Tag[]
} }
model Exercise { model Exercise {
...@@ -47,27 +65,119 @@ model Exercise { ...@@ -47,27 +65,119 @@ model Exercise {
gitlabId Int gitlabId Int
gitlabLink String gitlabLink String
gitlabCreationInfo Json @db.Json gitlabCreationInfo Json @db.Json
gitlabCreationDate DateTime
gitlabLastInfo Json @db.Json gitlabLastInfo Json @db.Json
gitlabLastInfoDate DateTime 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) assignment Assignment @relation(fields: [assignmentName], references: [name], onDelete: NoAction, onUpdate: Cascade)
members User[] members User[]
results Result[] results Result[]
tags Tag[]
} }
model Result { model Result {
exerciseId String @db.Char(36) exerciseId String @db.Char(36)
dateTime DateTime @default(now()) dateTime DateTime @default(now())
success Boolean success Boolean
exitCode Int sonarGatePass Boolean?
commit Json @db.Json exitCode Int
results Json @db.Json commit Json @db.Json
files Json @db.Json results Json @db.Json
files Json @db.Json
exercise Exercise @relation(fields: [exerciseId], references: [id], onDelete: Cascade, onUpdate: Cascade) exercise Exercise @relation(fields: [exerciseId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@id([exerciseId, dateTime]) @@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 * as process from 'process';
import SharedConfig from '../src/shared/config/SharedConfig'; import SharedConfig from '../src/shared/config/SharedConfig.js';
import { UserRole } from '@prisma/client'; import { TagType, UserRole } from '@prisma/client';
import logger from '../src/shared/logging/WinstonLogger'; import logger from '../src/shared/logging/WinstonLogger.js';
import db from '../src/helpers/DatabaseHelper'; import db from '../src/helpers/DatabaseHelper.js';
async function main() { async function main() {
...@@ -12,12 +13,13 @@ async function main() { ...@@ -12,12 +13,13 @@ async function main() {
await assignments(); await assignments();
await exercises(); await exercises();
await results(); await results();
await tag();
} }
main().then(async () => { main().then(async () => {
await db.$disconnect(); await db.$disconnect();
}).catch(async (e) => { }).catch(async e => {
logger.error(e); logger.error(JSON.stringify(e));
await db.$disconnect(); await db.$disconnect();
process.exit(1); process.exit(1);
}); });
...@@ -215,6 +217,7 @@ async function assignments() { ...@@ -215,6 +217,7 @@ async function assignments() {
'issue_branch_template' : null, 'issue_branch_template' : null,
'autoclose_referenced_issues' : true 'autoclose_referenced_issues' : true
}, },
gitlabCreationDate: new Date('2021-10-14T14:20:15.239Z'),
gitlabLastInfo : { gitlabLastInfo : {
'id' : 13862, 'id' : 13862,
'description' : 'Dojo assignment repository.\n\nName of the assignment: c_hello_world', 'description' : 'Dojo assignment repository.\n\nName of the assignment: c_hello_world',
...@@ -338,7 +341,8 @@ async function assignments() { ...@@ -338,7 +341,8 @@ async function assignments() {
'autoclose_referenced_issues' : true 'autoclose_referenced_issues' : true
}, },
gitlabLastInfoDate: new Date('2021-10-14T14:20:15.239Z'), gitlabLastInfoDate: new Date('2021-10-14T14:20:15.239Z'),
published : false published : false,
secret : '77B9BB29-82F8-4D1F-B6D2-A201ABFB8509'
} }
}); });
await db.assignment.upsert({ await db.assignment.upsert({
...@@ -470,6 +474,7 @@ async function assignments() { ...@@ -470,6 +474,7 @@ async function assignments() {
'issue_branch_template' : null, 'issue_branch_template' : null,
'autoclose_referenced_issues' : true 'autoclose_referenced_issues' : true
}, },
gitlabCreationDate: new Date('2023-11-07T20:35:54.692Z'),
gitlabLastInfo : { gitlabLastInfo : {
'id' : 13893, 'id' : 13893,
'description' : 'Dojo assignment repository.\n\nName of the assignment: Technique de compilation - TP', 'description' : 'Dojo assignment repository.\n\nName of the assignment: Technique de compilation - TP',
...@@ -593,7 +598,8 @@ async function assignments() { ...@@ -593,7 +598,8 @@ async function assignments() {
'autoclose_referenced_issues' : true 'autoclose_referenced_issues' : true
}, },
gitlabLastInfoDate: new Date('2023-11-07T20:35:54.692Z'), 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() { ...@@ -763,6 +769,7 @@ async function exercises() {
'issue_branch_template' : null, 'issue_branch_template' : null,
'autoclose_referenced_issues' : true 'autoclose_referenced_issues' : true
}, },
gitlabCreationDate: new Date('2023-12-14T14:54:35.692Z'),
gitlabLastInfo : { gitlabLastInfo : {
'id' : 14232, 'id' : 14232,
'description' : 'Dojo exercise repository based on the the assignment: Technique de compilation - TP', 'description' : 'Dojo exercise repository based on the the assignment: Technique de compilation - TP',
...@@ -1579,4 +1586,383 @@ async function results() { ...@@ -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 SharedConfig from './shared/config/SharedConfig.js';
import ClusterManager from './process/ClusterManager'; import WorkerRole from './process/WorkerRole.js';
import API from './express/API'; import ClusterManager from './process/ClusterManager.js';
import HttpManager from './managers/HttpManager'; import API from './express/API.js';
import HttpManager from './managers/HttpManager.js';
HttpManager.registerAxiosInterceptor(); HttpManager.registerAxiosInterceptor();
(new ClusterManager([ { if ( SharedConfig.production ) {
role : WorkerRole.API, (new ClusterManager([ {
quantity : ClusterManager.CORES, role : WorkerRole.API,
restartOnFail: true, quantity : ClusterManager.CORES,
loadTask : () => { restartOnFail: true,
return new API(); loadTask : () => new API()
} } ])).run();
} ])).run(); } else {
(new API()).run();
}
import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { Exercise } from '../types/DatabaseTypes'; import { Exercise } from '../types/DatabaseTypes.js';
import JSON5 from 'json5'; import JSON5 from 'json5';
import GitlabVisibility from '../shared/types/Gitlab/GitlabVisibility.js';
type ConfigGitlabBadge = { type ConfigGitlabBadge = {
...@@ -19,7 +19,7 @@ class Config { ...@@ -19,7 +19,7 @@ class Config {
version: { version: {
[client: string]: string [client: string]: string
} }
}; // { version: { CLIENT: CONDITION } } };
public readonly dojoCLI: { public readonly dojoCLI: {
versionUpdatePeriodMs: number versionUpdatePeriodMs: number
...@@ -33,18 +33,26 @@ class Config { ...@@ -33,18 +33,26 @@ class Config {
public readonly login: { public readonly login: {
gitlab: { gitlab: {
client: { client: {
secret: string id: string, secret: string
}, url: {
redirect: string, token: string
} }
} }
}; };
public readonly dockerhub: {
repositories: {
assignmentChecker: string, exerciseChecker: string
}
};
public readonly gitlab: { public readonly gitlab: {
urls: Array<string>; repository: { url: string; urls: Array<string>; repository: {
timeoutAfterCreation: number; timeoutAfterCreation: number;
}; account: { }; account: {
id: number; username: string; token: string; id: number; username: string; token: string;
}; group: { }; group: {
root: number; templates: number; assignments: number; exercises: number; root: number; templates: number; assignments: number; exercises: number; deletedAssignments: number; deletedExercises: number;
}, badges: { }, badges: {
pipeline: ConfigGitlabBadge pipeline: ConfigGitlabBadge
} }
...@@ -52,19 +60,19 @@ class Config { ...@@ -52,19 +60,19 @@ class Config {
public readonly assignment: { public readonly assignment: {
default: { 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 }; baseFiles: Array<string>; filename: string
}; };
public readonly exercise: { public readonly exercise: {
maxSameName: number; maxPerAssignment: number; resultsFolder: string, pipelineResultsFolder: string; default: { maxSameName: number; maxPerAssignment: number; resultsFolder: string, pipelineResultsFolder: string; default: {
description: string; visibility: string; description: string; visibility: GitlabVisibility;
}; };
}; };
constructor() { constructor() {
this.api = { 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": {}}'); this.requestClientValidation = JSON5.parse(process.env.REQUEST_CLIENT_VALIDATION || '{"version": {}}');
...@@ -82,12 +90,25 @@ class Config { ...@@ -82,12 +90,25 @@ class Config {
this.login = { this.login = {
gitlab: { gitlab: {
client: { client: {
id : process.env.LOGIN_GITLAB_CLIENT_ID || '',
secret: process.env.LOGIN_GITLAB_CLIENT_SECRET || '' 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 = { this.gitlab = {
url : process.env.GITLAB_URL || '',
urls : JSON5.parse(process.env.GITLAB_URLS || '[]'), urls : JSON5.parse(process.env.GITLAB_URLS || '[]'),
account : { account : {
id : Number(process.env.GITLAB_DOJO_ACCOUNT_ID || 0), id : Number(process.env.GITLAB_DOJO_ACCOUNT_ID || 0),
...@@ -98,10 +119,12 @@ class Config { ...@@ -98,10 +119,12 @@ class Config {
timeoutAfterCreation: Number(process.env.GITLAB_REPOSITORY_CREATION_TIMEOUT || 5000) timeoutAfterCreation: Number(process.env.GITLAB_REPOSITORY_CREATION_TIMEOUT || 5000)
}, },
group : { group : {
root : Number(process.env.GITLAB_GROUP_ROOT_ID || 0), root : Number(process.env.GITLAB_GROUP_ROOT_ID || 0),
templates : Number(process.env.GITLAB_GROUP_TEMPLATES_ID || 0), templates : Number(process.env.GITLAB_GROUP_TEMPLATES_ID || 0),
assignments: Number(process.env.GITLAB_GROUP_ASSIGNMENTS_ID || 0), assignments : Number(process.env.GITLAB_GROUP_ASSIGNMENTS_ID || 0),
exercises : Number(process.env.GITLAB_GROUP_EXERCISES_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 : { badges : {
pipeline: { pipeline: {
...@@ -116,7 +139,7 @@ class Config { ...@@ -116,7 +139,7 @@ class Config {
description : process.env.ASSIGNMENT_DEFAULT_DESCRIPTION?.convertWithEnvVars() ?? '', description : process.env.ASSIGNMENT_DEFAULT_DESCRIPTION?.convertWithEnvVars() ?? '',
initReadme : process.env.ASSIGNMENT_DEFAULT_INIT_README?.toBoolean() ?? false, initReadme : process.env.ASSIGNMENT_DEFAULT_INIT_README?.toBoolean() ?? false,
sharedRunnersEnabled: process.env.ASSIGNMENT_DEFAULT_SHARED_RUNNERS_ENABLED?.toBoolean() ?? true, 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, 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) ?? '' template : process.env.ASSIGNMENT_DEFAULT_TEMPLATE?.replace('{{USERNAME}}', this.gitlab.account.username).replace('{{TOKEN}}', this.gitlab.account.token) ?? ''
}, },
...@@ -131,7 +154,7 @@ class Config { ...@@ -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 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 : { default : {
description: process.env.EXERCISE_DEFAULT_DESCRIPTION?.convertWithEnvVars() ?? '', 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 { getReasonPhrase, StatusCodes } from 'http-status-codes';
import * as jwt from 'jsonwebtoken'; import * as jwt from 'jsonwebtoken';
import { JwtPayload } from 'jsonwebtoken'; import { JwtPayload } from 'jsonwebtoken';
import Config from '../config/Config'; import Config from '../config/Config.js';
import express from 'express'; import express from 'express';
import UserManager from '../managers/UserManager'; import UserManager from '../managers/UserManager.js';
import { User } from '../types/DatabaseTypes'; import { User } from '../types/DatabaseTypes.js';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse'; import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse.js';
class Session { class Session {
...@@ -19,30 +19,27 @@ class Session { ...@@ -19,30 +19,27 @@ class Session {
this._profile = newProfile; this._profile = newProfile;
} }
constructor() { }
async initSession(req: express.Request, res: express.Response) { async initSession(req: express.Request, res: express.Response) {
const authorization = req.headers.authorization; const authorization = req.headers.authorization;
if ( authorization ) { if ( authorization && authorization.startsWith('Bearer ') ) {
if ( authorization.startsWith('Bearer ') ) { const jwtToken = authorization.replace('Bearer ', '');
const jwtToken = authorization.replace('Bearer ', '');
try { try {
const jwtData = jwt.verify(jwtToken, Config.jwtConfig.secret) as JwtPayload; const jwtData = jwt.verify(jwtToken, Config.jwtConfig.secret) as JwtPayload;
if ( jwtData.profile ) { if ( jwtData.profile ) {
this.profile = jwtData.profile; this.profile = jwtData.profile;
this.profile = await UserManager.getById(this.profile.id!) ?? this.profile; this.profile = await UserManager.getById(this.profile.id) ?? this.profile;
}
} catch ( err ) {
res.sendStatus(StatusCodes.UNAUTHORIZED).end();
} }
} catch ( err ) {
res.sendStatus(StatusCodes.UNAUTHORIZED).end();
} }
} }
} }
private static getToken(profileJson: unknown): string | null { 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>> { private async getResponse<T>(code: number, data: T, descriptionOverride?: string): Promise<DojoBackendResponse<T>> {
...@@ -67,12 +64,14 @@ class Session { ...@@ -67,12 +64,14 @@ class Session {
Send a response to the client 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 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) { sendResponse(res: express.Response | undefined, code: number, data?: unknown, descriptionOverride?: string, internalCode?: number) {
Promise.resolve(data).then((toReturn: unknown) => { if ( res ) {
this.getResponse(internalCode ?? code, toReturn, descriptionOverride).then(response => { Promise.resolve(data).then((toReturn: unknown) => {
res.status(code).json(response); this.getResponse(internalCode ?? code, toReturn, descriptionOverride).then(response => {
res.status(code).json(response);
});
}); });
}); }
} }
} }
......
import { Express } from 'express-serve-static-core'; import { Express } from 'express-serve-static-core';
import cors from 'cors'; import cors from 'cors';
import morganMiddleware from '../logging/MorganMiddleware'; import morganMiddleware from '../logging/MorganMiddleware.js';
import { AddressInfo } from 'net'; import { AddressInfo } from 'net';
import http from 'http'; import http from 'http';
import helmet from 'helmet'; import helmet from 'helmet';
import express from 'express'; import express from 'express';
import WorkerTask from '../process/WorkerTask'; import WorkerTask from '../process/WorkerTask.js';
import multer from 'multer'; import multer from 'multer';
import SessionMiddleware from '../middlewares/SessionMiddleware'; import SessionMiddleware from '../middlewares/SessionMiddleware.js';
import Config from '../config/Config'; import Config from '../config/Config.js';
import logger from '../shared/logging/WinstonLogger'; import logger from '../shared/logging/WinstonLogger.js';
import ParamsCallbackManager from '../middlewares/ParamsCallbackManager'; import ParamsCallbackManager from '../middlewares/ParamsCallbackManager.js';
import ApiRoutesManager from '../routes/ApiRoutesManager'; import ApiRoutesManager from '../routes/ApiRoutesManager.js';
import compression from 'compression'; import compression from 'compression';
import ClientVersionCheckerMiddleware from '../middlewares/ClientVersionCheckerMiddleware'; import ClientVersionCheckerMiddleware from '../middlewares/ClientVersionCheckerMiddleware.js';
import swaggerUi from 'swagger-ui-express'; import swaggerUi from 'swagger-ui-express';
import path from 'path'; import path from 'path';
import DojoCliVersionHelper from '../helpers/DojoCliVersionHelper';
class API implements WorkerTask { class API implements WorkerTask {
private readonly backend: Express; private readonly backend: Express;
private server: http.Server | undefined; private server!: http.Server;
constructor() { constructor() {
this.backend = express(); this.backend = express();
...@@ -45,10 +44,15 @@ class API implements WorkerTask { ...@@ -45,10 +44,15 @@ class API implements WorkerTask {
this.backend.use(cors()); //Allow CORS requests this.backend.use(cors()); //Allow CORS requests
this.backend.use(compression()); //Compress responses this.backend.use(compression()); //Compress responses
this.backend.use(async (req, res, next) => { /*
res.header('dojocli-latest-version', await DojoCliVersionHelper.getLatestVersion()); this.backend.use((_req, res, next) => {
next();
DojoCliVersionHelper.getLatestVersion().then(latestVersion => {
res.header('dojocli-latest-version', latestVersion);
next();
});
}); });
*/
} }
private initOpenAPI() { private initOpenAPI() {
...@@ -59,9 +63,9 @@ class API implements WorkerTask { ...@@ -59,9 +63,9 @@ class API implements WorkerTask {
url: '../OpenAPI.yaml' 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.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) => { this.backend.get('/docs/', (req, res) => {
const prefix = req.url.slice(-1) === '/' ? '' : 'docs/'; const prefix = req.url.slice(-1) === '/' ? '' : 'docs/';
...@@ -89,7 +93,7 @@ class API implements WorkerTask { ...@@ -89,7 +93,7 @@ class API implements WorkerTask {
const { const {
port, port,
address address
} = this.server!.address() as AddressInfo; } = this.server.address() as AddressInfo;
logger.info(`Server started on http://${ address }:${ port }`); logger.info(`Server started on http://${ address }:${ port }`);
}); });
} }
......
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import logger from '../shared/logging/WinstonLogger'; import logger from '../shared/logging/WinstonLogger.js';
import UserQueryExtension from './Prisma/Extensions/UserQueryExtension'; import UserQueryExtension from './Prisma/Extensions/UserQueryExtension.js';
import UserResultExtension from './Prisma/Extensions/UserResultExtension'; import UserResultExtension from './Prisma/Extensions/UserResultExtension.js';
import AssignmentResultExtension from './Prisma/Extensions/AssignmentResultExtension'; import AssignmentResultExtension from './Prisma/Extensions/AssignmentResultExtension.js';
import ExerciseResultExtension from './Prisma/Extensions/ExerciseResultExtension'; import ExerciseResultExtension from './Prisma/Extensions/ExerciseResultExtension.js';
const prisma = new PrismaClient({ const prisma = new PrismaClient({
...@@ -31,7 +31,7 @@ prisma.$on('warn', e => logger.warn(`Prisma => ${ e.message }`)); ...@@ -31,7 +31,7 @@ prisma.$on('warn', e => logger.warn(`Prisma => ${ e.message }`));
prisma.$on('error', e => logger.error(`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; export default DatabaseHelper;
\ No newline at end of file \ No newline at end of file
import Config from '../config/Config'; import Config from '../config/Config.js';
import GitlabRelease from '../shared/types/Gitlab/GitlabRelease'; import GitlabManager from '../managers/GitlabManager.js';
import GitlabManager from '../managers/GitlabManager'; import * as Gitlab from '@gitbeaker/rest';
class DojoCliVersionHelper { class DojoCliVersionHelper {
private latestUpdate: Date | undefined; private latestUpdate: Date | undefined;
private latestVersion: string | undefined; private latestVersion: string | undefined;
constructor() { }
private async updateVersion(): Promise<void> { 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 ) { for ( const release of releases ) {
if ( !isNaN(+release.tag_name.replace('.', '')) ) { if ( !isNaN(+release.tag_name.replace('.', '')) ) {
this.latestVersion = release.tag_name; this.latestVersion = release.tag_name;
......
import LazyVal from '../shared/helpers/LazyVal'; import LazyVal from '../shared/helpers/LazyVal.js';
class DojoModelsHelper { class DojoModelsHelper {
...@@ -9,8 +9,7 @@ class DojoModelsHelper { ...@@ -9,8 +9,7 @@ class DojoModelsHelper {
* @param depth The depth of the search for LazyVal instances * @param depth The depth of the search for LazyVal instances
*/ */
async getFullSerializableObject<T extends NonNullable<unknown>>(obj: T, depth: number = 0): Promise<unknown> { async getFullSerializableObject<T extends NonNullable<unknown>>(obj: T, depth: number = 0): Promise<unknown> {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const result: { [key: string]: unknown } = {};
const result: any = {};
for ( const key in obj ) { for ( const key in obj ) {
let value: unknown = obj[key]; 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 { StatusCodes } from 'http-status-codes';
import { CustomValidator, ErrorMessage, FieldMessageFactory, Meta } from 'express-validator/src/base';
import { BailOptions, ValidationChain } from 'express-validator/src/chain'; import { BailOptions, ValidationChain } from 'express-validator/src/chain';
import GitlabManager from '../managers/GitlabManager'; import { Language } from '@prisma/client';
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';
declare type DojoMeta = Meta & { declare type DojoMeta = Meta & {
...@@ -31,11 +34,11 @@ class DojoValidators { ...@@ -31,11 +34,11 @@ class DojoValidators {
} }
readonly nullSanitizer = this.toValidatorSchemaOptions({ readonly nullSanitizer = this.toValidatorSchemaOptions({
options: (value) => { options: value => {
try { try {
return value == 'null' || value == 'undefined' || value == '' ? null : value; return value === 'null' || value === 'undefined' || value === '' ? null : value;
} catch ( error ) { } catch ( error ) {
logger.error(`null sanitizer error: ${ error }`); logger.error(`null sanitizer error: ${ JSON.stringify(error) }`);
return value; return value;
} }
...@@ -43,9 +46,9 @@ class DojoValidators { ...@@ -43,9 +46,9 @@ class DojoValidators {
}); });
readonly jsonSanitizer = this.toValidatorSchemaOptions({ readonly jsonSanitizer = this.toValidatorSchemaOptions({
options: (value) => { options: value => {
try { try {
return JSON.parse(value as string); return JSON.parse(value as string) as unknown;
} catch ( e ) { } catch ( e ) {
return value; return value;
} }
...@@ -62,8 +65,8 @@ class DojoValidators { ...@@ -62,8 +65,8 @@ class DojoValidators {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const template = this.getParamValue(req, path) as string; const template = this.getParamValue(req, path) as string;
if ( template ) { if ( template ) {
GitlabManager.checkTemplateAccess(template, req).then((templateAccess) => { GitlabManager.checkTemplateAccess(template, req).then(templateAccess => {
templateAccess !== StatusCodes.OK ? reject() : resolve(true); templateAccess ? resolve(true) : reject();
}); });
} }
resolve(true); resolve(true);
...@@ -78,13 +81,14 @@ class DojoValidators { ...@@ -78,13 +81,14 @@ class DojoValidators {
}) => { }) => {
try { try {
const template = this.getParamValue(req, path); const template = this.getParamValue(req, path);
if ( template ) { if ( template && Toolbox.isString(template) ) {
return `${ Config.gitlab.urls[0].replace(/^([a-z]{3,5}:\/{2})?(.*)/, `$1${ Config.gitlab.account.username }:${ Config.gitlab.account.token }@$2`) }${ template }.git`; 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 { } else {
return Config.assignment.default.template; return Config.assignment.default.template;
} }
} catch ( error ) { } catch ( error ) {
logger.error(`Template url sanitizer error: ${ error }`); logger.error(`Template url sanitizer error: ${ JSON.stringify(error) }`);
return value; return value;
} }
...@@ -121,7 +125,7 @@ class DojoValidators { ...@@ -121,7 +125,7 @@ class DojoValidators {
if ( exerciseIdOrUrl ) { if ( exerciseIdOrUrl ) {
ParamsCallbackManager.initBoundParams(req); ParamsCallbackManager.initBoundParams(req);
ExerciseManager.get(exerciseIdOrUrl).then((exercise) => { ExerciseManager.get(exerciseIdOrUrl).then(exercise => {
req.boundParams.exercise = exercise; req.boundParams.exercise = exercise;
exercise !== undefined ? resolve(true) : reject(); exercise !== undefined ? resolve(true) : reject();
...@@ -134,6 +138,23 @@ class DojoValidators { ...@@ -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 express from 'express';
import GitlabRepository from '../shared/types/Gitlab/GitlabRepository'; import logger from '../shared/logging/WinstonLogger.js';
import logger from '../shared/logging/WinstonLogger'; import GitlabManager from '../managers/GitlabManager.js';
import GitlabManager from '../managers/GitlabManager'; import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import { AxiosError } from 'axios'; import { StatusCodes } from 'http-status-codes';
import { StatusCodes } from 'http-status-codes'; import { GitbeakerRequestError } from '@gitbeaker/requester-utils';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; import * as Gitlab from '@gitbeaker/rest';
import SonarProjectCreation from '../shared/types/Sonar/SonarProjectCreation';
import SonarManager from '../managers/SonarManager';
class GlobalHelper { class GlobalHelper {
async repositoryCreationError(message: string, error: unknown, req: express.Request, res: express.Response, gitlabError: DojoStatusCode, internalError: DojoStatusCode, repositoryToRemove?: GitlabRepository): Promise<void> { repoCreationFnExecCreator(req: express.Request, res: express.Response, gitlabError: DojoStatusCode, internalError: DojoStatusCode, repositoryToRemove?: Gitlab.ProjectSchema, sonarToRemove?: SonarProjectCreation) {
logger.error(message); return async (toExec: () => Promise<unknown>, errorMessage?: string) => {
logger.error(error); try {
return await toExec();
} catch ( error ) {
if ( errorMessage ) {
logger.error(errorMessage);
logger.error(JSON.stringify(error));
try { try {
if ( repositoryToRemove ) { if ( repositoryToRemove ) {
await GitlabManager.deleteRepository(repositoryToRemove.id); 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 undefined;
return req.session.sendResponse(res, StatusCodes.INTERNAL_SERVER_ERROR, {}, `Unknown gitlab error: ${ message }`, gitlabError); };
} }
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 { Prisma } from '@prisma/client';
import { Exercise } from '../../../types/DatabaseTypes'; import { Exercise } from '../../../types/DatabaseTypes.js';
import db from '../../DatabaseHelper'; import db from '../../DatabaseHelper.js';
import LazyVal from '../../../shared/helpers/LazyVal'; import LazyVal from '../../../shared/helpers/LazyVal.js';
async function getCorrections(assignment: { name: string }): Promise<Array<Partial<Exercise>> | undefined> { async function getCorrections(assignment: { name: string }): Promise<Array<Partial<Exercise>> | undefined> {
...@@ -30,9 +30,7 @@ export default Prisma.defineExtension(client => { ...@@ -30,9 +30,7 @@ export default Prisma.defineExtension(client => {
assignment: { assignment: {
corrections: { corrections: {
compute(assignment) { compute(assignment) {
return new LazyVal<Array<Partial<Exercise>> | undefined>(() => { return new LazyVal<Array<Partial<Exercise>> | undefined>(() => assignment.published ? getCorrections(assignment) : []);
return getCorrections(assignment);
});
} }
} }
} }
......
import { Prisma, UserRole } from '@prisma/client'; import { Prisma, UserRole } from '@prisma/client';
import LazyVal from '../../../shared/helpers/LazyVal'; import LazyVal from '../../../shared/helpers/LazyVal.js';
import GitlabUser from '../../../shared/types/Gitlab/GitlabUser'; import * as Gitlab from '@gitbeaker/rest';
import GitlabManager from '../../../managers/GitlabManager'; import GitlabManager from '../../../managers/GitlabManager.js';
export default Prisma.defineExtension(client => { export default Prisma.defineExtension(client => {
...@@ -13,7 +13,7 @@ export default Prisma.defineExtension(client => { ...@@ -13,7 +13,7 @@ export default Prisma.defineExtension(client => {
role: true role: true
}, },
compute(user) { compute(user) {
return user.role == UserRole.TEACHING_STAFF || user.role == UserRole.ADMIN; return user.role === UserRole.TEACHING_STAFF || user.role === UserRole.ADMIN;
} }
}, },
isAdmin : { isAdmin : {
...@@ -21,14 +21,12 @@ export default Prisma.defineExtension(client => { ...@@ -21,14 +21,12 @@ export default Prisma.defineExtension(client => {
role: true role: true
}, },
compute(user) { compute(user) {
return user.role == UserRole.ADMIN; return user.role === UserRole.ADMIN;
} }
}, },
gitlabProfile : { gitlabProfile : {
compute(user) { compute(user) {
return new LazyVal<GitlabUser | undefined>(() => { return new LazyVal<Gitlab.UserSchema | undefined>(() => GitlabManager.getUserById(user.id));
return 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 morgan, { StreamOptions } from 'morgan';
import logger from '../shared/logging/WinstonLogger'; import logger from '../shared/logging/WinstonLogger.js';
const stream: StreamOptions = { const stream: StreamOptions = {
write: (message) => logger.http(message) write: message => logger.http(message)
}; };
const skip = () => { const skip = () => false;
return false; //SharedConfig.production;
};
const morganMiddleware = morgan(':method :url :status :res[content-length] - :response-time ms', { const morganMiddleware = morgan(':method :url :status :res[content-length] - :response-time ms', {
stream, stream,
......
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { Assignment, User } from '../types/DatabaseTypes'; import { Assignment, User } from '../types/DatabaseTypes.js';
import db from '../helpers/DatabaseHelper'; import db from '../helpers/DatabaseHelper.js';
class AssignmentManager { class AssignmentManager {
async isUserAllowedToAccessAssignment(assignment: Assignment, user: User): Promise<boolean> { async isUserAllowedToAccessAssignment(assignment: Assignment, user: User): Promise<boolean> {
if (user === null || user === undefined) {
return false;
}
if ( !assignment.staff ) { if ( !assignment.staff ) {
assignment.staff = await db.assignment.findUnique({ assignment.staff = await db.assignment.findUnique({
where: { where: {
...@@ -17,21 +20,32 @@ class AssignmentManager { ...@@ -17,21 +20,32 @@ class AssignmentManager {
async getByName(name: string, include: Prisma.AssignmentInclude | undefined = undefined): Promise<Assignment | undefined> { async getByName(name: string, include: Prisma.AssignmentInclude | undefined = undefined): Promise<Assignment | undefined> {
return await db.assignment.findUnique({ return await db.assignment.findUnique({
where : { where : {
name: name name: name
}, include: include },
include: include
}) as unknown as Assignment ?? undefined; }) as unknown as Assignment ?? undefined;
} }
getByGitlabLink(gitlabLink: string, include: Prisma.AssignmentInclude | undefined = undefined): Promise<Assignment | undefined> { async getByGitlabLink(gitlabLink: string, include: Prisma.AssignmentInclude | undefined = undefined): Promise<Assignment | undefined> {
const name = gitlabLink.replace('.git', '').split('/').pop()!; const nameInUrl = gitlabLink.replace('.git', '').split('/').pop()!;
const result = await db.assignment.findMany({
return this.getByName(name, include); 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> { 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 if ( nameOrUrl.includes('://') ) {
return this.getByGitlabLink(nameOrUrl, include); return this.getByGitlabLink(nameOrUrl, include);
} else {
return this.getByName(nameOrUrl, include);
}
} }
} }
......
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { Exercise } from '../types/DatabaseTypes'; import { Exercise, User } from '../types/DatabaseTypes.js';
import db from '../helpers/DatabaseHelper'; import db from '../helpers/DatabaseHelper.js';
class ExerciseManager { class ExerciseManager {
...@@ -15,13 +15,30 @@ class ExerciseManager { ...@@ -15,13 +15,30 @@ class ExerciseManager {
}) as unknown as Exercise ?? undefined; }) as unknown as Exercise ?? undefined;
} }
async getFromAssignment(assignmentName: string, include: Prisma.ExerciseInclude | undefined = undefined): Promise<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
return await db.exercise.findMany({ getFromAssignment(assignmentName: string, deleteFilter: boolean | undefined = undefined, include: Prisma.ExerciseInclude | undefined = undefined): Promise<Array<Exercise>> {
where : { return db.exercise.findMany({
assignmentName: assignmentName where : { assignmentName: assignmentName, ...(deleteFilter !== undefined ? { deleted: deleteFilter } : {}) },
}, include: include
include: include }) as Promise<Array<Exercise>>;
}) as Array<Exercise> ?? undefined; }
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;
} }
} }
......