From 07cacf51803bdad12f5a12ad4cb42aa02a190089 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marvin=20=C3=96hlerking?=
<103562092+MarvinOehlerkingCap@users.noreply.github.com>
Date: Mon, 26 Aug 2024 14:13:48 +0200
Subject: [PATCH] N21-2133 Authentication (#10)
---
ansible/group_vars/develop/shd-client.yml | 2 +-
.../roles/shd-client-core/defaults/main.yml | 2 +-
.../shd-client-core/templates/ingress.yml.j2 | 6 -
config/webpack/webpack.dev.js | 1 +
package-lock.json | 325 ++++++++----------
package.json | 8 +-
src/App.vue | 14 +-
src/locales/de.ts | 7 +-
src/locales/en.ts | 7 +-
src/locales/es.ts | 7 +-
src/locales/uk.ts | 7 +-
src/main.ts | 11 +-
.../application-error/applicationError.ts | 30 ++
.../applicationError.unit.ts | 126 +++++++
src/modules/data/auth/auth.store.ts | 61 +++-
src/modules/data/auth/auth.store.unit.ts | 205 ++++++++++-
src/modules/data/auth/index.ts | 1 +
src/modules/data/auth/jwtCookie.composable.ts | 41 +++
.../data/auth/jwtCookie.composable.unit.ts | 121 +++++++
src/modules/feature/logout/LogoutBtn.unit.ts | 103 ++++++
src/modules/feature/logout/LogoutBtn.vue | 24 ++
src/modules/feature/logout/index.ts | 3 +
src/modules/page/AboutView.unit.ts | 25 --
src/modules/page/AboutView.vue | 12 -
src/modules/page/HomeView.vue | 4 +-
src/modules/page/Login.page.unit.ts | 182 ++++++++++
src/modules/page/Login.page.vue | 98 ++++++
src/plugins/applicationErrorHandler.ts | 3 +-
src/router/guards/is-authenticated.guard.ts | 38 +-
src/router/guards/is-authenticated.unit.ts | 114 +++---
src/router/guards/login-redirect-url.ts | 37 --
src/router/guards/login-redirect-url.unit.ts | 87 -----
src/router/index.ts | 4 +-
src/router/postLoginRoute.ts | 1 +
src/router/routes.ts | 9 +-
src/serverApi/v3/.openapi-generator/FILES | 3 +-
src/serverApi/v3/api/courses-api.ts | 83 +++++
src/serverApi/v3/api/oauth2-api.ts | 10 +-
...urse-common-cartridge-metadata-response.ts | 49 +++
.../v3/models/import-user-response.ts | 6 +
src/serverApi/v3/models/index.ts | 2 +
src/serverApi/v3/models/login-response.ts | 58 +---
.../models/oauth-provider-login-response.ts | 87 +++++
src/styles/main.scss | 2 +-
src/styles/typography.scss | 32 ++
src/utils/api/api.ts | 8 +-
tests/test-utils/factory/index.ts | 2 +
.../factory/loginResponseFactory.ts | 8 +
tests/test-utils/factory/router/index.ts | 1 +
.../router/routeLocationNormalizedFactory.ts | 20 ++
50 files changed, 1567 insertions(+), 530 deletions(-)
create mode 100644 src/modules/data/application-error/applicationError.unit.ts
create mode 100644 src/modules/data/auth/jwtCookie.composable.ts
create mode 100644 src/modules/data/auth/jwtCookie.composable.unit.ts
create mode 100644 src/modules/feature/logout/LogoutBtn.unit.ts
create mode 100644 src/modules/feature/logout/LogoutBtn.vue
create mode 100644 src/modules/feature/logout/index.ts
delete mode 100644 src/modules/page/AboutView.unit.ts
delete mode 100644 src/modules/page/AboutView.vue
create mode 100644 src/modules/page/Login.page.unit.ts
create mode 100644 src/modules/page/Login.page.vue
delete mode 100644 src/router/guards/login-redirect-url.ts
delete mode 100644 src/router/guards/login-redirect-url.unit.ts
create mode 100644 src/router/postLoginRoute.ts
create mode 100644 src/serverApi/v3/models/course-common-cartridge-metadata-response.ts
create mode 100644 src/serverApi/v3/models/oauth-provider-login-response.ts
create mode 100644 tests/test-utils/factory/loginResponseFactory.ts
create mode 100644 tests/test-utils/factory/router/index.ts
create mode 100644 tests/test-utils/factory/router/routeLocationNormalizedFactory.ts
diff --git a/ansible/group_vars/develop/shd-client.yml b/ansible/group_vars/develop/shd-client.yml
index 434fda7..a5e2fb9 100644
--- a/ansible/group_vars/develop/shd-client.yml
+++ b/ansible/group_vars/develop/shd-client.yml
@@ -1,3 +1,3 @@
---
SHD_CLIENT_IMAGE: ghcr.io/hpi-schul-cloud/shd-client
-SHD_CLIENT_PREFIX: shd2-
\ No newline at end of file
+SHD_CLIENT_PREFIX: superhero-
\ No newline at end of file
diff --git a/ansible/roles/shd-client-core/defaults/main.yml b/ansible/roles/shd-client-core/defaults/main.yml
index 98b2ed5..a1ef41b 100644
--- a/ansible/roles/shd-client-core/defaults/main.yml
+++ b/ansible/roles/shd-client-core/defaults/main.yml
@@ -1,3 +1,3 @@
---
SHD_CLIENT_IMAGE: quay.io/schulcloudverbund/shd-client
-SHD_CLIENT_PREFIX: dashboard2.
+SHD_CLIENT_PREFIX: superhero.
diff --git a/ansible/roles/shd-client-core/templates/ingress.yml.j2 b/ansible/roles/shd-client-core/templates/ingress.yml.j2
index d27dab5..b273477 100644
--- a/ansible/roles/shd-client-core/templates/ingress.yml.j2
+++ b/ansible/roles/shd-client-core/templates/ingress.yml.j2
@@ -6,12 +6,6 @@ metadata:
namespace: {{ NAMESPACE }}
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "{{ TLS_ENABLED|default("false") }}"
- # type of authentication
- nginx.ingress.kubernetes.io/auth-type: basic
- # name of the secret that contains the user/password definitions
- nginx.ingress.kubernetes.io/auth-secret: shd-basic-auth-secret
- # message to display with an appropriate context why the authentication is required
- nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required'
{% if CLUSTER_ISSUER is defined %}
cert-manager.io/cluster-issuer: {{ CLUSTER_ISSUER }}
{% endif %}
diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.js
index 9c04ed8..17973f2 100644
--- a/config/webpack/webpack.dev.js
+++ b/config/webpack/webpack.dev.js
@@ -17,6 +17,7 @@ module.exports = merge(common, {
process.env.NODE_ENV === "development"
? {
port: 4100,
+ historyApiFallback: true,
allowedHosts: "all",
client: {
overlay: false,
diff --git a/package-lock.json b/package-lock.json
index 749db56..d1d7cf3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"dependencies": {
"@vueuse/core": "^10.11.0",
"axios": "^1.7.2",
+ "cross-env": "^7.0.3",
"dayjs": "^1.11.12",
"pinia": "^2.2.0",
"ts-node": "^10.9.2",
@@ -2061,9 +2062,9 @@
}
},
"node_modules/@golevelup/ts-jest": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/@golevelup/ts-jest/-/ts-jest-0.5.1.tgz",
- "integrity": "sha512-1DfWJ4d2X3Ik7wKH2+WOtVHoYgdQAhgptoTRJOgJm18f02eKEnDyppxL7SREM3dbLx+jlFma+RcM+e5dl2Dymg==",
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@golevelup/ts-jest/-/ts-jest-0.5.3.tgz",
+ "integrity": "sha512-PoGR1S8qw2N05yT0wymYZhBQL/UdapX2qDHHflrIvX4quaXthPNR3S9vuhvwNxq2Ike1GKCsPJdDJ0zQxGYiWw==",
"dev": true,
"license": "MIT"
},
@@ -3912,12 +3913,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "22.2.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.2.0.tgz",
- "integrity": "sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==",
+ "version": "22.3.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz",
+ "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==",
"license": "MIT",
"dependencies": {
- "undici-types": "~6.13.0"
+ "undici-types": "~6.18.2"
}
},
"node_modules/@types/node-forge": {
@@ -4276,39 +4277,39 @@
"license": "ISC"
},
"node_modules/@vue/compiler-core": {
- "version": "3.4.37",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.37.tgz",
- "integrity": "sha512-ZDDT/KiLKuCRXyzWecNzC5vTcubGz4LECAtfGPENpo0nrmqJHwuWtRLxk/Sb9RAKtR9iFflFycbkjkY+W/PZUQ==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.38.tgz",
+ "integrity": "sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.24.7",
- "@vue/shared": "3.4.37",
- "entities": "^5.0.0",
+ "@vue/shared": "3.4.38",
+ "entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.4.37",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.37.tgz",
- "integrity": "sha512-rIiSmL3YrntvgYV84rekAtU/xfogMUJIclUMeIKEtVBFngOL3IeZHhsH3UaFEgB5iFGpj6IW+8YuM/2Up+vVag==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.38.tgz",
+ "integrity": "sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-core": "3.4.37",
- "@vue/shared": "3.4.37"
+ "@vue/compiler-core": "3.4.38",
+ "@vue/shared": "3.4.38"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.4.37",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.37.tgz",
- "integrity": "sha512-vCfetdas40Wk9aK/WWf8XcVESffsbNkBQwS5t13Y/PcfqKfIwJX2gF+82th6dOpnpbptNMlMjAny80li7TaCIg==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.38.tgz",
+ "integrity": "sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.24.7",
- "@vue/compiler-core": "3.4.37",
- "@vue/compiler-dom": "3.4.37",
- "@vue/compiler-ssr": "3.4.37",
- "@vue/shared": "3.4.37",
+ "@vue/compiler-core": "3.4.38",
+ "@vue/compiler-dom": "3.4.38",
+ "@vue/compiler-ssr": "3.4.38",
+ "@vue/shared": "3.4.38",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.10",
"postcss": "^8.4.40",
@@ -4316,13 +4317,13 @@
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.4.37",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.37.tgz",
- "integrity": "sha512-TyAgYBWrHlFrt4qpdACh8e9Ms6C/AZQ6A6xLJaWrCL8GCX5DxMzxyeFAEMfU/VFr4tylHm+a2NpfJpcd7+20XA==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.38.tgz",
+ "integrity": "sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.4.37",
- "@vue/shared": "3.4.37"
+ "@vue/compiler-dom": "3.4.38",
+ "@vue/shared": "3.4.38"
}
},
"node_modules/@vue/devtools-api": {
@@ -4357,53 +4358,53 @@
}
},
"node_modules/@vue/reactivity": {
- "version": "3.4.37",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.37.tgz",
- "integrity": "sha512-UmdKXGx0BZ5kkxPqQr3PK3tElz6adTey4307NzZ3whZu19i5VavYal7u2FfOmAzlcDVgE8+X0HZ2LxLb/jgbYw==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.38.tgz",
+ "integrity": "sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==",
"license": "MIT",
"dependencies": {
- "@vue/shared": "3.4.37"
+ "@vue/shared": "3.4.38"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.4.37",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.37.tgz",
- "integrity": "sha512-MNjrVoLV/sirHZoD7QAilU1Ifs7m/KJv4/84QVbE6nyAZGQNVOa1HGxaOzp9YqCG+GpLt1hNDC4RbH+KtanV7w==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.38.tgz",
+ "integrity": "sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==",
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.4.37",
- "@vue/shared": "3.4.37"
+ "@vue/reactivity": "3.4.38",
+ "@vue/shared": "3.4.38"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.4.37",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.37.tgz",
- "integrity": "sha512-Mg2EwgGZqtwKrqdL/FKMF2NEaOHuH+Ks9TQn3DHKyX//hQTYOun+7Tqp1eo0P4Ds+SjltZshOSRq6VsU0baaNg==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.38.tgz",
+ "integrity": "sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==",
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.4.37",
- "@vue/runtime-core": "3.4.37",
- "@vue/shared": "3.4.37",
+ "@vue/reactivity": "3.4.38",
+ "@vue/runtime-core": "3.4.38",
+ "@vue/shared": "3.4.38",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.4.37",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.37.tgz",
- "integrity": "sha512-jZ5FAHDR2KBq2FsRUJW6GKDOAG9lUTX8aBEGq4Vf6B/35I9fPce66BornuwmqmKgfiSlecwuOb6oeoamYMohkg==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.38.tgz",
+ "integrity": "sha512-NggOTr82FbPEkkUvBm4fTGcwUY8UuTsnWC/L2YZBmvaQ4C4Jl/Ao4HHTB+l7WnFCt5M/dN3l0XLuyjzswGYVCA==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-ssr": "3.4.37",
- "@vue/shared": "3.4.37"
+ "@vue/compiler-ssr": "3.4.38",
+ "@vue/shared": "3.4.38"
},
"peerDependencies": {
- "vue": "3.4.37"
+ "vue": "3.4.38"
}
},
"node_modules/@vue/shared": {
- "version": "3.4.37",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.37.tgz",
- "integrity": "sha512-nIh8P2fc3DflG8+5Uw8PT/1i17ccFn0xxN/5oE9RfV5SVnd7G0XEFRwakrnNFE/jlS95fpGXDVG5zDETS26nmg==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.38.tgz",
+ "integrity": "sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==",
"license": "MIT"
},
"node_modules/@vue/test-utils": {
@@ -5061,9 +5062,9 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz",
- "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
+ "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -5263,24 +5264,27 @@
}
},
"node_modules/babel-preset-current-node-syntax": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
- "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
+ "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/plugin-syntax-async-generators": "^7.8.4",
"@babel/plugin-syntax-bigint": "^7.8.3",
- "@babel/plugin-syntax-class-properties": "^7.8.3",
- "@babel/plugin-syntax-import-meta": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5",
+ "@babel/plugin-syntax-import-attributes": "^7.24.7",
+ "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-syntax-json-strings": "^7.8.3",
- "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
- "@babel/plugin-syntax-numeric-separator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4",
"@babel/plugin-syntax-object-rest-spread": "^7.8.3",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
"@babel/plugin-syntax-optional-chaining": "^7.8.3",
- "@babel/plugin-syntax-top-level-await": "^7.8.3"
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+ "@babel/plugin-syntax-top-level-await": "^7.14.5"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
@@ -5835,60 +5839,6 @@
"wrap-ansi": "^7.0.0"
}
},
- "node_modules/cliui/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/cliui/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/cliui/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cliui/node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
"node_modules/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
@@ -6481,11 +6431,28 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"license": "MIT"
},
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -7141,9 +7108,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.6",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz",
- "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==",
+ "version": "1.5.8",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.8.tgz",
+ "integrity": "sha512-4Nx0gP2tPNBLTrFxBMHpkQbtn2hidPVr/+/FTtcCiBYTucqc70zRyVZiOLj17Ui3wTO7SQ1/N+hkHYzJjBzt6A==",
"devOptional": true,
"license": "ISC"
},
@@ -7202,9 +7169,9 @@
}
},
"node_modules/entities": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-5.0.0.tgz",
- "integrity": "sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==",
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -9312,6 +9279,21 @@
"node": ">=8"
}
},
+ "node_modules/inquirer/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/interpret": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
@@ -9575,7 +9557,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
"license": "ISC"
},
"node_modules/isobject": {
@@ -10112,24 +10093,6 @@
"node": ">=8"
}
},
- "node_modules/jest-cli/node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
"node_modules/jest-cli/node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -12980,19 +12943,6 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/parse5/node_modules/entities": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
- "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -13038,7 +12988,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -13388,9 +13337,9 @@
}
},
"node_modules/postcss-selector-parser": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
- "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14402,7 +14351,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -14415,7 +14363,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -14780,9 +14727,9 @@
}
},
"node_modules/terser": {
- "version": "5.31.5",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz",
- "integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==",
+ "version": "5.31.6",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz",
+ "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==",
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -15480,9 +15427,9 @@
}
},
"node_modules/undici-types": {
- "version": "6.13.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
- "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==",
+ "version": "6.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz",
+ "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==",
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -15701,16 +15648,16 @@
}
},
"node_modules/vue": {
- "version": "3.4.37",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.37.tgz",
- "integrity": "sha512-3vXvNfkKTBsSJ7JP+LyR7GBuwQuckbWvuwAid3xbqK9ppsKt/DUvfqgZ48fgOLEfpy1IacL5f8QhUVl77RaI7A==",
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.38.tgz",
+ "integrity": "sha512-f0ZgN+mZ5KFgVv9wz0f4OgVKukoXtS3nwET4c2vLBGQR50aI8G0cqbFtLlX9Yiyg3LFGBitruPHt2PxwTduJEw==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.4.37",
- "@vue/compiler-sfc": "3.4.37",
- "@vue/runtime-dom": "3.4.37",
- "@vue/server-renderer": "3.4.37",
- "@vue/shared": "3.4.37"
+ "@vue/compiler-dom": "3.4.38",
+ "@vue/compiler-sfc": "3.4.38",
+ "@vue/runtime-dom": "3.4.38",
+ "@vue/server-renderer": "3.4.38",
+ "@vue/shared": "3.4.38"
},
"peerDependencies": {
"typescript": "*"
@@ -15931,9 +15878,9 @@
"license": "MIT"
},
"node_modules/vuetify": {
- "version": "3.6.14",
- "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.6.14.tgz",
- "integrity": "sha512-iSa3CgdTEt/7B0aGDmkBARe8rxDDycEYHu1zNtOf1Xpvs/Tv7Ql5yHGqM2XCY0h7SL8Dme39pJIovzg3q4JLbQ==",
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.0.tgz",
+ "integrity": "sha512-x+UaU4SPYNcJSE/voCTBFrNn0q9Spzx2EMfDdUj0NYgHGKb59OqnZte+AjaJaoOXy1AHYIGEpm5Ryk2BEfgWuw==",
"license": "MIT",
"engines": {
"node": "^12.20 || >=14.13"
@@ -15988,9 +15935,9 @@
}
},
"node_modules/watchpack": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
- "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
+ "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -16467,7 +16414,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -16497,9 +16443,9 @@
}
},
"node_modules/wrap-ansi": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
- "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -16508,7 +16454,10 @@
"strip-ansi": "^6.0.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
diff --git a/package.json b/package.json
index 96e1826..07c5015 100644
--- a/package.json
+++ b/package.json
@@ -7,10 +7,9 @@
"jest"
],
"scripts": {
- "serve": "NODE_ENV=development webpack serve --config config/webpack/webpack.dev.js",
- "serve:windows": "set NODE_ENV=development&& webpack serve --config config/webpack/webpack.dev.js",
- "build": "NODE_ENV=production webpack --config config/webpack/webpack.prod.js",
- "test": "npm run test:unit",
+ "serve": "cross-env NODE_ENV=development webpack serve --config config/webpack/webpack.dev.js",
+ "build": "cross-env NODE_ENV=production webpack --config config/webpack/webpack.prod.js",
+ "test": "cross-env NODE_ENV=test npm run test:unit",
"test:unit": "npx jest",
"test:unit:ci": "npm run test:unit -- --coverage --ci --maxWorkers=4",
"lint": "npx eslint 'src/**/*.{ts,js,vue}'",
@@ -19,6 +18,7 @@
"dependencies": {
"@vueuse/core": "^10.11.0",
"axios": "^1.7.2",
+ "cross-env": "^7.0.3",
"dayjs": "^1.11.12",
"pinia": "^2.2.0",
"ts-node": "^10.9.2",
diff --git a/src/App.vue b/src/App.vue
index 96f450a..8f6105b 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -8,22 +8,28 @@
New Superhero-Dashboard
+
+
+
-
-
-
+
+
+
+
+
diff --git a/src/locales/de.ts b/src/locales/de.ts
index 455453b..0a7e4c2 100644
--- a/src/locales/de.ts
+++ b/src/locales/de.ts
@@ -1,4 +1,9 @@
export default {
+ "common.actions.login": "Anmelden",
+ "common.actions.logout": "Abmelden",
+ "common.words.emailAddress": "E-Mail Adresse",
+ "common.words.password": "Passwort",
"error.generic": "Ein Fehler ist aufgetreten",
- "error.load": "Fehler beim Laden der Daten.",
+ "error.load": "Fehler beim Laden der Daten",
+ "error.login.noSuperhero": "Der ausgewählte Account ist kein Superhero",
};
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 2dbe1d4..a7e486a 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -1,4 +1,9 @@
export default {
+ "common.actions.login": "Log in",
+ "common.actions.logout": "Log out",
+ "common.words.emailAddress": "E-Mail Address",
+ "common.words.password": "Password",
"error.generic": "An error has occurred",
- "error.load": "Error while loading the data.",
+ "error.load": "Error while loading the data",
+ "error.login.noSuperhero": "The selected account is not a superhero",
};
diff --git a/src/locales/es.ts b/src/locales/es.ts
index 2e52cb3..06f97a9 100644
--- a/src/locales/es.ts
+++ b/src/locales/es.ts
@@ -1,4 +1,9 @@
export default {
+ "common.actions.login": "Conectarse",
+ "common.actions.logout": "Cerrar sesión",
+ "common.words.emailAddress": "Correo electrónico",
+ "common.words.password": "Contraseña",
"error.generic": "Se ha producido un error",
- "error.load": "Error al cargar los datos.",
+ "error.load": "Error al cargar los datos",
+ "error.login.noSuperhero": "La cuenta seleccionada no es un superhéroe",
};
diff --git a/src/locales/uk.ts b/src/locales/uk.ts
index 2fa7655..5be33f4 100644
--- a/src/locales/uk.ts
+++ b/src/locales/uk.ts
@@ -1,4 +1,9 @@
export default {
+ "common.actions.login": "зареєструватися",
+ "common.actions.logout": "Вийти",
+ "common.words.emailAddress": "адреса електронної пошти",
+ "common.words.password": "пароль",
"error.generic": "Виникла помилка",
- "error.load": "Помилка під час завантаження даних.",
+ "error.load": "Помилка під час завантаження даних",
+ "error.login.noSuperhero": "Вибраний обліковий запис не є супергероєм",
};
diff --git a/src/main.ts b/src/main.ts
index cb23d0e..d1b9daf 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -4,12 +4,11 @@ import { createI18n } from "@/plugins/i18n";
import vuetify from "@/plugins/vuetify";
import router from "@/router";
import { initializeAxios } from "@/utils/api";
-import { useAuthStore } from "@data/auth";
+import { useAuthStore, useJwtCookie } from "@data/auth";
import { useEnvConfigStore } from "@data/env-config";
import { htmlConfig } from "@feature/render-html";
import axios from "axios";
import { createPinia } from "pinia";
-import Cookies from "universal-cookie";
import { createApp } from "vue";
import VueDOMPurifyHTML from "vue-dompurify-html";
import App from "./App.vue";
@@ -38,13 +37,15 @@ app.use(VueDOMPurifyHTML, {
await useEnvConfigStore().loadConfig();
- const cookies = new Cookies();
- const jwt = cookies.get("jwt");
+ const authStore = useAuthStore();
+ const { getJwt } = useJwtCookie();
+ const jwt = getJwt();
if (jwt) {
axios.defaults.headers.common["Authorization"] = "Bearer " + jwt;
+
try {
- await useAuthStore().login();
+ await authStore.fetchMe();
} catch (e) {
// eslint-disable-next-line no-console
console.error("### JWT invalid: ", e);
diff --git a/src/modules/data/application-error/applicationError.ts b/src/modules/data/application-error/applicationError.ts
index 9be2940..967ed3d 100644
--- a/src/modules/data/application-error/applicationError.ts
+++ b/src/modules/data/application-error/applicationError.ts
@@ -1,3 +1,8 @@
+import {
+ ApiResponseError,
+ ApiValidationError,
+ mapAxiosErrorToResponseError,
+} from "@/utils/api";
import { HttpStatusCode } from "./httpStatusCode.enum";
export interface ApplicationErrorProps {
@@ -18,4 +23,29 @@ export class ApplicationError extends Error {
this.statusCode = props.statusCode;
this.translationKey = props.translationKey;
}
+
+ static fromUnknown(
+ error: unknown,
+ translationKey?: string
+ ): ApplicationError {
+ if (error instanceof ApplicationError) {
+ return error;
+ }
+
+ const apiError: ApiResponseError | ApiValidationError =
+ mapAxiosErrorToResponseError(error);
+
+ return ApplicationError.fromApiError(apiError, translationKey);
+ }
+
+ static fromApiError(
+ apiError: ApiResponseError | ApiValidationError,
+ translationKey = "error.generic"
+ ): ApplicationError {
+ return new ApplicationError({
+ statusCode: apiError.code,
+ message: `${apiError.title}: ${apiError.message}`,
+ translationKey,
+ });
+ }
}
diff --git a/src/modules/data/application-error/applicationError.unit.ts b/src/modules/data/application-error/applicationError.unit.ts
new file mode 100644
index 0000000..57d2de4
--- /dev/null
+++ b/src/modules/data/application-error/applicationError.unit.ts
@@ -0,0 +1,126 @@
+import { defaultApiError, mapAxiosErrorToResponseError } from "@/utils/api";
+import {
+ apiResponseErrorFactory,
+ applicationErrorFactory,
+ axiosErrorFactory,
+} from "@@/tests/test-utils/factory";
+import { ApplicationError } from "./applicationError";
+
+describe(ApplicationError.name, () => {
+ describe("fromUnknown", () => {
+ describe("when the error is already an application error", () => {
+ const setup = () => {
+ const error = applicationErrorFactory.build();
+
+ return {
+ error,
+ };
+ };
+
+ it("should return the application error", () => {
+ const { error } = setup();
+
+ const result = ApplicationError.fromUnknown(error);
+
+ expect(result).toEqual(error);
+ });
+ });
+
+ describe("when the error is an axios error", () => {
+ const setup = () => {
+ const error = axiosErrorFactory.build();
+
+ return {
+ error,
+ };
+ };
+
+ it("should return an application error from the api error", () => {
+ const { error } = setup();
+
+ const result = ApplicationError.fromUnknown(error);
+
+ expect(result).toEqual(
+ ApplicationError.fromApiError(mapAxiosErrorToResponseError(error))
+ );
+ });
+ });
+
+ describe("when the error is not a known type", () => {
+ const setup = () => {
+ const error = new Error("test");
+
+ return {
+ error,
+ };
+ };
+
+ it("should return a generic application error from the default api error", () => {
+ const { error } = setup();
+
+ const result = ApplicationError.fromUnknown(error);
+
+ expect(result).toEqual(
+ new ApplicationError({
+ statusCode: defaultApiError.code,
+ message: `${defaultApiError.title}: ${defaultApiError.message}`,
+ translationKey: "error.generic",
+ })
+ );
+ });
+ });
+ });
+
+ describe("fromApiError", () => {
+ describe("when using the default translation key", () => {
+ const setup = () => {
+ const apiError = apiResponseErrorFactory.build();
+
+ return {
+ apiError,
+ };
+ };
+
+ it("should return an application error with a generic message", () => {
+ const { apiError } = setup();
+
+ const result = ApplicationError.fromApiError(apiError);
+
+ expect(result).toEqual(
+ new ApplicationError({
+ statusCode: apiError.code,
+ message: `${apiError.title}: ${apiError.message}`,
+ translationKey: "error.generic",
+ })
+ );
+ });
+ });
+
+ describe("when specifying a translation key", () => {
+ const setup = () => {
+ const apiError = apiResponseErrorFactory.build();
+
+ return {
+ apiError,
+ };
+ };
+
+ it("should return an application error with the translation key", () => {
+ const { apiError } = setup();
+
+ const result = ApplicationError.fromApiError(
+ apiError,
+ "error.not.generic"
+ );
+
+ expect(result).toEqual(
+ new ApplicationError({
+ statusCode: apiError.code,
+ message: `${apiError.title}: ${apiError.message}`,
+ translationKey: "error.not.generic",
+ })
+ );
+ });
+ });
+ });
+});
diff --git a/src/modules/data/auth/auth.store.ts b/src/modules/data/auth/auth.store.ts
index fbb091b..2e16278 100644
--- a/src/modules/data/auth/auth.store.ts
+++ b/src/modules/data/auth/auth.store.ts
@@ -1,28 +1,83 @@
-import { MeApiFactory, MeApiInterface, MeResponse } from "@/serverApi/v3";
+import {
+ AuthenticationApiFactory,
+ AuthenticationApiInterface,
+ MeApiFactory,
+ MeApiInterface,
+ MeResponse,
+ MeRoleResponse,
+ RoleName,
+} from "@/serverApi/v3";
import { $axios } from "@/utils/api";
+import { ApplicationError, HttpStatusCode } from "@data/application-error";
import { defineStore } from "pinia";
import { computed, ComputedRef, Ref, ref } from "vue";
+import { useJwtCookie } from "./jwtCookie.composable";
export const useAuthStore = defineStore("auth", () => {
+ const { hasJwt, setJwt, removeJwt } = useJwtCookie();
+
const me: Ref = ref(null);
const meApi = (): MeApiInterface => {
return MeApiFactory(undefined, "v3", $axios);
};
- const login = async (): Promise => {
+ const authenticationApi = (): AuthenticationApiInterface => {
+ return AuthenticationApiFactory(undefined, "v3", $axios);
+ };
+
+ const login = async (username: string, password: string): Promise => {
+ const { data } = await authenticationApi().loginControllerLoginLocal({
+ username,
+ password,
+ });
+
+ setJwt(data.accessToken);
+
+ await fetchMe();
+
+ if (
+ !me.value?.roles.find(
+ (role: MeRoleResponse): boolean => role.name === RoleName.SUPERHERO
+ )
+ ) {
+ await logout();
+
+ throw new ApplicationError({
+ statusCode: HttpStatusCode.Forbidden,
+ message: "User is not a superhero",
+ translationKey: "error.login.noSuperhero",
+ });
+ }
+ };
+
+ const logout = async (): Promise => {
+ try {
+ await $axios.delete("/v1/authentication");
+ } catch (e: unknown) {
+ // Ignore error
+ }
+
+ me.value = null;
+
+ removeJwt();
+ };
+
+ const fetchMe = async (): Promise => {
const { data } = await meApi().meControllerMe();
me.value = data;
};
const isLoggedIn: ComputedRef = computed(() => {
- return me.value !== null;
+ return me.value !== null && hasJwt();
});
return {
me,
login,
+ logout,
+ fetchMe,
isLoggedIn,
};
});
diff --git a/src/modules/data/auth/auth.store.unit.ts b/src/modules/data/auth/auth.store.unit.ts
index 089ba2e..49dc414 100644
--- a/src/modules/data/auth/auth.store.unit.ts
+++ b/src/modules/data/auth/auth.store.unit.ts
@@ -1,54 +1,216 @@
+import { RoleName } from "@/serverApi/v3";
+import * as serverAuthenticationApi from "@/serverApi/v3/api/authentication-api";
import * as serverMeApi from "@/serverApi/v3/api/me-api";
-import { meResponseFactory } from "@@/tests/test-utils/factory";
+import { initializeAxios } from "@/utils/api";
+import {
+ loginResponseFactory,
+ meResponseFactory,
+} from "@@/tests/test-utils/factory";
import { mockApiResponse } from "@@/tests/test-utils/mockApiResponse";
import { createMock, DeepMocked } from "@golevelup/ts-jest";
+import { AxiosInstance } from "axios";
import { createPinia, setActivePinia } from "pinia";
import { useAuthStore } from "./auth.store";
+import { useJwtCookie } from "./jwtCookie.composable";
+
+jest.mock("./jwtCookie.composable");
describe("AuthStore", () => {
let meApi: DeepMocked;
+ let authenticationApi: DeepMocked;
+ let useJwtCookieMock: DeepMocked>;
+ let axiosMock: DeepMocked;
beforeEach(() => {
setActivePinia(createPinia());
meApi = createMock();
+ authenticationApi =
+ createMock();
+ useJwtCookieMock = createMock>();
+ axiosMock = createMock();
jest.spyOn(serverMeApi, "MeApiFactory").mockReturnValue(meApi);
+ jest
+ .spyOn(serverAuthenticationApi, "AuthenticationApiFactory")
+ .mockReturnValue(authenticationApi);
+ jest.mocked(useJwtCookie).mockReturnValue(useJwtCookieMock);
+ initializeAxios(axiosMock);
});
afterEach(() => {
- jest.clearAllMocks();
+ jest.resetAllMocks();
});
describe("login", () => {
- describe("when logging in", () => {
+ describe("when the user is a superhero", () => {
const setup = () => {
const store = useAuthStore();
- const meResponse = meResponseFactory.build();
+ const username = "username1";
+ const password = "password1";
+ const loginResponse = loginResponseFactory.build();
+ const meResponse = meResponseFactory.build({
+ roles: [{ id: "superheroRole", name: RoleName.SUPERHERO }],
+ });
+ authenticationApi.loginControllerLoginLocal.mockResolvedValueOnce(
+ mockApiResponse({ data: loginResponse })
+ );
meApi.meControllerMe.mockResolvedValueOnce(
mockApiResponse({ data: meResponse })
);
return {
store,
+ username,
+ password,
meResponse,
+ loginResponse,
};
};
+ it("should set the jwt", async () => {
+ const { store, username, password, loginResponse } = setup();
+
+ await store.login(username, password);
+
+ expect(useJwtCookieMock.setJwt).toHaveBeenCalledWith(
+ loginResponse.accessToken
+ );
+ });
+
it("should set me", async () => {
- const { store, meResponse } = setup();
+ const { store, username, password, meResponse } = setup();
- await store.login();
+ await store.login(username, password);
expect(store.me).toEqual(meResponse);
});
});
+
+ describe("when the user is not a superhero", () => {
+ const setup = () => {
+ const store = useAuthStore();
+
+ const username = "username1";
+ const password = "password1";
+ const loginResponse = loginResponseFactory.build();
+ const meResponse = meResponseFactory.build({
+ roles: [{ id: "adminRole", name: RoleName.ADMINISTRATOR }],
+ });
+
+ authenticationApi.loginControllerLoginLocal.mockResolvedValueOnce(
+ mockApiResponse({ data: loginResponse })
+ );
+ meApi.meControllerMe.mockResolvedValueOnce(
+ mockApiResponse({ data: meResponse })
+ );
+
+ let hasJwtCookie = false;
+ useJwtCookieMock.setJwt.mockImplementation(() => (hasJwtCookie = true));
+ useJwtCookieMock.removeJwt.mockImplementation(
+ () => (hasJwtCookie = false)
+ );
+
+ return {
+ store,
+ username,
+ password,
+ meResponse,
+ loginResponse,
+ hasJwtCookie,
+ };
+ };
+
+ it("should log the user out", async () => {
+ const { store, username, password, hasJwtCookie } = setup();
+
+ await expect(store.login(username, password)).rejects.toThrow();
+
+ expect(axiosMock.delete).toHaveBeenCalledWith("/v1/authentication");
+ expect(store.me).toEqual(null);
+ expect(hasJwtCookie).toEqual(false);
+ });
+ });
});
- describe("isLoggedIn", () => {
- describe("when logged in", () => {
+ describe("logout", () => {
+ describe("when the logout api call succeeds", () => {
+ const setup = () => {
+ const store = useAuthStore();
+ store.me = meResponseFactory.build();
+
+ return {
+ store,
+ };
+ };
+
+ it("should log the user out", async () => {
+ const { store } = setup();
+
+ await store.logout();
+
+ expect(axiosMock.delete).toHaveBeenCalledWith("/v1/authentication");
+ });
+
+ it("should reset me", async () => {
+ const { store } = setup();
+
+ await store.logout();
+
+ expect(store.me).toEqual(null);
+ });
+
+ it("should remove the jwt", async () => {
+ const { store } = setup();
+
+ await store.logout();
+
+ expect(useJwtCookieMock.removeJwt).toHaveBeenCalled();
+ });
+ });
+
+ describe("when the logout api call fails", () => {
+ const setup = () => {
+ const store = useAuthStore();
+ store.me = meResponseFactory.build();
+
+ axiosMock.delete.mockRejectedValueOnce(new Error());
+
+ return {
+ store,
+ };
+ };
+
+ it("should log the user out", async () => {
+ const { store } = setup();
+
+ await store.logout();
+
+ expect(axiosMock.delete).toHaveBeenCalledWith("/v1/authentication");
+ });
+
+ it("should reset me", async () => {
+ const { store } = setup();
+
+ await store.logout();
+
+ expect(store.me).toEqual(null);
+ });
+
+ it("should remove the jwt", async () => {
+ const { store } = setup();
+
+ await store.logout();
+
+ expect(useJwtCookieMock.removeJwt).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe("fetchMe", () => {
+ describe("when loading the users data", () => {
const setup = () => {
const store = useAuthStore();
@@ -58,6 +220,31 @@ describe("AuthStore", () => {
mockApiResponse({ data: meResponse })
);
+ return {
+ store,
+ meResponse,
+ };
+ };
+
+ it("should set me", async () => {
+ const { store, meResponse } = setup();
+
+ await store.fetchMe();
+
+ expect(store.me).toEqual(meResponse);
+ });
+ });
+ });
+
+ describe("isLoggedIn", () => {
+ describe("when logged in", () => {
+ const setup = () => {
+ const store = useAuthStore();
+
+ store.me = meResponseFactory.build();
+
+ useJwtCookieMock.hasJwt.mockReturnValue(true);
+
return {
store,
};
@@ -66,8 +253,6 @@ describe("AuthStore", () => {
it("should return true", async () => {
const { store } = setup();
- await store.login();
-
expect(store.isLoggedIn).toEqual(true);
});
});
diff --git a/src/modules/data/auth/index.ts b/src/modules/data/auth/index.ts
index 6030aba..dcfe808 100644
--- a/src/modules/data/auth/index.ts
+++ b/src/modules/data/auth/index.ts
@@ -1 +1,2 @@
export { useAuthStore } from "./auth.store";
+export { useJwtCookie } from "./jwtCookie.composable";
diff --git a/src/modules/data/auth/jwtCookie.composable.ts b/src/modules/data/auth/jwtCookie.composable.ts
new file mode 100644
index 0000000..e69e417
--- /dev/null
+++ b/src/modules/data/auth/jwtCookie.composable.ts
@@ -0,0 +1,41 @@
+import axios from "axios";
+import Cookies from "universal-cookie";
+
+export const useJwtCookie = () => {
+ const getJwt = (): string | undefined => {
+ const cookies = new Cookies();
+
+ const jwt: string | undefined = cookies.get("jwt");
+
+ return jwt;
+ };
+
+ const hasJwt = (): boolean => {
+ return !!getJwt();
+ };
+
+ const setJwt = (jwt: string): void => {
+ const cookies = new Cookies();
+
+ cookies.set("jwt", jwt, {
+ expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
+ });
+
+ axios.defaults.headers.common["Authorization"] = "Bearer " + jwt;
+ };
+
+ const removeJwt = (): void => {
+ const cookies = new Cookies();
+
+ cookies.remove("jwt");
+
+ delete axios.defaults.headers.common["Authorization"];
+ };
+
+ return {
+ getJwt,
+ hasJwt,
+ setJwt,
+ removeJwt,
+ };
+};
diff --git a/src/modules/data/auth/jwtCookie.composable.unit.ts b/src/modules/data/auth/jwtCookie.composable.unit.ts
new file mode 100644
index 0000000..b0903b2
--- /dev/null
+++ b/src/modules/data/auth/jwtCookie.composable.unit.ts
@@ -0,0 +1,121 @@
+import axios from "axios";
+import { useJwtCookie } from "./jwtCookie.composable";
+
+jest.mock("axios");
+
+describe("Jwt Cookie Utils", () => {
+ beforeEach(() => {
+ document.cookie = `jwt=1; expires=1 Jan 1970 00:00:00 GMT;`;
+ });
+
+ describe("getJwt", () => {
+ describe("when there is no jwt cookie", () => {
+ it("should return undefined", () => {
+ const result = useJwtCookie().getJwt();
+
+ expect(result).toEqual(undefined);
+ });
+ });
+
+ describe("when there is a jwt cookie", () => {
+ const setup = () => {
+ const cookieValue = "testValue";
+
+ document.cookie = `jwt=${cookieValue}`;
+
+ return {
+ cookieValue,
+ };
+ };
+
+ it("should return the cookie value", () => {
+ const { cookieValue } = setup();
+
+ const result = useJwtCookie().getJwt();
+
+ expect(result).toEqual(cookieValue);
+ });
+ });
+ });
+
+ describe("hasJwt", () => {
+ describe("when there is no jwt cookie", () => {
+ it("should return false", () => {
+ const result = useJwtCookie().hasJwt();
+
+ expect(result).toEqual(false);
+ });
+ });
+
+ describe("when there is a jwt cookie", () => {
+ const setup = () => {
+ document.cookie = "jwt=testValue";
+ };
+
+ it("should return true", () => {
+ setup();
+
+ const result = useJwtCookie().hasJwt();
+
+ expect(result).toEqual(true);
+ });
+ });
+ });
+
+ describe("setJwt", () => {
+ describe("when setting a jwt cookie", () => {
+ const setup = () => {
+ const cookieValue = "testValue";
+
+ return {
+ cookieValue,
+ };
+ };
+
+ it("should return set the cookie", () => {
+ const { cookieValue } = setup();
+
+ useJwtCookie().setJwt(cookieValue);
+
+ expect(document.cookie).toEqual(`jwt=${cookieValue}`);
+ });
+
+ it("should return set the bearer token for axios", () => {
+ const { cookieValue } = setup();
+
+ useJwtCookie().setJwt(cookieValue);
+
+ expect(axios.defaults.headers.common["Authorization"]).toEqual(
+ `Bearer ${cookieValue}`
+ );
+ });
+ });
+ });
+
+ describe("removeJwt", () => {
+ describe("when removing the jwt", () => {
+ const setup = () => {
+ document.cookie = "jwt=testValue";
+ axios.defaults.headers.common["Authorization"] = "testValue";
+ };
+
+ it("should clear the jwt cookie", () => {
+ setup();
+
+ useJwtCookie().removeJwt();
+
+ expect(document.cookie).toEqual("");
+ });
+
+ it("should clear the bearer token for axios", () => {
+ setup();
+
+ useJwtCookie().removeJwt();
+
+ expect(axios.defaults.headers.common["Authorization"]).toEqual(
+ undefined
+ );
+ });
+ });
+ });
+});
diff --git a/src/modules/feature/logout/LogoutBtn.unit.ts b/src/modules/feature/logout/LogoutBtn.unit.ts
new file mode 100644
index 0000000..3432151
--- /dev/null
+++ b/src/modules/feature/logout/LogoutBtn.unit.ts
@@ -0,0 +1,103 @@
+import { meResponseFactory } from "@@/tests/test-utils/factory";
+import {
+ createTestingI18n,
+ createTestingVuetify,
+} from "@@/tests/test-utils/setup";
+import { useAuthStore } from "@data/auth";
+import { createMock, DeepMocked } from "@golevelup/ts-jest";
+import { createTestingPinia } from "@pinia/testing";
+import { mount } from "@vue/test-utils";
+import { setActivePinia } from "pinia";
+import { Router, useRouter } from "vue-router";
+import LogoutBtn from "./LogoutBtn.vue";
+
+jest.mock("vue-router");
+
+describe("LogoutBtn", () => {
+ let routerMock: DeepMocked;
+
+ const getWrapper = () => {
+ const wrapper = mount(LogoutBtn, {
+ global: {
+ plugins: [createTestingVuetify(), createTestingI18n()],
+ },
+ });
+
+ return {
+ wrapper,
+ };
+ };
+
+ beforeEach(() => {
+ setActivePinia(createTestingPinia());
+
+ routerMock = createMock();
+
+ jest.mocked(useRouter).mockReturnValue(routerMock);
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe("Username", () => {
+ const setup = () => {
+ const firstName = "firstName";
+ const lastName = "lastName";
+ const me = meResponseFactory.build({
+ user: {
+ firstName,
+ lastName,
+ },
+ });
+ const authStore = useAuthStore();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ authStore.isLoggedIn = true;
+ authStore.me = me;
+
+ const { wrapper } = getWrapper();
+
+ return {
+ wrapper,
+ authStore,
+ firstName,
+ lastName,
+ };
+ };
+
+ it("should display the users name", async () => {
+ const { wrapper, firstName, lastName } = setup();
+
+ const username = wrapper.find("[data-testid=username]");
+
+ expect(username.text()).toEqual(`${firstName} ${lastName}`);
+ });
+ });
+
+ describe("when clicking the logout button", () => {
+ const setup = () => {
+ const authStore = useAuthStore();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ authStore.isLoggedIn = true;
+
+ const { wrapper } = getWrapper();
+
+ return {
+ wrapper,
+ authStore,
+ };
+ };
+
+ it("should log the user out and redirect to login", async () => {
+ const { wrapper, authStore } = setup();
+
+ const logoutBtn = wrapper.find("[data-testid=logout-btn]");
+ await logoutBtn.trigger("click");
+
+ expect(authStore.logout).toHaveBeenCalled();
+ expect(routerMock.push).toHaveBeenCalledWith("/login");
+ });
+ });
+});
diff --git a/src/modules/feature/logout/LogoutBtn.vue b/src/modules/feature/logout/LogoutBtn.vue
new file mode 100644
index 0000000..4306203
--- /dev/null
+++ b/src/modules/feature/logout/LogoutBtn.vue
@@ -0,0 +1,24 @@
+
+
+
+ {{ `${authStore.me?.user.firstName} ${authStore.me?.user.lastName}` }}
+
+ {{
+ $t("common.actions.logout")
+ }}
+
+
+
+
diff --git a/src/modules/feature/logout/index.ts b/src/modules/feature/logout/index.ts
new file mode 100644
index 0000000..e3f284f
--- /dev/null
+++ b/src/modules/feature/logout/index.ts
@@ -0,0 +1,3 @@
+import LogoutBtn from "./LogoutBtn.vue";
+
+export { LogoutBtn };
diff --git a/src/modules/page/AboutView.unit.ts b/src/modules/page/AboutView.unit.ts
deleted file mode 100644
index 4b458b3..0000000
--- a/src/modules/page/AboutView.unit.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { createTestingVuetify } from "@@/tests/test-utils/setup";
-import { mount } from "@vue/test-utils";
-import { VBtn } from "vuetify/lib/components/index.mjs";
-import AboutView from "./AboutView.vue";
-
-describe("AboutView", () => {
- const getWrapper = () => {
- const wrapper = mount(AboutView, {
- global: { plugins: [createTestingVuetify()] },
- });
-
- return {
- wrapper,
- };
- };
-
- it("should increase the counter button on click", async () => {
- const { wrapper } = getWrapper();
-
- const counter = wrapper.getComponent(VBtn);
- await counter.trigger("click");
-
- expect(counter.text()).toEqual("1");
- });
-});
diff --git a/src/modules/page/AboutView.vue b/src/modules/page/AboutView.vue
deleted file mode 100644
index c45afce..0000000
--- a/src/modules/page/AboutView.vue
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
- This is the new and improved Superhero-Dashboard!
- {{ counter }}
-
-
-
-
diff --git a/src/modules/page/HomeView.vue b/src/modules/page/HomeView.vue
index 5e481cf..8e8e9c6 100644
--- a/src/modules/page/HomeView.vue
+++ b/src/modules/page/HomeView.vue
@@ -1,7 +1,5 @@
-
- Welcome to the new and improved Superhero-Dashboard!
-
+ Welcome to the new and improved Superhero-Dashboard!
diff --git a/src/modules/page/Login.page.unit.ts b/src/modules/page/Login.page.unit.ts
new file mode 100644
index 0000000..d224314
--- /dev/null
+++ b/src/modules/page/Login.page.unit.ts
@@ -0,0 +1,182 @@
+import { postLoginRoute } from "@/router/postLoginRoute";
+import {
+ createTestingI18n,
+ createTestingVuetify,
+} from "@@/tests/test-utils/setup";
+import { useAuthStore } from "@data/auth";
+import { createMock, DeepMocked } from "@golevelup/ts-jest";
+import { createTestingPinia } from "@pinia/testing";
+import { mount } from "@vue/test-utils";
+import { setActivePinia } from "pinia";
+import { Router, useRouter } from "vue-router";
+import LoginPage from "./Login.page.vue";
+
+jest.mock("vue-router");
+
+describe("LoginPage", () => {
+ let routerMock: DeepMocked;
+
+ const getWrapper = () => {
+ const wrapper = mount(LoginPage, {
+ global: {
+ plugins: [createTestingVuetify(), createTestingI18n()],
+ },
+ });
+
+ return {
+ wrapper,
+ };
+ };
+
+ beforeEach(() => {
+ setActivePinia(createTestingPinia());
+
+ routerMock = createMock();
+
+ jest.mocked(useRouter).mockReturnValue(routerMock);
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe("Login", () => {
+ describe("when logging in as a superhero", () => {
+ const setup = () => {
+ const username = "username@test.com";
+ const password = "password1";
+ const authStore = useAuthStore();
+
+ const { wrapper } = getWrapper();
+
+ return {
+ wrapper,
+ authStore,
+ username,
+ password,
+ };
+ };
+
+ it("should login the user and redirect to the home page", async () => {
+ const { wrapper, username, password, authStore } = setup();
+
+ const usernameInput = wrapper.find(
+ "[data-testid=login-input-username] input"
+ );
+ const passwordInput = wrapper.find(
+ "[data-testid=login-input-password] input"
+ );
+ const loginBtn = wrapper.find("[data-testid=login-btn]");
+
+ await usernameInput.setValue(username);
+ await passwordInput.setValue(password);
+ await loginBtn.trigger("click");
+
+ expect(authStore.login).toHaveBeenCalledWith(username, password);
+ expect(routerMock.push).toHaveBeenCalledWith(postLoginRoute);
+ });
+ });
+
+ describe("when a redirect query is defined", () => {
+ const setup = () => {
+ const username = "username@test.com";
+ const password = "password1";
+ const redirect = "/test";
+ const authStore = useAuthStore();
+
+ routerMock.currentRoute.value.query.redirect = redirect;
+
+ const { wrapper } = getWrapper();
+
+ return {
+ wrapper,
+ authStore,
+ username,
+ password,
+ redirect,
+ };
+ };
+
+ it("should redirect to the specified page", async () => {
+ const { wrapper, username, password, redirect } = setup();
+
+ const usernameInput = wrapper.find(
+ "[data-testid=login-input-username] input"
+ );
+ const passwordInput = wrapper.find(
+ "[data-testid=login-input-password] input"
+ );
+ const loginBtn = wrapper.find("[data-testid=login-btn]");
+
+ await usernameInput.setValue(username);
+ await passwordInput.setValue(password);
+ await loginBtn.trigger("click");
+
+ expect(routerMock.push).toHaveBeenCalledWith(redirect);
+ });
+ });
+
+ describe("when the login fails", () => {
+ const setup = () => {
+ const username = "username@test.com";
+ const password = "password1";
+ const authStore = useAuthStore();
+
+ authStore.login = jest.fn().mockRejectedValueOnce(new Error());
+
+ const { wrapper } = getWrapper();
+
+ return {
+ wrapper,
+ authStore,
+ username,
+ password,
+ };
+ };
+
+ it("should display an error", async () => {
+ const { wrapper, username, password } = setup();
+
+ const usernameInput = wrapper.find(
+ "[data-testid=login-input-username] input"
+ );
+ const passwordInput = wrapper.find(
+ "[data-testid=login-input-password] input"
+ );
+ const loginBtn = wrapper.find("[data-testid=login-btn]");
+
+ await usernameInput.setValue(username);
+ await passwordInput.setValue(password);
+ await loginBtn.trigger("click");
+
+ const errorAlert = wrapper.find("[data-testid=login-error]");
+
+ expect(errorAlert.isVisible()).toEqual(true);
+ expect(errorAlert.text()).toBeTruthy();
+ expect(routerMock.push).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("when no username or password is defined", () => {
+ const setup = () => {
+ const authStore = useAuthStore();
+
+ const { wrapper } = getWrapper();
+
+ return {
+ wrapper,
+ authStore,
+ };
+ };
+
+ it("should do nothing", async () => {
+ const { wrapper, authStore } = setup();
+
+ const loginBtn = wrapper.find("[data-testid=login-btn]");
+ await loginBtn.trigger("click");
+
+ expect(authStore.login).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/src/modules/page/Login.page.vue b/src/modules/page/Login.page.vue
new file mode 100644
index 0000000..530982f
--- /dev/null
+++ b/src/modules/page/Login.page.vue
@@ -0,0 +1,98 @@
+
+
+ SuperHero-Login
+
+
+
+
+
+
+ {{ $t("common.actions.login") }}
+
+
+
+ {{ $t(loginError.translationKey) }}
+
+
+
+
+
+
+
diff --git a/src/plugins/applicationErrorHandler.ts b/src/plugins/applicationErrorHandler.ts
index 52c1def..a9cdc80 100644
--- a/src/plugins/applicationErrorHandler.ts
+++ b/src/plugins/applicationErrorHandler.ts
@@ -1,5 +1,6 @@
import {
ApplicationError,
+ HttpStatusCode,
useApplicationErrorStore,
} from "@data/application-error";
@@ -19,7 +20,7 @@ export const handleApplicationError = (err: unknown) => {
} else {
applicationErrorStore.setError(
new ApplicationError({
- statusCode: 500,
+ statusCode: HttpStatusCode.InternalServerError,
translationKey: "error.generic",
})
);
diff --git a/src/router/guards/is-authenticated.guard.ts b/src/router/guards/is-authenticated.guard.ts
index fe8eadc..d958647 100644
--- a/src/router/guards/is-authenticated.guard.ts
+++ b/src/router/guards/is-authenticated.guard.ts
@@ -1,23 +1,37 @@
import { useAuthStore } from "@data/auth";
-import { NavigationGuardNext, RouteLocationNormalized } from "vue-router";
-import { getLoginUrlWithRedirect } from "./login-redirect-url";
+import {
+ NavigationGuard,
+ NavigationGuardReturn,
+ RouteLocationNormalized,
+} from "vue-router";
+import { postLoginRoute } from "../postLoginRoute";
-export const isAuthenticatedGuard = (
- to: RouteLocationNormalized,
- from: RouteLocationNormalized,
- next: NavigationGuardNext
-) => {
+export const isAuthenticatedGuard: NavigationGuard = (
+ to: RouteLocationNormalized
+): NavigationGuardReturn => {
const userIsLoggedIn = useAuthStore().isLoggedIn;
- if (userIsLoggedIn || isRoutePublic(to)) {
- next();
+ if (userIsLoggedIn && to.path.startsWith("/login")) {
+ window.location.assign(postLoginRoute);
+ } else if (userIsLoggedIn || isRoutePublic(to)) {
+ return true;
} else {
- const loginUrl = getLoginUrlWithRedirect(to.fullPath);
- window.location.assign(loginUrl);
+ const loginUrl = new URL("/login", window.location.origin);
+ loginUrl.searchParams.set("redirect", to.fullPath);
+
+ const relativePath = toPathString(loginUrl);
+
+ window.location.assign(relativePath);
}
+
+ return false;
+};
+
+export const toPathString = (url: URL): string => {
+ return url.pathname + url.search + url.hash;
};
-const isRoutePublic = (route: RouteLocationNormalized) => {
+const isRoutePublic = (route: RouteLocationNormalized): boolean => {
if (typeof route.meta?.isPublic === "boolean") {
return route.meta.isPublic;
} else {
diff --git a/src/router/guards/is-authenticated.unit.ts b/src/router/guards/is-authenticated.unit.ts
index d500338..5a3a936 100644
--- a/src/router/guards/is-authenticated.unit.ts
+++ b/src/router/guards/is-authenticated.unit.ts
@@ -1,17 +1,26 @@
-import { meResponseFactory } from "@@/tests/test-utils/factory";
+import { routeLocationNormalizedFactory } from "@@/tests/test-utils/factory";
import { useAuthStore } from "@data/auth";
import { createTestingPinia } from "@pinia/testing";
import { setActivePinia } from "pinia";
import { RouteLocationNormalized } from "vue-router";
+import { postLoginRoute } from "../postLoginRoute";
import { isAuthenticatedGuard } from "./is-authenticated.guard";
-jest.mock("./login-redirect-url", () => ({
- getLoginUrlWithRedirect: () => "login-url",
-}));
-
describe("Authentication Guard", () => {
+ const from: RouteLocationNormalized = {} as RouteLocationNormalized;
+ const next = jest.fn();
+
beforeEach(() => {
setActivePinia(createTestingPinia());
+
+ const assign = jest.fn();
+ Object.defineProperty(window, "location", {
+ configurable: true,
+ value: {
+ assign,
+ origin: "https://test.com",
+ },
+ });
});
afterEach(() => {
@@ -21,41 +30,55 @@ describe("Authentication Guard", () => {
describe("isAuthenticatedGuard", () => {
describe("when authenticated", () => {
const setup = () => {
- const to: RouteLocationNormalized = {
- fullPath: "/test",
- } as RouteLocationNormalized;
- const from: RouteLocationNormalized = {} as RouteLocationNormalized;
- const next = jest.fn();
+ const to = routeLocationNormalizedFactory("/test");
+
const authStore = useAuthStore();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ authStore.isLoggedIn = true;
+
+ return {
+ to,
+ };
+ };
+
+ it("should pass", async () => {
+ const { to } = setup();
- authStore.me = meResponseFactory.build();
+ const result = await isAuthenticatedGuard(to, from, next);
+
+ expect(result).toEqual(true);
+ });
+ });
+
+ describe("when accessing the login page while authenticated", () => {
+ const setup = () => {
+ const to = routeLocationNormalizedFactory("/login");
+
+ const authStore = useAuthStore();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ authStore.isLoggedIn = true;
return {
to,
- from,
- next,
};
};
- it("should pass", () => {
- const { to, from, next } = setup();
+ it("should redirect to the main page", async () => {
+ const { to } = setup();
- isAuthenticatedGuard(to, from, next);
+ await isAuthenticatedGuard(to, from, next);
- expect(next).toHaveBeenCalled();
+ expect(window.location.assign).toHaveBeenCalledWith(postLoginRoute);
});
});
- describe("when the url is public", () => {
+ describe("when not authenticated and the url is public", () => {
const setup = () => {
- const to: RouteLocationNormalized = {
- fullPath: "/test",
- meta: {
- isPublic: true,
- },
- } as unknown as RouteLocationNormalized;
- const from: RouteLocationNormalized = {} as RouteLocationNormalized;
- const next = jest.fn();
+ const to = routeLocationNormalizedFactory("/test", {
+ meta: { isPublic: true },
+ });
const authStore = useAuthStore();
authStore.me = null;
@@ -67,54 +90,37 @@ describe("Authentication Guard", () => {
};
};
- it("should pass", () => {
- const { to, from, next } = setup();
+ it("should pass", async () => {
+ const { to } = setup();
- isAuthenticatedGuard(to, from, next);
+ const result = await isAuthenticatedGuard(to, from, next);
- expect(next).toHaveBeenCalled();
+ expect(result).toEqual(true);
});
});
describe("when not authenticated and the url is not public", () => {
const setup = () => {
- const to: RouteLocationNormalized = {
- fullPath: "/test",
- } as RouteLocationNormalized;
- const from: RouteLocationNormalized = {} as RouteLocationNormalized;
- const next = jest.fn();
+ const to = routeLocationNormalizedFactory("/test?param1=value1#hash1");
const authStore = useAuthStore();
authStore.me = null;
- const assign = jest.fn();
- Object.defineProperty(window, "location", {
- configurable: true,
- value: { assign },
- });
-
return {
to,
from,
next,
- assign,
};
};
- it("should redirect to login", () => {
- const { to, from, next, assign } = setup();
-
- isAuthenticatedGuard(to, from, next);
-
- expect(assign).toHaveBeenCalledWith("login-url");
- });
-
- it("should not pass", () => {
- const { to, from, next } = setup();
+ it("should redirect to login with post login redirect query", async () => {
+ const { to } = setup();
- isAuthenticatedGuard(to, from, next);
+ await isAuthenticatedGuard(to, from, next);
- expect(next).not.toHaveBeenCalled();
+ expect(window.location.assign).toHaveBeenCalledWith(
+ "/login?redirect=%2Ftest%3Fparam1%3Dvalue1%23hash1"
+ );
});
});
});
diff --git a/src/router/guards/login-redirect-url.ts b/src/router/guards/login-redirect-url.ts
deleted file mode 100644
index 32d9aee..0000000
--- a/src/router/guards/login-redirect-url.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useEnvConfigStore } from "@data/env-config";
-
-export const getLoginUrlWithRedirect = (targetPath: string): string => {
- const currentOrigin = window.location.origin;
-
- const currentUrl = new URL(targetPath, currentOrigin);
- const loginUrl = new URL(
- useEnvConfigStore().getEnvs.NOT_AUTHENTICATED_REDIRECT_URL,
- currentOrigin // fallback to current origin, if a relative url is configured
- );
-
- const isInternalUrl = currentUrl.origin === loginUrl.origin;
-
- if (isInternalUrl) {
- return addRedirectAsParam(loginUrl, currentUrl);
- }
-
- return addRedirectAsParamToUrlParams(loginUrl, currentUrl);
-};
-
-const addRedirectAsParam = (loginUrl: URL, currentUrl: URL) => {
- loginUrl.searchParams.set("redirect", currentUrl.toString());
- return loginUrl.toString();
-};
-
-const addRedirectAsParamToUrlParams = (loginUrl: URL, currentUrl: URL) => {
- for (const [name, value] of loginUrl.searchParams.entries()) {
- const isSchulcloudUrl = value.indexOf(currentUrl.origin) === 0;
- if (isSchulcloudUrl) {
- const urlInParameters = new URL(value);
- urlInParameters.searchParams.set("redirect", currentUrl.toString());
- loginUrl.searchParams.set(name, urlInParameters.toString());
- }
- }
-
- return loginUrl.toString();
-};
diff --git a/src/router/guards/login-redirect-url.unit.ts b/src/router/guards/login-redirect-url.unit.ts
deleted file mode 100644
index dd1d479..0000000
--- a/src/router/guards/login-redirect-url.unit.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { envsFactory } from "@@/tests/test-utils/factory";
-import { useEnvConfigStore } from "@data/env-config";
-import { createPinia, setActivePinia } from "pinia";
-import { getLoginUrlWithRedirect } from "./login-redirect-url";
-
-describe("Login Redirect Util", () => {
- beforeEach(() => {
- setActivePinia(createPinia());
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- describe("getLoginUrlWithRedirect", () => {
- describe("when redirecting to internal login before redirecting to the target url", () => {
- const setup = () => {
- const loginUrl = "https://test.com/login";
- const targetPath = "/dashboard";
- const origin = "https://test.com";
- const envConfigStore = useEnvConfigStore();
-
- envConfigStore.setEnvs(
- envsFactory.build({
- NOT_AUTHENTICATED_REDIRECT_URL: loginUrl,
- })
- );
-
- jest.spyOn(window, "location", "get").mockReturnValue({
- origin: origin,
- } as Location);
-
- return {
- loginUrl,
- targetPath,
- origin,
- };
- };
-
- it("should redirect to internal login with post-login-redirect to internal target url", () => {
- const { loginUrl, targetPath, origin } = setup();
-
- const result = getLoginUrlWithRedirect(targetPath);
-
- expect(result).toEqual(
- `${loginUrl}?redirect=${encodeURIComponent(`${origin}${targetPath}`)}`
- );
- });
- });
-
- describe("when redirecting to an external login before redirecting to the target url", () => {
- const setup = () => {
- const origin = "https://test.com";
- const loginUrl = `https://external-login.thr/login?service=${encodeURIComponent(origin)}`;
- const targetPath = "/dashboard";
- const envConfigStore = useEnvConfigStore();
-
- envConfigStore.setEnvs(
- envsFactory.build({
- NOT_AUTHENTICATED_REDIRECT_URL: loginUrl,
- })
- );
-
- jest.spyOn(window, "location", "get").mockReturnValue({
- origin: origin,
- } as Location);
-
- return {
- loginUrl,
- targetPath,
- origin,
- };
- };
-
- it("should redirect to external login with post-login-redirect to internal target url", () => {
- const { loginUrl, targetPath, origin } = setup();
-
- const result = getLoginUrlWithRedirect(targetPath);
-
- const redirectUri = encodeURIComponent(`${origin}${targetPath}`);
- expect(result).toEqual(
- `${loginUrl}${encodeURIComponent(`/?redirect=${redirectUri}`)}`
- );
- });
- });
- });
-});
diff --git a/src/router/index.ts b/src/router/index.ts
index 7ddb1a6..b4fc67b 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -1,4 +1,5 @@
import { createRouter, createWebHistory } from "vue-router";
+import { isAuthenticatedGuard } from "./guards";
import { routes } from "./routes";
const router = createRouter({
@@ -6,7 +7,6 @@ const router = createRouter({
routes,
});
-// TODO N21-2133: Add authentication
-// router.beforeEach(isAuthenticatedGuard);
+router.beforeResolve(isAuthenticatedGuard);
export default router;
diff --git a/src/router/postLoginRoute.ts b/src/router/postLoginRoute.ts
new file mode 100644
index 0000000..b6494f8
--- /dev/null
+++ b/src/router/postLoginRoute.ts
@@ -0,0 +1 @@
+export const postLoginRoute = "/";
diff --git a/src/router/routes.ts b/src/router/routes.ts
index 3de40d2..c195ded 100644
--- a/src/router/routes.ts
+++ b/src/router/routes.ts
@@ -9,8 +9,11 @@ export const routes: Readonly = [
component: HomeView,
},
{
- path: "/about",
- name: "about",
- component: () => import("@page/AboutView.vue"),
+ path: "/login",
+ name: "login",
+ component: () => import("@page/Login.page.vue"),
+ meta: {
+ isPublic: true,
+ },
},
];
diff --git a/src/serverApi/v3/.openapi-generator/FILES b/src/serverApi/v3/.openapi-generator/FILES
index 2d22a2f..25d421d 100644
--- a/src/serverApi/v3/.openapi-generator/FILES
+++ b/src/serverApi/v3/.openapi-generator/FILES
@@ -1,6 +1,5 @@
.gitignore
.npmignore
-.openapi-generator-ignore
api.ts
api/account-api.ts
api/admin-students-api.ts
@@ -93,6 +92,7 @@ models/context-external-tool-response.ts
models/context-external-tool-search-list-response.ts
models/copy-api-response.ts
models/county-response.ts
+models/course-common-cartridge-metadata-response.ts
models/course-export-body-params.ts
models/course-info-response.ts
models/course-metadata-list-response.ts
@@ -195,6 +195,7 @@ models/oauth-client-create-body.ts
models/oauth-client-response.ts
models/oauth-client-update-body.ts
models/oauth-config-response.ts
+models/oauth-provider-login-response.ts
models/oauth-token-dto.ts
models/oauth2-authorization-body-params.ts
models/oauth2-migration-params.ts
diff --git a/src/serverApi/v3/api/courses-api.ts b/src/serverApi/v3/api/courses-api.ts
index f0c56a9..7c5f3bb 100644
--- a/src/serverApi/v3/api/courses-api.ts
+++ b/src/serverApi/v3/api/courses-api.ts
@@ -21,6 +21,8 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base';
// @ts-ignore
+import { CourseCommonCartridgeMetadataResponse } from '../models';
+// @ts-ignore
import { CourseExportBodyParams } from '../models';
// @ts-ignore
import { CourseMetadataListResponse } from '../models';
@@ -114,6 +116,44 @@ export const CoursesApiAxiosParamCreator = function (configuration?: Configurati
+ setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Get common cartridge metadata of a course by Id.
+ * @param {string} courseId The id of the course
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ courseControllerGetCourseCcMetadataById: async (courseId: string, options: any = {}): Promise => {
+ // verify required parameter 'courseId' is not null or undefined
+ assertParamExists('courseControllerGetCourseCcMetadataById', 'courseId', courseId)
+ const localVarPath = `/courses/{courseId}/cc-metadata`
+ .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearer required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -277,6 +317,17 @@ export const CoursesApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.courseControllerFindForUser(skip, limit, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
+ /**
+ *
+ * @summary Get common cartridge metadata of a course by Id.
+ * @param {string} courseId The id of the course
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async courseControllerGetCourseCcMetadataById(courseId: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.courseControllerGetCourseCcMetadataById(courseId, options);
+ return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+ },
/**
*
* @summary Get permissions for a user in a course.
@@ -341,6 +392,16 @@ export const CoursesApiFactory = function (configuration?: Configuration, basePa
courseControllerFindForUser(skip?: number, limit?: number, options?: any): AxiosPromise {
return localVarFp.courseControllerFindForUser(skip, limit, options).then((request) => request(axios, basePath));
},
+ /**
+ *
+ * @summary Get common cartridge metadata of a course by Id.
+ * @param {string} courseId The id of the course
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ courseControllerGetCourseCcMetadataById(courseId: string, options?: any): AxiosPromise {
+ return localVarFp.courseControllerGetCourseCcMetadataById(courseId, options).then((request) => request(axios, basePath));
+ },
/**
*
* @summary Get permissions for a user in a course.
@@ -401,6 +462,16 @@ export interface CoursesApiInterface {
*/
courseControllerFindForUser(skip?: number, limit?: number, options?: any): AxiosPromise;
+ /**
+ *
+ * @summary Get common cartridge metadata of a course by Id.
+ * @param {string} courseId The id of the course
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof CoursesApiInterface
+ */
+ courseControllerGetCourseCcMetadataById(courseId: string, options?: any): AxiosPromise;
+
/**
*
* @summary Get permissions for a user in a course.
@@ -465,6 +536,18 @@ export class CoursesApi extends BaseAPI implements CoursesApiInterface {
return CoursesApiFp(this.configuration).courseControllerFindForUser(skip, limit, options).then((request) => request(this.axios, this.basePath));
}
+ /**
+ *
+ * @summary Get common cartridge metadata of a course by Id.
+ * @param {string} courseId The id of the course
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof CoursesApi
+ */
+ public courseControllerGetCourseCcMetadataById(courseId: string, options?: any) {
+ return CoursesApiFp(this.configuration).courseControllerGetCourseCcMetadataById(courseId, options).then((request) => request(this.axios, this.basePath));
+ }
+
/**
*
* @summary Get permissions for a user in a course.
diff --git a/src/serverApi/v3/api/oauth2-api.ts b/src/serverApi/v3/api/oauth2-api.ts
index 8b3e0e5..22eabe3 100644
--- a/src/serverApi/v3/api/oauth2-api.ts
+++ b/src/serverApi/v3/api/oauth2-api.ts
@@ -29,14 +29,14 @@ import { ConsentSessionResponse } from '../models';
// @ts-ignore
import { LoginRequestBody } from '../models';
// @ts-ignore
-import { LoginResponse } from '../models';
-// @ts-ignore
import { OauthClientCreateBody } from '../models';
// @ts-ignore
import { OauthClientResponse } from '../models';
// @ts-ignore
import { OauthClientUpdateBody } from '../models';
// @ts-ignore
+import { OauthProviderLoginResponse } from '../models';
+// @ts-ignore
import { RedirectResponse } from '../models';
/**
* Oauth2Api - axios parameter creator
@@ -581,7 +581,7 @@ export const Oauth2ApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- async oauthProviderControllerGetLoginRequest(challenge: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ async oauthProviderControllerGetLoginRequest(challenge: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
const localVarAxiosArgs = await localVarAxiosParamCreator.oauthProviderControllerGetLoginRequest(challenge, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@@ -714,7 +714,7 @@ export const Oauth2ApiFactory = function (configuration?: Configuration, basePat
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- oauthProviderControllerGetLoginRequest(challenge: string, options?: any): AxiosPromise {
+ oauthProviderControllerGetLoginRequest(challenge: string, options?: any): AxiosPromise {
return localVarFp.oauthProviderControllerGetLoginRequest(challenge, options).then((request) => request(axios, basePath));
},
/**
@@ -839,7 +839,7 @@ export interface Oauth2ApiInterface {
* @throws {RequiredError}
* @memberof Oauth2ApiInterface
*/
- oauthProviderControllerGetLoginRequest(challenge: string, options?: any): AxiosPromise;
+ oauthProviderControllerGetLoginRequest(challenge: string, options?: any): AxiosPromise;
/**
*
diff --git a/src/serverApi/v3/models/course-common-cartridge-metadata-response.ts b/src/serverApi/v3/models/course-common-cartridge-metadata-response.ts
new file mode 100644
index 0000000..4d6c1e7
--- /dev/null
+++ b/src/serverApi/v3/models/course-common-cartridge-metadata-response.ts
@@ -0,0 +1,49 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Schulcloud-Verbund-Software Server API
+ * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1.
+ *
+ * The version of the OpenAPI document: 3.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+
+
+/**
+ *
+ * @export
+ * @interface CourseCommonCartridgeMetadataResponse
+ */
+export interface CourseCommonCartridgeMetadataResponse {
+ /**
+ * The id of the course
+ * @type {string}
+ * @memberof CourseCommonCartridgeMetadataResponse
+ */
+ id: string;
+ /**
+ * Title of the course
+ * @type {string}
+ * @memberof CourseCommonCartridgeMetadataResponse
+ */
+ title: string;
+ /**
+ * Creation date of the course
+ * @type {string}
+ * @memberof CourseCommonCartridgeMetadataResponse
+ */
+ creationDate: string;
+ /**
+ * Copy right owners of the course
+ * @type {Array}
+ * @memberof CourseCommonCartridgeMetadataResponse
+ */
+ copyRightOwners: Array;
+}
+
+
diff --git a/src/serverApi/v3/models/import-user-response.ts b/src/serverApi/v3/models/import-user-response.ts
index ee0e1a4..1976a0b 100644
--- a/src/serverApi/v3/models/import-user-response.ts
+++ b/src/serverApi/v3/models/import-user-response.ts
@@ -69,6 +69,12 @@ export interface ImportUserResponse {
* @memberof ImportUserResponse
*/
flagged: boolean;
+ /**
+ * exact user roles from the external system
+ * @type {Array}
+ * @memberof ImportUserResponse
+ */
+ externalRoleNames?: Array;
}
/**
diff --git a/src/serverApi/v3/models/index.ts b/src/serverApi/v3/models/index.ts
index 86cca12..83ea2c8 100644
--- a/src/serverApi/v3/models/index.ts
+++ b/src/serverApi/v3/models/index.ts
@@ -45,6 +45,7 @@ export * from './context-external-tool-response';
export * from './context-external-tool-search-list-response';
export * from './copy-api-response';
export * from './county-response';
+export * from './course-common-cartridge-metadata-response';
export * from './course-export-body-params';
export * from './course-info-response';
export * from './course-metadata-list-response';
@@ -151,6 +152,7 @@ export * from './oauth-client-create-body';
export * from './oauth-client-response';
export * from './oauth-client-update-body';
export * from './oauth-config-response';
+export * from './oauth-provider-login-response';
export * from './oidc-context-response';
export * from './parent-consent-response';
export * from './patch-group-params';
diff --git a/src/serverApi/v3/models/login-response.ts b/src/serverApi/v3/models/login-response.ts
index ace81b4..d434821 100644
--- a/src/serverApi/v3/models/login-response.ts
+++ b/src/serverApi/v3/models/login-response.ts
@@ -13,8 +13,6 @@
*/
-import { OauthClientResponse } from './oauth-client-response';
-import { OidcContextResponse } from './oidc-context-response';
/**
*
@@ -22,66 +20,12 @@ import { OidcContextResponse } from './oidc-context-response';
* @interface LoginResponse
*/
export interface LoginResponse {
- /**
- * Id of the corresponding client.
- * @type {string}
- * @memberof LoginResponse
- */
- client_id: string;
- /**
- * The id/challenge of the consent login request.
- * @type {string}
- * @memberof LoginResponse
- */
- challenge: string;
/**
*
- * @type {OauthClientResponse}
- * @memberof LoginResponse
- */
- client: OauthClientResponse;
- /**
- *
- * @type {OidcContextResponse}
- * @memberof LoginResponse
- */
- oidc_context: OidcContextResponse;
- /**
- * The original oauth2.0 authorization url request by the client.
- * @type {string}
- * @memberof LoginResponse
- */
- request_url: string;
- /**
- *
- * @type {Array}
- * @memberof LoginResponse
- */
- requested_access_token_audience: Array;
- /**
- * The request scopes of the login request.
- * @type {Array}
- * @memberof LoginResponse
- */
- requested_scope?: Array;
- /**
- * The login session id. This parameter is used as sid for the oidc front-/backchannel logout.
- * @type {string}
- * @memberof LoginResponse
- */
- session_id: string;
- /**
- * Skip, if true, implies that the client has requested the same scopes from the same user previously.
- * @type {boolean}
- * @memberof LoginResponse
- */
- skip: boolean;
- /**
- * User id of the end-user that is authenticated.
* @type {string}
* @memberof LoginResponse
*/
- subject: string;
+ accessToken: string;
}
diff --git a/src/serverApi/v3/models/oauth-provider-login-response.ts b/src/serverApi/v3/models/oauth-provider-login-response.ts
new file mode 100644
index 0000000..ce58925
--- /dev/null
+++ b/src/serverApi/v3/models/oauth-provider-login-response.ts
@@ -0,0 +1,87 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Schulcloud-Verbund-Software Server API
+ * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1.
+ *
+ * The version of the OpenAPI document: 3.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+
+import { OauthClientResponse } from './oauth-client-response';
+import { OidcContextResponse } from './oidc-context-response';
+
+/**
+ *
+ * @export
+ * @interface OauthProviderLoginResponse
+ */
+export interface OauthProviderLoginResponse {
+ /**
+ * Id of the corresponding client.
+ * @type {string}
+ * @memberof OauthProviderLoginResponse
+ */
+ client_id: string;
+ /**
+ * The id/challenge of the consent login request.
+ * @type {string}
+ * @memberof OauthProviderLoginResponse
+ */
+ challenge: string;
+ /**
+ *
+ * @type {OauthClientResponse}
+ * @memberof OauthProviderLoginResponse
+ */
+ client: OauthClientResponse;
+ /**
+ *
+ * @type {OidcContextResponse}
+ * @memberof OauthProviderLoginResponse
+ */
+ oidc_context: OidcContextResponse;
+ /**
+ * The original oauth2.0 authorization url request by the client.
+ * @type {string}
+ * @memberof OauthProviderLoginResponse
+ */
+ request_url: string;
+ /**
+ *
+ * @type {Array}
+ * @memberof OauthProviderLoginResponse
+ */
+ requested_access_token_audience: Array;
+ /**
+ * The request scopes of the login request.
+ * @type {Array}
+ * @memberof OauthProviderLoginResponse
+ */
+ requested_scope?: Array;
+ /**
+ * The login session id. This parameter is used as sid for the oidc front-/backchannel logout.
+ * @type {string}
+ * @memberof OauthProviderLoginResponse
+ */
+ session_id: string;
+ /**
+ * Skip, if true, implies that the client has requested the same scopes from the same user previously.
+ * @type {boolean}
+ * @memberof OauthProviderLoginResponse
+ */
+ skip: boolean;
+ /**
+ * User id of the end-user that is authenticated.
+ * @type {string}
+ * @memberof OauthProviderLoginResponse
+ */
+ subject: string;
+}
+
+
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 3250557..a9103ff 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -1,4 +1,4 @@
@use './fonts';
@use './typography';
@use './settings';
-@use 'vuetify';
\ No newline at end of file
+@use 'vuetify';
diff --git a/src/styles/typography.scss b/src/styles/typography.scss
index 6e99847..09042c7 100644
--- a/src/styles/typography.scss
+++ b/src/styles/typography.scss
@@ -4,4 +4,36 @@ $system-fonts: -apple-system, blinkmacsystemfont, "Segoe UI", roboto, ubuntu,
:root {
--font-primary: "PT Sans", #{$system-fonts};
--font-accent: "PT Sans Narrow", #{$system-fonts};
+
+ /* set base values */
+ --text-base-size: 1rem;
+ --text-scale-ratio: 1.2;
+ --text-device-scale-ratio: 1.125;
+
+ /* heading sizes */
+ --heading-1: calc(var(--heading-2) * var(--text-scale-ratio));
+ --heading-2: calc(var(--heading-3) * var(--text-scale-ratio));
+ --heading-3: calc(var(--heading-4) * var(--text-scale-ratio));
+ --heading-4: calc(var(--heading-5) * var(--text-scale-ratio));
+ --heading-5: calc(var(--heading-6) * var(--text-scale-ratio));
+ --heading-6: calc(var(--text-base-size) * var(--text-scale-ratio));
+
+ /* text sizes */
+ --text-xs: calc(
+ var(--text-base-size) / var(--text-scale-ratio) / var(--text-scale-ratio)
+ );
+ --text-sm: calc(var(--text-base-size) / var(--text-scale-ratio));
+ --text-md: var(--text-base-size);
+ --text-lg: calc(var(--text-base-size) * var(--text-scale-ratio));
+
+ /* line-height */
+ --line-height-sm: 1.05;
+ --line-height-md: 1.2;
+ --line-height-lg: 1.4;
+ --line-height-xl: 2;
+
+ /* font-weight */
+ --font-weight-light: 200;
+ --font-weight-normal: 400;
+ --font-weight-bold: 700;
}
diff --git a/src/utils/api/api.ts b/src/utils/api/api.ts
index 3c0abd4..7405100 100644
--- a/src/utils/api/api.ts
+++ b/src/utils/api/api.ts
@@ -14,10 +14,10 @@ export const initializeAxios = (axios: AxiosInstance) => {
};
export const defaultApiError: ApiResponseError | ApiValidationError = {
- message: "UNKNOWN_ERROR",
- code: 1,
- title: "",
- type: "Unknown error",
+ message: "Unknown error",
+ code: 500,
+ title: "Unknown Error",
+ type: "UNKNOWN_ERROR",
};
export const mapAxiosErrorToResponseError = (
diff --git a/tests/test-utils/factory/index.ts b/tests/test-utils/factory/index.ts
index 95fe836..ce38d75 100644
--- a/tests/test-utils/factory/index.ts
+++ b/tests/test-utils/factory/index.ts
@@ -1,4 +1,6 @@
export * from "./api";
+export * from "./router";
export * from "./envsFactory";
export * from "./meResponseFactory";
export * from "./applicationErrorFactory";
+export * from "./loginResponseFactory";
diff --git a/tests/test-utils/factory/loginResponseFactory.ts b/tests/test-utils/factory/loginResponseFactory.ts
new file mode 100644
index 0000000..7542ea6
--- /dev/null
+++ b/tests/test-utils/factory/loginResponseFactory.ts
@@ -0,0 +1,8 @@
+import { LoginResponse } from "@/serverApi/v3";
+import { Factory } from "fishery";
+
+export const loginResponseFactory = Factory.define(
+ ({ sequence }) => ({
+ accessToken: `accessToken-${sequence}`,
+ })
+);
diff --git a/tests/test-utils/factory/router/index.ts b/tests/test-utils/factory/router/index.ts
new file mode 100644
index 0000000..df387b8
--- /dev/null
+++ b/tests/test-utils/factory/router/index.ts
@@ -0,0 +1 @@
+export * from "./routeLocationNormalizedFactory";
diff --git a/tests/test-utils/factory/router/routeLocationNormalizedFactory.ts b/tests/test-utils/factory/router/routeLocationNormalizedFactory.ts
new file mode 100644
index 0000000..ba87d94
--- /dev/null
+++ b/tests/test-utils/factory/router/routeLocationNormalizedFactory.ts
@@ -0,0 +1,20 @@
+import { RouteLocationNormalized } from "vue-router";
+
+export const routeLocationNormalizedFactory = (
+ path: string,
+ overwrite: Partial = {}
+): RouteLocationNormalized => {
+ const url = new URL(path, "https://dummy.com");
+ return {
+ fullPath: url.pathname + url.search + url.hash,
+ path: url.pathname,
+ meta: {},
+ params: {},
+ query: Object.fromEntries(url.searchParams.entries()),
+ name: undefined,
+ hash: url.hash,
+ matched: [],
+ redirectedFrom: undefined,
+ ...overwrite,
+ };
+};