diff --git a/NodeApp/.idea/material_theme_project_new.xml b/NodeApp/.idea/material_theme_project_new.xml index d8084f3aa2a34d701302528b05104bac64d7bc07..16e830f2d917c0815e814dcbf57e219c3b85c2e2 100644 --- a/NodeApp/.idea/material_theme_project_new.xml +++ b/NodeApp/.idea/material_theme_project_new.xml @@ -3,7 +3,9 @@ <component name="MaterialThemeProjectNewConfig"> <option name="metadata"> <MTProjectMetadataState> - <option name="userId" value="104e8585:19002424fea:-7f91" /> + <option name="migrated" value="true" /> + <option name="pristineConfig" value="false" /> + <option name="userId" value="104e8585:19002424fea:-7ffe" /> </MTProjectMetadataState> </option> </component> diff --git a/NodeApp/package-lock.json b/NodeApp/package-lock.json index 407e5f0faba574e4d0bc00dc9a606e2fc126087e..2ae9954cb1e3dbd04653ce0d2d34f80e8e23fdf8 100644 --- a/NodeApp/package-lock.json +++ b/NodeApp/package-lock.json @@ -18,6 +18,7 @@ "axios": "^1.7.2", "boxen": "^5.1.2", "chalk": "^4.1.2", + "cli-table3": "^0.6.5", "commander": "^12.1.0", "form-data": "^4.0.0", "fs-extra": "^11.2.0", @@ -2349,6 +2350,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", diff --git a/NodeApp/package.json b/NodeApp/package.json index 2bad118b89ede5c0f0a450cb6fa1b63c162cd06c..5cc93c6a724a90acd4c5a5c2b643ed7a8494694e 100644 --- a/NodeApp/package.json +++ b/NodeApp/package.json @@ -3,7 +3,7 @@ "description" : "CLI of the Dojo project", "version" : "4.2.0", "license" : "AGPLv3", - "author" : "Michaël Minelli <dojo@minelli.me>", + "author" : "Michaël Minelli <dojo@mail.minelli.swiss>", "main" : "dist/app.js", "bin" : { "dojo": "./dist/app.js" @@ -29,7 +29,7 @@ "lint" : "npx eslint .", "genversion" : "npx genversion -s -e src/config/Version.ts", "build" : "npm run genversion; npx tsc", - "start:dev" : "npm run genversion; npm run lint; tsc --noEmit && npx tsx src/app.ts", + "start:dev" : "npm run genversion; npm run lint; tsc --noEmit && npx tsx --no-warnings src/app.ts", "test" : "echo \"Error: no test specified\" && exit 1" }, "dependencies" : { @@ -42,6 +42,7 @@ "axios" : "^1.7.2", "boxen" : "^5.1.2", "chalk" : "^4.1.2", + "cli-table3" : "^0.6.5", "commander" : "^12.1.0", "form-data" : "^4.0.0", "fs-extra" : "^11.2.0", diff --git a/NodeApp/src/commander/CommanderApp.ts b/NodeApp/src/commander/CommanderApp.ts index 11a70b035680f25752044a352b054e5d91a20e65..30e9cfe3ba8195af46113d33add1ecff63aa2550 100644 --- a/NodeApp/src/commander/CommanderApp.ts +++ b/NodeApp/src/commander/CommanderApp.ts @@ -13,7 +13,7 @@ import AuthCommand from './auth/AuthCommand.js'; import SessionCommand from './auth/SessionCommand.js'; import UpgradeCommand from './UpgradeCommand.js'; import TextStyle from '../types/TextStyle.js'; -import TagCommand from './tags/TagCommand'; +import TagCommand from './tag/TagCommand'; class CommanderApp { diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts index 922c126c5d7205de5de12136b80fcc980686ddcc..86a5bc585c2f68c833fb8b3618792da99fa9b439 100644 --- a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts +++ b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts @@ -33,9 +33,7 @@ class AssignmentCreateCommand extends CommanderCommand { private async dataRetrieval(options: CommandOptions) { console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...')); - if ( !await AccessesHelper.checkTeachingStaff() ) { - throw new Error(); - } + await AccessesHelper.checkTeachingStaff(); this.members = await GitlabManager.fetchMembers(options); if ( !this.members ) { diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts index 73a67c5387e448c4e904808e9c4341cbde33c971..3db1510582c5927fcf506ac6acc360f04548aa8c 100644 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts @@ -33,9 +33,7 @@ class ExerciseCreateCommand extends CommanderCommand { private async dataRetrieval(options: CommandOptions) { console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...')); - if ( !await AccessesHelper.checkStudent() ) { - throw new Error(); - } + await AccessesHelper.checkStudent(); this.members = await GitlabManager.fetchMembers(options); if ( !this.members ) { diff --git a/NodeApp/src/commander/tag/TagCommand.ts b/NodeApp/src/commander/tag/TagCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc4deff11b301a020626a9e44800f7a2850008df --- /dev/null +++ b/NodeApp/src/commander/tag/TagCommand.ts @@ -0,0 +1,25 @@ +import CommanderCommand from '../CommanderCommand'; +import TagCreateCommand from './subcommands/TagCreateCommand'; +import TagDelete from './subcommands/TagDeleteCommand'; +import TagProposalCommand from './subcommands/proposal/TagProposalCommand'; + + +class AddTagCommand extends CommanderCommand { + protected commandName: string = 'tag'; + + protected defineCommand() { + this.command + .description('manage tags'); + } + + protected defineSubCommands() { + TagCreateCommand.registerOnCommand(this.command); + TagDelete.registerOnCommand(this.command); + TagProposalCommand.registerOnCommand(this.command); + } + + protected async commandAction(): Promise<void> { } +} + + +export default new AddTagCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tag/subcommands/TagCreateCommand.ts b/NodeApp/src/commander/tag/subcommands/TagCreateCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcbf63e76e94c741e2f08c1f0f2603d9cdd29990 --- /dev/null +++ b/NodeApp/src/commander/tag/subcommands/TagCreateCommand.ts @@ -0,0 +1,60 @@ +import CommanderCommand from '../../CommanderCommand'; +import DojoBackendManager from '../../../managers/DojoBackendManager'; +import { Option } from 'commander'; +import TextStyle from '../../../types/TextStyle'; +import SessionManager from '../../../managers/SessionManager'; +import ora from 'ora'; + + +type CommandOptions = { name: string, type: 'Language' | 'Framework' | 'Theme' | 'UserDefined' } + + +class TagCreateCommand extends CommanderCommand { + protected commandName: string = 'create'; + + protected defineCommand() { + this.command + .description('create a new tag') + .requiredOption('-n, --name <name>', 'name of the tag') + .addOption(new Option('-t, --type <type>', 'type of the tag').choices([ 'Language', 'Framework', 'Theme', 'UserDefined' ]).makeOptionMandatory(true)) + .action(this.commandAction.bind(this)); + } + + private async dataRetrieval(options: CommandOptions) { + console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...')); + + const sessionResult = await SessionManager.testSession(true, [ 'admin' ]); + + if ( !sessionResult ) { + throw new Error(); + } + + if ( options.type !== 'UserDefined' && !sessionResult.admin ) { + ora({ + text : `Only admins can create non UserDefined tags`, + indent: 4 + }).start().fail(); + throw new Error(); + } + } + + private async createTag(options: CommandOptions) { + console.log(TextStyle.BLOCK('Please wait while we are creating the tag...')); + + const tag = await DojoBackendManager.createTag(options.name, options.type); + if ( !tag ) { + throw new Error(); + } + } + + protected async commandAction(options: CommandOptions): Promise<void> { + try { + await this.dataRetrieval(options); + await this.createTag(options); + } catch ( e ) { /* Do nothing */ } + } + +} + + +export default new TagCreateCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tag/subcommands/TagDeleteCommand.ts b/NodeApp/src/commander/tag/subcommands/TagDeleteCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..dae2d18a27f8dc3d060e94678781d4b6b44bb370 --- /dev/null +++ b/NodeApp/src/commander/tag/subcommands/TagDeleteCommand.ts @@ -0,0 +1,40 @@ +import CommanderCommand from '../../CommanderCommand'; +import DojoBackendManager from '../../../managers/DojoBackendManager'; +import TextStyle from '../../../types/TextStyle'; +import AccessesHelper from '../../../helpers/AccessesHelper'; + + +class TagDeleteCommand extends CommanderCommand { + protected commandName: string = 'delete'; + + protected defineCommand() { + this.command + .description('Delete a tag') + .argument('<name>', 'name of the tag') + .action(this.commandAction.bind(this)); + } + + private async dataRetrieval() { + console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...')); + + await AccessesHelper.checkAdmin(); + } + + private async deleteTag(name: string) { + console.log(TextStyle.BLOCK('Please wait while we are deleting the tag...')); + + if ( !await DojoBackendManager.deleteTag(name) ) { + throw new Error(); + } + } + + protected async commandAction(name: string): Promise<void> { + try { + await this.dataRetrieval(); + await this.deleteTag(name); + } catch ( e ) { /* Do nothing */ } + } +} + + +export default new TagDeleteCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tag/subcommands/proposal/TagProposalCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/TagProposalCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..3af5cc94960d434b5b6748c9885b0cf06c394d30 --- /dev/null +++ b/NodeApp/src/commander/tag/subcommands/proposal/TagProposalCommand.ts @@ -0,0 +1,26 @@ +import CommanderCommand from '../../../CommanderCommand'; +import TagProposalListCommand from './subcommands/TagProposalListCommand'; +import TagProposalCreateCommand from './subcommands/TagProposalCreateCommand'; +import TagProposalApproveCommand from './subcommands/TagProposalApproveCommand'; +import TagProposalDeclineCommand from './subcommands/TagProposalDeclineCommand'; + + +class TagProposalCommand extends CommanderCommand { + protected commandName: string = 'proposal'; + + protected defineCommand() { + this.command.description('manage tag proposals'); + } + + protected defineSubCommands() { + TagProposalListCommand.registerOnCommand(this.command); + TagProposalCreateCommand.registerOnCommand(this.command); + TagProposalApproveCommand.registerOnCommand(this.command); + TagProposalDeclineCommand.registerOnCommand(this.command); + } + + protected async commandAction(): Promise<void> { } +} + + +export default new TagProposalCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalAnswerCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalAnswerCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0b1523d754264c62179398b94070974ab8e94bf --- /dev/null +++ b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalAnswerCommand.ts @@ -0,0 +1,44 @@ +import CommanderCommand from '../../../../CommanderCommand'; +import DojoBackendManager from '../../../../../managers/DojoBackendManager'; +import TextStyle from '../../../../../types/TextStyle'; +import AccessesHelper from '../../../../../helpers/AccessesHelper'; + + +type CommandOptions = { commentary?: string } + + +abstract class TagProposalAnswerCommand extends CommanderCommand { + protected abstract state: 'Approved' | 'Declined'; + + protected defineCommand() { + this.command + .description(`${ this.state === 'Approved' ? 'Approve' : 'Decline' } a tag proposition`) + .argument('<name>', 'name of the tag proposition') + .option('-c, --commentary <comment>', 'add a commentary to the answer') + .action(this.commandAction.bind(this)); + } + + private async dataRetrieval() { + console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...')); + + await AccessesHelper.checkAdmin(); + } + + private async answerTag(name: string, options: CommandOptions) { + console.log(TextStyle.BLOCK('Please wait while we are answering to the tag proposal...')); + + if ( !await DojoBackendManager.answerTagProposal(name, this.state, options.commentary ?? '') ) { + throw new Error(); + } + } + + protected async commandAction(name: string, options: CommandOptions): Promise<void> { + try { + await this.dataRetrieval(); + await this.answerTag(name, options); + } catch ( e ) { /* Do nothing */ } + } +} + + +export default TagProposalAnswerCommand; \ No newline at end of file diff --git a/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalApproveCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalApproveCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..e97b32310dfc84065f54680c383ee693b55f08d9 --- /dev/null +++ b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalApproveCommand.ts @@ -0,0 +1,10 @@ +import TagProposalAnswerCommand from './TagProposalAnswerCommand'; + + +class TagProposalApproveCommand extends TagProposalAnswerCommand { + protected commandName: string = 'approve'; + protected state: 'Approved' | 'Declined' = 'Approved'; +} + + +export default new TagProposalApproveCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalCreateCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalCreateCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9fe4c62851f0a6d12b4efb277aa6342d5620ebc --- /dev/null +++ b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalCreateCommand.ts @@ -0,0 +1,46 @@ +import CommanderCommand from '../../../../CommanderCommand'; +import DojoBackendManager from '../../../../../managers/DojoBackendManager'; +import { Option } from 'commander'; +import TextStyle from '../../../../../types/TextStyle'; +import AccessesHelper from '../../../../../helpers/AccessesHelper'; + + +type CommandOptions = { name: string, type: 'Language' | 'Framework' | 'Theme' | 'UserDefined' } + + +class TagProposalCreateCommand extends CommanderCommand { + protected commandName: string = 'create'; + + protected defineCommand() { + this.command + .description('Propose a tag proposition') + .requiredOption('-n, --name <name>', 'name of the tag') + .addOption(new Option('-t, --type <type>', 'type of the tag').choices([ 'Language', 'Framework', 'Theme', 'UserDefined' ]).makeOptionMandatory(true)) + .action(this.commandAction.bind(this)); + } + + private async dataRetrieval() { + console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...')); + + await AccessesHelper.checkTeachingStaff(); + } + + private async createTagProposal(options: CommandOptions) { + console.log(TextStyle.BLOCK('Please wait while we are creating the tag proposal...')); + + const tag = await DojoBackendManager.createTagProposal(options.name, options.type); + if ( !tag ) { + throw new Error(); + } + } + + protected async commandAction(options: CommandOptions): Promise<void> { + try { + await this.dataRetrieval(); + await this.createTagProposal(options); + } catch ( e ) { /* Do nothing */ } + } +} + + +export default new TagProposalCreateCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalDeclineCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalDeclineCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..aae1ccc120b1011cc96a0f0429b806574d6508de --- /dev/null +++ b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalDeclineCommand.ts @@ -0,0 +1,10 @@ +import TagProposalAnswerCommand from './TagProposalAnswerCommand'; + + +class TagProposalDeclineCommand extends TagProposalAnswerCommand { + protected commandName: string = 'decline'; + protected state: 'Approved' | 'Declined' = 'Declined'; +} + + +export default new TagProposalDeclineCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalListCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalListCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..f73bbd0caaf0d7ed01e35d02f419df7b9b90d934 --- /dev/null +++ b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalListCommand.ts @@ -0,0 +1,68 @@ +import CommanderCommand from '../../../../CommanderCommand'; +import TagProposal from '../../../../../sharedByClients/models/TagProposal'; +import DojoBackendManager from '../../../../../managers/DojoBackendManager'; +import TextStyle from '../../../../../types/TextStyle'; +import AccessesHelper from '../../../../../helpers/AccessesHelper'; +import { Option } from 'commander'; +import ora from 'ora'; +import Table from 'cli-table3'; + + +type CommandOptions = { state: 'PendingApproval' | 'Approved' | 'Declined' } + + +class TagProposalListCommand extends CommanderCommand { + protected commandName: string = 'list'; + + protected defineCommand() { + this.command + .description('Get a tag proposition') + .addOption(new Option('-s, --state <state>', 'state of the tag proposal').choices([ 'PendingApproval', 'Approved', 'Declined' ]).default('PendingApproval')) + .action(this.commandAction.bind(this)); + } + + private async dataRetrieval() { + console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...')); + + await AccessesHelper.checkAdmin(); + } + + private async listTagProposals(options: CommandOptions) { + console.log(TextStyle.BLOCK('Please wait while we are creating the tag...')); + + const spinner: ora.Ora = ora('Retrieving tag proposals...'); + + const tags = await DojoBackendManager.getTagProposals(options.state); + + if ( tags && tags.length > 0 ) { + spinner.succeed(`Tag proposals retrieved. Here is the list of tag proposal with '${ options.state }' state:`); + + const table = new Table({ + head: [ 'Name', 'Type', 'Details' ] + }); + + tags.forEach((tag: TagProposal) => { + table.push([ tag.name, tag.type, tag.details ]); + }); + + console.log(table.toString()); + } else if ( tags ) { + spinner.fail(`There is no tag proposal with '${ options.state }' state.`); + throw new Error(); + } else { + spinner.fail('Failed to retrieve tag proposals.'); + throw new Error(); + } + } + + protected async commandAction(options: CommandOptions): Promise<void> { + try { + await this.dataRetrieval(); + await this.listTagProposals(options); + } catch ( e ) { /* Do nothing */ } + } + +} + + +export default new TagProposalListCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tags/TagCommand.ts b/NodeApp/src/commander/tags/TagCommand.ts deleted file mode 100644 index 28bf1d24672fecaa366119a9c71e87c5f5425c23..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/tags/TagCommand.ts +++ /dev/null @@ -1,30 +0,0 @@ -import CommanderCommand from '../CommanderCommand'; -import TagAdd from './subcommands/TagAdd'; -import TagDelete from './subcommands/TagDelete'; -import TagGetPropose from './subcommands/TagGetPropose'; -import TagPostPropose from './subcommands/TagPostPropose'; -import TagAnswerPropose from './subcommands/TagAnswerPropose'; - - - -class AddTagCommand extends CommanderCommand { - protected commandName: string = 'tag'; - - protected defineCommand() { - this.command - .description('Manages tags'); - } - - protected defineSubCommands() { - TagAdd.registerOnCommand(this.command); - TagDelete.registerOnCommand(this.command); - TagGetPropose.registerOnCommand(this.command); - TagPostPropose.registerOnCommand(this.command); - TagAnswerPropose.registerOnCommand(this.command); - } - - protected async commandAction(): Promise<void> { } -} - - -export default new AddTagCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tags/subcommands/TagAdd.ts b/NodeApp/src/commander/tags/subcommands/TagAdd.ts deleted file mode 100644 index 904a357de6f03f2357b8778a15ee5a5867d81b5b..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/tags/subcommands/TagAdd.ts +++ /dev/null @@ -1,33 +0,0 @@ -import CommanderCommand from '../../CommanderCommand'; -import SessionManager from '../../../managers/SessionManager'; -import DojoBackendManager from "../../../managers/DojoBackendManager"; -import Tags from '../../../sharedByClients/models/Tag'; - - -class TagAddCommand extends CommanderCommand { - protected commandName: string = 'add'; - - protected defineCommand() { - this.command - .description('Add a tag') - .argument('<tagName>', 'name of the tag') //test - .argument('<tagType>', 'type of the tag') - .action(this.commandAction.bind(this)); - } - - protected async commandAction(name : string, type: string): Promise<void> { - let tag : Tags | undefined; - { - if ( !await SessionManager.testSession(true, null) ) { - return; - } - tag = await DojoBackendManager.addTag(name, type); - if ( !tag ) { - return; - } - } - } - -} - -export default new TagAddCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tags/subcommands/TagAnswerPropose.ts b/NodeApp/src/commander/tags/subcommands/TagAnswerPropose.ts deleted file mode 100644 index b7b9178cdea0ad07f9251c571ff0309175cd84bb..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/tags/subcommands/TagAnswerPropose.ts +++ /dev/null @@ -1,35 +0,0 @@ -import CommanderCommand from '../../CommanderCommand'; -import SessionManager from '../../../managers/SessionManager'; -import TagSubmit from '../../../sharedByClients/models/TagSubmit'; -import DojoBackendManager from "../../../managers/DojoBackendManager"; - - -class TagAnswerProposeCommand extends CommanderCommand { - protected commandName: string = 'answer'; - - protected defineCommand() { - this.command - .description('Answer to a tag proposition') - .argument('<tagProposalName>', 'name of the tag') - .argument('<tagType>', 'name of the tag') - .argument('<tagState>', 'name of the tag') - .argument('<tagDetail>', 'name of the tag') - .action(this.commandAction.bind(this)); - } - - protected async commandAction(tagProposalName: string, type: string, state: string, detail: string): Promise<void> { - let tag : TagSubmit | undefined; - { - if ( !await SessionManager.testSession(true, null) ) { - return; - } - tag = await DojoBackendManager.answerProposeTag(tagProposalName, type, state, detail); - if ( !tag ) { - return; - } - } - } - -} - -export default new TagAnswerProposeCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tags/subcommands/TagDelete.ts b/NodeApp/src/commander/tags/subcommands/TagDelete.ts deleted file mode 100644 index 4490eacd2a552b8aba0fd2bb2a9753e4271a012e..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/tags/subcommands/TagDelete.ts +++ /dev/null @@ -1,33 +0,0 @@ -import CommanderCommand from '../../CommanderCommand'; -import SessionManager from '../../../managers/SessionManager'; -import DojoBackendManager from "../../../managers/DojoBackendManager"; -import Tags from "../../../sharedByClients/models/Tag"; - - -class TagDeleteCommand extends CommanderCommand { - protected commandName: string = 'delete'; - - protected defineCommand() { - this.command - .description('Delete a tag') - .argument('<tagName>', 'name of the tag') - .action(this.commandAction.bind(this)); - } - - protected async commandAction(name : string): Promise<void> { - let tag : Tags; - { - if ( !await SessionManager.testSession(true, null) ) { - return; - } - - tag = await DojoBackendManager.deleteTag(name); - if ( !tag ) { - return; - } - } - } - -} - -export default new TagDeleteCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tags/subcommands/TagGetPropose.ts b/NodeApp/src/commander/tags/subcommands/TagGetPropose.ts deleted file mode 100644 index 3ce3665f546e05c64359bb3d3c853d5a8618fe38..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/tags/subcommands/TagGetPropose.ts +++ /dev/null @@ -1,36 +0,0 @@ -import CommanderCommand from '../../CommanderCommand'; -import SessionManager from '../../../managers/SessionManager'; -import TagSubmit from '../../../sharedByClients/models/TagSubmit'; -import DojoBackendManager from "../../../managers/DojoBackendManager"; - - -class TagGetProposeCommand extends CommanderCommand { - protected commandName: string = 'get-propose'; - - protected defineCommand() { - this.command - .description('Get a tag proposition') - .argument('<stateTag>', 'state of the tags') - .action(this.commandAction.bind(this)); - } - - protected async commandAction(state : string): Promise<void> { - let tag : TagSubmit | undefined; - if(state == null){ - state = "PendingApproval"; - } - { - if ( !await SessionManager.testSession(true, null) ) { - return; - } - - tag = await DojoBackendManager.getProposeTag(state); - if ( !tag ) { - return; - } - } - } - -} - -export default new TagGetProposeCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/tags/subcommands/TagPostPropose.ts b/NodeApp/src/commander/tags/subcommands/TagPostPropose.ts deleted file mode 100644 index 985148566dd2dd8464e6cf290c35117523a4fe65..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/tags/subcommands/TagPostPropose.ts +++ /dev/null @@ -1,33 +0,0 @@ -import CommanderCommand from '../../CommanderCommand'; -import SessionManager from '../../../managers/SessionManager'; -import DojoBackendManager from "../../../managers/DojoBackendManager"; -import TagSubmit from '../../../sharedByClients/models/TagSubmit'; - -class TagPostProposeCommand extends CommanderCommand { - protected commandName: string = 'post-propose'; - - protected defineCommand() { - this.command - .description('Propose a tag') - .argument('<tagName>', 'name of the tag') - .argument('<tagType>', 'type of the tag') - .action(this.commandAction.bind(this)); - } - - protected async commandAction(name : string, type: string): Promise<void> { - let tag : TagSubmit | undefined; - { - if ( !await SessionManager.testSession(true, null) ) { - return; - } - - tag = await DojoBackendManager.postProposeTag(name, type); - if ( !tag ) { - return; - } - } - } - -} - -export default new TagPostProposeCommand(); \ No newline at end of file diff --git a/NodeApp/src/helpers/AccessesHelper.ts b/NodeApp/src/helpers/AccessesHelper.ts index 1be0777e85227aae8e950cb6974e5df2866e72ba..a07b7aa24a40229be5460f1126e0e99eb4455138 100644 --- a/NodeApp/src/helpers/AccessesHelper.ts +++ b/NodeApp/src/helpers/AccessesHelper.ts @@ -3,32 +3,28 @@ import GitlabManager from '../managers/GitlabManager.js'; class AccessesHelper { - async checkStudent(testGitlab: boolean = false): Promise<boolean> { - const sessionResult = await SessionManager.testSession(true, [ 'student' ]); + private async checkAccess(accessName: string, testGitlab: boolean = false) { + const sessionResult = await SessionManager.testSession(true, [ accessName ]); - if ( !sessionResult ) { - return false; + if ( !sessionResult || !(sessionResult as unknown as { [key: string]: boolean })[accessName] ) { + throw new Error(); } - if ( testGitlab ) { - return (await GitlabManager.testToken(true)).every(result => result); - } else { - return true; + if ( testGitlab && !(await GitlabManager.testToken(true)).every(result => result) ) { + throw new Error(); } } - async checkTeachingStaff(testGitlab: boolean = false): Promise<boolean> { - const sessionResult = await SessionManager.testSession(true, [ 'teachingStaff' ]); + async checkStudent(testGitlab: boolean = false) { + await this.checkAccess('student', testGitlab); + } - if ( !sessionResult || !sessionResult.teachingStaff ) { - return false; - } + async checkTeachingStaff(testGitlab: boolean = false) { + await this.checkAccess('teachingStaff', testGitlab); + } - if ( testGitlab ) { - return (await GitlabManager.testToken(true)).every(result => result); - } else { - return true; - } + async checkAdmin(testGitlab: boolean = false) { + await this.checkAccess('admin', testGitlab); } } diff --git a/NodeApp/src/managers/DojoBackendManager.ts b/NodeApp/src/managers/DojoBackendManager.ts index 5a740d48b8b5bacc9d4a7addf09818f8e58b9b96..414945f897192e5422b9cf10088403e67c5e554b 100644 --- a/NodeApp/src/managers/DojoBackendManager.ts +++ b/NodeApp/src/managers/DojoBackendManager.ts @@ -11,8 +11,8 @@ import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js'; import * as Gitlab from '@gitbeaker/rest'; import DojoBackendHelper from '../sharedByClients/helpers/Dojo/DojoBackendHelper.js'; import GitlabPipelineStatus from '../shared/types/Gitlab/GitlabPipelineStatus.js'; -import Tags from '../sharedByClients/models/Tag'; -import TagSubmit from '../sharedByClients/models/TagSubmit'; +import Tag from '../sharedByClients/models/Tag'; +import TagProposal from '../sharedByClients/models/TagProposal'; class DojoBackendManager { @@ -61,6 +61,15 @@ class DojoBackendManager { 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; + case DojoStatusCode.TAG_ONLY_ADMIN_CREATION: + spinner.fail(`Only admins can create non UserDefined tags.`); + break; + case DojoStatusCode.TAG_WITH_ACTIVE_LINK_DELETION: + spinner.fail(`This tag is used in resources (e.g. assignments). Please remove this tag from these resources before deleting it.`); + break; + case DojoStatusCode.TAG_PROPOSAL_ANSWER_NOT_PENDING: + spinner.fail(`This tag proposal have already been answered.`); + break; default: if ( otherErrorHandler ) { otherErrorHandler(error, spinner, verbose); @@ -250,33 +259,113 @@ class DojoBackendManager { return false; } - } - public async addTag(name : string, type: string) : Promise<Tags | undefined> { - return (await axios.post<DojoBackendResponse<Tags>>(this.getApiUrl(ApiRoute.ADD_TAG),{ - name : name, - type :type - })).data.data; } - public async deleteTag(name : string) : Promise<Tags> { - return (await axios.delete<DojoBackendResponse<Tags>>(this.getApiUrl(ApiRoute.DELETE_TAG).replace('{{tageName}}', name))).data.data; + + public async createTag(name: string, type: string, verbose: boolean = true): Promise<Tag | undefined> { + const spinner: ora.Ora = ora('Creating tag...'); + + if ( verbose ) { + spinner.start(); + } + + try { + const response = await axios.post<DojoBackendResponse<Tag>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_CREATE), { + name: name, + type: type + }); + + if ( verbose ) { + spinner.succeed(`Tag successfully created`); + } + + return response.data.data; + } catch ( error ) { + this.handleApiError(error, spinner, verbose, `Tag creation error: ${ error }`); + + return undefined; + } } - public async getProposeTag(state : string) : Promise<TagSubmit | undefined> { - return (await axios.get<DojoBackendResponse<TagSubmit>>(this.getApiUrl(ApiRoute.PROPOSE_TAG).replace('{{tagState}}', state))).data.data; + + public async deleteTag(name: string, verbose: boolean = true): Promise<boolean> { + const spinner: ora.Ora = ora('Deleting tag...'); + + if ( verbose ) { + spinner.start(); + } + + try { + await axios.delete<DojoBackendResponse<Tag>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_DELETE, { tagName: name })); + + if ( verbose ) { + spinner.succeed(`Tag successfully deleted`); + } + + return true; + } catch ( error ) { + this.handleApiError(error, spinner, verbose, `Tag deletion error: ${ error }`); + + return false; + } } - public async postProposeTag(name : string, type: string) : Promise<TagSubmit | undefined> { - return (await axios.post<DojoBackendResponse<TagSubmit>>(this.getApiUrl(ApiRoute.PROPOSE_TAG).replace('{{tagState}}', ""),{ - name : name, - type :type - })).data.data; + + public async getTagProposals(state: string | undefined): Promise<Array<TagProposal> | undefined> { + try { + return (await axios.get<DojoBackendResponse<Array<TagProposal>>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_GET_CREATE), { params: { stateFilter: state } })).data.data; + } catch ( error ) { + return undefined; + } } - public async answerProposeTag(tagProposalName : string, type: string, state: string, detail: string) : Promise<TagSubmit | undefined> { - return (await axios.patch<DojoBackendResponse<TagSubmit>>(this.getApiUrl(ApiRoute.PROPOSE_TAG).replace('{{tagState}}', ""), { - tagProposalName: tagProposalName, - type: type, - state: state, - detail: detail - })).data.data; + + public async createTagProposal(name: string, type: string, verbose: boolean = true): Promise<TagProposal | undefined> { + const spinner: ora.Ora = ora('Creating tag...'); + + if ( verbose ) { + spinner.start(); + } + + try { + const response = await axios.post<DojoBackendResponse<TagProposal>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_GET_CREATE), { + name: name, + type: type + }); + + if ( verbose ) { + spinner.succeed(`Tag proposal successfully created`); + } + + return response.data.data; + } catch ( error ) { + this.handleApiError(error, spinner, verbose, `Tag proposal creation error: ${ error }`); + + return undefined; + } + } + + public async answerTagProposal(tagProposalName: string, state: 'Approved' | 'Declined', details: string, verbose: boolean = true): Promise<boolean> { + const spinner: ora.Ora = ora('Answering tag proposal...'); + + if ( verbose ) { + spinner.start(); + } + + try { + await axios.patch<DojoBackendResponse<TagProposal>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_UPDATE, { tagName: tagProposalName }), { + state : state, + details: details + }); + + if ( verbose ) { + spinner.succeed(`Tag proposal ${ state.toLowerCase() } with success`); + } + + return true; + } catch ( error ) { + this.handleApiError(error, spinner, verbose, `Tag proposal answer error: ${ error }`); + + return false; + } } } + export default new DojoBackendManager(); diff --git a/NodeApp/src/shared b/NodeApp/src/shared index c2afa861bf6306ddec79ffd465a4c7b0edcd3453..708a3c0805fb2b2d853a781bde86a10d5282545a 160000 --- a/NodeApp/src/shared +++ b/NodeApp/src/shared @@ -1 +1 @@ -Subproject commit c2afa861bf6306ddec79ffd465a4c7b0edcd3453 +Subproject commit 708a3c0805fb2b2d853a781bde86a10d5282545a diff --git a/NodeApp/src/sharedByClients b/NodeApp/src/sharedByClients index 60ce3995edec4f907f62dd03a32cc24660de51b1..026cd6d1569987dc89081d0cd81ccf05a83d87de 160000 --- a/NodeApp/src/sharedByClients +++ b/NodeApp/src/sharedByClients @@ -1 +1 @@ -Subproject commit 60ce3995edec4f907f62dd03a32cc24660de51b1 +Subproject commit 026cd6d1569987dc89081d0cd81ccf05a83d87de