diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d0f582d4aa88849532df7fc7799d85b497d7dd..3eb4d6417cec6278e06f7e49881d844e104a2583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,21 @@ **💥 Breaking:** **⚠️ Deprecation:** + +### 🔨 Internal / Developers +- No modifications / Keep major and minors versions in sync with all parts of the project --> +## 3.0.0 (?) + +### ✨ Feature +- Login to Dojo app via Gitlab OAuth +- User access type management + - **student**: default access type for new users + - **teachingStaff**: student access + possibility of creating assignment + - **admin**: teachingStaff access + access to user access management + + ## 2.2.0 (2023-10-16) ### ✨ Feature diff --git a/NodeApp/.env.vault b/NodeApp/.env.vault index 08c024706a6acbd81885bf354454c5ddc6d6c4ea..e6e7181907e0b3b11f2a65563dd356f6db2f4ea4 100644 --- a/NodeApp/.env.vault +++ b/NodeApp/.env.vault @@ -4,8 +4,8 @@ #/--------------------------------------------------/ # development -DOTENV_VAULT_DEVELOPMENT="FvCrekiDgv/1bvov01DTRTEdipq7tduoYf/cAjX3+slE4zZSVG5S7AuQ2bXf4gzSHrfxjsx2kTl5ild1l+pSQGiDrLqdj0EigMleCya/xRiEDuVBeGRLZLJfWS/AGYi31/sNxtCT2FBcYoEZ+6KF0pq7jyMNTwaFFoSZgTBd+kk3GG1iF36pxt+lGHyeI6TOQQtFFgyoVzxLLZeBQp0V2OJAltVvXnCHzgKyY4zyHb5teyNVEKa2HOf5UivnWJqk89HSD8c+ijvS8Jq8JRu7nzI81JJQwGmU3MNUyRkt+jVJxv0H67YU5Cw18Fqa3NEb8K9mDeHJImwfU2HGhv8WZzqWEXKC/so1hjO6qWOHxwWPqko5YYNPOQw0vPwEzXZPcY/OSH70gHYqkJl3zOA1GwdHY1jBmwdPWOu6GSekxPED8gDrtW6x0TsvdSfLGRODDTIXJVCKM3xkkkV7oU+4hO743HwrVQxvyWrLQucPFSMQVEe+XA0kUddNGcgj9A3VfpWJwm2NycQXqYWuOEX0Q3qyLVi75KvZsPr8vBR3EcZfWtYGfC/2a8rl+kAZ9w8wpBoca6F8nnFkh7deB+/fV5uMRSDiEHI+aJufDFcOXq4J+uljQRDlCo8trrRkcqkYLJWg/ctvDWBG7A6ODgWLk31ycCryj/w297UsE7mew84uA5eCFQIbO3PT9MIKMy0hK3pFkoQq3KhL9S9/dl8VtvvyfNu1zfPyNl6ckiBJz0zG7zIV5sxGUcSdClUBluaQBxdU1kA2kkoAYrPTX56XaC9ZLx9VvbloWRAJxMwxnQV6w1vxWxRgtV3tzlZBK2JyWtEu+cykbnLJM4cfhP/7bysZqnidOuwv7cZy33plIFqzXxOiujCxpShI3IoALnx+nRo/vS4QvLJwU9Lh5QsSuKzHXoheobCpqgZM2aflNPmFdUPAgCqXJ5caEjRSdqlGf/lFE84qrwl3XB8rU4Emn1jIK7SwlcNbk4fVq6BccOIzS0tZzB4EPZNg6YYO8jfRNVhr2On8WOAwtVH9Jikvq2ffD88GQMZu+d/YDLwAmgg4eIjQscNHPvERyjlhyKE5" +DOTENV_VAULT_DEVELOPMENT="Dp7/4qdTYrrd++/Ny+nykGpmkSKB8DCqhJkuX9/ZQkDB+cvsX5wRTgXBnxA4aIp+HX7QZNnKRKbU3kkzwa3sLHqu58E4Ns1gNGxSYeConwM+ebopPujewxhZbnQ8CxVqMA4cG/GB556yBDtbYYngw2EPdxFlAvLinD5MQbsJvpzDDUiPD9S6bxM20ciMmtsIcV+NI9lInwINIS8vLVWaXVdulnvB4SwVlLiEgfGfiMt2L2YahOG76AnACyL+xKC/QCmAhc3tR6nn6xPdovY3TDWOJe/25Qg2kgm1EvY5ry5r0nskxYPxCe/sIIqhIhE5rvp+uvCCG81Ilec1+RSA7EP7vJ0VJ8zOrurYZn09cZSkrLC+EYuQtlRXk0dBTALfuLWnqpwYN3ADC16YMEc1nI7VtPFwDBnO4cUHikot8XXXNpzsQRKE7S2kt5Cl3YOkO+JYIvNSbqc4OEiWEPZHJp0bNhXPaen0sqACD13GFA5WwaNR+bF9JZKYEZk9AJNp4pHYfuYDbCnTIfxXQ7atO9o90RYZynmX2BtbWCSmj2nWRc2UNfM+P7uZnGWxzrCAY3lb0P0BFrPMBiCzHXqUeOWjHc8s2RU55V/P0JNKEL4ePww9lCAq7dWHQcVtPQCMNU73GgJWtSbmXLw3WRXPtu+Ewe8o+zXuQYd4rUaieyhI36+A50lMEfvbdEpxoqd+guOCzLEo7MDXjNrDnaki2iIs1LHXu0X2p4qycfMHy0xs7lU/2maiUyZwEs3Uyrima1U8SofwPB+akpyfi3qTaIkV73yZWP4tdIEZfCz/lpM+uhyiD5g/YLOU+VoMRy+loUn4PyMIc3dDKLox06fbG8MJPS10goYTwHmsK9K2ywgVJmfQr4JVEY4rsgv22/IKfse1bPF12MCIxUSAKBzQA1rzRSkI8BUtU7cphVX50A3oOJeJe+t5/SdKTF4HwIE895rb2u4YceJt5UYKjxxTibmx1q9qzrLeeUbGemO9vvCrkxzSt7OUvMKR52GEwakEO3/+i+0G35SVBKoghRKVtMMvNIp9a4KnOogx5K0AErkbIYgXXoFH5gsiXU5oUBsU4Pz0QPAxCOvA+rLwDsssEPPmO0Yn1cMMb/aBdQl5AtLTY4GG8u9UvTZSo1Ly6z6Q6HxbeJsazfvEJuySSD/ckExRJoS817JvgpeKJViONN3td7qdtxZwMlP+AmSECv6uGUHbphkox17PE5FAN+lFaDyOGR+4VJoTPaeGds4CnDYDBLfYhaWktkvQbierlvj15HuhffP+IeziHd/YkpFtp9yLNRjjCwouCkgTnpo0CqI9V8uXW0aMPZXV6HEsj46sx2no6rWTkCj5u6SUufx/iCLLwCk6nOzW3ftbzP7vO3qTIPuWJ6sdYPSgWw6PyXhwxp3OEiV/jCpbq85pYQbS2YH4wDi9sZCtweud0nctPf+EnW/Imgy49/xuw7HEmay9okwUSsfr/Pw9p78GEJTm/eF9BIlxvly4aDCrYtqQGqOhRqNQICAd0RTw0z7hqAjrCKzhdQt79YSHCVvKy3tE01XDtgpiUL78HVRAYfdYGp1of1fPIpLzgnwOkER5rgGltYNVaJjvHXisyrL4+v4ww5NP2spxGsGEGUuQZes/YkDjafP026x8ZLusWNB1K96+REESXLOWnz7D/QYinZ9MP8f3CGF9ARFnYv9dKJuUIsAPPbZxAd4R9KtMHja5izkg37PHrRs/9cSD2gghu2+S4EYJ68mqGs1b6ao0fCYlcy/m+0U=" # production -DOTENV_VAULT_PRODUCTION="XJI0mhrbeefm0pw+Ii2qnr8DLtnxVYRAt+jdzIudrQmrepJBuj7ujWT3+e4Fydw5zgqQeSZHLolSNV/hCh97cNmTd4+vEipb6pfoEPlTDoiv869kcOW9oJ2St5RcWK4ZtTTlJNXqD9AWp5ST+Ox83SUsGjZpqTdz7pN9YnnlnoWaCDxnWxPI25CqKfwI66ALvbZ3/GYplT8nWC9cVll9UnwgpFF8ol+AXO57cccLz1dzEjGI4bODbrjhxO9ZkgNbVYHpemY10hV7BeVsSR5wwuxXI8B0cG4jhlXX6Uf/GnGSJ++4LsIraA7FnNmtBpxIXnsImU6G4j3ILJvtnjhl6aIL39zenlZep7ZeTQAtQPhHUmS/tF9xPBqFSUTghE+ZrLOHnqvoXl1/n4ynPCz20VzJtQ4MaoxaO4qOoktyMXU9c1YpqyG7qfLD3N2z6DK3jnIz8RhAhYimKpRb9QuH8gbKHu4BsQlNrl1VQz78sFo9XxtrrxLIOQgMNoANtqROfiVQdcpl4MdK/H3iPpPI/H2esovjjuL+h42tDLbjPq7H2ghjHzckK14ZwO20zyStK9hRY5fAfeO61v6n5EOrsFsjbO/6tIuyG0JQniUnDFWtyxFq78i8F/hom6UbJunHvyV69uSfqdST61vAU/yFtgBZ0WosTUQB7aBJ5nkRRfliZhYu/UCAJ/nJDtjnKgv0kbl0KmTMDTa3HeOfDpK6Yd53IrvPxgfaTkALB0yfAm6W9vGdV4675aLBIJ8A7PjdLz+Ydw6QmDWUROe+XGXGDKlQe2YGBdjT9fO9qEf7Hiv68xizdh3XYdkbr6cj8YDDI84ZpswoUjhVnzEb5erg1OIgHFAYtVV9jdmI8N+c426QcyEOXXSQI1/AM0rDKitdftZZahiaCRKtHDs1Zfnn+LulOKaJP7abgYlPYFkj7LpDVUaPZqo34uMbWatoltQpUbGIYGc=" +DOTENV_VAULT_PRODUCTION="eAPpmIk1d1HWEthoyl/LYayruvbFk/uJ7uotJLFO+a5DsD6pm6jImtbHfVy7v2AZ2ts37H0CK0H+zUpKEKbkqTeCkFxup2ogmhN+/grERHrjdhajxzOHzD7mHGLgWtI2ZOcrZYbpeB6pzbK/a1HG/jLqgHld93oq2J354u6GVbZcjYNQIAfDntlqKmrAaXTh9t5HpwiXmuAueKqsjoH6DGKVXC7UNeenP0OTOCYpYUlJlONba9ebmNPYwmM7ppnwS4ccOvtDdB42Enfd73XtS/ZzW93NnPP2kabINt3TcZdQtKHEd+YHAuRW+50bu1Wgbj1kyeWGRBWVe59wm7C6rhrTBjBRDBjxX7QMhznCIaTUbHFdx6by/O2p5XGzWSpzOlJwFfE4jfHOroxWnVIwN4hFUVu6g3eU+rvKTMtFpOSZ3FJlRiZtvgD7HqoOngEykFNgEEE07UGoOz91nKHia3msD/gzbAadgILB6G/GtuCSlUjW/rQyi8ujVTNxR8I09EKsjiVzRwp0uLGfqgUWEQPSncdkkRS0+82PSKrQXbOoLS+5WvnN95kVFWuHus+5RRup8wnO8CfJIX0TPxxJ9qWy3okku+fIuLbbIxLlxz6DtypwqqthzjaI21Qx+6qjr4vTE0Wc+uWH1csxFNcoo+CzBCt5lbb+QHFjXfmhITXBVz6Bbpt3F0e/yyr/xDhpIp8jo+VxsmOO6jHZd2L2IdUU3SPk+1d1HW0OkocS3s6Iya9CHyDD6tMDUgSdTDVayab6B1DWQYNcTo6a1WMcV/oRtynJnnYEsIEvU/WRT59syMsuoPzDFHTxgow4q1icNCkUHcHBKNKxYdrlLmCLuUwp1lggDyrAzFqNjcYVb/73K3mphi+njtDJdBq8sWrMGtjOScnrJ1rHmdmhT0ub2Qgwxgg6jZ2aKlZC2TFr41IkAtqDAI1moaV/OEsgBfIiDUusn6xKKrDxLiGM7vcQI0szd7Vkr2mfPWiQLOmhxKj11c+7QkUNF6QLQxVYEzwvHcB+RTusSlvJTXN9YfVjqrxBdvuV2UHk85zOYOkBKVIZcj3xojgnDkWVhHGnhfi8nn8d+ROIQIMhQ5izgai5PVO51MOcmgevMYj7L8Jf1HEuwD2ykwaS8KeewvTihEflY8YjOA6fjY70Q4Uqa0LIjsTNegeUPvvQ3E8s1w+4xrgRkA4uW1wRRs7NfoEt02cq2/ZBckXTjdXuuS2Xcdr6LFWEjp5iPlccZV7qBW7+AteROIRjEc/uzvhII5cKRLhgv3y2sehUXEdv04hrO5pKGStrwySZ93P2pnUk9UOdoNtCH5HMZ4jLGnb2hFDoP2o6b57khJZcanjpRvvhkWjpn9eMwjxWK+CX6n/uDaXeYBA9FZtSJyUAniXxNFBz/Zurisl74Bjl8CRCVaqq7ATPXeR+IsVd6B8Cs1QPWWuxU5Z1PCiot6kWTp69p45nHQMmUEdl5eUSATaG4PgwLlZ1vysB/O4J6npmYHF4IFAE8yLNbiWg7IuhlEFTYk9UgvUC+gLaymDTTtr/DqwjU3F/oFYkjR3/Go7L2s03VU3pU5PShua+AWg8Wfli1NQ9n3/rbwXAnsrS2J0qV6n3oFm9yv/0g/+RiOIko39UgJ1syZzbTpDRMtqJOg==" diff --git a/NodeApp/package-lock.json b/NodeApp/package-lock.json index 38eccb9553557d7eef62152401c803a1503e3983..bed60a32df6a7f752acf5f8c5e5d9541ad70c8b2 100644 --- a/NodeApp/package-lock.json +++ b/NodeApp/package-lock.json @@ -1,12 +1,12 @@ { "name": "dojo_cli", - "version": "2.2.0", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dojo_cli", - "version": "2.2.0", + "version": "3.0.0", "license": "AGPLv3", "dependencies": { "ajv": "^8.12.0", @@ -22,6 +22,7 @@ "inquirer": "^8.2.5", "json5": "^2.2.3", "jsonwebtoken": "^8.5.1", + "openurl": "^1.1.1", "ora": "^5.4.1", "tar-stream": "^3.1.6", "winston": "^3.10.0", @@ -35,6 +36,7 @@ "@types/inquirer": "^8.2.6", "@types/jsonwebtoken": "^8.5.9", "@types/node": "^18.17.2", + "@types/openurl": "^1.0.1", "@types/tar-stream": "^2.2.2", "dotenv-vault": "^1.25.0", "genversion": "^3.1.1", @@ -683,6 +685,15 @@ "integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA==", "dev": true }, + "node_modules/@types/openurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/openurl/-/openurl-1.0.1.tgz", + "integrity": "sha512-1XWIANTcgHenp3tboMdCiyzc2hBFfhf7Us2LHJ7X+kPiw648trTuV+lMsd0NUG3hv/44VM38r2vLYvtFpbc9sw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/tar-stream": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-2.2.3.tgz", @@ -2600,6 +2611,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openurl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", + "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==" + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", diff --git a/NodeApp/package.json b/NodeApp/package.json index b4f8edfe6b9748cec7c7440185ce460496da54cd..ff0f2a74b32053f8e01a435960914e6872be964a 100644 --- a/NodeApp/package.json +++ b/NodeApp/package.json @@ -1,7 +1,7 @@ { "name" : "dojo_cli", "description" : "CLI of the Dojo project", - "version" : "2.2.0", + "version" : "3.0.0", "license" : "AGPLv3", "author" : "Michaël Minelli <dojo@minelli.me>", "main" : "dist/app.js", @@ -45,6 +45,7 @@ "inquirer" : "^8.2.5", "json5" : "^2.2.3", "jsonwebtoken" : "^8.5.1", + "openurl" : "^1.1.1", "ora" : "^5.4.1", "tar-stream" : "^3.1.6", "winston" : "^3.10.0", @@ -55,6 +56,7 @@ "@types/inquirer" : "^8.2.6", "@types/jsonwebtoken": "^8.5.9", "@types/node" : "^18.17.2", + "@types/openurl" : "^1.0.1", "@types/tar-stream" : "^2.2.2", "dotenv-vault" : "^1.25.0", "genversion" : "^3.1.1", diff --git a/NodeApp/src/app.ts b/NodeApp/src/app.ts index 634f276293e465b7a1e2fd599d502d6aeb371586..236e2d79bff4df0a07333e01c129ca008994c0cc 100644 --- a/NodeApp/src/app.ts +++ b/NodeApp/src/app.ts @@ -11,10 +11,8 @@ require('./shared/helpers/TypeScriptExtensions'); // ATTENTION : This line MUST import CommanderApp from './commander/CommanderApp'; import HttpManager from './managers/HttpManager'; -import LocalConfig from './config/LocalConfig'; -LocalConfig.loadConfig(); HttpManager.registerAxiosInterceptor(); new CommanderApp(); \ No newline at end of file diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts index 0b28a1c7ff9302c5b0416043e8bc3421cbbfb96b..7a5bea4403c7e963a948ccc04c945ee17e35fa4d 100644 --- a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts +++ b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts @@ -61,14 +61,15 @@ class AssignmentCreateCommand extends CommanderCommand { // Create the assignment { - console.log(chalk.cyan('Please wait while we are creating the assignment...')); + console.log(chalk.cyan('Please wait while we are creating the assignment (approximately 10 seconds)...')); try { const assignment: Assignment = await DojoBackendManager.createAssignment(options.name, members, templateIdOrNamespace); const oraInfo = (message: string) => { ora({ - text: message, indent: 4 + text : message, + indent: 4 }).start().info(); }; diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts index 41593c08b1ac4967b10ebdd62d953016af74e907..acc361c3ffd9e01d2adf65a55bc187c7180720c5 100644 --- a/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts +++ b/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts @@ -62,7 +62,7 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand { text : 'Checking accesses', indent: 8 }).start(); - if ( !assignment.staff.some(staff => staff.gitlabId === SessionManager.profile?.gitlabId) ) { + if ( !assignment.staff.some(staff => staff.id === SessionManager.profile?.id) ) { assignmentCheckAccessSpinner.fail(`You are not in the staff of this assignment`); return; } diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts index bb51c2e6ece83361bd4fa5bd011b8d2e5731f97b..5b547df82b9c26433806d861a5ae5f7da581a8c7 100644 --- a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts +++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts @@ -40,7 +40,8 @@ class ExerciseCreateCommand extends CommanderCommand { ora('Checking assignment:').start().info(); const assignmentGetSpinner: ora.Ora = ora({ - text: 'Checking if assignment exists', indent: 4 + text : 'Checking if assignment exists', + indent: 4 }).start(); assignment = await DojoBackendManager.getAssignment(options.assignment); if ( !assignment ) { @@ -50,7 +51,8 @@ class ExerciseCreateCommand extends CommanderCommand { assignmentGetSpinner.succeed(`Assignment "${ options.assignment }" exists`); const assignmentPublishedSpinner: ora.Ora = ora({ - text: 'Checking if assignment is published', indent: 4 + text : 'Checking if assignment is published', + indent: 4 }).start(); if ( !assignment.published ) { assignmentPublishedSpinner.fail(`Assignment "${ assignment.name }" isn't published`); @@ -61,14 +63,15 @@ class ExerciseCreateCommand extends CommanderCommand { //Create the exercise { - console.log(chalk.cyan('Please wait while we are creating the exercise...')); + console.log(chalk.cyan('Please wait while we are creating the exercise (approximately 10 seconds)...')); try { const exercise: Exercise = await DojoBackendManager.createExercise((assignment as Assignment).name, members); const oraInfo = (message: string) => { ora({ - text: message, indent: 4 + text : message, + indent: 4 }).start().info(); }; diff --git a/NodeApp/src/commander/session/SessionCommand.ts b/NodeApp/src/commander/session/SessionCommand.ts index 8ec16854a4bc15f6cdfb8534f57c28c2bef128c8..bbf0374ee9617ccc44e4463bd84554db2f8d4aa7 100644 --- a/NodeApp/src/commander/session/SessionCommand.ts +++ b/NodeApp/src/commander/session/SessionCommand.ts @@ -1,7 +1,7 @@ import CommanderCommand from '../CommanderCommand'; import SessionTestCommand from './subcommands/SessionTestCommand'; -import SessionAppCommand from './subcommands/SessionAppCommand'; -import SessionGitlabCommand from './subcommands/SessionGitlabCommand'; +import SessionLoginCommand from './subcommands/SessionLoginCommand'; +import SessionLogoutCommand from './subcommands/SessionLogoutCommand'; class SessionCommand extends CommanderCommand { @@ -13,8 +13,8 @@ class SessionCommand extends CommanderCommand { } protected defineSubCommands() { - SessionAppCommand.registerOnCommand(this.command); - SessionGitlabCommand.registerOnCommand(this.command); + SessionLoginCommand.registerOnCommand(this.command); + SessionLogoutCommand.registerOnCommand(this.command); SessionTestCommand.registerOnCommand(this.command); } diff --git a/NodeApp/src/commander/session/subcommands/SessionAppCommand.ts b/NodeApp/src/commander/session/subcommands/SessionAppCommand.ts deleted file mode 100644 index f8d179efead33b4652c0a994d8aed4cdc9e28e81..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/session/subcommands/SessionAppCommand.ts +++ /dev/null @@ -1,23 +0,0 @@ -import CommanderCommand from '../../CommanderCommand'; -import SessionAppLoginCommand from './application/SessionAppLoginCommand'; -import SessionAppLogoutCommand from './application/SessionAppLogoutCommand'; - - -class SessionAppCommand extends CommanderCommand { - protected commandName: string = 'application'; - - protected defineCommand() { - this.command - .description('manage application session'); - } - - protected defineSubCommands() { - SessionAppLoginCommand.registerOnCommand(this.command); - SessionAppLogoutCommand.registerOnCommand(this.command); - } - - protected async commandAction(): Promise<void> { } -} - - -export default new SessionAppCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/session/subcommands/SessionGitlabCommand.ts b/NodeApp/src/commander/session/subcommands/SessionGitlabCommand.ts deleted file mode 100644 index e80ceb264f1d16f458b1f4cb770dc82dd93da817..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/session/subcommands/SessionGitlabCommand.ts +++ /dev/null @@ -1,23 +0,0 @@ -import CommanderCommand from '../../CommanderCommand'; -import SessionGitlabLoginCommand from './gitlab/SessionGitlabLoginCommand'; -import SessionGitlabLogoutCommand from './gitlab/SessionGitlabLogoutCommand'; - - -class SessionGitlabCommand extends CommanderCommand { - protected commandName: string = 'gitlab'; - - protected defineCommand() { - this.command - .description('manage Gitlab session'); - } - - protected defineSubCommands() { - SessionGitlabLoginCommand.registerOnCommand(this.command); - SessionGitlabLogoutCommand.registerOnCommand(this.command); - } - - protected async commandAction(): Promise<void> { } -} - - -export default new SessionGitlabCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/session/subcommands/SessionLoginCommand.ts b/NodeApp/src/commander/session/subcommands/SessionLoginCommand.ts new file mode 100644 index 0000000000000000000000000000000000000000..53610a2fab179d5577f373cce51f3633209464df --- /dev/null +++ b/NodeApp/src/commander/session/subcommands/SessionLoginCommand.ts @@ -0,0 +1,27 @@ +import chalk from 'chalk'; +import CommanderCommand from '../../CommanderCommand'; +import SessionManager from '../../../managers/SessionManager'; + + +class SessionLoginCommand extends CommanderCommand { + protected commandName: string = 'login'; + + protected defineCommand() { + this.command + .description('login to Dojo') + .option('-c, --cli', 'proceed to the login in headless mode (do not try to open web browser).') + .action(this.commandAction.bind(this)); + } + + protected async commandAction(options: { cli: boolean }): Promise<void> { + try { + console.log(chalk.cyan('Please wait while we login you into Dojo...')); + await SessionManager.login(options.cli); + } catch ( error ) { + + } + } +} + + +export default new SessionLoginCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/session/subcommands/application/SessionAppLogoutCommand.ts b/NodeApp/src/commander/session/subcommands/SessionLogoutCommand.ts similarity index 63% rename from NodeApp/src/commander/session/subcommands/application/SessionAppLogoutCommand.ts rename to NodeApp/src/commander/session/subcommands/SessionLogoutCommand.ts index 7d051a31a7ebf761ef5fd119b0fb1bdd4b8aa6d6..e0c7bb374c5138cd01ee3fd08db04d38e1e6472c 100644 --- a/NodeApp/src/commander/session/subcommands/application/SessionAppLogoutCommand.ts +++ b/NodeApp/src/commander/session/subcommands/SessionLogoutCommand.ts @@ -1,15 +1,15 @@ -import CommanderCommand from '../../../CommanderCommand'; +import CommanderCommand from '../../CommanderCommand'; import inquirer from 'inquirer'; -import SessionManager from '../../../../managers/SessionManager'; import ora from 'ora'; +import SessionManager from '../../../managers/SessionManager'; -class SessionAppLogoutCommand extends CommanderCommand { +class SessionLogoutCommand extends CommanderCommand { protected commandName: string = 'logout'; protected defineCommand() { this.command - .description('logout of the application') + .description('logout of Dojo') .option('-f, --force', 'attempt to logout without prompting for confirmation') .action(this.commandAction.bind(this)); } @@ -17,7 +17,10 @@ class SessionAppLogoutCommand extends CommanderCommand { protected async commandAction(options: any): Promise<void> { if ( !options.force ) { const confirm: boolean = (await inquirer.prompt({ - name: 'confirm', message: 'Are you sure?', type: 'confirm', default: false + name : 'confirm', + message: 'Are you sure?', + type : 'confirm', + default: false })).confirm; if ( !confirm ) { @@ -32,4 +35,4 @@ class SessionAppLogoutCommand extends CommanderCommand { } -export default new SessionAppLogoutCommand(); \ No newline at end of file +export default new SessionLogoutCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/session/subcommands/application/SessionAppLoginCommand.ts b/NodeApp/src/commander/session/subcommands/application/SessionAppLoginCommand.ts deleted file mode 100644 index 88deb2e8b9a40cdc8d399393a81257ff327e4992..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/session/subcommands/application/SessionAppLoginCommand.ts +++ /dev/null @@ -1,34 +0,0 @@ -import chalk from 'chalk'; -import CommanderCommand from '../../../CommanderCommand'; -import inquirer from 'inquirer'; -import SessionManager from '../../../../managers/SessionManager'; - - -class SessionAppLoginCommand extends CommanderCommand { - protected commandName: string = 'login'; - - protected defineCommand() { - this.command - .description('login into the application') - .requiredOption('-u, --user <string>', '[required] username to use when connecting to server') - .option('-p, --password <string>', 'password to use when connecting to server. If password is not given it\'s asked') - .action(this.commandAction.bind(this)); - } - - protected async commandAction(options: { user: string, password: string }): Promise<void> { - if ( !options.password ) { - options.password = (await inquirer.prompt({ - type: 'password', name: 'password', message: 'Please enter your password', mask: '' - })).password; - } - - console.log(chalk.cyan('Please wait while we are logging in you to Dojo...')); - - await SessionManager.login(options.user, options.password); - - SessionManager.checkPermissions(true, 4); - } -} - - -export default new SessionAppLoginCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLoginCommand.ts b/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLoginCommand.ts deleted file mode 100644 index fae91b6cf934f21e1c40a64f878c9a526b6f8241..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLoginCommand.ts +++ /dev/null @@ -1,33 +0,0 @@ -import chalk from 'chalk'; -import CommanderCommand from '../../../CommanderCommand'; -import GitlabManager from '../../../../managers/GitlabManager'; -import inquirer from 'inquirer'; - - -class SessionGitlabLoginCommand extends CommanderCommand { - protected commandName: string = 'login'; - - protected defineCommand() { - this.command - .description('register the gitlab token') - .option('-t, --token <string>', 'personal access token from GitLab with api scope') - .action(this.commandAction.bind(this)); - } - - protected async commandAction(options: { token: string }): Promise<void> { - if ( !options.token ) { - options.token = (await inquirer.prompt({ - type: 'password', name: 'token', message: 'Please enter your gitlab token', mask: '' - })).token; - } - - console.log(chalk.cyan('Please wait while we are testing your Gitlab token...')); - - GitlabManager.login(options.token); - - await GitlabManager.testToken(); - } -} - - -export default new SessionGitlabLoginCommand(); \ No newline at end of file diff --git a/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLogoutCommand.ts b/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLogoutCommand.ts deleted file mode 100644 index 4d43d379fbbc185cb571cf1039460e1c620965dc..0000000000000000000000000000000000000000 --- a/NodeApp/src/commander/session/subcommands/gitlab/SessionGitlabLogoutCommand.ts +++ /dev/null @@ -1,35 +0,0 @@ -import CommanderCommand from '../../../CommanderCommand'; -import inquirer from 'inquirer'; -import ora from 'ora'; -import GitlabManager from '../../../../managers/GitlabManager'; - - -class SessionGitlabLogoutCommand extends CommanderCommand { - protected commandName: string = 'logout'; - - protected defineCommand() { - this.command - .description('logout of Gitlab') - .option('-f, --force', 'attempt to logout without prompting for confirmation') - .action(this.commandAction.bind(this)); - } - - protected async commandAction(options: any): Promise<void> { - if ( !options.force ) { - const confirm: boolean = (await inquirer.prompt({ - name: 'confirm', message: 'Are you sure?', type: 'confirm', default: false - })).confirm; - - if ( !confirm ) { - return; - } - } - - const spinner: ora.Ora = ora('Please wait while we are logout you from Gitlab...').start(); - GitlabManager.logout(); - spinner.succeed('You are now logged out from Gitlab.'); - } -} - - -export default new SessionGitlabLogoutCommand(); \ No newline at end of file diff --git a/NodeApp/src/config/Config.ts b/NodeApp/src/config/Config.ts index d1935ab6335da38804364d3065639f65f1629882..15b13e064b562939785bb669f6a8ab6c52e63542 100644 --- a/NodeApp/src/config/Config.ts +++ b/NodeApp/src/config/Config.ts @@ -3,13 +3,23 @@ import getAppDataPath from 'appdata-path'; class Config { public readonly localConfig: { - folder: string; file: string; + folder: string; sessionFile: string; }; public readonly gitlab: { cliReleasePage: string }; + public readonly login: { + server: { + port: number, route: string + }, gitlab: { + url: { + code: string + } + } + }; + public readonly folders: { defaultLocalExercise: string }; @@ -20,14 +30,26 @@ class Config { constructor() { this.localConfig = { - folder: getAppDataPath('DojoCLI'), - file : process.env.LOCAL_CONFIG_FILE || '' + folder : getAppDataPath('DojoCLI'), + sessionFile: process.env.LOCAL_CONFIG_FILE_SESSION || '' }; this.gitlab = { cliReleasePage: process.env.GITLAB_CLI_RELEASE_PAGE || '' }; + this.login = { + server: { + port : Number(process.env.LOGIN_SERVER_PORT || 30992), + route: process.env.LOGIN_SERVER_ROUTE || '' + }, + gitlab: { + url: { + code: process.env.LOGIN_GITLAB_URL_CODE || '' + } + } + }; + this.folders = { defaultLocalExercise: process.env.LOCAL_EXERCISE_DEFAULT_FOLDER || './' }; diff --git a/NodeApp/src/config/LocalConfig.ts b/NodeApp/src/config/LocalConfig.ts deleted file mode 100644 index 656c0c6bc47dc684556b816178548989086c9313..0000000000000000000000000000000000000000 --- a/NodeApp/src/config/LocalConfig.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as fs from 'fs'; -import SessionManager from '../managers/SessionManager'; -import Config from './Config'; -import LocalConfigKeys from '../types/LocalConfigKeys'; -import GitlabManager from '../managers/GitlabManager'; -import JSON5 from 'json5'; -import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; - - -class LocalConfig { - private get configPath(): string { - return `${ Config.localConfig.folder }/${ Config.localConfig.file }`; - } - - private _config: { [key in LocalConfigKeys]?: any } = {}; - - loadConfig() { - if ( !fs.existsSync(this.configPath) ) { - fs.mkdirSync(Config.localConfig.folder, { recursive: true }); - - fs.writeFileSync(this.configPath, JSON5.stringify({})); - } - - try { - this._config = JSON5.parse(fs.readFileSync(this.configPath).toString()); - - if ( LocalConfigKeys.API_TOKEN_ENV in this._config && ClientsSharedConfig.apiURL in this._config[LocalConfigKeys.API_TOKEN_ENV] ) { - SessionManager.token = this._config[LocalConfigKeys.API_TOKEN_ENV][ClientsSharedConfig.apiURL]; - } else { - SessionManager.token = this._config[LocalConfigKeys.API_TOKEN]; - } - - GitlabManager.token = this._config[LocalConfigKeys.GITLAB_PERSONAL_TOKEN]; - } catch ( error ) { - console.log(error); - } - } - - updateConfig(key: LocalConfigKeys, value: any) { - let previousValue = (this._config as any)[key]; - if ( key === LocalConfigKeys.API_TOKEN && (!(LocalConfigKeys.API_TOKEN_ENV in this._config) || !(ClientsSharedConfig.apiURL in this._config[LocalConfigKeys.API_TOKEN_ENV])) ) { - previousValue = null; - } - - if ( previousValue === value ) { - return; - } - - if ( key === LocalConfigKeys.API_TOKEN ) { - delete (this._config as any)[LocalConfigKeys.API_TOKEN]; - - if ( !(LocalConfigKeys.API_TOKEN_ENV in this._config) ) { - (this._config as any)[LocalConfigKeys.API_TOKEN_ENV] = {}; - } - - (this._config as any)[LocalConfigKeys.API_TOKEN_ENV][ClientsSharedConfig.apiURL] = value; - } else { - (this._config as any)[key] = value; - } - - try { - fs.writeFileSync(this.configPath, JSON5.stringify(this._config, null, 4)); - } catch ( error ) { } - } -} - - -export default new LocalConfig(); \ No newline at end of file diff --git a/NodeApp/src/config/LocalConfigFile.ts b/NodeApp/src/config/LocalConfigFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..33cabe255728f5c990ceab23a4143fb0271877ed --- /dev/null +++ b/NodeApp/src/config/LocalConfigFile.ts @@ -0,0 +1,57 @@ +import * as fs from 'fs'; +import Config from './Config'; +import JSON5 from 'json5'; + + +class LocalConfigFile { + constructor(private filename: string) {} + + private get configPath(): string { + return `${ Config.localConfig.folder }/${ this.filename }`; + } + + private _config: { [key: string]: any } = {}; + + loadConfig() { + if ( !fs.existsSync(this.configPath) ) { + fs.mkdirSync(Config.localConfig.folder, { recursive: true }); + + fs.writeFileSync(this.configPath, JSON5.stringify({})); + } + + try { + this._config = JSON5.parse(fs.readFileSync(this.configPath).toString()); + } catch ( error ) { + console.log(error); + } + } + + getParam(key: string): any | null { + const value = key in this._config ? this._config[key] : null; + if ( value === null ) { + return null; + } else if ( typeof value === 'object' ) { + return { ...value }; + } else { + return value; + } + } + + setParam(key: string, value: any): void { + let previousValue = this.getParam(key); + if ( JSON5.stringify(previousValue) === JSON5.stringify(value) ) { + return; + } + + this._config[key] = value; + + try { + fs.writeFileSync(this.configPath, JSON5.stringify(this._config, null, 4)); + } catch ( error ) { + console.log(error); + } + } +} + + +export default LocalConfigFile; \ No newline at end of file diff --git a/NodeApp/src/managers/DojoBackendManager.ts b/NodeApp/src/managers/DojoBackendManager.ts index 64c56cb34e1e25cc4b0fb3198d09d94e4b5993dd..42bda3fa67ec31b4f9be6bba3e5da406c8f8224e 100644 --- a/NodeApp/src/managers/DojoBackendManager.ts +++ b/NodeApp/src/managers/DojoBackendManager.ts @@ -7,6 +7,9 @@ import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig import Assignment from '../sharedByClients/models/Assignment'; import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse'; import Exercise from '../sharedByClients/models/Exercise'; +import GitlabToken from '../shared/types/Gitlab/GitlabToken'; +import User from '../sharedByClients/models/User'; +import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; class DojoBackendManager { @@ -14,6 +17,26 @@ class DojoBackendManager { return `${ ClientsSharedConfig.apiURL }${ route }`; } + + public async login(gitlabTokens: GitlabToken): Promise<User | undefined> { + try { + return (await axios.post<DojoBackendResponse<User>>(this.getApiUrl(ApiRoute.LOGIN), { + accessToken : gitlabTokens.access_token, + refreshToken: gitlabTokens.refresh_token + })).data.data; + } catch ( error ) { + return undefined; + } + } + + + public async refreshTokens(refreshToken: string): Promise<GitlabToken> { + return (await axios.post<DojoBackendResponse<GitlabToken>>(this.getApiUrl(ApiRoute.REFRESH_TOKENS), { + refreshToken: refreshToken + })).data.data; + } + + public async getAssignment(nameOrUrl: string): Promise<Assignment | undefined> { try { return (await axios.get<DojoBackendResponse<Assignment>>(this.getApiUrl(ApiRoute.ASSIGNMENT_GET).replace('{{nameOrUrl}}', encodeURIComponent(nameOrUrl)))).data.data; @@ -84,7 +107,11 @@ class DojoBackendManager { if ( error.response.status === StatusCodes.CONFLICT ) { spinner.fail(`The assignment name is already used. Please choose another one.`); } else { - spinner.fail(`Assignment creation error: ${ error.response.statusText }`); + if ( (error.response.data as DojoBackendResponse<any>).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 { @@ -118,7 +145,11 @@ class DojoBackendManager { if ( error.response.status === StatusCodes.CONFLICT ) { spinner.fail(`You've already reached the max number of exercise of this assignment.`); } else { - spinner.fail(`Exercise creation error: ${ error.response.statusText }`); + if ( (error.response.data as DojoBackendResponse<any>).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 { diff --git a/NodeApp/src/managers/GitlabManager.ts b/NodeApp/src/managers/GitlabManager.ts index 92ce994a5027dfbac721c413d1625c44621f32b7..6562ad9980914e02cb7ef3b67b96cd85342e0748 100644 --- a/NodeApp/src/managers/GitlabManager.ts +++ b/NodeApp/src/managers/GitlabManager.ts @@ -1,41 +1,15 @@ -import LocalConfig from '../config/LocalConfig'; -import LocalConfigKeys from '../types/LocalConfigKeys'; -import axios from 'axios'; -import ora from 'ora'; -import GitlabUser from '../shared/types/Gitlab/GitlabUser'; -import GitlabRoute from '../shared/types/Gitlab/GitlabRoute'; -import SharedConfig from '../shared/config/SharedConfig'; +import axios from 'axios'; +import ora from 'ora'; +import GitlabUser from '../shared/types/Gitlab/GitlabUser'; +import GitlabRoute from '../shared/types/Gitlab/GitlabRoute'; +import SharedConfig from '../shared/config/SharedConfig'; class GitlabManager { - private _token: string | null = null; - private getApiUrl(route: GitlabRoute): string { return `${ SharedConfig.gitlab.apiURL }${ route }`; } - get isLogged(): boolean { - return this._token !== null; - } - - get token(): string { - return this._token || ''; - } - - set token(token: string) { - this._token = token; - - LocalConfig.updateConfig(LocalConfigKeys.GITLAB_PERSONAL_TOKEN, token); - } - - login(token: string): void { - this.token = token; - } - - logout(): void { - this.token = ''; - } - public async testToken(verbose: boolean = true): Promise<[ boolean, boolean ]> { if ( verbose ) { ora('Checking Gitlab token: ').start().info(); diff --git a/NodeApp/src/managers/HttpManager.ts b/NodeApp/src/managers/HttpManager.ts index 5b8ea67c2a7d802a599ecb304bc04056737219dc..e90f666c4967739d857af9a604a39ca0e82fad34 100644 --- a/NodeApp/src/managers/HttpManager.ts +++ b/NodeApp/src/managers/HttpManager.ts @@ -1,7 +1,6 @@ import axios, { AxiosRequestHeaders } from 'axios'; import SessionManager from './SessionManager'; import FormData from 'form-data'; -import GitlabManager from './GitlabManager'; import { StatusCodes } from 'http-status-codes'; import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; import { version } from '../config/Version'; @@ -35,15 +34,15 @@ class HttpManager { } if ( SessionManager.isLogged ) { - config.headers.Authorization = `Bearer ${ SessionManager.token }`; + config.headers.Authorization = `Bearer ${ SessionManager.apiToken }`; } config.headers['client'] = 'DojoCLI'; config.headers['client-version'] = version; } - if ( GitlabManager.isLogged && config.url && config.url.indexOf(SharedConfig.gitlab.apiURL) !== -1 ) { - config.headers['PRIVATE-TOKEN'] = GitlabManager.token; + if ( SessionManager.gitlabCredentials.accessToken && config.url && config.url.indexOf(SharedConfig.gitlab.apiURL) !== -1 ) { + config.headers.Authorization = `Bearer ${ SessionManager.gitlabCredentials.accessToken }`; } return config; @@ -66,13 +65,35 @@ class HttpManager { private registerResponseInterceptor() { axios.interceptors.response.use((response) => { if ( response.data && response.data.sessionToken ) { - SessionManager.token = response.data.sessionToken; + SessionManager.apiToken = response.data.sessionToken; } return response; - }, (error) => { + }, async (error) => { if ( error.response ) { - if ( error.response.status === StatusCodes.METHOD_NOT_ALLOWED && error.response.data ) { + 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: any ) { + 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<{}> = error.response.data; switch ( data.code ) { @@ -88,7 +109,7 @@ class HttpManager { } if ( this.handleAuthorizationCommandErrors ) { - if ( error.response.url && error.response.url.indexOf(ClientsSharedConfig.apiURL) !== -1 ) { + if ( isFromApi ) { switch ( error.response.status ) { case StatusCodes.UNAUTHORIZED: this.requestError('Session expired or does not exist. Please login again.'); diff --git a/NodeApp/src/managers/SessionManager.ts b/NodeApp/src/managers/SessionManager.ts index 28082f59a9345997c53ddb8cb4a04eeab60e43eb..580b6d854da7fa201243682ee8cd126aff84e8af 100644 --- a/NodeApp/src/managers/SessionManager.ts +++ b/NodeApp/src/managers/SessionManager.ts @@ -1,31 +1,105 @@ -import * as jwt from 'jsonwebtoken'; -import User from '../sharedByClients/models/User'; -import LocalConfig from '../config/LocalConfig'; -import LocalConfigKeys from '../types/LocalConfigKeys'; -import axios, { AxiosError } from 'axios'; -import HttpManager from './HttpManager'; -import ora from 'ora'; -import Permissions from '../types/Permissions'; -import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute'; -import DojoBackendManager from './DojoBackendManager'; -import { StatusCodes } from 'http-status-codes'; +import * as jwt from 'jsonwebtoken'; +import User from '../sharedByClients/models/User'; +import LocalConfigFile from '../config/LocalConfigFile'; +import LocalConfigKeys from '../types/LocalConfigKeys'; +import axios, { HttpStatusCode } from 'axios'; +import HttpManager from './HttpManager'; +import ora from 'ora'; +import Permissions from '../types/Permissions'; +import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute'; +import DojoBackendManager from './DojoBackendManager'; +import Config from '../config/Config'; +import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; +import DojoGitlabCredentials from '../sharedByClients/types/Dojo/DojoGitlabCredentials'; +import * as http from 'http'; +import * as openurl from 'openurl'; +import EventEmitter from 'events'; +import SharedConfig from '../shared/config/SharedConfig'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import SharedGitlabManager from '../shared/managers/SharedGitlabManager'; +import GitlabManager from './GitlabManager'; +import GitlabToken from '../shared/types/Gitlab/GitlabToken'; + + +class LoginServer { + readonly events: EventEmitter = new EventEmitter(); + private server: http.Server; + + constructor() { + this.server = http.createServer((req, res) => { + const sendError = (error: string) => { + this.events.emit('error', error); + res.writeHead(HttpStatusCode.InternalServerError, { 'Content-Type': 'text/html' }); + res.write(`<html lang="en"><body><h1 style="color: red">DojoCLI login error</h1><h3>Please look at your CLI for more informations.</h3></body></html>`); + res.end(); + }; + + if ( req.url?.match(Config.login.server.route) ) { + let urlParts = req.url.split('='); + if ( urlParts.length > 0 ) { + this.events.emit('code', urlParts[1]); + + res.writeHead(HttpStatusCode.Ok, { 'Content-Type': 'text/html' }); + res.write(`<html lang="en"><body><h1 style="color: green">DojoCLI login successful</h1><h3>You can close this window.</h3></body></html>`); + res.end(); + return; + } + + sendError(`Incorrect call => ${ req.url }`); + return; + } + + //sendError(`Unknown route call => ${ req.url }`); + }); + } + + start() { + try { + this.server.listen(Config.login.server.port); + this.events.emit('started'); + } catch ( error ) { + this.events.emit('error', error); + } + } + + stop() { + try { + this.server.close(); + this.events.emit('stopped'); + } catch ( error ) { + this.events.emit('error', error); + } + } +} class SessionManager { - private _token: string | null = null; + private configFile: LocalConfigFile = new LocalConfigFile(Config.localConfig.sessionFile); public profile: User | undefined = undefined; get isLogged(): boolean { - return this._token !== null; + return this.apiToken !== null && this.apiToken !== ''; } - get token(): string { - return this._token || ''; + get apiToken(): string { + const apisToken = this.configFile.getParam(LocalConfigKeys.APIS_TOKEN); + + if ( apisToken !== null && ClientsSharedConfig.apiURL in apisToken ) { + return apisToken[ClientsSharedConfig.apiURL]; + } + + return ''; } - set token(token: string) { - this._token = token; + set apiToken(token: string) { + let apisToken = this.configFile.getParam(LocalConfigKeys.APIS_TOKEN); + if ( apisToken === null ) { + apisToken = {}; + } + apisToken[ClientsSharedConfig.apiURL] = token; + this.configFile.setParam(LocalConfigKeys.APIS_TOKEN, apisToken); try { const payload = jwt.decode(token); @@ -36,38 +110,166 @@ class SessionManager { } catch ( error ) { this.profile = undefined; } + } - LocalConfig.updateConfig(LocalConfigKeys.API_TOKEN, token); + get gitlabCredentials(): DojoGitlabCredentials { + return this.configFile.getParam(LocalConfigKeys.GITLAB); } - async login(user: string, password: string) { - const spinner: ora.Ora = ora('Logging in').start(); - try { - this.profile = undefined; + set gitlabCredentials(credentials: DojoGitlabCredentials) { + this.configFile.setParam(LocalConfigKeys.GITLAB, credentials); + } + + constructor() { + this.configFile.loadConfig(); + } + + 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(chalk.blue(`${ 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): ${ chalk.blue(`${ SharedConfig.login.gitlab.url.redirect }?code=`) }${ chalk.green('123456') }`); + return (await inquirer.prompt({ + type : 'password', + name : 'code', + message: `${ chalk.green('?') } Please paste the Gitlab code here`, + mask : '*', + prefix : ' ' + })).code; + } + + private getGitlabCodeFromGraphicEnvironment(): Promise<string> { + return new Promise<string>((resolve, reject) => { + let currentSpinner: ora.Ora = ora({ + text : 'Starting login server', + indent: 4 + }).start(); + + const loginServer = new LoginServer(); + loginServer.events.on('started', () => { + currentSpinner.succeed('Login server started'); + currentSpinner = ora({ + text : 'Waiting for user to authorize the application in his web browser', + indent: 4 + }).start(); + openurl.open(Config.login.gitlab.url.code, (error) => { + if ( error ) { + currentSpinner.warn('Error while opening the web browser. Attempting the login in CLI only mode (headless mode).'); + currentSpinner = ora({ + text : 'Stopping login server', + indent: 4 + }).start(); + loginServer.events.on('stopped', () => { + try { + resolve(this.getGitlabCodeFromHeadlessEnvironment()); + } catch ( error ) { + reject(error); + } + }); + loginServer.stop(); + } + }); + }); + loginServer.events.on('error', (error: string) => { + currentSpinner.fail(`Login server error: ${ error }`); + reject(); + }); + loginServer.events.on('stopped', () => { + currentSpinner.succeed('Login server stopped'); + }); + + + loginServer.events.on('code', (code: string) => { + currentSpinner.succeed('Login code received'); + currentSpinner = ora({ + text : 'Stopping login server', + indent: 4 + }).start(); + loginServer.events.on('stopped', () => { + resolve(code); + }); + loginServer.stop(); - const response = await axios.post(DojoBackendManager.getApiUrl(ApiRoute.LOGIN), { - user : user, - password: password + resolve(code); }); - spinner.succeed('Logged in'); + loginServer.start(); + }); + } + + async login(headless: boolean = false) { + try { + this.logout(); } catch ( error ) { - if ( error instanceof AxiosError ) { - if ( error.response ) { - if ( error.response.status === StatusCodes.NOT_FOUND ) { - spinner.fail('User not found or password incorrect'); - } else { - spinner.fail(`Login error: ${ error.response.statusText }`); - } - } - } else { - spinner.fail(`Login error: ${ error }`); - } + console.log(error); + ora('Unknown error').start().fail(); + throw error; + } + + ora(`Login with Gitlab (${ SharedConfig.gitlab.URL }):`).start().info(); + + let gitlabCode: string; + if ( !headless ) { + gitlabCode = await this.getGitlabCodeFromGraphicEnvironment(); + } else { + gitlabCode = await this.getGitlabCodeFromHeadlessEnvironment(); } + + let gitlabTokensSpinner = ora({ + text : 'Retrieving gitlab tokens', + indent: 4 + }).start(); + let gitlabTokens: GitlabToken; + try { + gitlabTokens = await SharedGitlabManager.getTokens(gitlabCode); + this.gitlabCredentials = { + refreshToken: gitlabTokens.refresh_token, + accessToken : gitlabTokens.access_token + }; + gitlabTokensSpinner.succeed('Gitlab tokens retrieved'); + } catch ( error ) { + gitlabTokensSpinner.fail('Error while retrieving gitlab tokens'); + throw error; + } + + const isGitlabTokensValid = (await GitlabManager.testToken()).every((value) => value); + if ( !isGitlabTokensValid ) { + throw new Error('Gitlab tokens are invalid'); + } + + ora(`Login to Dojo backend:`).start().info(); + let dojoLoginSpinner = ora({ + text : 'Login to Dojo backend', + indent: 4 + }).start(); + + try { + await DojoBackendManager.login(gitlabTokens); + dojoLoginSpinner.succeed('Logged in'); + } catch ( error ) { + dojoLoginSpinner.fail('Login failed'); + throw error; + } + + await this.testSession(true); + } + + async refreshTokens() { + let gitlabTokens = await DojoBackendManager.refreshTokens(this.gitlabCredentials.refreshToken!); + + this.gitlabCredentials = { + refreshToken: gitlabTokens.refresh_token, + accessToken : gitlabTokens.access_token + }; } logout() { - this.token = ''; + this.apiToken = ''; + this.gitlabCredentials = { + refreshToken: '', + accessToken : '' + }; } checkPermissions(verbose: boolean = true, indent: number = 8, checkPermissions: Array<string> | null = []): Permissions { @@ -86,8 +288,9 @@ class SessionManager { }; 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, - student : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('student')) ? hasPermission(() => true, 'Student permissions') : false + admin : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('admin')) ? hasPermission(() => this.profile?.isAdmin ?? false, 'Admin permissions') : false }; } diff --git a/NodeApp/src/shared b/NodeApp/src/shared index efe1bf313f57d1826faf935c183d37a0835f8c2d..4a5eb68209ae9204b6d4cc8020bd62cf6a5be989 160000 --- a/NodeApp/src/shared +++ b/NodeApp/src/shared @@ -1 +1 @@ -Subproject commit efe1bf313f57d1826faf935c183d37a0835f8c2d +Subproject commit 4a5eb68209ae9204b6d4cc8020bd62cf6a5be989 diff --git a/NodeApp/src/sharedByClients b/NodeApp/src/sharedByClients index d9379b055a4626e4b35cf4cc4a7429040a4aeaf7..dc12d17660bf9e92656c6abcb24ec7ce6ab3d675 160000 --- a/NodeApp/src/sharedByClients +++ b/NodeApp/src/sharedByClients @@ -1 +1 @@ -Subproject commit d9379b055a4626e4b35cf4cc4a7429040a4aeaf7 +Subproject commit dc12d17660bf9e92656c6abcb24ec7ce6ab3d675 diff --git a/NodeApp/src/types/LocalConfigKeys.ts b/NodeApp/src/types/LocalConfigKeys.ts index f64751d4b278c48815dbab243a567257cc21ed04..eba97d6dc199b45ad1ca2b7b48f7c89ea3c27a3f 100644 --- a/NodeApp/src/types/LocalConfigKeys.ts +++ b/NodeApp/src/types/LocalConfigKeys.ts @@ -1,7 +1,6 @@ enum LocalConfigKeys { - API_TOKEN = 'apiToken', - API_TOKEN_ENV = 'apiTokenEnv', - GITLAB_PERSONAL_TOKEN = 'gitlabPersonalToken', + APIS_TOKEN = 'apisToken', + GITLAB = 'gitlab', } diff --git a/NodeApp/src/types/Permissions.ts b/NodeApp/src/types/Permissions.ts index 8d91a16df47d6f9abb395281e71fbd64cf4ed331..b77f2f4b967d210f22992561eb7503d55e6e59fc 100644 --- a/NodeApp/src/types/Permissions.ts +++ b/NodeApp/src/types/Permissions.ts @@ -1,6 +1,7 @@ interface Permissions { + student: boolean, teachingStaff: boolean, - student: boolean + admin: boolean }