diff --git a/NodeApp/package-lock.json b/NodeApp/package-lock.json index 1337801ee848bc94b5964f1db8653f732a687c17..ba7bd845261fe22f359c5c422f92c34a7d1d9420 100644 --- a/NodeApp/package-lock.json +++ b/NodeApp/package-lock.json @@ -17,6 +17,7 @@ "commander": "^11.1.0", "dotenv": "^16.3.1", "dotenv-expand": "^10.0.0", + "form-data": "^4.0.0", "fs-extra": "^11.2.0", "http-status-codes": "^2.3.0", "inquirer": "^8.2.6", diff --git a/NodeApp/package.json b/NodeApp/package.json index 06adaf3e94bcefedc33f493eb1999fafbd25d851..87b5a5fcb2eda88e01efea2f3a8e4cdcd0b61c1b 100644 --- a/NodeApp/package.json +++ b/NodeApp/package.json @@ -41,6 +41,7 @@ "commander" : "^11.1.0", "dotenv" : "^16.3.1", "dotenv-expand" : "^10.0.0", + "form-data" : "^4.0.0", "fs-extra" : "^11.2.0", "http-status-codes" : "^2.3.0", "inquirer" : "^8.2.6", @@ -51,6 +52,7 @@ "semver" : "^7.5.4", "tar-stream" : "^3.1.6", "winston" : "^3.11.0", + "winston-transport" : "^4.7.0", "yaml" : "^2.3.4", "zod" : "^3.22.4", "zod-validation-error": "^3.0.0" diff --git a/NodeApp/src/app.ts b/NodeApp/src/app.ts index be01e40ef06b23244940be8b6e228a38344a74d3..2f9948806c16dcbecd7fce1b7b94e9317f4383a0 100644 --- a/NodeApp/src/app.ts +++ b/NodeApp/src/app.ts @@ -20,4 +20,4 @@ import HttpManager from './managers/HttpManager'; HttpManager.registerAxiosInterceptor(); -new CommanderApp(); \ No newline at end of file +(new CommanderApp()).parse(); \ No newline at end of file diff --git a/NodeApp/src/commander/CommanderApp.ts b/NodeApp/src/commander/CommanderApp.ts index b944b9e4f902307c3d8d3d469fa8ba7cb9edecd6..1b117859ab168b2d16bab98a778e94ca54b14579 100644 --- a/NodeApp/src/commander/CommanderApp.ts +++ b/NodeApp/src/commander/CommanderApp.ts @@ -18,21 +18,21 @@ class CommanderApp { constructor() { this.program - .name('dojo') - .description('CLI of the Dojo application') - .version('{{VERSION}}') - .showHelpAfterError() - .configureHelp({ - showGlobalOptions: true, - sortOptions : true, - sortSubcommands : true - }) - .option('-H, --host <string>', 'override the Dojo API endpoint', ClientsSharedConfig.apiURL) - .option('-I, --interactive', 'show interactive interface when available', Config.interactiveMode) - .addOption(new Option('--debug').hideHelp()) - .hook('preAction', () => { - this.warnDevelopmentVersion(); - }).hook('postAction', () => { + .name('dojo') + .description('CLI of the Dojo application') + .version('{{VERSION}}') + .showHelpAfterError() + .configureHelp({ + showGlobalOptions: true, + sortOptions : true, + sortSubcommands : true + }) + .option('-H, --host <string>', 'override the Dojo API endpoint', ClientsSharedConfig.apiURL) + .option('-I, --interactive', 'show interactive interface when available', Config.interactiveMode) + .addOption(new Option('--debug').hideHelp()) + .hook('preAction', () => { + this.warnDevelopmentVersion(); + }).hook('postAction', () => { this.informNewVersion(); }); @@ -53,6 +53,10 @@ class CommanderApp { this.program.parse(); } + public parse() { + this.program.parse(); + } + private warnDevelopmentVersion() { if ( !SharedConfig.production ) { console.log(boxen(`This is a development (unstable) version of the DojoCLI. @@ -73,20 +77,18 @@ https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/releases/Latest`, { if ( SharedConfig.production ) { const latestDojoCliVersion = stateConfigFile.getParam('latestDojoCliVersion') as string | null || '0.0.0'; const latestDojoCliVersionNotification = stateConfigFile.getParam('latestDojoCliVersionNotification') as number | null || 0; - if ( semver.lt(version, latestDojoCliVersion) ) { - if ( (new Date()).getTime() - latestDojoCliVersionNotification >= Config.versionUpdateInformationPeriodHours * 60 * 60 * 1000 ) { - console.log(boxen(`The ${ latestDojoCliVersion } version of the DojoCLI is available: + if ( semver.lt(version, latestDojoCliVersion) && (new Date()).getTime() - latestDojoCliVersionNotification >= Config.versionUpdateInformationPeriodHours * 60 * 60 * 1000 ) { + console.log(boxen(`The ${ latestDojoCliVersion } version of the DojoCLI is available: https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/releases/Latest`, { - title : 'Information', - titleAlignment: 'center', - borderColor : 'blue', - borderStyle : 'bold', - margin : 1, - padding : 1, - textAlignment : 'left' - })); - stateConfigFile.setParam('latestDojoCliVersionNotification', (new Date()).getTime()); - } + title : 'Information', + titleAlignment: 'center', + borderColor : 'blue', + borderStyle : 'bold', + margin : 1, + padding : 1, + textAlignment : 'left' + })); + stateConfigFile.setParam('latestDojoCliVersionNotification', (new Date()).getTime()); } } } diff --git a/NodeApp/src/commander/CommanderCommand.ts b/NodeApp/src/commander/CommanderCommand.ts index 258606926813aed8e554614cfb5011f767f3accd..4a34ad5c3e5ecc33c26d5fb245ad42e18c911421 100644 --- a/NodeApp/src/commander/CommanderCommand.ts +++ b/NodeApp/src/commander/CommanderCommand.ts @@ -18,7 +18,12 @@ abstract class CommanderCommand { protected abstract defineCommand(): void; - protected defineSubCommands() {} + protected defineSubCommands() { + /* + * No action + * Override this method only if you need to define subcommands + * */ + } protected abstract commandAction(...args: Array<unknown>): Promise<void>; } diff --git a/NodeApp/src/commander/assignment/AssignmentCommand.ts b/NodeApp/src/commander/assignment/AssignmentCommand.ts index b54c2cb00fcd0622f818ed9c621b823d9b7699e4..d306aca8c891a2958298d476df4c20bf9ab38e55 100644 --- a/NodeApp/src/commander/assignment/AssignmentCommand.ts +++ b/NodeApp/src/commander/assignment/AssignmentCommand.ts @@ -12,7 +12,7 @@ class AssignmentCommand extends CommanderCommand { protected defineCommand() { this.command - .description('manage an assignment'); + .description('manage an assignment'); } protected defineSubCommands() { @@ -24,7 +24,9 @@ class AssignmentCommand extends CommanderCommand { AssignmentCorrectionCommand.registerOnCommand(this.command); } - protected async commandAction(): Promise<void> { } + protected async commandAction(): Promise<void> { + // No action + } } diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentCheckCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentCheckCommand.ts index c83a3d16aa5900ecd5e1f2a04648f3bdef1abc1f..0f5a726119df1220275af13dcb706abe7c44a0de 100644 --- a/NodeApp/src/commander/assignment/subcommands/AssignmentCheckCommand.ts +++ b/NodeApp/src/commander/assignment/subcommands/AssignmentCheckCommand.ts @@ -10,15 +10,69 @@ import GlobalHelper from '../../../helpers/GlobalHelper'; class AssignmentCheckCommand extends CommanderCommand { protected commandName: string = 'check'; + protected currentSpinner: ora.Ora = ora(); + private verbose: boolean = false; + private superVerbose: boolean = false; + private buildPhase: boolean | undefined = undefined; protected defineCommand() { GlobalHelper.runCommandDefinition(this.command) - .description('locally run a check of an assignment') - .action(this.commandAction.bind(this)); + .description('locally run a check of an assignment') + .action(this.commandAction.bind(this)); + } + + private logsEvent(log: string, _error: boolean, displayable: boolean, _currentStep: string, currentSubStep: string) { + for ( const line of log.split('\n') ) { + if ( displayable && this.buildPhase === undefined && line.startsWith('#') ) { + this.buildPhase = true; + } + + if ( currentSubStep === 'COMPOSE_RUN' && this.buildPhase === true && line !== '' && !line.startsWith('#') ) { + this.buildPhase = false; + } + + if ( SharedConfig.debug || (displayable && (this.superVerbose || this.buildPhase === false)) ) { + console.log(line); + } + } + } + + private subStepEvent(name: string, message: string) { + this.currentSpinner = ora({ + text : message, + indent: 4 + }).start(); + + if ( this.verbose && name === 'COMPOSE_RUN' ) { + this.currentSpinner.info(); + } + } + + private endSubStepEvent(stepName: string, message: string, error: boolean) { + if ( error ) { + if ( this.verbose && stepName === 'COMPOSE_RUN' ) { + ora({ + text : message, + indent: 4 + }).start().fail(); + } else { + this.currentSpinner.fail(message); + } + } else { + if ( this.verbose && stepName === 'COMPOSE_RUN' ) { + ora({ + text : message, + indent: 4 + }).start().succeed(); + } else { + this.currentSpinner.succeed(message); + } + } } protected async commandAction(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> { - const verbose: boolean = options.verbose || options.superVerbose || SharedConfig.debug; + this.superVerbose = options.superVerbose; + this.verbose = options.verbose || options.superVerbose || SharedConfig.debug; const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise; @@ -26,69 +80,19 @@ class AssignmentCheckCommand extends CommanderCommand { try { await new Promise<void>((resolve, reject) => { - let spinner: ora.Ora; - - if ( verbose ) { - let buildPhase: boolean | undefined = undefined; - assignmentValidator.events.on('logs', (log: string, error: boolean, displayable: boolean, _currentStep: string, currentSubStep: string) => { - for ( const line of log.split('\n') ) { - if ( displayable && buildPhase == undefined && line.startsWith('#') ) { - buildPhase = true; - } - - if ( currentSubStep == 'COMPOSE_RUN' && buildPhase === true && line != '' && !line.startsWith('#') ) { - buildPhase = false; - } - - if ( SharedConfig.debug || (displayable && (options.superVerbose || buildPhase === false)) ) { - console.log(line); - } - } - }); + if ( this.verbose ) { + assignmentValidator.events.on('logs', this.logsEvent.bind(this)); } - assignmentValidator.events.on('step', (name: string, message: string) => { - console.log(chalk.cyan(message)); - }); + assignmentValidator.events.on('step', (_name: string, message: string) => console.log(chalk.cyan(message))); - assignmentValidator.events.on('subStep', (name: string, message: string) => { - spinner = ora({ - text : message, - indent: 4 - }).start(); + assignmentValidator.events.on('subStep', this.subStepEvent.bind(this)); + + assignmentValidator.events.on('endSubStep', this.endSubStepEvent.bind(this)); + + assignmentValidator.events.on('finished', (success: boolean) => success ? resolve() : reject()); - if ( verbose && name == 'COMPOSE_RUN' ) { - spinner.info(); - } - }); - - assignmentValidator.events.on('endSubStep', (stepName: string, message: string, error: boolean) => { - if ( error ) { - if ( verbose && stepName == 'COMPOSE_RUN' ) { - ora({ - text : message, - indent: 4 - }).start().fail(); - } else { - spinner.fail(message); - } - } else { - if ( verbose && stepName == 'COMPOSE_RUN' ) { - ora({ - text : message, - indent: 4 - }).start().succeed(); - } else { - spinner.succeed(message); - } - } - }); - - assignmentValidator.events.on('finished', (success: boolean) => { - success ? resolve() : reject(); - }); - - assignmentValidator.run(true); + assignmentValidator.run(); }); } catch ( error ) { /* empty */ } diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts index 7844d44021863c0cf3a8c67e1404635778d08ae9..fed344e1177221865e279869473aabd492d28215 100644 --- a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts +++ b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts @@ -14,13 +14,13 @@ class AssignmentCreateCommand extends CommanderCommand { protected defineCommand() { this.command - .description('create a new repository for an assignment') - .requiredOption('-n, --name <name>', 'name of the assignment') - .option('-i, --members_id <ids...>', 'list of gitlab members ids (teaching staff) to add to the repository') - .option('-u, --members_username <usernames...>', 'list of gitlab members username (teaching staff) to add to the repository') - .option('-t, --template <string>', 'id or url of the template (http/s and ssh urls are possible)') - .option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)') - .action(this.commandAction.bind(this)); + .description('create a new repository for an assignment') + .requiredOption('-n, --name <name>', 'name of the assignment') + .option('-i, --members_id <ids...>', 'list of gitlab members ids (teaching staff) to add to the repository') + .option('-u, --members_username <usernames...>', 'list of gitlab members username (teaching staff) to add to the repository') + .option('-t, --template <string>', 'id or url of the template (http/s and ssh urls are possible)') + .option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)') + .action(this.commandAction.bind(this)); } protected async commandAction(options: { name: string, template?: string, members_id?: Array<number>, members_username?: Array<string>, clone?: string | boolean }): Promise<void> { @@ -52,10 +52,10 @@ class AssignmentCreateCommand extends CommanderCommand { templateIdOrNamespace = options.template; if ( Number.isNaN(Number(templateIdOrNamespace)) ) { - templateIdOrNamespace = Toolbox.urlToPath(templateIdOrNamespace as string); + templateIdOrNamespace = Toolbox.urlToPath(templateIdOrNamespace); } - if ( !await DojoBackendManager.checkTemplateAccess(encodeURIComponent(templateIdOrNamespace as string)) ) { + if ( !await DojoBackendManager.checkTemplateAccess(encodeURIComponent(templateIdOrNamespace)) ) { return; } } diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts index 3b44f98a6370b7bd95cd7888897329e8a3021b33..81636fd36ca1ca49c528b234090ada9a9e69c638 100644 --- a/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts +++ b/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts @@ -8,12 +8,12 @@ class AssignmentRunCommand extends CommanderCommand { protected defineCommand() { GlobalHelper.runCommandDefinition(this.command) - .description('locally run the assignment as an exercise') - .action(this.commandAction.bind(this)); + .description('locally run the assignment as an exercise') + .action(this.commandAction.bind(this)); } protected async commandAction(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> { - await ExerciseRunHelper.run(options); + await (new ExerciseRunHelper(options)).run(); } } diff --git a/NodeApp/src/commander/assignment/subcommands/correction/AssignmentCorrectionCommand.ts b/NodeApp/src/commander/assignment/subcommands/correction/AssignmentCorrectionCommand.ts index 3f30a994e7fdcd0a773401db5daa36abba861579..bd7ebce061a2f99b8b1c62d94178e92ce37621b9 100644 --- a/NodeApp/src/commander/assignment/subcommands/correction/AssignmentCorrectionCommand.ts +++ b/NodeApp/src/commander/assignment/subcommands/correction/AssignmentCorrectionCommand.ts @@ -8,7 +8,7 @@ class AssignmentCorrectionCommand extends CommanderCommand { protected defineCommand() { this.command - .description('manage corrections of an assignment'); + .description('manage corrections of an assignment'); } protected defineSubCommands() { @@ -16,7 +16,9 @@ class AssignmentCorrectionCommand extends CommanderCommand { AssignmentCorrectionUpdateCommand.registerOnCommand(this.command); } - protected async commandAction(): Promise<void> { } + protected async commandAction(): Promise<void> { + // No action + } } diff --git a/NodeApp/src/commander/auth/AuthCommand.ts b/NodeApp/src/commander/auth/AuthCommand.ts index c5f4e00afdfa037f1f732c940fee25b34b401822..85fac503754f6994f0f44224088850cea0de64a9 100644 --- a/NodeApp/src/commander/auth/AuthCommand.ts +++ b/NodeApp/src/commander/auth/AuthCommand.ts @@ -9,7 +9,7 @@ class AuthCommand extends CommanderCommand { protected defineCommand() { this.command - .description('manage Dojo and Gitlab sessions'); + .description('manage Dojo and Gitlab sessions'); } protected defineSubCommands() { @@ -18,7 +18,9 @@ class AuthCommand extends CommanderCommand { SessionTestCommand.registerOnCommand(this.command); } - protected async commandAction(): Promise<void> { } + protected async commandAction(): Promise<void> { + // No action + } } diff --git a/NodeApp/src/commander/completion/CompletionCommand.ts b/NodeApp/src/commander/completion/CompletionCommand.ts index c24a7d6f4876f8a20812f6a07f8b5d8c82e9eacb..1ee0668b994796b04d303357de071c7a3b4f75ff 100644 --- a/NodeApp/src/commander/completion/CompletionCommand.ts +++ b/NodeApp/src/commander/completion/CompletionCommand.ts @@ -9,7 +9,7 @@ class CompletionCommand extends CommanderCommand { protected defineCommand() { this.command - .description('generate completions for bash, fish, or zsh'); + .description('generate completions for bash, fish, or zsh'); } protected defineSubCommands() { @@ -18,7 +18,9 @@ class CompletionCommand extends CommanderCommand { CompletionZshCommand.registerOnCommand(this.command); } - protected async commandAction(): Promise<void> { } + protected async commandAction(): Promise<void> { + // No action + } } diff --git a/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts index 1b29eab1a9daa083f89a174f32b1be7e5ea44297..2be5716ee17ae7347d146533f48c8594801b73e5 100644 --- a/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts +++ b/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts @@ -11,7 +11,7 @@ import GlobalHelper from '../../../helpers class CompletionFishCommand extends CommanderCommand { protected commandName: string = 'fish'; - private installPath = path.join(os.homedir(), '.config/fish/completions/dojo.fish'); + private readonly installPath = path.join(os.homedir(), '.config/fish/completions/dojo.fish'); protected defineCommand() { GlobalHelper.completionCommandDefinition(this.command) @@ -29,10 +29,11 @@ class CompletionFishCommand extends CommanderCommand { spinner.succeed(`Fish completion successfully written in ${ filename }.`); if ( showInstructions ) { + const cpCommand = ` cp -i ${ filename } ~/.config/fish/completions # interactive cp to avoid accidents `; console.log(` The easiest way to install the completion is to copy the ${ TextStyle.CODE(filename) } into the ${ TextStyle.CODE('~/.config/fish/completions') } directory. -${ TextStyle.CODE(` cp -i ${ filename } ~/.config/fish/completions # interactive cp to avoid accidents `) }`); +${ TextStyle.CODE(cpCommand) }`); } } catch ( error ) { spinner.fail(`Fish completion error: ${ error }.`); diff --git a/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts index f7f4b8721846f94ce6f303c030069e9c37e998b7..f9d279eb2e86b29f567db78797f98333b70c4ec2 100644 --- a/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts +++ b/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts @@ -37,6 +37,11 @@ source ${ this.bashCompletion } spinner.succeed(`Bash completion successfully written in ${ TextStyle.CODE(filename) }`); if ( showInstructions ) { + const zprofileContent = TextStyle.CODE(` +autoload -U +X compinit && compinit +autoload -U +X bashcompinit && bashcompinit +source ${ filename } +`); console.log(` The easiest way to install the completion is to append the content of the generated file to the end of the ${ TextStyle.CODE(filename) } file or to overwrite it, if it only contains the 'dojo' completion. @@ -45,54 +50,40 @@ cat ${ filename } > ~/.bash_completion # overwrites .bash_completion cat ${ filename } >> ~/.bash_completion # appends to .bash_completion`) } For more details: ${ TextStyle.URL('https://github.com/scop/bash-completion/blob/master/README.md') } -Next add the following lines to your ${ TextStyle.CODE(`~/.zprofile`) } file: ${ TextStyle.CODE(` -autoload -U +X compinit && compinit -autoload -U +X bashcompinit && bashcompinit -source ${ filename } -`) } `); +Next add the following lines to your ${ TextStyle.CODE('~/.zprofile') } file: ${ zprofileContent } `); } } catch ( error ) { spinner.fail(`Bash completion writing error: ${ error }`); } } - protected addToZprofile(path: string, bash_path: string) { - const spinner: ora.Ora = ora(`Modifying ${ path } ...`).start(); - if ( fs.existsSync(path) ) { - const data = fs.readFileSync(path); + protected addToZprofile(zprofilePath: string, bashPath: string) { + const spinner: ora.Ora = ora(`Modifying ${ zprofilePath } ...`).start(); + if ( fs.existsSync(zprofilePath) ) { + const data = fs.readFileSync(zprofilePath); let updated = false; - if ( !data.includes('autoload -U +X compinit && compinit') ) { - try { - fs.appendFileSync(path, '\nautoload -U +X compinit && compinit'); + try { + if ( !data.includes('autoload -U +X compinit && compinit') ) { + fs.appendFileSync(zprofilePath, '\nautoload -U +X compinit && compinit'); updated = true; - } catch { - spinner.fail(`Error appending in ${ this.zprofile }`); } - } - if ( !data.includes('autoload -U +X bashcompinit && bashcompinit') ) { - try { - fs.appendFileSync(path, '\nautoload -U +X bashcompinit && bashcompinit'); + if ( !data.includes('autoload -U +X bashcompinit && bashcompinit') ) { + fs.appendFileSync(zprofilePath, '\nautoload -U +X bashcompinit && bashcompinit'); updated = true; - } catch { - spinner.fail(`Error appending in ${ this.zprofile }`); } - } - if ( !data.includes(`source ${ bash_path }`) ) { - try { - fs.appendFileSync(path, `\nsource ${ bash_path }`); + if ( !data.includes(`source ${ bashPath }`) ) { + fs.appendFileSync(zprofilePath, `\nsource ${ bashPath }`); updated = true; - } catch { - spinner.fail(`Error appending in ${ this.zprofile }`); } + } catch { + spinner.fail(`Error appending in ${ this.zprofile }`); + return; } - if ( updated ) { - spinner.succeed(`Zsh profile updated.`); - } else { - spinner.succeed(`Zsh profile already up to date.`); - } + + spinner.succeed(updated ? `Zsh profile updated.` : `Zsh profile already up to date.`); } else { try { - fs.writeFileSync(path, this.loadBashCompletion); + fs.writeFileSync(zprofilePath, this.loadBashCompletion); spinner.succeed(`Zsh profile written.`); } catch ( error ) { spinner.fail(`Error writing in ${ this.zprofile }`); diff --git a/NodeApp/src/commander/exercise/ExerciseCommand.ts b/NodeApp/src/commander/exercise/ExerciseCommand.ts index 2a93b7a40c66845444f75d2453f731ab8fa6925f..13bd0b3992b9983fc210513203bb6ede5909b13a 100644 --- a/NodeApp/src/commander/exercise/ExerciseCommand.ts +++ b/NodeApp/src/commander/exercise/ExerciseCommand.ts @@ -9,7 +9,7 @@ class ExerciseCommand extends CommanderCommand { protected defineCommand() { this.command - .description('manage an exercise'); + .description('manage an exercise'); } protected defineSubCommands() { @@ -18,7 +18,9 @@ class ExerciseCommand extends CommanderCommand { ExerciseCorrectionCommand.registerOnCommand(this.command); } - protected async commandAction(): Promise<void> { } + protected async commandAction(): Promise<void> { + // No action + } } diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts index 03641466226d60d598814addff6ad1e1963a505e..0e9882660eb9cb19e555cc56d59db1f8bebeeba5 100644 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts @@ -17,9 +17,9 @@ class ExerciseCorrectionCommand extends CommanderCommand { protected defineCommand() { this.command - .description('link an exercise repo as a correction for an assignment') - .requiredOption('-a, --assignment <string>', 'id or url of the assignment of the correction') - .action(this.commandAction.bind(this)); + .description('link an exercise repo as a correction for an assignment') + .requiredOption('-a, --assignment <string>', 'id or url of the assignment of the correction') + .action(this.commandAction.bind(this)); } protected async commandAction(options: { assignment: string }): Promise<void> { @@ -34,7 +34,6 @@ class ExerciseCorrectionCommand extends CommanderCommand { Config.interactiveMode ? await this.showCorrectionsInteractive(assignment, assignmentGetSpinner) : this.showCorrections(assignment, assignmentGetSpinner); } else { assignmentGetSpinner.fail(`The assignment doesn't have any corrections yet`); - return; } } diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts index 8f7673d0049654d3b9b394cbc9cf986ef07442c0..2fc6bf7bb8da17746bf56cfceed927aed91fb65e 100644 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts @@ -14,12 +14,12 @@ class ExerciseCreateCommand extends CommanderCommand { protected defineCommand() { this.command - .description('create a new exercise from an assignment') - .requiredOption('-a, --assignment <value>', 'assignment source (Dojo assignment ID, Dojo assignment name or Gitlab assignment URL)') - .option('-i, --members_id <ids...>', 'list of gitlab members ids (group\'s student) to add to the repository') - .option('-u, --members_username <usernames...>', 'list of gitlab members username (group\'s student) to add to the repository') - .option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)') - .action(this.commandAction.bind(this)); + .description('create a new exercise from an assignment') + .requiredOption('-a, --assignment <value>', 'assignment source (Dojo assignment ID, Dojo assignment name or Gitlab assignment URL)') + .option('-i, --members_id <ids...>', 'list of gitlab members ids (group\'s student) to add to the repository') + .option('-u, --members_username <usernames...>', 'list of gitlab members username (group\'s student) to add to the repository') + .option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)') + .action(this.commandAction.bind(this)); } protected async commandAction(options: { assignment: string, members_id?: Array<number>, members_username?: Array<string>, clone?: string | boolean }): Promise<void> { @@ -68,7 +68,7 @@ class ExerciseCreateCommand extends CommanderCommand { console.log(TextStyle.BLOCK('Please wait while we are creating the exercise (approximately 10 seconds)...')); try { - exercise = await DojoBackendManager.createExercise((assignment as Assignment).name, members); + exercise = await DojoBackendManager.createExercise(assignment.name, members); const oraInfo = (message: string) => { ora({ diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts index 75af9463ee742490d62857b8d97f686367516fc6..db7be9be2bc62e0fdcc5b2f12526bc405a2b101d 100644 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts @@ -8,12 +8,12 @@ class ExerciseRunCommand extends CommanderCommand { protected defineCommand() { GlobalHelper.runCommandDefinition(this.command) - .description('locally run an exercise') - .action(this.commandAction.bind(this)); + .description('locally run an exercise') + .action(this.commandAction.bind(this)); } protected async commandAction(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> { - await ExerciseRunHelper.run(options); + await (new ExerciseRunHelper(options)).run(); } } diff --git a/NodeApp/src/config/LocalConfigFile.ts b/NodeApp/src/config/LocalConfigFile.ts index ecc0530b3ce4144a99a10f1d9ec87fcb11ab99c3..390e95b955cdd52c787793dc802a4e5dc21686ff 100644 --- a/NodeApp/src/config/LocalConfigFile.ts +++ b/NodeApp/src/config/LocalConfigFile.ts @@ -4,7 +4,11 @@ import JSON5 from 'json5'; class LocalConfigFile { - constructor(private filename: string) { + private readonly filename: string; + + constructor(filename: string) { + this.filename = filename; + this.loadConfig(); } diff --git a/NodeApp/src/helpers/AutoCompletionHelper.ts b/NodeApp/src/helpers/AutoCompletionHelper.ts index f0c480a82d97af2be721b88ecc7285f9c242abf8..56988ebdceb9f25b1ba53fff4a6021059ca138c9 100644 --- a/NodeApp/src/helpers/AutoCompletionHelper.ts +++ b/NodeApp/src/helpers/AutoCompletionHelper.ts @@ -1,48 +1,49 @@ -import { Command } from 'commander'; +import { Command } from 'commander'; import { existsSync, renameSync } from 'fs'; -import ora from 'ora'; -import TextStyle from '../types/TextStyle'; -import inquirer from 'inquirer'; +import ora from 'ora'; +import TextStyle from '../types/TextStyle'; +import inquirer from 'inquirer'; + function renameFile(filename: string, showWarning: boolean) { - const old_filename = `${filename}.old` - const spinner: ora.Ora = ora(`Renaming ${TextStyle.CODE(filename)} in ${TextStyle.CODE(old_filename)} ...`).start(); + const oldFilename = `${ filename }.old`; + const spinner: ora.Ora = ora(`Renaming ${ TextStyle.CODE(filename) } in ${ TextStyle.CODE(oldFilename) } ...`).start(); try { - renameSync(filename, old_filename); - spinner.succeed(`Renaming success: ${TextStyle.CODE(filename)} in ${TextStyle.CODE(old_filename)}`); + renameSync(filename, oldFilename); + spinner.succeed(`Renaming success: ${ TextStyle.CODE(filename) } in ${ TextStyle.CODE(oldFilename) }`); - if (showWarning) { - console.log(`${TextStyle.WARNING('Warning:')} Your ${TextStyle.CODE(filename)} was renamed ${TextStyle.CODE(old_filename)}. If this was not intended please revert this change.`); + if ( showWarning ) { + console.log(`${ TextStyle.WARNING('Warning:') } Your ${ TextStyle.CODE(filename) } was renamed ${ TextStyle.CODE(oldFilename) }. If this was not intended please revert this change.`); } - } catch (error) { - spinner.fail(`Renaming failed: ${error}.`); + } catch ( error ) { + spinner.fail(`Renaming failed: ${ error }.`); } } async function askConfirmation(msg: string): Promise<boolean> { return (await inquirer.prompt({ - name: 'confirm', - message: msg, - type: 'confirm', - default: false - })).confirm + name : 'confirm', + message: msg, + type : 'confirm', + default: false + })).confirm; } // Returns false, when the renaming is interrupted export async function tryRenameFile(path: string, force: boolean): Promise<boolean> { - const fileExists = existsSync(path) - if (fileExists && force) { - renameFile(path, false) - } else if (fileExists) { - const confirm = (await askConfirmation(`${TextStyle.CODE(path)} in ${TextStyle.CODE(path + '.old')}. Are you sure?`)) - if (confirm) { - renameFile(path, true) + const fileExists = existsSync(path); + if ( fileExists && force ) { + renameFile(path, false); + } else if ( fileExists ) { + const confirm = (await askConfirmation(`${ TextStyle.CODE(path) } in ${ TextStyle.CODE(path + '.old') }. Are you sure?`)); + if ( confirm ) { + renameFile(path, true); } else { - console.log(`${TextStyle.BLOCK('Completion generation interrupted.')}`) - return false + console.log(`${ TextStyle.BLOCK('Completion generation interrupted.') }`); + return false; } } - return true + return true; } const fishFunction = ` @@ -64,28 +65,28 @@ complete -f -c dojo `; function isHidden(cmd: Command): boolean { - return (cmd as Command & { _hidden: boolean })._hidden + return (cmd as Command & { _hidden: boolean })._hidden; } function isLeaf(cmd: Command): boolean { - return cmd.commands.length == 0; + return cmd.commands.length === 0; } function flatten(cmd: Command): Array<Command> { - if (isLeaf(cmd)) { - return [cmd]; + if ( isLeaf(cmd) ) { + return [ cmd ]; } else { return cmd.commands .filter(c => !isHidden(c)) .map(child => flatten(child)) - .reduce((acc, cmd) => acc.concat(cmd), [cmd]); + .reduce((acc, subCmd) => acc.concat(subCmd), [ cmd ]); } } // Computes the maximum number of commands until a leaf is reached function computeDepth(cmd: Command | undefined): number { - if (cmd === undefined) { + if ( cmd === undefined ) { return 0; } else { return 1 + cmd.commands.filter(c => !isHidden(c)).map(subCmd => computeDepth(subCmd)).reduce((acc, depth) => depth > acc ? depth : acc, 0); @@ -96,7 +97,7 @@ function computeDepth(cmd: Command | undefined): number { function computeHeight(cmd: Command | null): number { let height = 0; let tmp = cmd; - while (tmp !== null) { + while ( tmp !== null ) { tmp = tmp.parent; height += 1; } @@ -105,7 +106,7 @@ function computeHeight(cmd: Command | null): number { // Computes the maximum number of commands until the root is reached export function getRoot(cmd: Command): Command { - if (cmd.parent == null) { + if ( cmd.parent == null ) { return cmd; } else { return getRoot(cmd.parent); @@ -114,7 +115,7 @@ export function getRoot(cmd: Command): Command { function getOptions(cmd: Command): string { // we remove <args>, [command], and , from option lines - return cmd.options.filter(opt => !opt.hidden).map(opt => opt.flags.replace(/<.*?>/, '').replace(/\[.*?\]/, '').replace(',', '').trimEnd()).join(' '); + return cmd.options.filter(opt => !opt.hidden).map(opt => opt.flags.replace(/<.*?>/, '').replace(/\[.*?]/, '').replace(',', '').trimEnd()).join(' '); } function commandsAndOptionsToString(cmd: Command): string { @@ -122,15 +123,14 @@ function commandsAndOptionsToString(cmd: Command): string { } function addLine(identLevel: number, pattern: string): string { - return `${' '.repeat(identLevel)}${pattern}\n`; + return `${ ' '.repeat(identLevel) }${ pattern }\n`; } function generateBashSubCommands(cmd: Command, current: number, maxDepth: number, ident: number): string { - if (current == maxDepth) { - return addLine(ident, `case "\${COMP_WORDS[$COMP_CWORD - ${maxDepth - current + 1}]}" in`) + addLine(ident + 1, `${cmd.name()})`) + addLine(ident + 2, `words="${commandsAndOptionsToString(cmd)}"`) + addLine(ident + 1, ';;') + addLine(ident + 1, '*)') + addLine(ident + 1, ';;') + addLine(ident, 'esac'); - + if ( current === maxDepth ) { + return addLine(ident, `case "\${COMP_WORDS[$COMP_CWORD - ${ maxDepth - current + 1 }]}" in`) + addLine(ident + 1, `${ cmd.name() })`) + addLine(ident + 2, `words="${ commandsAndOptionsToString(cmd) }"`) + addLine(ident + 1, ';;') + addLine(ident + 1, '*)') + addLine(ident + 1, ';;') + addLine(ident, 'esac'); } else { - let data = addLine(ident, `case "\${COMP_WORDS[$COMP_CWORD - ${maxDepth - current + 1}]}" in`) + addLine(ident + 1, `${cmd.name()})`); + let data = addLine(ident, `case "\${COMP_WORDS[$COMP_CWORD - ${ maxDepth - current + 1 }]}" in`) + addLine(ident + 1, `${ cmd.name() })`); cmd.commands.filter(c => !isHidden(c)).forEach(subCmd => { data += generateBashSubCommands(subCmd, current + 1, maxDepth, ident + 2); }); @@ -142,8 +142,8 @@ function generateBashSubCommands(cmd: Command, current: number, maxDepth: number export function generateBashCompletion(root: Command): string { const depth = computeDepth(root); let data = addLine(0, '#/usr/bin/env bash\nfunction _dojo_completions()') + addLine(0, '{') + addLine(1, 'latest="${COMP_WORDS[$COMP_CWORD]}"'); - for (let i = 1; i <= depth; i++) { - data += addLine(1, `${i == 1 ? 'if' : 'elif'} [ $COMP_CWORD -eq ${depth - i + 1} ]`) + addLine(1, 'then'); + for ( let i = 1 ; i <= depth ; i++ ) { + data += addLine(1, `${ i === 1 ? 'if' : 'elif' } [ $COMP_CWORD -eq ${ depth - i + 1 } ]`) + addLine(1, 'then'); data += generateBashSubCommands(root, i, depth, 2); } data += addLine(1, 'fi') + addLine(1, 'COMPREPLY=($(compgen -W "$words" -- $latest))') + addLine(1, 'return 0') + addLine(0, '}') + addLine(0, 'complete -F _dojo_completions dojo'); @@ -155,8 +155,8 @@ const prefix = 'complete -f -c dojo -n \'__fish_dojo_using_commands'; function generateCommandChain(cmd: Command | null): string { let data = ''; - while (cmd !== null) { - data = cmd.name().concat(` ${data}`); + while ( cmd !== null ) { + data = cmd.name().concat(` ${ data }`); cmd = cmd.parent; } return data.trimEnd(); @@ -167,21 +167,15 @@ function hasOptions(cmd: Command): boolean { } function optionsToString(cmd: Command): string { - return cmd.options.filter(opt => !opt.hidden).map(opt => { - return `${prefix} ${computeHeight(cmd)} ${generateCommandChain(cmd)}' -a "${opt.short ?? ''} ${opt.long ?? ''}" -d "${opt.description}"`; - }).join('\n').concat('\n'); + return cmd.options.filter(opt => !opt.hidden).map(opt => `${ prefix } ${ computeHeight(cmd) } ${ generateCommandChain(cmd) }' -a "${ opt.short ?? '' } ${ opt.long ?? '' }" -d "${ opt.description }"`).join('\n').concat('\n'); } export function generateFishCompletion(root: Command): string { const commands = flatten(root); - const data = fishFunction.concat(// add completions for options - commands.filter(c => !isHidden(c)).filter(cmd => hasOptions(cmd)).map(cmd => optionsToString(cmd)).filter(str => str != '').join('')).concat(// add completions for commands and subcommands - commands.filter(c => !isHidden(c)).filter(cmd => !isLeaf(cmd)).map(cmd => cmd.commands.filter(c => !isHidden(c)).map(subCmd => { - return `${prefix} ${computeHeight(cmd)} ${generateCommandChain(cmd)}' -a ${subCmd.name()} -d "${subCmd.description()}"`; - }).join('\n').concat('\n')).join('')); - - return data; + return fishFunction.concat(// add completions for options + commands.filter(c => !isHidden(c)).filter(cmd => hasOptions(cmd)).map(cmd => optionsToString(cmd)).filter(str => str !== '').join('')).concat(// add completions for commands and subcommands + commands.filter(c => !isHidden(c)).filter(cmd => !isLeaf(cmd)).map(cmd => cmd.commands.filter(c => !isHidden(c)).map(subCmd => `${ prefix } ${ computeHeight(cmd) } ${ generateCommandChain(cmd) }' -a ${ subCmd.name() } -d "${ subCmd.description() }"`).join('\n').concat('\n')).join('')); } diff --git a/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts b/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts index 07fa2f5b4de4d383c4cfd77765efe02ac8349886..da2ff6f6de73e73a257cc29d1f127b692663c5f9 100644 --- a/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts +++ b/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts @@ -31,6 +31,25 @@ class ExerciseRunHelper { private readonly fileComposeLogs: string = path.join(this.folderResultsDojo, `dockerComposeLogs.txt`); + private readonly options: { path: string, verbose: boolean, superVerbose: boolean }; + private readonly verbose: boolean; + private readonly localExercisePath: string; + + private assignmentFile!: AssignmentFile; + private exerciseDockerCompose!: ExerciseDockerCompose; + private exerciseResultsValidation!: ExerciseResultsSanitizerAndValidator; + + private haveResultsVolume!: boolean; + + private exerciseRunSpinner!: ora.Ora; + private buildPhase: boolean | undefined = undefined; + + constructor(options: { path: string, verbose: boolean, superVerbose: boolean }) { + this.options = options; + this.verbose = options.verbose || options.superVerbose || SharedConfig.debug; + this.localExercisePath = options.path ?? Config.folders.defaultLocalExercise; + } + private displayExecutionLogs() { ora({ text : `${ chalk.magenta('Execution logs folder:') } ${ this.folderResultsVolume }`, @@ -38,231 +57,239 @@ class ExerciseRunHelper { }).start().info(); } - async run(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> { - const verbose: boolean = options.verbose || options.superVerbose || SharedConfig.debug; + /** + * Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running) + * @private + */ + private async checkRequirements() { + console.log(TextStyle.BLOCK('Please wait while we are checking and creating dependencies...')); - const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise; + // Create result temp folder + fs.mkdirSync(this.folderResultsVolume, { recursive: true }); + fs.mkdirSync(this.folderResultsDojo, { recursive: true }); + fs.mkdirSync(this.folderResultsExercise, { recursive: true }); - let assignmentFile: AssignmentFile; - let exerciseDockerCompose: ExerciseDockerCompose; - let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator; - let haveResultsVolume: boolean; + ora({ + text : `Checking exercise content:`, + indent: 4 + }).start().info(); - // Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running) + // Exercise folder { - console.log(TextStyle.BLOCK('Please wait while we are checking and creating dependencies...')); + const spinner: ora.Ora = ora({ + text : `Checking exercise folder`, + indent: 8 + }).start(); - // Create result temp folder - fs.mkdirSync(this.folderResultsVolume, { recursive: true }); - fs.mkdirSync(this.folderResultsDojo, { recursive: true }); - fs.mkdirSync(this.folderResultsExercise, { recursive: true }); + const files = fs.readdirSync(this.options.path); + const missingFiles = Config.exercise.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]); + if ( missingFiles.length > 0 ) { + spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`); + throw new Error(); + } - ora({ - text : `Checking exercise content:`, - indent: 4 - }).start().info(); + spinner.succeed(`The exercise folder contains all the needed files`); + } + + // dojo_assignment.json validity + { + const spinner: ora.Ora = ora({ + text : `Checking ${ ClientsSharedConfig.assignment.filename } file`, + indent: 8 + }).start(); + + const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(this.options.path, ClientsSharedConfig.assignment.filename)); + if ( !validationResults.isValid ) { + spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ validationResults.error }`); + throw new Error(); + } else { + this.assignmentFile = validationResults.content!; + } - // Exercise folder - { - const spinner: ora.Ora = ora({ - text : `Checking exercise folder`, - indent: 8 - }).start(); + this.haveResultsVolume = this.assignmentFile.result.volume !== undefined; - const files = fs.readdirSync(options.path); - const missingFiles = Config.exercise.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]); + spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`); + } - if ( missingFiles.length > 0 ) { - spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`); - return; - } + // Docker daemon + { + const spinner: ora.Ora = ora({ + text : `Checking Docker daemon`, + indent: 4 + }).start(); - spinner.succeed(`The exercise folder contains all the needed files`); + try { + await execAsync(`docker ps`); + } catch ( error ) { + spinner.fail(`The Docker daemon is not running`); + throw new Error(); } - // dojo_assignment.json validity - { - const spinner: ora.Ora = ora({ - text : `Checking ${ ClientsSharedConfig.assignment.filename } file`, - indent: 8 - }).start(); - - const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(options.path, ClientsSharedConfig.assignment.filename)); - if ( !validationResults.isValid ) { - spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ validationResults.error }`); - return; - } else { - assignmentFile = validationResults.content!; - } + spinner.succeed(`The Docker daemon is running`); + } + } - haveResultsVolume = assignmentFile.result.volume !== undefined; + private logsEvent(log: string, _error: boolean, displayable: boolean, currentStep: string) { + for ( const line of log.split('\n') ) { + if ( displayable && this.buildPhase === undefined && line.startsWith('#') ) { + this.buildPhase = true; + } - spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`); + if ( currentStep === 'COMPOSE_RUN' && this.buildPhase === true && line !== '' && !line.startsWith('#') ) { + this.buildPhase = false; } - // Docker daemon - { - const spinner: ora.Ora = ora({ - text : `Checking Docker daemon`, - indent: 4 - }).start(); - - try { - await execAsync(`docker ps`); - } catch ( error ) { - spinner.fail(`The Docker daemon is not running`); - return; - } + if ( SharedConfig.debug || (displayable && (this.options.superVerbose || this.buildPhase === false)) ) { + console.log(line); + } + } + } + + private stepEvent(name: string, message: string) { + this.exerciseRunSpinner = ora({ + text : message, + indent: 4 + }).start(); + + if ( this.verbose && name === 'COMPOSE_RUN' ) { + this.exerciseRunSpinner.info(); + } + } - spinner.succeed(`The Docker daemon is running`); + private endStepEvent(stepName: string, message: string, error: boolean) { + if ( error ) { + if ( this.verbose && stepName === 'COMPOSE_RUN' ) { + ora({ + text : message, + indent: 4 + }).start().fail(); + } else { + this.exerciseRunSpinner.fail(message); } + } else if ( this.verbose && stepName === 'COMPOSE_RUN' ) { + ora({ + text : message, + indent: 4 + }).start().succeed(); + } else { + this.exerciseRunSpinner.succeed(message); } + } - // Step 2: Run docker-compose file - { - console.log(TextStyle.BLOCK('Please wait while we are running the exercise...')); + /** + * Step 2: Run docker-compose file + * @private + */ + private async runDockerCompose() { + console.log(TextStyle.BLOCK('Please wait while we are running the exercise...')); - let composeFileOverride: string[] = []; - const composeOverridePath: string = path.join(localExercisePath, 'docker-compose-override.yml'); - if ( haveResultsVolume ) { - const composeOverride = fs.readFileSync(path.join(__dirname, '../../../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', this.folderResultsExercise); - fs.writeFileSync(composeOverridePath, composeOverride); + let composeFileOverride: string[] = []; + const composeOverridePath: string = path.join(this.localExercisePath, 'docker-compose-override.yml'); + if ( this.haveResultsVolume ) { + const composeOverride = fs.readFileSync(path.join(__dirname, '../../../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', this.assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', this.folderResultsExercise); + fs.writeFileSync(composeOverridePath, composeOverride); - composeFileOverride = [ composeOverridePath ]; - } + composeFileOverride = [ composeOverridePath ]; + } - exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, assignmentFile, localExercisePath, composeFileOverride); + this.exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, this.assignmentFile, this.localExercisePath, composeFileOverride); - try { - await new Promise<void>((resolve, reject) => { - let spinner: ora.Ora; - - if ( verbose ) { - let buildPhase: boolean | undefined = undefined; - exerciseDockerCompose.events.on('logs', (log: string, _error: boolean, displayable: boolean, currentStep: string) => { - for ( const line of log.split('\n') ) { - if ( displayable && buildPhase == undefined && line.startsWith('#') ) { - buildPhase = true; - } - - if ( currentStep == 'COMPOSE_RUN' && buildPhase === true && line != '' && !line.startsWith('#') ) { - buildPhase = false; - } - - if ( SharedConfig.debug || (displayable && (options.superVerbose || buildPhase === false)) ) { - console.log(line); - } - } - }); - } + try { + await new Promise<void>((resolve, reject) => { + if ( this.verbose ) { + this.exerciseDockerCompose.events.on('logs', this.logsEvent.bind(this)); + } - exerciseDockerCompose.events.on('step', (name: string, message: string) => { - spinner = ora({ - text : message, - indent: 4 - }).start(); + this.exerciseDockerCompose.events.on('step', this.stepEvent.bind(this)); - if ( verbose && name == 'COMPOSE_RUN' ) { - spinner.info(); - } - }); - - exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => { - if ( error ) { - if ( verbose && stepName == 'COMPOSE_RUN' ) { - ora({ - text : message, - indent: 4 - }).start().fail(); - } else { - spinner.fail(message); - } - } else { - if ( verbose && stepName == 'COMPOSE_RUN' ) { - ora({ - text : message, - indent: 4 - }).start().succeed(); - } else { - spinner.succeed(message); - } - } - }); + this.exerciseDockerCompose.events.on('endStep', this.endStepEvent.bind(this)); - exerciseDockerCompose.events.on('finished', (success: boolean) => { - success ? resolve() : reject(); - }); + this.exerciseDockerCompose.events.on('finished', (success: boolean) => success ? resolve() : reject()); - exerciseDockerCompose.run(true); - }); - } catch ( error ) { /* empty */ } + this.exerciseDockerCompose.run(true); + }); + } catch ( error ) { /* empty */ } - fs.rmSync(composeOverridePath, { force: true }); - fs.writeFileSync(this.fileComposeLogs, exerciseDockerCompose.allLogs); + fs.rmSync(composeOverridePath, { force: true }); + fs.writeFileSync(this.fileComposeLogs, this.exerciseDockerCompose.allLogs); - if ( !exerciseDockerCompose.success ) { - this.displayExecutionLogs(); - return; - } + if ( !this.exerciseDockerCompose.success ) { + this.displayExecutionLogs(); + throw new Error(); } + } + /** + * Step 3: Get results + * @private + */ + private async getResults() { + console.log(TextStyle.BLOCK('Please wait while we are checking the results...')); - // Step 3: Get results - { - console.log(TextStyle.BLOCK('Please wait while we are checking the results...')); + this.exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, this.exerciseDockerCompose.exitCode); - exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, exerciseDockerCompose.exitCode); + try { + await new Promise<void>((resolve, reject) => { + let spinner: ora.Ora; - try { - await new Promise<void>((resolve, reject) => { - let spinner: ora.Ora; + this.exerciseResultsValidation.events.on('step', (_name: string, message: string) => { + spinner = ora({ + text : message, + indent: 4 + }).start(); + }); - exerciseResultsValidation.events.on('step', (_name: string, message: string) => { - spinner = ora({ - text : message, - indent: 4 - }).start(); - }); - - exerciseResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => { - if ( error ) { - if ( stepName == 'CHECK_SIZE' ) { - spinner.warn(message); - } else { - spinner.fail(message); - } + this.exerciseResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => { + if ( error ) { + if ( stepName === 'CHECK_SIZE' ) { + spinner.warn(message); } else { - spinner.succeed(message); + spinner.fail(message); } - }); - - exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => { - success || exitCode == ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject(); - }); + } else { + spinner.succeed(message); + } + }); - exerciseResultsValidation.run(); + this.exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => { + success || exitCode === ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject(); }); - } catch ( error ) { - this.displayExecutionLogs(); - return; - } + + this.exerciseResultsValidation.run(); + }); + } catch ( error ) { + this.displayExecutionLogs(); + throw new Error(); } + } + /** + * Step 4: Display results + Volume location + * @private + */ + private async displayResults() { + const info = chalk.magenta.bold.italic; + ClientsSharedExerciseHelper.displayExecutionResults(this.exerciseResultsValidation.exerciseResults, this.exerciseDockerCompose.exitCode, { + INFO : info, + SUCCESS: chalk.green, + FAILURE: chalk.red + }, `\n\n${ info('Execution results folder: ') }${ this.folderResultsVolume }`); + } - // Step 4: Display results + Volume location - { - const info = chalk.magenta.bold.italic; - ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults!, exerciseDockerCompose.exitCode, { - INFO : info, - SUCCESS: chalk.green, - FAILURE: chalk.red - }, `\n\n${ info('Execution results folder: ') }${ this.folderResultsVolume }`); + async run(): Promise<void> { + try { + await this.checkRequirements(); + await this.runDockerCompose(); + await this.getResults(); + await this.displayResults(); + } catch ( error ) { + return; } } } -export default new ExerciseRunHelper(); +export default ExerciseRunHelper; diff --git a/NodeApp/src/managers/DojoBackendManager.ts b/NodeApp/src/managers/DojoBackendManager.ts index fcd749df9a44d151c202498cb8d5a8dfe5f2df2a..b2ef6295a811b254a32f333d7f4fd2800bd91420 100644 --- a/NodeApp/src/managers/DojoBackendManager.ts +++ b/NodeApp/src/managers/DojoBackendManager.ts @@ -1,7 +1,6 @@ import axios, { AxiosError } from 'axios'; import ora from 'ora'; import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute'; -import { StatusCodes } from 'http-status-codes'; import GitlabUser from '../shared/types/Gitlab/GitlabUser'; import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; import Assignment from '../sharedByClients/models/Assignment'; @@ -13,6 +12,49 @@ import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; class DojoBackendManager { + private handleApiError(error: unknown, spinner: ora.Ora, verbose: boolean, defaultErrorMessage?: string, otherErrorHandler?: (error: AxiosError, spinner: ora.Ora, verbose: boolean) => void) { + if ( verbose ) { + if ( error instanceof AxiosError ) { + switch ( error.response?.data?.code ) { + case DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED: + spinner.fail(`The exercise does not belong to the assignment.`); + break; + case DojoStatusCode.EXERCISE_CORRECTION_ALREADY_EXIST: + spinner.fail(`This exercise is already labelled as a correction. If you want to update it, please use the update command.`); + break; + case DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST: + spinner.fail(`The exercise is not labelled as a correction so it's not possible to update it.`); + break; + case DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR: + spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on Gitlab. Please try again later or contact an administrator.`); + break; + case DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED: + spinner.fail(`The following users have reached the maximum number of exercise of this assignment : ${ ((error.response.data as DojoBackendResponse<Array<GitlabUser>>).data).map(user => user.name).join(', ') }.`); + break; + case DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR: + spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on Gitlab. Please try again later or contact an administrator.`); + break; + case DojoStatusCode.GITLAB_TEMPLATE_NOT_FOUND: + spinner.fail(`Template not found or access denied. Please check the template ID or url. Also, please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`); + break; + case DojoStatusCode.GITLAB_TEMPLATE_ACCESS_UNAUTHORIZED: + spinner.fail(`Please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`); + break; + default: + if ( otherErrorHandler ) { + otherErrorHandler(error, spinner, verbose); + } else { + spinner.fail(defaultErrorMessage ?? 'Unknown error'); + } + break; + } + } else { + spinner.fail(defaultErrorMessage ?? 'Unknown error'); + } + } + } + + public getApiUrl(route: ApiRoute): string { return `${ ClientsSharedConfig.apiURL }${ route }`; } @@ -62,21 +104,7 @@ class DojoBackendManager { return true; } catch ( error ) { - if ( verbose ) { - if ( error instanceof AxiosError ) { - if ( error.response ) { - if ( error.response.status === StatusCodes.NOT_FOUND ) { - spinner.fail(`Template not found or access denied. Please check the template ID or url. Also, please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`); - } else if ( error.response.status === StatusCodes.UNAUTHORIZED ) { - spinner.fail(`Please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`); - } else { - spinner.fail(`Template error: ${ error.response.statusText }`); - } - } - } else { - spinner.fail(`Template error: ${ error }`); - } - } + this.handleApiError(error, spinner, verbose, `Template error: ${ error }`); return false; } @@ -101,23 +129,7 @@ class DojoBackendManager { return response.data.data; } catch ( error ) { - if ( verbose ) { - if ( error instanceof AxiosError ) { - if ( error.response ) { - if ( error.response.status === StatusCodes.CONFLICT ) { - spinner.fail(`The assignment name is already used. Please choose another one.`); - } else { - if ( (error.response.data as DojoBackendResponse<unknown>).code === DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR ) { - spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on Gitlab. Please try again later or contact an administrator.`); - } else { - spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on Dojo server. Please try again later or contact an administrator.`); - } - } - } - } else { - spinner.fail(`Assignment creation error: unknown error`); - } - } + this.handleApiError(error, spinner, verbose, `Assignment creation error: unknown error`); throw error; } @@ -139,27 +151,7 @@ class DojoBackendManager { return response.data.data; } catch ( error ) { - if ( verbose ) { - if ( error instanceof AxiosError ) { - if ( error.response ) { - if ( error.response.status === StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE ) { - if ( error.response.data && (error.response.data as DojoBackendResponse<Array<GitlabUser>>).code === DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED ) { - spinner.fail(`The following users have reached the maximum number of exercise of this assignment : ${ ((error.response.data as DojoBackendResponse<Array<GitlabUser>>).data as Array<GitlabUser>).map(user => user.name).join(', ') }.`); - } else { - spinner.fail(`You've already reached the max number of exercise of this assignment.`); - } - } else { - if ( (error.response.data as DojoBackendResponse<unknown>).code === DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR ) { - spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on Gitlab. Please try again later or contact an administrator.`); - } else { - spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on Dojo server. Please try again later or contact an administrator.`); - } - } - } - } else { - spinner.fail(`Exercise creation error: unknown error`); - } - } + this.handleApiError(error, spinner, verbose, `Exercise creation error: unknown error`); throw error; } @@ -214,23 +206,7 @@ class DojoBackendManager { return true; } catch ( error ) { - if ( verbose ) { - if ( error instanceof AxiosError ) { - if ( error.response?.data ) { - if ( error.response.data.code === DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED ) { - spinner.fail(`The exercise does not belong to the assignment.`); - } else if ( error.response.data.code === DojoStatusCode.EXERCISE_CORRECTION_ALREADY_EXIST ) { - spinner.fail(`This exercise is already labelled as a correction. If you want to update it, please use the update command.`); - } else if ( error.response.data.code === DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST ) { - spinner.fail(`The exercise is not labelled as a correction so it's not possible to update it.`); - } - } else { - spinner.fail(`Correction ${ isUpdate ? 'update' : 'link' } error: ${ error.response?.statusText }`); - } - } else { - spinner.fail(`Correction ${ isUpdate ? 'update' : 'link' } error: ${ error }`); - } - } + this.handleApiError(error, spinner, verbose, `Correction ${ isUpdate ? 'update' : 'link' } error: ${ error }`); return false; } diff --git a/NodeApp/src/managers/GitlabManager.ts b/NodeApp/src/managers/GitlabManager.ts index 356d9e26d61f1e55a591be9e6c811271c9ef8323..f8ee3f16dd36dfc68418dd6eab1baf2384fc64ad 100644 --- a/NodeApp/src/managers/GitlabManager.ts +++ b/NodeApp/src/managers/GitlabManager.ts @@ -63,7 +63,7 @@ class GitlabManager { try { const oldSettings = notificationSettings; - const newSettings = { level: someLevelTypes[someLevelTypes[0] == oldSettings.level ? 1 : 0] }; + const newSettings = { level: someLevelTypes[someLevelTypes[0] === oldSettings.level ? 1 : 0] }; await this.setNotificationSettings(newSettings); await this.setNotificationSettings(oldSettings); @@ -111,11 +111,14 @@ class GitlabManager { if ( verbose ) { spinner.succeed(`${ gitlabUser.username } (${ gitlabUser.id })`); } + return gitlabUser; } else { if ( verbose ) { spinner.fail(`${ param }`); } + + return undefined; } })); } catch ( e ) { @@ -124,15 +127,15 @@ class GitlabManager { } public async getUsersById(ids: Array<number>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> { - return await this.getGitlabUsers(ids, 'id', verbose, verboseIndent); + return this.getGitlabUsers(ids, 'id', verbose, verboseIndent); } public async getUsersByUsername(usernames: Array<string>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> { - return await this.getGitlabUsers(usernames, 'search', verbose, verboseIndent); + return this.getGitlabUsers(usernames, 'search', verbose, verboseIndent); } public async getRepository(repoId: number): Promise<GitlabRepository> { - return await axios.get(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', repoId.toString())); + return axios.get(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', repoId.toString())); } public async fetchMembers(options: { members_id?: Array<number>, members_username?: Array<string> }): Promise<Array<GitlabUser> | false> { @@ -143,10 +146,10 @@ class GitlabManager { let members: Array<GitlabUser> = []; async function getMembers<T>(context: unknown, functionName: string, paramsToSearch: Array<T>): Promise<boolean> { - const result = await ((context as { [functionName: string]: (arg: Array<T>, verbose: boolean, verboseIndent: number) => Promise<Array<GitlabUser | undefined>> })[functionName])(paramsToSearch, true, 8); + const gitlabUsers = await ((context as { [functionName: string]: (arg: Array<T>, verbose: boolean, verboseIndent: number) => Promise<Array<GitlabUser | undefined>> })[functionName])(paramsToSearch, true, 8); - if ( result.every(user => user) ) { - members = members.concat(result as Array<GitlabUser>); + if ( gitlabUsers.every(user => user) ) { + members = members.concat(gitlabUsers as Array<GitlabUser>); return true; } else { return false; @@ -205,8 +208,8 @@ class GitlabManager { shell: true }); - gitClone.on('exit', (code) => { - code !== null && code == 0 ? resolve() : reject(); + gitClone.on('exit', code => { + code !== null && code === 0 ? resolve() : reject(); }); }); diff --git a/NodeApp/src/managers/HttpManager.ts b/NodeApp/src/managers/HttpManager.ts index 6c75099ca1b1d5bc86f33336502b7d29cad4be93..38462759e0596492c2231f2ad291810e2af8a1e1 100644 --- a/NodeApp/src/managers/HttpManager.ts +++ b/NodeApp/src/managers/HttpManager.ts @@ -1,15 +1,15 @@ -import axios, { AxiosError, AxiosRequestHeaders } from 'axios'; -import SessionManager from './SessionManager'; -import FormData from 'form-data'; -import { StatusCodes } from 'http-status-codes'; -import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; -import { version } from '../config/Version'; -import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse'; -import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; -import boxen from 'boxen'; -import Config from '../config/Config'; -import SharedConfig from '../shared/config/SharedConfig'; -import { stateConfigFile } from '../config/ConfigFiles'; +import axios, { AxiosError, AxiosRequestConfig, AxiosRequestHeaders } from 'axios'; +import SessionManager from './SessionManager'; +import FormData from 'form-data'; +import { StatusCodes } from 'http-status-codes'; +import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; +import { version } from '../config/Version'; +import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse'; +import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; +import boxen from 'boxen'; +import Config from '../config/Config'; +import SharedConfig from '../shared/config/SharedConfig'; +import { stateConfigFile } from '../config/ConfigFiles'; class HttpManager { @@ -21,10 +21,10 @@ class HttpManager { } private registerRequestInterceptor() { - axios.interceptors.request.use((config) => { + axios.interceptors.request.use(config => { if ( config.data instanceof FormData ) { - config.headers = { ...config.headers, ...(config.data as FormData).getHeaders() } as AxiosRequestHeaders; + config.headers = { ...config.headers, ...config.data.getHeaders() } as AxiosRequestHeaders; } if ( config.url && (config.url.indexOf(ClientsSharedConfig.apiURL) !== -1) ) { @@ -63,8 +63,66 @@ class HttpManager { process.exit(1); } + private apiMethodNotAllowed(error: AxiosError, isFromApi: boolean) { + if ( error.response?.status === StatusCodes.METHOD_NOT_ALLOWED && isFromApi && error.response.data ) { + const data: DojoBackendResponse<void> = error.response.data as DojoBackendResponse<void>; + + switch ( data.code ) { + case DojoStatusCode.CLIENT_NOT_SUPPORTED: + this.requestError('Client not recognized by the server. Please contact the administrator.'); + break; + case DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED: + this.requestError(`CLI version not anymore supported by the server. Please update the CLI.\nYou can download the latest stable version on this page:\n${ Config.gitlab.cliReleasePage }`); + break; + default: + break; + } + } + } + + private apiAuthorizationError(error: AxiosError, isFromApi: boolean) { + if ( this.handleAuthorizationCommandErrors && isFromApi && error.response ) { + switch ( error.response.status ) { + case StatusCodes.UNAUTHORIZED: + this.requestError('Session expired or does not exist. Please login again.'); + break; + case StatusCodes.FORBIDDEN: + this.requestError('Forbidden access.'); + break; + default: + this.requestError('Unknown error.'); + break; + } + } else { + this.handleAuthorizationCommandErrors = true; + } + } + + private async gitlabAuthorizationError(error: AxiosError, isFromGitlab: boolean): Promise<Promise<unknown> | undefined> { + const originalConfig = Object.assign({}, error.config, { _retry: false }); + + // Try to refresh the Gitlab tokens if the request have failed with a 401 error + if ( error.response && error.response.status === StatusCodes.UNAUTHORIZED && isFromGitlab && !originalConfig?._retry ) { + originalConfig._retry = true; + + try { + await SessionManager.refreshTokens(); + + return axios(error.config as AxiosRequestConfig); + } catch ( subError ) { + if ( subError instanceof AxiosError && subError.response && subError.response.data ) { + return Promise.reject(subError.response.data); + } + + return Promise.reject(subError); + } + } + + return undefined; + } + private registerResponseInterceptor() { - axios.interceptors.response.use((response) => { + axios.interceptors.response.use(response => { if ( response.data && response.data.sessionToken ) { SessionManager.apiToken = response.data.sessionToken; } @@ -80,61 +138,15 @@ class HttpManager { } return response; - }, async (error) => { + }, async error => { if ( error.response ) { - const originalConfig = error.config; const isFromApi = error.response.config.url && error.response.config.url.indexOf(ClientsSharedConfig.apiURL) !== -1; const isFromGitlab = error.response.config.url && error.response.config.url.indexOf(SharedConfig.gitlab.URL) !== -1; - // Try to refresh the Gitlab tokens if the request have failed with a 401 error - if ( error.response.status === StatusCodes.UNAUTHORIZED && isFromGitlab && !originalConfig._retry ) { - originalConfig._retry = true; - - try { - await SessionManager.refreshTokens(); - - return axios(originalConfig); - } catch ( error: unknown ) { - if ( error instanceof AxiosError ) { - if ( error.response && error.response.data ) { - return Promise.reject(error.response.data); - } - } - - return Promise.reject(error); - } - } - - if ( error.response.status === StatusCodes.METHOD_NOT_ALLOWED && isFromApi && error.response.data ) { - const data: DojoBackendResponse<void> = error.response.data; - - switch ( data.code ) { - case DojoStatusCode.CLIENT_NOT_SUPPORTED: - this.requestError('Client not recognized by the server. Please contact the administrator.'); - break; - case DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED: - this.requestError(`CLI version not anymore supported by the server. Please update the CLI.\nYou can download the latest stable version on this page:\n${ Config.gitlab.cliReleasePage }`); - break; - default: - break; - } - } - - if ( this.handleAuthorizationCommandErrors ) { - if ( isFromApi ) { - switch ( error.response.status ) { - case StatusCodes.UNAUTHORIZED: - this.requestError('Session expired or does not exist. Please login again.'); - break; - case StatusCodes.FORBIDDEN: - this.requestError('Forbidden access.'); - break; - } - } - } else { - this.handleAuthorizationCommandErrors = true; - } + await this.gitlabAuthorizationError(error, isFromGitlab); + this.apiMethodNotAllowed(error, isFromApi); + this.apiAuthorizationError(error, isFromApi); } else { this.requestError('Error connecting to the server. Please check your internet connection. If the problem persists, please contact the administrator.'); } diff --git a/NodeApp/src/managers/SessionManager.ts b/NodeApp/src/managers/SessionManager.ts index d06fef9e44ec22ec7e89d045f066995b67606fad..8b1ed3299e3ec76f86873389938776d43cb34797 100644 --- a/NodeApp/src/managers/SessionManager.ts +++ b/NodeApp/src/managers/SessionManager.ts @@ -25,7 +25,7 @@ import TextStyle from '../types/TextStyle'; class LoginServer { readonly events: EventEmitter = new EventEmitter(); - private server: http.Server; + private readonly server: http.Server; constructor() { this.server = http.createServer((req, res) => { @@ -48,10 +48,7 @@ class LoginServer { } sendError(`Incorrect call => ${ req.url }`); - return; } - - //sendError(`Unknown route call => ${ req.url }`); }); } @@ -120,14 +117,12 @@ class SessionManager { sessionConfigFile.setParam(LocalConfigKeys.GITLAB, credentials); } - constructor() { } - private async getGitlabCodeFromHeadlessEnvironment(): Promise<string> { const indent: string = ' '; console.log(`${ indent }Please open the following URL in your web browser and accept to give the requested permissions to Dojo:`); console.log(TextStyle.URL(`${ indent }${ Config.login.gitlab.url.code }`)); console.log(`${ indent }Then, copy the code at the end of the redirected url and paste it bellow.`); - console.log(`${ indent }Example of url (here the code is 123456): ${ TextStyle.URL(`${ SharedConfig.login.gitlab.url.redirect }?code=`) }${ chalk.green('123456') }`); + console.log(`${ indent }Example of url (here the code is 123456): ${ TextStyle.URL(SharedConfig.login.gitlab.url.redirect + '?code=') }${ chalk.green('123456') }`); return (await inquirer.prompt({ type : 'password', name : 'code', @@ -220,7 +215,7 @@ class SessionManager { throw error; } - const isGitlabTokensValid = (await GitlabManager.testToken()).every((value) => value); + const isGitlabTokensValid = (await GitlabManager.testToken()).every(value => value); if ( !isGitlabTokensValid ) { throw new Error('Gitlab tokens are invalid'); } @@ -259,25 +254,25 @@ class SessionManager { }; } - checkPermissions(verbose: boolean = true, indent: number = 8, checkPermissions: Array<string> | null = []): Permissions { - const hasPermission = (permissionPredicate: () => boolean, verboseText: string): boolean => { - const isAllowed: boolean = this.profile !== undefined && permissionPredicate(); + private hasPermission(permissionPredicate: () => boolean, verbose: boolean, verboseText: string, indent: number): boolean { + const isAllowed: boolean = this.profile !== undefined && permissionPredicate(); - if ( verbose ) { - const spinner: ora.Ora = ora({ - text : verboseText, - indent: indent - }).start(); - isAllowed ? spinner.succeed() : spinner.fail(); - } + if ( verbose ) { + const spinner: ora.Ora = ora({ + text : verboseText, + indent: indent + }).start(); + isAllowed ? spinner.succeed() : spinner.fail(); + } - return isAllowed; - }; + return isAllowed; + } + checkPermissions(verbose: boolean = true, indent: number = 8, checkPermissions: Array<string> | null = []): Permissions { return { - student : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('student')) ? hasPermission(() => true, 'Student permissions') : false, - teachingStaff: checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('teachingStaff')) ? hasPermission(() => this.profile?.isTeachingStaff ?? false, 'Teaching staff permissions') : false, - admin : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('admin')) ? hasPermission(() => this.profile?.isAdmin ?? false, 'Admin permissions') : false + student : checkPermissions && (checkPermissions.length === 0 || checkPermissions.includes('student')) ? this.hasPermission(() => true, verbose, 'Student permissions', indent) : false, + teachingStaff: checkPermissions && (checkPermissions.length === 0 || checkPermissions.includes('teachingStaff')) ? this.hasPermission(() => this.profile?.isTeachingStaff ?? false, verbose, 'Teaching staff permissions', indent) : false, + admin : checkPermissions && (checkPermissions.length === 0 || checkPermissions.includes('admin')) ? this.hasPermission(() => this.profile?.isAdmin ?? false, verbose, 'Admin permissions', indent) : false }; } diff --git a/NodeApp/src/shared b/NodeApp/src/shared index 6214acbd799d9eed3f5b6840858f8d5ecda82c86..fca59c4d155603b53d48a30401aabab82d91fc59 160000 --- a/NodeApp/src/shared +++ b/NodeApp/src/shared @@ -1 +1 @@ -Subproject commit 6214acbd799d9eed3f5b6840858f8d5ecda82c86 +Subproject commit fca59c4d155603b53d48a30401aabab82d91fc59 diff --git a/NodeApp/src/sharedByClients b/NodeApp/src/sharedByClients index 4efff1c5127c6f84104016d7041d0cf281d981f8..2b2b2376b0389a39283327bba5bfaf7d1ce136ac 160000 --- a/NodeApp/src/sharedByClients +++ b/NodeApp/src/sharedByClients @@ -1 +1 @@ -Subproject commit 4efff1c5127c6f84104016d7041d0cf281d981f8 +Subproject commit 2b2b2376b0389a39283327bba5bfaf7d1ce136ac