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