diff --git a/package-lock.json b/package-lock.json
index ae3815bc8c..64aae1d8ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -39,6 +39,7 @@
"flag-icons": "^7.0.2",
"hammerjs": "^2.0.8",
"json-query": "^2.2.2",
+ "keycloak-angular": "^15.0.0",
"keycloak-js": "^22.0.5",
"leaflet": "^1.9.4",
"lodash-es": "^4.17.21",
@@ -921,9 +922,9 @@
"integrity": "sha512-Z22MksLzyf1bQAPftRouF58sOoXH14mlp9iqe9LqmhA8DcCBkPHyyWzzYFq9E9eELNSiMT2Arm/mDwZ8bYy88g=="
},
"node_modules/@babel/code-frame": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz",
- "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
+ "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dependencies": {
"@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
@@ -933,28 +934,28 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.23.3",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz",
- "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
+ "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.23.3",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz",
- "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz",
+ "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.3",
+ "@babel/code-frame": "^7.23.5",
+ "@babel/generator": "^7.23.5",
"@babel/helper-compilation-targets": "^7.22.15",
"@babel/helper-module-transforms": "^7.23.3",
- "@babel/helpers": "^7.23.2",
- "@babel/parser": "^7.23.3",
+ "@babel/helpers": "^7.23.5",
+ "@babel/parser": "^7.23.5",
"@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.3",
- "@babel/types": "^7.23.3",
+ "@babel/traverse": "^7.23.5",
+ "@babel/types": "^7.23.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -970,11 +971,11 @@
}
},
"node_modules/@babel/core/node_modules/@babel/generator": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz",
- "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz",
+ "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==",
"dependencies": {
- "@babel/types": "^7.23.4",
+ "@babel/types": "^7.23.5",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -1058,17 +1059,17 @@
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz",
- "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.5.tgz",
+ "integrity": "sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
- "@babel/helper-member-expression-to-functions": "^7.22.15",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-member-expression-to-functions": "^7.23.0",
"@babel/helper-optimise-call-expression": "^7.22.5",
- "@babel/helper-replace-supers": "^7.22.9",
+ "@babel/helper-replace-supers": "^7.22.20",
"@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"semver": "^6.3.1"
@@ -1309,9 +1310,9 @@
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
- "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
+ "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
"engines": {
"node": ">=6.9.0"
}
@@ -1331,13 +1332,13 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.4.tgz",
- "integrity": "sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz",
+ "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==",
"dependencies": {
"@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.4",
- "@babel/types": "^7.23.4"
+ "@babel/traverse": "^7.23.5",
+ "@babel/types": "^7.23.5"
},
"engines": {
"node": ">=6.9.0"
@@ -1357,9 +1358,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz",
- "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz",
+ "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -1838,9 +1839,9 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
- "version": "7.23.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.3.tgz",
- "integrity": "sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz",
+ "integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
@@ -2479,13 +2480,13 @@
}
},
"node_modules/@babel/plugin-transform-typescript": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.4.tgz",
- "integrity": "sha512-39hCCOl+YUAyMOu6B9SmUTiHUU0t/CxJNUmY3qRdJujbqi+lrQcL11ysYUsAvFWPBdhihrv1z0oRG84Yr3dODQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.5.tgz",
+ "integrity": "sha512-2fMkXEJkrmwgu2Bsv1Saxgj30IXZdJ+84lQcKKI7sm719oXs0BBw2ZENKdJdR1PjWndgLCEBNXJOri0fk7RYQA==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-create-class-features-plugin": "^7.22.15",
+ "@babel/helper-create-class-features-plugin": "^7.23.5",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-typescript": "^7.23.3"
},
@@ -2881,18 +2882,18 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz",
- "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz",
+ "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==",
"dependencies": {
- "@babel/code-frame": "^7.23.4",
- "@babel/generator": "^7.23.4",
+ "@babel/code-frame": "^7.23.5",
+ "@babel/generator": "^7.23.5",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.23.4",
- "@babel/types": "^7.23.4",
+ "@babel/parser": "^7.23.5",
+ "@babel/types": "^7.23.5",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -2901,11 +2902,11 @@
}
},
"node_modules/@babel/traverse/node_modules/@babel/generator": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz",
- "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz",
+ "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==",
"dependencies": {
- "@babel/types": "^7.23.4",
+ "@babel/types": "^7.23.5",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -2915,9 +2916,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz",
- "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz",
+ "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
@@ -3101,15 +3102,15 @@
}
},
"node_modules/@compodoc/compodoc/node_modules/@babel/preset-env": {
- "version": "7.23.3",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.3.tgz",
- "integrity": "sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.5.tgz",
+ "integrity": "sha512-0d/uxVD6tFGWXGDSfyMD1p2otoaKmu6+GD+NfAx0tMaH+dxORnp7T9TaVQ6mKyya7iBtCIVxHjWT7MuzzM9z+A==",
"dev": true,
"dependencies": {
- "@babel/compat-data": "^7.23.3",
+ "@babel/compat-data": "^7.23.5",
"@babel/helper-compilation-targets": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-validator-option": "^7.22.15",
+ "@babel/helper-validator-option": "^7.23.5",
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3",
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3",
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3",
@@ -3133,25 +3134,25 @@
"@babel/plugin-syntax-top-level-await": "^7.14.5",
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
"@babel/plugin-transform-arrow-functions": "^7.23.3",
- "@babel/plugin-transform-async-generator-functions": "^7.23.3",
+ "@babel/plugin-transform-async-generator-functions": "^7.23.4",
"@babel/plugin-transform-async-to-generator": "^7.23.3",
"@babel/plugin-transform-block-scoped-functions": "^7.23.3",
- "@babel/plugin-transform-block-scoping": "^7.23.3",
+ "@babel/plugin-transform-block-scoping": "^7.23.4",
"@babel/plugin-transform-class-properties": "^7.23.3",
- "@babel/plugin-transform-class-static-block": "^7.23.3",
- "@babel/plugin-transform-classes": "^7.23.3",
+ "@babel/plugin-transform-class-static-block": "^7.23.4",
+ "@babel/plugin-transform-classes": "^7.23.5",
"@babel/plugin-transform-computed-properties": "^7.23.3",
"@babel/plugin-transform-destructuring": "^7.23.3",
"@babel/plugin-transform-dotall-regex": "^7.23.3",
"@babel/plugin-transform-duplicate-keys": "^7.23.3",
- "@babel/plugin-transform-dynamic-import": "^7.23.3",
+ "@babel/plugin-transform-dynamic-import": "^7.23.4",
"@babel/plugin-transform-exponentiation-operator": "^7.23.3",
- "@babel/plugin-transform-export-namespace-from": "^7.23.3",
+ "@babel/plugin-transform-export-namespace-from": "^7.23.4",
"@babel/plugin-transform-for-of": "^7.23.3",
"@babel/plugin-transform-function-name": "^7.23.3",
- "@babel/plugin-transform-json-strings": "^7.23.3",
+ "@babel/plugin-transform-json-strings": "^7.23.4",
"@babel/plugin-transform-literals": "^7.23.3",
- "@babel/plugin-transform-logical-assignment-operators": "^7.23.3",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.23.4",
"@babel/plugin-transform-member-expression-literals": "^7.23.3",
"@babel/plugin-transform-modules-amd": "^7.23.3",
"@babel/plugin-transform-modules-commonjs": "^7.23.3",
@@ -3159,15 +3160,15 @@
"@babel/plugin-transform-modules-umd": "^7.23.3",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5",
"@babel/plugin-transform-new-target": "^7.23.3",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.3",
- "@babel/plugin-transform-numeric-separator": "^7.23.3",
- "@babel/plugin-transform-object-rest-spread": "^7.23.3",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4",
+ "@babel/plugin-transform-numeric-separator": "^7.23.4",
+ "@babel/plugin-transform-object-rest-spread": "^7.23.4",
"@babel/plugin-transform-object-super": "^7.23.3",
- "@babel/plugin-transform-optional-catch-binding": "^7.23.3",
- "@babel/plugin-transform-optional-chaining": "^7.23.3",
+ "@babel/plugin-transform-optional-catch-binding": "^7.23.4",
+ "@babel/plugin-transform-optional-chaining": "^7.23.4",
"@babel/plugin-transform-parameters": "^7.23.3",
"@babel/plugin-transform-private-methods": "^7.23.3",
- "@babel/plugin-transform-private-property-in-object": "^7.23.3",
+ "@babel/plugin-transform-private-property-in-object": "^7.23.4",
"@babel/plugin-transform-property-literals": "^7.23.3",
"@babel/plugin-transform-regenerator": "^7.23.3",
"@babel/plugin-transform-reserved-words": "^7.23.3",
@@ -4198,45 +4199,45 @@
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
- "version": "6.4.2",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
- "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.0.tgz",
+ "integrity": "sha512-vYC8oN2l8meu05sRi1j3Iie/HNFAeIxpitYFhsUrBc11TxiMken9QdXnSQ0q16FYsOSt/6soxs5ghdk+VYGiog==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
- "version": "6.4.2",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz",
- "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.0.tgz",
+ "integrity": "sha512-5DrR+oxQr+ruRQ3CEVV8DSCT/q8Atm56+FzAs0P6eW/epW47OmecSpSwc/YTlJ3u5BfPKUBSGyPR2qjZ+5eIgA==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.4.2"
+ "@fortawesome/fontawesome-common-types": "6.5.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
- "version": "6.4.2",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz",
- "integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.0.tgz",
+ "integrity": "sha512-RaBW/y0jKcCyEPM+NYuBs3bQXuLYZHnXABQPmg6qwuRxNb2EUmyCcVUECUH2dkFmMjggh/xvl6n6y62Pl19JkA==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.4.2"
+ "@fortawesome/fontawesome-common-types": "6.5.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
- "version": "6.4.2",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz",
- "integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.0.tgz",
+ "integrity": "sha512-6ZPq8mme67Q7O9Fbp2O+Z7mPZbcWTsRv555JLftLaTodiV0Wq+99YgMhyQ8O6mgNQfComqS9OEvqs7M8ySA92g==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.4.2"
+ "@fortawesome/fontawesome-common-types": "6.5.0"
},
"engines": {
"node": ">=6"
@@ -6161,9 +6162,9 @@
}
},
"node_modules/@percy/storybook": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/@percy/storybook/-/storybook-5.0.0.tgz",
- "integrity": "sha512-tzNBrFIh8KDRhTjZhYyleQm8+yNevhnCUNHhymDBQ1wWZsmsJC/hqOvGJCwuZm3kudgdAnzxVTxX3GJ2ljtmCQ==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@percy/storybook/-/storybook-5.0.1.tgz",
+ "integrity": "sha512-L9naTGb+DQC0lOnNmdV6wTZW3q67bSIL2lUgMDTyhVOhKvtn8cgXwh3PH+rqEgXiHl1Ult5dJ4kv2WbLnutbsQ==",
"dev": true,
"dependencies": {
"@percy/cli-command": "^1.24.0",
@@ -7060,12 +7061,12 @@
"dev": true
},
"node_modules/@storybook/addon-actions": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.0.tgz",
- "integrity": "sha512-yc4d/6j0XaTQPkEMkT0JxWPjRwZUg0oC929/vpouYhaC60Ch/b3PnzUFkSQ2BqgeUUH0c9wfzs/9np6USRXpBQ==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.1.tgz",
+ "integrity": "sha512-KSaIv1LKGyaCJuPuEIwwLtMp/Md8t/jprng3FwYW/lChbsYw40lexiLYRy2LHomsyvXT0acsjLuSmJ8FrynSoA==",
"dev": true,
"dependencies": {
- "@storybook/core-events": "7.6.0",
+ "@storybook/core-events": "7.6.1",
"@storybook/global": "^5.0.0",
"@types/uuid": "^9.0.1",
"dequal": "^2.0.2",
@@ -7078,9 +7079,9 @@
}
},
"node_modules/@storybook/addon-backgrounds": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.0.tgz",
- "integrity": "sha512-8BMwVXiazDQlqYS8Snzowzn6MbcsigaUxhWzWoCkLUmyOvciywvOjQD9241wbEdLc/rdFZAPRGoQLZfn/1BV9g==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.1.tgz",
+ "integrity": "sha512-o4PFWTAmc9VjSQA3zT1XOM6WoFBmUxtrH0lFsBeeXHh9mIhnCmmYOGDt09ZVC20QrAfp35qo0UouVIOy5MC6ng==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -7093,12 +7094,12 @@
}
},
"node_modules/@storybook/addon-controls": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-7.6.0.tgz",
- "integrity": "sha512-x6OszUmbi+xO4TlFAmw7nFq3IEzM10yPtAMC+RZ8jfSnlfevtZvT/KYb6WySo0H3b7b6GIxLwzbHceZrkHYPHg==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-7.6.1.tgz",
+ "integrity": "sha512-yc9uLEGCjEdMlp12gwPesZOklD2rQfELMI8VZXR0PUa5lhmj4oMkdRAMYltf7Fk5CtMgjdDs7jVn5UnfxwqEWg==",
"dev": true,
"dependencies": {
- "@storybook/blocks": "7.6.0",
+ "@storybook/blocks": "7.6.1",
"lodash": "^4.17.21",
"ts-dedent": "^2.0.0"
},
@@ -7108,26 +7109,26 @@
}
},
"node_modules/@storybook/addon-docs": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-7.6.0.tgz",
- "integrity": "sha512-gvBMqERBilXEQaDFbsmUZSVSRjBJUSmHaGRYLMb72PnK1pfoRSxvi0TrIqbOScTQLUukKN42OwA83xLS+2eubg==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-7.6.1.tgz",
+ "integrity": "sha512-f4d+0R582DUxJvoALgOtPAGnWOQ7aB1OOovMp3YNTv6rO2bMpC9umhjvBaf4RJCM83MLHiGA9CkReHJz6V0sUA==",
"dev": true,
"dependencies": {
"@jest/transform": "^29.3.1",
"@mdx-js/react": "^2.1.5",
- "@storybook/blocks": "7.6.0",
- "@storybook/client-logger": "7.6.0",
- "@storybook/components": "7.6.0",
- "@storybook/csf-plugin": "7.6.0",
- "@storybook/csf-tools": "7.6.0",
+ "@storybook/blocks": "7.6.1",
+ "@storybook/client-logger": "7.6.1",
+ "@storybook/components": "7.6.1",
+ "@storybook/csf-plugin": "7.6.1",
+ "@storybook/csf-tools": "7.6.1",
"@storybook/global": "^5.0.0",
"@storybook/mdx2-csf": "^1.0.0",
- "@storybook/node-logger": "7.6.0",
- "@storybook/postinstall": "7.6.0",
- "@storybook/preview-api": "7.6.0",
- "@storybook/react-dom-shim": "7.6.0",
- "@storybook/theming": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/node-logger": "7.6.1",
+ "@storybook/postinstall": "7.6.1",
+ "@storybook/preview-api": "7.6.1",
+ "@storybook/react-dom-shim": "7.6.1",
+ "@storybook/theming": "7.6.1",
+ "@storybook/types": "7.6.1",
"fs-extra": "^11.1.0",
"remark-external-links": "^8.0.0",
"remark-slug": "^6.0.0",
@@ -7143,24 +7144,24 @@
}
},
"node_modules/@storybook/addon-essentials": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-7.6.0.tgz",
- "integrity": "sha512-cLmLradZqGxh3xavAhPTmJYUdV66NZsEyAZKdc2Fjo9sVWbWstODe/IC1xhD1dRgxEMCTlBpmUENqeCbbnkpqA==",
- "dev": true,
- "dependencies": {
- "@storybook/addon-actions": "7.6.0",
- "@storybook/addon-backgrounds": "7.6.0",
- "@storybook/addon-controls": "7.6.0",
- "@storybook/addon-docs": "7.6.0",
- "@storybook/addon-highlight": "7.6.0",
- "@storybook/addon-measure": "7.6.0",
- "@storybook/addon-outline": "7.6.0",
- "@storybook/addon-toolbars": "7.6.0",
- "@storybook/addon-viewport": "7.6.0",
- "@storybook/core-common": "7.6.0",
- "@storybook/manager-api": "7.6.0",
- "@storybook/node-logger": "7.6.0",
- "@storybook/preview-api": "7.6.0",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-7.6.1.tgz",
+ "integrity": "sha512-MNhA6wd/oYvU2Xd8lCDuyY9vazEPn40/TO2tmT3+8VdxzzP8D7fISjNhy4L3Cg8SvQqrQxeWmh+yETtn48VplA==",
+ "dev": true,
+ "dependencies": {
+ "@storybook/addon-actions": "7.6.1",
+ "@storybook/addon-backgrounds": "7.6.1",
+ "@storybook/addon-controls": "7.6.1",
+ "@storybook/addon-docs": "7.6.1",
+ "@storybook/addon-highlight": "7.6.1",
+ "@storybook/addon-measure": "7.6.1",
+ "@storybook/addon-outline": "7.6.1",
+ "@storybook/addon-toolbars": "7.6.1",
+ "@storybook/addon-viewport": "7.6.1",
+ "@storybook/core-common": "7.6.1",
+ "@storybook/manager-api": "7.6.1",
+ "@storybook/node-logger": "7.6.1",
+ "@storybook/preview-api": "7.6.1",
"ts-dedent": "^2.0.0"
},
"funding": {
@@ -7173,9 +7174,9 @@
}
},
"node_modules/@storybook/addon-highlight": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-7.6.0.tgz",
- "integrity": "sha512-9Ho4L47k9e36kIgNa0RA9ZXzn7AWmq/sXBMRK7PcgrMiKvYdRQMU0AMHDvICg2vI8IkMVXTEaT/CQPefG3XT0Q==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-7.6.1.tgz",
+ "integrity": "sha512-NcaIQA1clfKY8P13PlL7yypASaxNcLQ3+fjYa6ysS1dnXw4dV2ecPpYQD8/+sUXOTVtbSdtlVZIvPlmKrO2c5g==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -7186,9 +7187,9 @@
}
},
"node_modules/@storybook/addon-measure": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-7.6.0.tgz",
- "integrity": "sha512-CtGZ45LUvylvM7z53TbUdJ8qx5QRgWXAA1Lk/+yBlIYgXQhvMzSupxmhjRyZZdj1rMj8S5NYuIg43nsQYijnsQ==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-7.6.1.tgz",
+ "integrity": "sha512-RREJsDnBtLcI7kpay6fXvSjnGOwEHQQ14TyuhtWjbSRmP/H3mGgUUk6Aty/qi0nKuQPoj6RFSM2r/khTYOd0eg==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -7200,9 +7201,9 @@
}
},
"node_modules/@storybook/addon-outline": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-7.6.0.tgz",
- "integrity": "sha512-W5KcuxM2w9VugZmU8nCwRa1FZdLj+sMcLZG4R1JcplY4SkWXrVtqMRJE0TNvyUEPUJpqcUwZWvL31fOZHQaOwg==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-7.6.1.tgz",
+ "integrity": "sha512-SaVqwbpD42iThYL69ypNozgQ2rOE5QaNRiKg4n5OkskFMsFi35/ZyOlufnKRBLgad2TUmVIS4vYerMiSGNOjew==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -7214,9 +7215,9 @@
}
},
"node_modules/@storybook/addon-toolbars": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-7.6.0.tgz",
- "integrity": "sha512-IZ6LGqu4QYsXSKof8KO0bL9+UR4KXho4z/+o0OYBwQRnO1KBQh73yRoeRxZPsJWE0Ms5zZnAMEA6iSZ+Zyvs5g==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-7.6.1.tgz",
+ "integrity": "sha512-nOE9q/57tuGed2z4+NGDhKfCii0Xh2SUt4C9O4tajoyVl2vs5MG+YnipbuIzmrmgPjs8oh0Bk+z7PtQGoRi7Xw==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -7224,9 +7225,9 @@
}
},
"node_modules/@storybook/addon-viewport": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-7.6.0.tgz",
- "integrity": "sha512-1NGgoEnDYLWw0HuRTTrIHCj8I0Xtc76PgPtArj92HQu+ENu4Hy0Y8MypZ+ZmAFddykaInwmZeQo2CD0GXU9qUQ==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-7.6.1.tgz",
+ "integrity": "sha512-f+CYKLk6ftEzwf7Q48CBiJBaERzGzxBcMlcS1/xPg6n2AGpVl++HvFRpvDR/vuE22YpHVz5B1mwUusoXHemhyw==",
"dev": true,
"dependencies": {
"memoizerific": "^1.11.3"
@@ -7237,24 +7238,24 @@
}
},
"node_modules/@storybook/angular": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/angular/-/angular-7.6.0.tgz",
- "integrity": "sha512-dXq5XYuEcyNLZC1c529KMcWiWQ0MFFmojweJz8BnP/IAnyWYt7CMMDSzlN3+fAbf4tQ3f/UBE0dmy/DDVlPjGg==",
- "dev": true,
- "dependencies": {
- "@storybook/builder-webpack5": "7.6.0",
- "@storybook/cli": "7.6.0",
- "@storybook/client-logger": "7.6.0",
- "@storybook/core-common": "7.6.0",
- "@storybook/core-events": "7.6.0",
- "@storybook/core-server": "7.6.0",
- "@storybook/core-webpack": "7.6.0",
- "@storybook/docs-tools": "7.6.0",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/angular/-/angular-7.6.1.tgz",
+ "integrity": "sha512-SH4TyvCvxob6SSrhVqTXznHVbqE4saygxjm4Z/ebXaM0BEbZNFAkg5w6MFO6beZNg+MDJO6arE3z9oZxyO0fyQ==",
+ "dev": true,
+ "dependencies": {
+ "@storybook/builder-webpack5": "7.6.1",
+ "@storybook/cli": "7.6.1",
+ "@storybook/client-logger": "7.6.1",
+ "@storybook/core-common": "7.6.1",
+ "@storybook/core-events": "7.6.1",
+ "@storybook/core-server": "7.6.1",
+ "@storybook/core-webpack": "7.6.1",
+ "@storybook/docs-tools": "7.6.1",
"@storybook/global": "^5.0.0",
- "@storybook/node-logger": "7.6.0",
- "@storybook/preview-api": "7.6.0",
- "@storybook/telemetry": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/node-logger": "7.6.1",
+ "@storybook/preview-api": "7.6.1",
+ "@storybook/telemetry": "7.6.1",
+ "@storybook/types": "7.6.1",
"@types/node": "^18.0.0",
"@types/react": "^16.14.34",
"@types/react-dom": "^16.9.14",
@@ -7320,22 +7321,22 @@
}
},
"node_modules/@storybook/blocks": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-7.6.0.tgz",
- "integrity": "sha512-S5g0h9dJevngPXnsFAjxQryOU/rQuA4cet40hIH0qRKesFwaXDFrsYHMZfxg6uGKygpeGbIEBoXckL8DT3SofA==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-7.6.1.tgz",
+ "integrity": "sha512-thK7W/RrhDRkGGwm+Le8E2ffVY3oXtLl7UPR/IT8+waBXgeBzQz6Mjl4KQiV38sloRyz9ZRUiXT96mnrXIkeMQ==",
"dev": true,
"dependencies": {
- "@storybook/channels": "7.6.0",
- "@storybook/client-logger": "7.6.0",
- "@storybook/components": "7.6.0",
- "@storybook/core-events": "7.6.0",
+ "@storybook/channels": "7.6.1",
+ "@storybook/client-logger": "7.6.1",
+ "@storybook/components": "7.6.1",
+ "@storybook/core-events": "7.6.1",
"@storybook/csf": "^0.1.2",
- "@storybook/docs-tools": "7.6.0",
+ "@storybook/docs-tools": "7.6.1",
"@storybook/global": "^5.0.0",
- "@storybook/manager-api": "7.6.0",
- "@storybook/preview-api": "7.6.0",
- "@storybook/theming": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/manager-api": "7.6.1",
+ "@storybook/preview-api": "7.6.1",
+ "@storybook/theming": "7.6.1",
+ "@storybook/types": "7.6.1",
"@types/lodash": "^4.14.167",
"color-convert": "^2.0.1",
"dequal": "^2.0.2",
@@ -7359,15 +7360,15 @@
}
},
"node_modules/@storybook/builder-manager": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.0.tgz",
- "integrity": "sha512-xbyc1aMdvJrmN6mk7GW1mv/+gGxDfk3Rrv6tZph5nAWUojibEEqVHv5k6IXq5yyTzwRU+IpCf8gzCUSEvORt7w==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.1.tgz",
+ "integrity": "sha512-Hxp2y3XR5UgfsWWqVM7R4jb+4DZNe8LBb9ko3PzMRAySY0ED45/lPTK+zEEFhOX4mc1Ftqmj0RAJHmbwr0vQRg==",
"dev": true,
"dependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
- "@storybook/core-common": "7.6.0",
- "@storybook/manager": "7.6.0",
- "@storybook/node-logger": "7.6.0",
+ "@storybook/core-common": "7.6.1",
+ "@storybook/manager": "7.6.1",
+ "@storybook/node-logger": "7.6.1",
"@types/ejs": "^3.1.1",
"@types/find-cache-dir": "^3.2.1",
"@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10",
@@ -7776,20 +7777,20 @@
}
},
"node_modules/@storybook/builder-webpack5": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-7.6.0.tgz",
- "integrity": "sha512-vbCAWpyb/d/mm4OJs+ZXKPqMuOp8XkqhreTZj+rgfwy+TexzKNOgalQyLinOLpmmFY/hRj4qgoJCqBLgyTwr9A==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-7.6.1.tgz",
+ "integrity": "sha512-0bo8nNU/YhIgrfNBaebd9ruWLKZZmn/C/5ubkYCc8Yo1JxxZIJj3Hm45JCuSWMW2RO9DLnTF5TGpzNyVPtODpA==",
"dev": true,
"dependencies": {
"@babel/core": "^7.23.2",
- "@storybook/channels": "7.6.0",
- "@storybook/client-logger": "7.6.0",
- "@storybook/core-common": "7.6.0",
- "@storybook/core-events": "7.6.0",
- "@storybook/core-webpack": "7.6.0",
- "@storybook/node-logger": "7.6.0",
- "@storybook/preview": "7.6.0",
- "@storybook/preview-api": "7.6.0",
+ "@storybook/channels": "7.6.1",
+ "@storybook/client-logger": "7.6.1",
+ "@storybook/core-common": "7.6.1",
+ "@storybook/core-events": "7.6.1",
+ "@storybook/core-webpack": "7.6.1",
+ "@storybook/node-logger": "7.6.1",
+ "@storybook/preview": "7.6.1",
+ "@storybook/preview-api": "7.6.1",
"@swc/core": "^1.3.82",
"@types/node": "^18.0.0",
"@types/semver": "^7.3.4",
@@ -7839,13 +7840,13 @@
}
},
"node_modules/@storybook/channels": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.0.tgz",
- "integrity": "sha512-Zobr57AkPIE+cdQMrIC9FdgQZDJt8XmpCR+QCxzhrhz6zJLVbIDjf866vKmy3EGSzGrlajfAg/G1PK4v7FdAcw==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.1.tgz",
+ "integrity": "sha512-wQBiY8gLRs6I3V8Cr5xHNx3OImDhzsYGOMM8tUnVKO4HpcxsJ7ipQ8UvIU88MNbk+gx3WCsM4FZBBPF4f1Ar/g==",
"dev": true,
"dependencies": {
- "@storybook/client-logger": "7.6.0",
- "@storybook/core-events": "7.6.0",
+ "@storybook/client-logger": "7.6.1",
+ "@storybook/core-events": "7.6.1",
"@storybook/global": "^5.0.0",
"qs": "^6.10.0",
"telejson": "^7.2.0",
@@ -7857,23 +7858,23 @@
}
},
"node_modules/@storybook/cli": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.0.tgz",
- "integrity": "sha512-bpkJDBrpdrwn4D0XlqpPHuKBplpTirAC6hAvYLE1yVXFYKcZjF/Xavd6uaSZ5IK8G8E6BWJ2cgNFHM/FtF510w==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.1.tgz",
+ "integrity": "sha512-o0KDj9E5VYVn55/l9j7YwncehlfIHX2CCBCp0opSmsv3ohw9ME9aTdl2q19j4SXV30UQFFbi/4Yn/FBWsShxRg==",
"dev": true,
"dependencies": {
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/types": "^7.23.0",
"@ndelangen/get-tarball": "^3.0.7",
- "@storybook/codemod": "7.6.0",
- "@storybook/core-common": "7.6.0",
- "@storybook/core-events": "7.6.0",
- "@storybook/core-server": "7.6.0",
- "@storybook/csf-tools": "7.6.0",
- "@storybook/node-logger": "7.6.0",
- "@storybook/telemetry": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/codemod": "7.6.1",
+ "@storybook/core-common": "7.6.1",
+ "@storybook/core-events": "7.6.1",
+ "@storybook/core-server": "7.6.1",
+ "@storybook/csf-tools": "7.6.1",
+ "@storybook/node-logger": "7.6.1",
+ "@storybook/telemetry": "7.6.1",
+ "@storybook/types": "7.6.1",
"@types/semver": "^7.3.4",
"@yarnpkg/fslib": "2.10.3",
"@yarnpkg/libzip": "2.3.0",
@@ -7990,9 +7991,9 @@
}
},
"node_modules/@storybook/client-logger": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.0.tgz",
- "integrity": "sha512-18XPPEWYHmmUav7i+PjZGwtImshNtay0xO2vh2DmQtzoCh2Lx/NVldqv9Li1eHCI88+4y7fyutmC5OIi0YASbg==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.1.tgz",
+ "integrity": "sha512-klaeDFGK2NAXoRDgp8+55Qn9mdVVNk16POb2/lbXYo8ydZDYaNjjLISO9+dLA65SL8NUEQw1m8YVhzyAQCE5bg==",
"dev": true,
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -8003,18 +8004,18 @@
}
},
"node_modules/@storybook/codemod": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.0.tgz",
- "integrity": "sha512-86/7AH5qg5uOE5e4ymnXjykfzA29eSFRQnTYJN0pbI/xlnFnPnh1mLQtinV03S2DtdcZKAm04UntfNgSFrSJNA==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.1.tgz",
+ "integrity": "sha512-V9zzrNVfJZ3y7Y0MIL3PySDmMuxKM207wE+XYectOCTTyz7pObylwpp3UugNRptj2vvinLfCx1SjKVQ6RpYarQ==",
"dev": true,
"dependencies": {
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/types": "^7.23.0",
"@storybook/csf": "^0.1.2",
- "@storybook/csf-tools": "7.6.0",
- "@storybook/node-logger": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/csf-tools": "7.6.1",
+ "@storybook/node-logger": "7.6.1",
+ "@storybook/types": "7.6.1",
"@types/cross-spawn": "^6.0.2",
"cross-spawn": "^7.0.3",
"globby": "^11.0.2",
@@ -8044,18 +8045,18 @@
}
},
"node_modules/@storybook/components": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.6.0.tgz",
- "integrity": "sha512-yV2krJOGYHldThfFShl5jC5EySUYVOWnhomwwT2b0J5e7odp04TCBycKmLxZhYmaFawnf5BNbDaIXvxcnY518A==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.6.1.tgz",
+ "integrity": "sha512-jZNGVepmn/iwZ/WSwchZfBo8r4knUpHLwrffMc8FSltmzyKxlEmtZwzlTbBCi48mBc0CqucI1GYtnhzzgX6N8w==",
"dev": true,
"dependencies": {
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-toolbar": "^1.0.4",
- "@storybook/client-logger": "7.6.0",
+ "@storybook/client-logger": "7.6.1",
"@storybook/csf": "^0.1.2",
"@storybook/global": "^5.0.0",
- "@storybook/theming": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/theming": "7.6.1",
+ "@storybook/types": "7.6.1",
"memoizerific": "^1.11.3",
"use-resize-observer": "^9.1.0",
"util-deprecate": "^1.0.2"
@@ -8070,14 +8071,14 @@
}
},
"node_modules/@storybook/core-common": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.0.tgz",
- "integrity": "sha512-Le11+Pcbi2D+i63utkhjHEAUIVO65CNiZiDFa/ZJI5aSajy209ece2eX0Z12wPecfYu5TXlqhqaeXAVBABAUow==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.1.tgz",
+ "integrity": "sha512-6K9UXUxO708T0H29Z2QDrLHU5nFL3VEWuS1vzbC/naJVt/yg+w1jzgknP7IFmmDf2e02tLQS886cE+M23249Tg==",
"dev": true,
"dependencies": {
- "@storybook/core-events": "7.6.0",
- "@storybook/node-logger": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/core-events": "7.6.1",
+ "@storybook/node-logger": "7.6.1",
+ "@storybook/types": "7.6.1",
"@types/find-cache-dir": "^3.2.1",
"@types/node": "^18.0.0",
"@types/node-fetch": "^2.6.4",
@@ -8567,9 +8568,9 @@
}
},
"node_modules/@storybook/core-events": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.0.tgz",
- "integrity": "sha512-13d4YOcXPu0j5PDjqE2iy+mG68w2TLit408cF/ZbJ8d6V4QwuUiz6mUt34vTuTc3yB93q5moYXYo6a/AhrsPnQ==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.1.tgz",
+ "integrity": "sha512-4B55//oGyvdiKQUCMRHLlA6v7HW69tf4mqjHdPbwETyj9PzFTpo4S8tsYxkba/azCxvqncotPDsMkxycritGbA==",
"dev": true,
"dependencies": {
"ts-dedent": "^2.0.0"
@@ -8580,26 +8581,26 @@
}
},
"node_modules/@storybook/core-server": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.0.tgz",
- "integrity": "sha512-9DmUDcMKbeZDaTENjRoV3cFvqLOKq64RNIh/eDffeRyzaj8PvY4E7MOd7XXx6pTSO7CmSpgcaZ4OYbmvu2xI/A==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.1.tgz",
+ "integrity": "sha512-WqiNpcyS8j+AgrJSqF4FLKvgXKVYpvk0kouNzLHyXmknjfqcGPySKpEP/MYD9tALdAnZhA80M5suuLFSAWlXxw==",
"dev": true,
"dependencies": {
"@aw-web-design/x-default-browser": "1.4.126",
"@discoveryjs/json-ext": "^0.5.3",
- "@storybook/builder-manager": "7.6.0",
- "@storybook/channels": "7.6.0",
- "@storybook/core-common": "7.6.0",
- "@storybook/core-events": "7.6.0",
+ "@storybook/builder-manager": "7.6.1",
+ "@storybook/channels": "7.6.1",
+ "@storybook/core-common": "7.6.1",
+ "@storybook/core-events": "7.6.1",
"@storybook/csf": "^0.1.2",
- "@storybook/csf-tools": "7.6.0",
+ "@storybook/csf-tools": "7.6.1",
"@storybook/docs-mdx": "^0.1.0",
"@storybook/global": "^5.0.0",
- "@storybook/manager": "7.6.0",
- "@storybook/node-logger": "7.6.0",
- "@storybook/preview-api": "7.6.0",
- "@storybook/telemetry": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/manager": "7.6.1",
+ "@storybook/node-logger": "7.6.1",
+ "@storybook/preview-api": "7.6.1",
+ "@storybook/telemetry": "7.6.1",
+ "@storybook/types": "7.6.1",
"@types/detect-port": "^1.3.0",
"@types/node": "^18.0.0",
"@types/pretty-hrtime": "^1.0.0",
@@ -8694,14 +8695,14 @@
}
},
"node_modules/@storybook/core-webpack": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-7.6.0.tgz",
- "integrity": "sha512-Qv0/jxdh8c4z75WO5T/guXzcXwIxRWToWKpLjixsJLhOYuwH8lHPVq/CWJ2gOCJ/2K24NtgS+0TudzlyvLWTDw==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-7.6.1.tgz",
+ "integrity": "sha512-IPxDICZAu5nAU+MHzmha5uaLirHrLpC6StNc2aeQ75YOk0MwdRj49F7AAqWegBlnvOy0wdfYf5jxQD7N/C//rA==",
"dev": true,
"dependencies": {
- "@storybook/core-common": "7.6.0",
- "@storybook/node-logger": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/core-common": "7.6.1",
+ "@storybook/node-logger": "7.6.1",
+ "@storybook/types": "7.6.1",
"@types/node": "^18.0.0",
"ts-dedent": "^2.0.0"
},
@@ -8729,12 +8730,12 @@
}
},
"node_modules/@storybook/csf-plugin": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-7.6.0.tgz",
- "integrity": "sha512-d/rcRcNad+tLGXV3GQiQdFJOBS4fj90fqa4joJIekgVh+LfRBS+KYuiPeukBxfnmz2AbhF9ezwXQMFIYYyHmzg==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-7.6.1.tgz",
+ "integrity": "sha512-wkUNl3nJGoUFyVk7lPFwStCqD4e51jqYFCxCSFifsLHtVKpNZPIR5AmiRTExmx4VL0xlmloTvOE9Mdkc7pWhiw==",
"dev": true,
"dependencies": {
- "@storybook/csf-tools": "7.6.0",
+ "@storybook/csf-tools": "7.6.1",
"unplugin": "^1.3.1"
},
"funding": {
@@ -8743,9 +8744,9 @@
}
},
"node_modules/@storybook/csf-tools": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.0.tgz",
- "integrity": "sha512-JhGJeLgnE96JfBBXM1DIPVR/JLQH2OTGH+yZ3ohiTPGWjf+aShB3jKUxTkBl7Fjq0xu57tnky7kNUO690vYypg==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.1.tgz",
+ "integrity": "sha512-+fkVma/Qbm5Lips9jwlN/wZybikYHJNGiAyjT1qjtfis9115XSUKXJtGhnAUtB6sbYtoFBNvBJ9QrFKa1lEZ/A==",
"dev": true,
"dependencies": {
"@babel/generator": "^7.23.0",
@@ -8753,7 +8754,7 @@
"@babel/traverse": "^7.23.2",
"@babel/types": "^7.23.0",
"@storybook/csf": "^0.1.2",
- "@storybook/types": "7.6.0",
+ "@storybook/types": "7.6.1",
"fs-extra": "^11.1.0",
"recast": "^0.23.1",
"ts-dedent": "^2.0.0"
@@ -8770,14 +8771,14 @@
"dev": true
},
"node_modules/@storybook/docs-tools": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-7.6.0.tgz",
- "integrity": "sha512-06M/Vo3AwOdr4VP1LbvnSih8eWT5zO6Mkm3ZZikMQVn+eDr5YJ9PzUeI2/SAymgCs4jH9qRf4lmKTPMl4bjGsQ==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-7.6.1.tgz",
+ "integrity": "sha512-F6mbKFWwqzCTM39xnsW5FH/wSxeQTZ/N9j1MM8J+x39QbWbLi2VodSuPjKyS1L7CPcYhc3N1Re/F3lIgpF2piw==",
"dev": true,
"dependencies": {
- "@storybook/core-common": "7.6.0",
- "@storybook/preview-api": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/core-common": "7.6.1",
+ "@storybook/preview-api": "7.6.1",
+ "@storybook/types": "7.6.1",
"@types/doctrine": "^0.0.3",
"assert": "^2.1.0",
"doctrine": "^3.0.0",
@@ -8795,9 +8796,9 @@
"dev": true
},
"node_modules/@storybook/manager": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.0.tgz",
- "integrity": "sha512-HJ1DCCf3GT+irAFCZg9WsPcGwSZlDyQiJHsaqxFVzuoPnz2lx10eHkXTnKa3t8x6hJeWK9BFHVyOXEFUV78ryg==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.1.tgz",
+ "integrity": "sha512-e2cYvMl01TZj+s++aqlCkeyvZoIFAUaRyG8d6rj2S1mIfzSl41NL2e76cdm5nIdrgkZFv4fMNaWpXyJp1p3wfg==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -8805,19 +8806,19 @@
}
},
"node_modules/@storybook/manager-api": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.6.0.tgz",
- "integrity": "sha512-P2ISRw8cmIDPrsMwDTOZvFOH6P9GN6O9wC2cSrfMWYE/aaXHWf/7f5gk5pX/zILHuLQeVnDBguS/zXmMDxJj7g==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.6.1.tgz",
+ "integrity": "sha512-Fo+sM5ExbZ3UWWVMd5A3+Mee9/YGtLAla3yActaemcWmR2r/THjoFtL4GU2qmUnUvTvSYLYir6s1+90cUyfSvA==",
"dev": true,
"dependencies": {
- "@storybook/channels": "7.6.0",
- "@storybook/client-logger": "7.6.0",
- "@storybook/core-events": "7.6.0",
+ "@storybook/channels": "7.6.1",
+ "@storybook/client-logger": "7.6.1",
+ "@storybook/core-events": "7.6.1",
"@storybook/csf": "^0.1.2",
"@storybook/global": "^5.0.0",
- "@storybook/router": "7.6.0",
- "@storybook/theming": "7.6.0",
- "@storybook/types": "7.6.0",
+ "@storybook/router": "7.6.1",
+ "@storybook/theming": "7.6.1",
+ "@storybook/types": "7.6.1",
"dequal": "^2.0.2",
"lodash": "^4.17.21",
"memoizerific": "^1.11.3",
@@ -8838,9 +8839,9 @@
"dev": true
},
"node_modules/@storybook/node-logger": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.0.tgz",
- "integrity": "sha512-Z+wVmjnTMhMG2ydL4T8F+gf/awvuAv3IAzH6T4D5UgjmdABqxVqWNAAF+Mgp48TUAGxiJCowzI6sGDg3iNJx2w==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.1.tgz",
+ "integrity": "sha512-aHQUfWJBJBBof4WfNN552I76QZmMwLHeerI+98NE3IphRHlM+OW96crKXKvX7DWaN5otgxdXroZFpEABKhHIwQ==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -8848,9 +8849,9 @@
}
},
"node_modules/@storybook/postinstall": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-7.6.0.tgz",
- "integrity": "sha512-saxxLh6dXpNYA1WQ5KnOfMsJ1U+WtxBLSgYv8DWYujbFirmafgzPKslxgCjP6OlV3erQgnoO/xBUK4YVTfsuag==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-7.6.1.tgz",
+ "integrity": "sha512-XYJPzJ4ZCxTn6r/mTXKCtLUBLo0yPiA4fnCPE+ltyYvhLZ0Hm7o0TP4ew7zvoXKuGJZUH4vuEBCt+AdqrSp6Qw==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -8858,9 +8859,9 @@
}
},
"node_modules/@storybook/preview": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/preview/-/preview-7.6.0.tgz",
- "integrity": "sha512-/zHTMl3aj1S3xxnffwaGzhMi1KySCKeln3xX15RBme014ZQ8cNYwnSDRAsiW/n3viDFFyZ6ybrtmw2HnpNBUhw==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/preview/-/preview-7.6.1.tgz",
+ "integrity": "sha512-5qC/skcs0gQenTfCNa2LLp7X9++ZuLQwoweNmAtRJ8Gxn9ac6Yd3/pf9xjxAhlLDhb4Ojdrb9owrPAOf9b4woA==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -8868,17 +8869,17 @@
}
},
"node_modules/@storybook/preview-api": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.0.tgz",
- "integrity": "sha512-//8mYKM8gkSDkIRcG3kSozGEvPUurVhfjBXDtaF8Y8cOZLzwe8/AZy+mUYHShh9HWFUXx5QAj5oU0U0PflfMeg==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.1.tgz",
+ "integrity": "sha512-lhuwkDHBZCq3UtQXRkGKTBntINkXPXyAA7fSYWrwfQBr/3NhaCoj859b+71Ax8sYcSFF4+U3S6+o8XE3dz+kwA==",
"dev": true,
"dependencies": {
- "@storybook/channels": "7.6.0",
- "@storybook/client-logger": "7.6.0",
- "@storybook/core-events": "7.6.0",
+ "@storybook/channels": "7.6.1",
+ "@storybook/client-logger": "7.6.1",
+ "@storybook/core-events": "7.6.1",
"@storybook/csf": "^0.1.2",
"@storybook/global": "^5.0.0",
- "@storybook/types": "7.6.0",
+ "@storybook/types": "7.6.1",
"@types/qs": "^6.9.5",
"dequal": "^2.0.2",
"lodash": "^4.17.21",
@@ -8894,9 +8895,9 @@
}
},
"node_modules/@storybook/react-dom-shim": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-7.6.0.tgz",
- "integrity": "sha512-DBsQ9OBwSjUEI2bvHcGqs+ucVy3UE8CjoWpD93kRcJZY913DCoNDrMSBWozhBHlcO65LhuBjrNm7oKdmwAKJsg==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-7.6.1.tgz",
+ "integrity": "sha512-COIVtG33Pfa8o8PR7VlQKjBOD5jG3dUPLuRBwvomy3+fkoS5JZNTnehZrgDDMkevpZr8eb0A+uCfLckicsg/ig==",
"dev": true,
"funding": {
"type": "opencollective",
@@ -8908,12 +8909,12 @@
}
},
"node_modules/@storybook/router": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.6.0.tgz",
- "integrity": "sha512-661mO2JtO/wdWJEtVqyaUjQ8tsy56LrsKqz4suzO0L32Z7NHCBu0IzbZbLON6MXje3PWXksw0vFbd8jwH/i//w==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.6.1.tgz",
+ "integrity": "sha512-zoIRDhes/4R9jsJ6c48xqmhcGYrSoPBzxEWnIaeSv0rEZFLhkTgruyAG0/rJ0FrCsSwFxVSI6ecYfCPVYMVr7w==",
"dev": true,
"dependencies": {
- "@storybook/client-logger": "7.6.0",
+ "@storybook/client-logger": "7.6.1",
"memoizerific": "^1.11.3",
"qs": "^6.10.0"
},
@@ -8923,14 +8924,14 @@
}
},
"node_modules/@storybook/telemetry": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.0.tgz",
- "integrity": "sha512-xUYiWiXicYX6oneDqHx5bq3zViTuckLoXi1QTzKN+WPO98vt4NBr532XeVNJG+x+UE8ERSKazT6CHkZ9XeqyMA==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.1.tgz",
+ "integrity": "sha512-xR2OUqoANSjHReu4RONXNAkoy9WPwZfk3IfajtqgMsYlwXY/yvLZqDkp/eZNhI6gps8IsP7gMx9E1WDqS3ghcA==",
"dev": true,
"dependencies": {
- "@storybook/client-logger": "7.6.0",
- "@storybook/core-common": "7.6.0",
- "@storybook/csf-tools": "7.6.0",
+ "@storybook/client-logger": "7.6.1",
+ "@storybook/core-common": "7.6.1",
+ "@storybook/csf-tools": "7.6.1",
"chalk": "^4.1.0",
"detect-package-manager": "^2.0.1",
"fetch-retry": "^5.0.2",
@@ -9006,13 +9007,13 @@
}
},
"node_modules/@storybook/theming": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.0.tgz",
- "integrity": "sha512-F5PTGkaRQ0TWIWRrZgQ2dmVxVcjX77vDc6QfUYxvOfez9/zrduKRHP5lGqHoqJlugJc8i2zpRNEFbL99frdUKg==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.1.tgz",
+ "integrity": "sha512-e6S5jRn90etnvlJKIJ4wlFauFn9tNQ2YJ/Uyo8yUUGrA665yc7zdBZPUwD6oHYXP6znEfXMJ2dbx/hs1I/V7Gw==",
"dev": true,
"dependencies": {
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
- "@storybook/client-logger": "7.6.0",
+ "@storybook/client-logger": "7.6.1",
"@storybook/global": "^5.0.0",
"memoizerific": "^1.11.3"
},
@@ -9026,12 +9027,12 @@
}
},
"node_modules/@storybook/types": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.0.tgz",
- "integrity": "sha512-mrbL9qrRekaPCAV3d7jYpege5wOpsvBvNW6pmATG3UvNXpqz5BOWe6RWZJXbtkvjyt01b6HE9CbVUFJppplr6w==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.1.tgz",
+ "integrity": "sha512-xwEdMHbms7nnyPugqrxjz62n1CoAZ6I+m6y7Rfk/2C0nsgpS0Go1UXUNmcWNxx2ZYX7bEgSPZtcYZa9buPSY3g==",
"dev": true,
"dependencies": {
- "@storybook/channels": "7.6.0",
+ "@storybook/channels": "7.6.1",
"@types/babel__core": "^7.0.0",
"@types/express": "^4.7.0",
"file-system-cache": "2.3.0"
@@ -15315,9 +15316,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.595",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.595.tgz",
- "integrity": "sha512-+ozvXuamBhDOKvMNUQvecxfbyICmIAwS4GpLmR0bsiSBlGnLaOcs2Cj7J8XSbW+YEaN3Xl3ffgpm+srTUWFwFQ=="
+ "version": "1.4.596",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.596.tgz",
+ "integrity": "sha512-zW3zbZ40Icb2BCWjm47nxwcFGYlIgdXkAx85XDO7cyky9J4QQfq8t0W19/TLZqq3JPQXtlv8BPIGmfa9Jb4scg=="
},
"node_modules/elkjs": {
"version": "0.8.2",
@@ -20133,6 +20134,20 @@
"integrity": "sha512-i/XBRTiLqRConPKioy2oq45vbv04e8x59b0mnsIRQM+7Ec/8BC7UcL5pnC4FMeGb8KwG7q4wOMw7CtNZf5tiIg==",
"dev": true
},
+ "node_modules/keycloak-angular": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/keycloak-angular/-/keycloak-angular-15.0.0.tgz",
+ "integrity": "sha512-GUiIKDspnH94YQ2jSL5zw+aXtpK80E78Znjn2en8d1Xhvs5kGy58qmemtuOVEafRBNeKUz/cPvD87Ha+EnPDRA==",
+ "dependencies": {
+ "tslib": "^2.3.1"
+ },
+ "peerDependencies": {
+ "@angular/common": "^17",
+ "@angular/core": "^17",
+ "@angular/router": "^17",
+ "keycloak-js": "^18 || ^19 || ^20 || ^21 || ^22"
+ }
+ },
"node_modules/keycloak-js": {
"version": "22.0.5",
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-22.0.5.tgz",
@@ -26687,12 +26702,12 @@
"dev": true
},
"node_modules/storybook": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.0.tgz",
- "integrity": "sha512-t844tajV8dcWiGmGV0zXUdmLzLnftTqQOfzX678AjJXh7ijhMkNi2dgSFLaAOLQqeljdGfyrgFrivveZxkaj2w==",
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.1.tgz",
+ "integrity": "sha512-JarHWZ7PLnGFPvapcSTdmneckGi1nCez18JozXsFXunDLZWUEn8kui0FLUhc579glZ8yDaSr9zsxWG8ZIFUm+A==",
"dev": true,
"dependencies": {
- "@storybook/cli": "7.6.0"
+ "@storybook/cli": "7.6.1"
},
"bin": {
"sb": "index.js",
diff --git a/package.json b/package.json
index 1444e52f95..f1414f1997 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,7 @@
"flag-icons": "^7.0.2",
"hammerjs": "^2.0.8",
"json-query": "^2.2.2",
+ "keycloak-angular": "^15.0.0",
"keycloak-js": "^22.0.5",
"leaflet": "^1.9.4",
"lodash-es": "^4.17.21",
diff --git a/src/app/app-initializers.ts b/src/app/app-initializers.ts
index 239ff53e89..dd9e2dffce 100644
--- a/src/app/app-initializers.ts
+++ b/src/app/app-initializers.ts
@@ -7,11 +7,12 @@ import { ConfigService } from "./core/config/config.service";
import { RouterService } from "./core/config/dynamic-routing/router.service";
import { EntityConfigService } from "./core/entity/entity-config.service";
import { Router } from "@angular/router";
-import { SessionService } from "./core/session/session-service/session.service";
import { AnalyticsService } from "./core/analytics/analytics.service";
import { LoginState } from "./core/session/session-states/login-state.enum";
import { LoggingService } from "./core/logging/logging.service";
import { environment } from "../environments/environment";
+import { LoginStateSubject } from "./core/session/session-type";
+import { CurrentUserSubject } from "./core/user/user";
export const appInitializers = {
provide: APP_INITIALIZER,
@@ -22,8 +23,9 @@ export const appInitializers = {
routerService: RouterService,
entityConfigService: EntityConfigService,
router: Router,
- sessionService: SessionService,
+ currentUser: CurrentUserSubject,
analyticsService: AnalyticsService,
+ loginState: LoginStateSubject,
) =>
async () => {
// Re-trigger services that depend on the config when something changes
@@ -35,9 +37,9 @@ export const appInitializers = {
});
// update the user context for remote error logging and tracking and load config initially
- sessionService.loginState.subscribe((newState) => {
+ loginState.subscribe((newState) => {
if (newState === LoginState.LOGGED_IN) {
- const username = sessionService.getCurrentUser().name;
+ const username = currentUser.value.name;
LoggingService.setLoggingContextUser(username);
analyticsService.setUser(username);
} else {
@@ -62,8 +64,9 @@ export const appInitializers = {
RouterService,
EntityConfigService,
Router,
- SessionService,
+ CurrentUserSubject,
AnalyticsService,
+ LoginStateSubject,
],
multi: true,
};
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index bf5af02f42..944413b690 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -63,7 +63,11 @@ import {
entityRegistry,
EntityRegistry,
} from "./core/entity/database-entity.decorator";
-import { LOCATION_TOKEN, WINDOW_TOKEN } from "./utils/di-tokens";
+import {
+ LOCATION_TOKEN,
+ NAVIGATOR_TOKEN,
+ WINDOW_TOKEN,
+} from "./utils/di-tokens";
import { AttendanceModule } from "./child-dev-project/attendance/attendance.module";
import { NotesModule } from "./child-dev-project/notes/notes.module";
import { SchoolsModule } from "./child-dev-project/schools/schools.module";
@@ -76,7 +80,6 @@ import { RouterModule } from "@angular/router";
import { TodosModule } from "./features/todos/todos.module";
import moment from "moment";
import { getLocaleFirstDayOfWeek } from "@angular/common";
-import { SessionService } from "./core/session/session-service/session.service";
import { waitForChangeTo } from "./core/session/session-states/session-utils";
import { LoginState } from "./core/session/session-states/login-state.enum";
import { appInitializers } from "./app-initializers";
@@ -87,6 +90,7 @@ import { BirthdayDashboardWidgetModule } from "./features/dashboard-widgets/birt
import { ConfigSetupModule } from "./features/config-setup/config-setup.module";
import { MarkdownPageModule } from "./features/markdown-page/markdown-page.module";
import { AdminModule } from "./features/admin/admin.module";
+import { LoginStateSubject } from "./core/session/session-type";
/**
* Main entry point of the application.
@@ -147,6 +151,7 @@ import { AdminModule } from "./features/admin/admin.module";
{ provide: EntityRegistry, useValue: entityRegistry },
{ provide: WINDOW_TOKEN, useValue: window },
{ provide: LOCATION_TOKEN, useValue: window.location },
+ { provide: NAVIGATOR_TOKEN, useValue: navigator },
{
provide: LOCALE_ID,
useValue:
@@ -161,12 +166,12 @@ import { AdminModule } from "./features/admin/admin.module";
},
{
provide: SwRegistrationOptions,
- useFactory: (session: SessionService) => ({
+ useFactory: (loginState: LoginStateSubject) => ({
enabled: environment.production,
registrationStrategy: () =>
- session.loginState.pipe(waitForChangeTo(LoginState.LOGGED_IN)),
+ loginState.pipe(waitForChangeTo(LoginState.LOGGED_IN)),
}),
- deps: [SessionService],
+ deps: [LoginStateSubject],
},
appInitializers,
],
diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
index f98f706ff0..61f1663b6c 100644
--- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
+++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
@@ -9,7 +9,6 @@ import { AttendanceService } from "../../attendance.service";
import { Note } from "../../../notes/model/note";
import { EntityMapperService } from "../../../../core/entity/entity-mapper/entity-mapper.service";
import { RecurringActivity } from "../../model/recurring-activity";
-import { SessionService } from "../../../../core/session/session-service/session.service";
import { NoteDetailsComponent } from "../../../notes/note-details/note-details.component";
import { FormDialogService } from "../../../../core/form-dialog/form-dialog.service";
import { AlertService } from "../../../../core/alerts/alert.service";
@@ -27,6 +26,7 @@ import { NgForOf, NgIf } from "@angular/common";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { ActivityCardComponent } from "../../activity-card/activity-card.component";
import { MatButtonModule } from "@angular/material/button";
+import { CurrentUserSubject } from "../../../../core/user/user";
@Component({
selector: "app-roll-call-setup",
@@ -76,7 +76,7 @@ export class RollCallSetupComponent implements OnInit {
constructor(
private entityMapper: EntityMapperService,
private attendanceService: AttendanceService,
- private sessionService: SessionService,
+ private currentUser: CurrentUserSubject,
private formDialog: FormDialogService,
private alertService: AlertService,
private filerService: FilterService,
@@ -105,7 +105,7 @@ export class RollCallSetupComponent implements OnInit {
this.visibleActivities = this.allActivities;
} else {
this.visibleActivities = this.allActivities.filter((a) =>
- a.isAssignedTo(this.sessionService.getCurrentUser().name),
+ a.isAssignedTo(this.currentUser.value.name),
);
if (this.visibleActivities.length === 0) {
this.visibleActivities = this.allActivities.filter(
@@ -155,7 +155,7 @@ export class RollCallSetupComponent implements OnInit {
activity,
this.date,
)) as NoteForActivitySetup;
- event.authors = [this.sessionService.getCurrentUser().name];
+ event.authors = [this.currentUser.value.name];
event.isNewFromActivity = true;
return event;
}
@@ -175,7 +175,7 @@ export class RollCallSetupComponent implements OnInit {
score += 1;
}
- if (assignedUsers.includes(this.sessionService.getCurrentUser().name)) {
+ if (assignedUsers.includes(this.currentUser.value.name)) {
score += 2;
}
@@ -189,7 +189,7 @@ export class RollCallSetupComponent implements OnInit {
createOneTimeEvent() {
const newNote = Note.create(new Date());
- newNote.authors = [this.sessionService.getCurrentUser().name];
+ newNote.authors = [this.currentUser.value.name];
this.formDialog
.openFormPopup(newNote, [], NoteDetailsComponent)
diff --git a/src/app/core/common-components/entity-form/entity-form.service.ts b/src/app/core/common-components/entity-form/entity-form.service.ts
index 8a0e448100..83750d3bca 100644
--- a/src/app/core/common-components/entity-form/entity-form.service.ts
+++ b/src/app/core/common-components/entity-form/entity-form.service.ts
@@ -12,12 +12,12 @@ import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes
import { ActivationStart, Router } from "@angular/router";
import { Subscription } from "rxjs";
import { filter } from "rxjs/operators";
-import { SessionService } from "../../session/session-service/session.service";
import {
EntitySchemaField,
PLACEHOLDERS,
} from "../../entity/schema/entity-schema-field";
import { isArrayDataType } from "../../basic-datatypes/datatype-utils";
+import { CurrentUserSubject } from "../../user/user";
/**
* These are utility types that allow to define the type of `FormGroup` the way it is returned by `EntityFormService.create`
@@ -40,7 +40,7 @@ export class EntityFormService {
private dynamicValidator: DynamicValidatorsService,
private ability: EntityAbility,
private unsavedChanges: UnsavedChangesService,
- private session: SessionService,
+ private currentUser: CurrentUserSubject,
router: Router,
) {
router.events
@@ -154,7 +154,7 @@ export class EntityFormService {
newVal = new Date();
break;
case PLACEHOLDERS.CURRENT_USER:
- newVal = this.session.getCurrentUser().name;
+ newVal = this.currentUser.value.name;
break;
default:
newVal = schema.defaultValue;
diff --git a/src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.ts b/src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.ts
index 37663f1a7e..6be9d11353 100644
--- a/src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.ts
+++ b/src/app/core/common-components/entity-subrecord/list-paginator/list-paginator.component.ts
@@ -1,10 +1,10 @@
import {
Component,
- ViewChild,
Input,
OnChanges,
- SimpleChanges,
OnInit,
+ SimpleChanges,
+ ViewChild,
} from "@angular/core";
import {
MatPaginator,
@@ -12,8 +12,7 @@ import {
PageEvent,
} from "@angular/material/paginator";
import { MatTableDataSource } from "@angular/material/table";
-import { User } from "../../../user/user";
-import { SessionService } from "../../../session/session-service/session.service";
+import { CurrentUserSubject, User } from "../../../user/user";
import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";
@Component({
@@ -35,7 +34,7 @@ export class ListPaginatorComponent implements OnChanges, OnInit {
pageSize = 10;
constructor(
- private sessionService: SessionService,
+ private currentUser: CurrentUserSubject,
private entityMapperService: EntityMapperService,
) {}
@@ -83,7 +82,7 @@ export class ListPaginatorComponent implements OnChanges, OnInit {
private async ensureUserIsLoaded(): Promise {
if (!this.user) {
- const currentUser = this.sessionService.getCurrentUser();
+ const currentUser = this.currentUser.value;
this.user = await this.entityMapperService
.load(User, currentUser.name)
.catch(() => undefined);
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index a87e0fc30c..56aea073e2 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -1,7 +1,7 @@
import { NgModule } from "@angular/core";
import { ComponentRegistry } from "../dynamic-components";
import { coreComponents } from "./core-components";
-import { User } from "./user/user";
+import { CurrentUserSubject, User } from "./user/user";
import { Config } from "./config/config";
import { StringDatatype } from "./basic-datatypes/string/string.datatype";
import { DefaultDatatype } from "./entity/default-datatype/default.datatype";
@@ -25,6 +25,7 @@ import { CommonModule } from "@angular/common";
*/
@NgModule({
providers: [
+ CurrentUserSubject,
// base dataTypes
{ provide: DefaultDatatype, useClass: StringDatatype, multi: true },
{ provide: DefaultDatatype, useClass: BooleanDatatype, multi: true },
diff --git a/src/app/core/database/sync.service.spec.ts b/src/app/core/database/sync.service.spec.ts
new file mode 100644
index 0000000000..5103521358
--- /dev/null
+++ b/src/app/core/database/sync.service.spec.ts
@@ -0,0 +1,116 @@
+import { fakeAsync, TestBed, tick } from "@angular/core/testing";
+
+import { SyncService } from "./sync.service";
+import { PouchDatabase } from "./pouch-database";
+import { Database } from "./database";
+import { LoginStateSubject, SyncStateSubject } from "../session/session-type";
+import { LoginState } from "../session/session-states/login-state.enum";
+import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
+import { HttpStatusCode } from "@angular/common/http";
+import PouchDB from "pouchdb-browser";
+
+describe("SyncService", () => {
+ let service: SyncService;
+ let loginState: LoginStateSubject;
+ let mockAuthService: jasmine.SpyObj;
+
+ beforeEach(() => {
+ mockAuthService = jasmine.createSpyObj(["login", "addAuthHeader"]);
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: KeycloakAuthService, useValue: mockAuthService },
+ { provide: Database, useClass: PouchDatabase },
+ LoginStateSubject,
+ SyncStateSubject,
+ ],
+ });
+ service = TestBed.inject(SyncService);
+ loginState = TestBed.inject(LoginStateSubject);
+ });
+
+ it("should be created", () => {
+ expect(service).toBeTruthy();
+ });
+
+ it("should restart the sync if it fails at one point", fakeAsync(() => {
+ let errorCallback, pauseCallback;
+ const syncHandle = {
+ on: (action, callback) => {
+ if (action === "error") {
+ errorCallback = callback;
+ }
+ if (action === "paused") {
+ pauseCallback = callback;
+ }
+ return syncHandle;
+ },
+ cancel: () => undefined,
+ };
+ const syncSpy = jasmine
+ .createSpy()
+ .and.returnValues(Promise.resolve("first"), syncHandle, syncHandle);
+ spyOn(
+ TestBed.inject(Database) as PouchDatabase,
+ "getPouchDB",
+ ).and.returnValue({ sync: syncSpy } as any);
+
+ service.startSync();
+ tick(1000);
+
+ // error + logged in -> sync should restart
+ loginState.next(LoginState.LOGGED_IN);
+ syncSpy.calls.reset();
+ errorCallback();
+ expect(syncSpy).toHaveBeenCalled();
+
+ // pause -> no restart required
+ syncSpy.calls.reset();
+ pauseCallback();
+ expect(syncSpy).not.toHaveBeenCalled();
+
+ // logout + error -> no restart
+ syncSpy.calls.reset();
+ loginState.next(LoginState.LOGGED_OUT);
+ tick();
+ errorCallback();
+ expect(syncSpy).not.toHaveBeenCalled();
+ }));
+
+ it("should try auto-login if fetch fails and fetch again", async () => {
+ // Make sync call pass
+ spyOn(
+ TestBed.inject(Database) as PouchDatabase,
+ "getPouchDB",
+ ).and.returnValues({ sync: () => Promise.resolve() } as any);
+ spyOn(PouchDB, "fetch").and.returnValues(
+ Promise.resolve({
+ status: HttpStatusCode.Unauthorized,
+ ok: false,
+ } as Response),
+ Promise.resolve({ status: HttpStatusCode.Ok, ok: true } as Response),
+ );
+ // providing "valid" token on second call
+ let calls = 0;
+ mockAuthService.addAuthHeader.and.callFake((headers) => {
+ headers.Authorization = calls++ === 1 ? "valid" : "invalid";
+ });
+ mockAuthService.login.and.resolveTo();
+ const initSpy = spyOn(service["remoteDatabase"], "initRemoteDB");
+ await service.startSync();
+ // taking fetch function from init call
+ const fetch = initSpy.calls.mostRecent().args[1];
+
+ const url = "/db/_changes";
+ const opts = { headers: {} };
+ await expectAsync(fetch(url, opts)).toBeResolved();
+
+ expect(PouchDB.fetch).toHaveBeenCalledTimes(2);
+ expect(PouchDB.fetch).toHaveBeenCalledWith(url, opts);
+ expect(opts.headers).toEqual({ Authorization: "valid" });
+ expect(mockAuthService.login).toHaveBeenCalled();
+ expect(mockAuthService.addAuthHeader).toHaveBeenCalledTimes(2);
+
+ // prevent live sync call
+ service["cancelLiveSync"]();
+ });
+});
diff --git a/src/app/core/database/sync.service.ts b/src/app/core/database/sync.service.ts
new file mode 100644
index 0000000000..9277281ec5
--- /dev/null
+++ b/src/app/core/database/sync.service.ts
@@ -0,0 +1,182 @@
+import { Injectable } from "@angular/core";
+import { Database } from "./database";
+import { PouchDatabase } from "./pouch-database";
+import { LoggingService } from "../logging/logging.service";
+import { AppSettings } from "../app-settings";
+import { HttpStatusCode } from "@angular/common/http";
+import PouchDB from "pouchdb-browser";
+import { SyncState } from "../session/session-states/sync-state.enum";
+import { LoginStateSubject, SyncStateSubject } from "../session/session-type";
+import { LoginState } from "../session/session-states/login-state.enum";
+import { filter } from "rxjs/operators";
+import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
+
+/**
+ * This service initializes the remote DB and manages the sync between the local and remote DB.
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class SyncService {
+ static readonly LAST_SYNC_KEY = "LAST_SYNC";
+ private readonly POUCHDB_SYNC_BATCH_SIZE = 500;
+ private _liveSyncHandle: any;
+ private _liveSyncScheduledHandle: any;
+ private remoteDatabase = new PouchDatabase(this.loggingService);
+ private remoteDB: PouchDB.Database;
+ private localDB: PouchDB.Database;
+
+ constructor(
+ private database: Database,
+ private loggingService: LoggingService,
+ private authService: KeycloakAuthService,
+ private syncStateSubject: SyncStateSubject,
+ private loginStateSubject: LoginStateSubject,
+ ) {
+ this.syncStateSubject
+ .pipe(filter((state) => state === SyncState.COMPLETED))
+ .subscribe(() =>
+ localStorage.setItem(
+ SyncService.LAST_SYNC_KEY,
+ new Date().toISOString(),
+ ),
+ );
+ }
+
+ /**
+ * Initializes the remote DB and starts the sync
+ */
+ startSync() {
+ this.initDatabases();
+ this.sync()
+ .catch((err) => this.loggingService.error(`Sync failed: ${err}`))
+ // Call live sync even when initial sync fails
+ .finally(() => this.liveSyncDeferred());
+ }
+
+ /**
+ * Create the remote DB and configure it to use correct cookies.
+ * @private
+ */
+ private initDatabases() {
+ this.remoteDatabase.initRemoteDB(
+ `${AppSettings.DB_PROXY_PREFIX}/${AppSettings.DB_NAME}`,
+ (url, opts: any) => {
+ if (typeof url === "string") {
+ const remoteUrl =
+ AppSettings.DB_PROXY_PREFIX +
+ url.split(AppSettings.DB_PROXY_PREFIX)[1];
+ return this.sendRequest(remoteUrl, opts).then((initialRes) =>
+ // retry login if request failed with unauthorized
+ initialRes.status === HttpStatusCode.Unauthorized
+ ? this.authService
+ .login()
+ .then(() => this.sendRequest(remoteUrl, opts))
+ // return initial response if request failed again
+ .then((newRes) => (newRes.ok ? newRes : initialRes))
+ .catch(() => initialRes)
+ : initialRes,
+ );
+ }
+ },
+ );
+ this.remoteDB = this.remoteDatabase.getPouchDB();
+ if (this.database instanceof PouchDatabase) {
+ this.localDB = this.database.getPouchDB();
+ }
+ }
+
+ private sendRequest(url: string, opts) {
+ this.authService.addAuthHeader(opts.headers);
+ return PouchDB.fetch(url, opts);
+ }
+
+ private sync(): Promise {
+ this.syncStateSubject.next(SyncState.STARTED);
+ return this.localDB
+ .sync(this.remoteDB, {
+ batch_size: this.POUCHDB_SYNC_BATCH_SIZE,
+ })
+ .then(() => {
+ this.syncStateSubject.next(SyncState.COMPLETED);
+ })
+ .catch((err) => {
+ this.syncStateSubject.next(SyncState.FAILED);
+ throw err;
+ });
+ }
+
+ /**
+ * Schedules liveSync to be started.
+ * This method should be used to start the liveSync after the initial non-live sync,
+ * so the browser makes a round trip to the UI and hides the potentially visible first-sync dialog.
+ * @param timeout ms to wait before starting the liveSync
+ */
+ private liveSyncDeferred(timeout = 1000) {
+ this._liveSyncScheduledHandle = setTimeout(() => this.liveSync(), timeout);
+ }
+
+ /**
+ * Start live sync in background.
+ */
+ private liveSync() {
+ this.cancelLiveSync(); // cancel any liveSync that may have been alive before
+ this.syncStateSubject.next(SyncState.STARTED);
+ this._liveSyncHandle = this.localDB.sync(this.remoteDB, {
+ live: true,
+ retry: true,
+ });
+ this._liveSyncHandle
+ .on("paused", () => {
+ // replication was paused: either because sync is finished or because of a failed sync (mostly due to lost connection). info is empty.
+ if (this.isLoggedIn()) {
+ this.syncStateSubject.next(SyncState.COMPLETED);
+ // We might end up here after a failed sync that is not due to offline errors.
+ // It shouldn't happen too often, as we have an initial non-live sync to catch those situations, but we can't find that out here
+ }
+ })
+ .on("active", () => {
+ // replication was resumed: either because new things to sync or because connection is available again. info contains the direction
+ this.syncStateSubject.next(SyncState.STARTED);
+ })
+ .on("error", this.handleFailedSync())
+ .on("complete", (info) => {
+ this.loggingService.info(
+ `Live sync completed: ${JSON.stringify(info)}`,
+ );
+ this.syncStateSubject.next(SyncState.COMPLETED);
+ });
+ }
+
+ private handleFailedSync() {
+ return (info) => {
+ if (this.isLoggedIn()) {
+ this.syncStateSubject.next(SyncState.FAILED);
+ const lastAuth = localStorage.getItem(
+ KeycloakAuthService.LAST_AUTH_KEY,
+ );
+ this.loggingService.warn(
+ `Live sync failed (last auth ${lastAuth}): ${JSON.stringify(info)}`,
+ );
+ this.liveSync();
+ }
+ };
+ }
+
+ private isLoggedIn(): boolean {
+ return this.loginStateSubject.value === LoginState.LOGGED_IN;
+ }
+
+ /**
+ * Cancels a currently running liveSync or a liveSync scheduled to start in the future.
+ */
+ private cancelLiveSync() {
+ if (this._liveSyncScheduledHandle) {
+ clearTimeout(this._liveSyncScheduledHandle);
+ }
+ if (this._liveSyncHandle) {
+ this._liveSyncHandle.cancel();
+ }
+ this.syncStateSubject.next(SyncState.UNSYNCED);
+ }
+}
diff --git a/src/app/core/demo-data/demo-data-initializer.service.spec.ts b/src/app/core/demo-data/demo-data-initializer.service.spec.ts
index 2c3a0d0666..42e104d6b0 100644
--- a/src/app/core/demo-data/demo-data-initializer.service.spec.ts
+++ b/src/app/core/demo-data/demo-data-initializer.service.spec.ts
@@ -3,24 +3,33 @@ import { fakeAsync, TestBed, tick } from "@angular/core/testing";
import { DemoDataInitializerService } from "./demo-data-initializer.service";
import { DemoDataService } from "./demo-data.service";
import { DemoUserGeneratorService } from "../user/demo-user-generator.service";
-import { LocalSession } from "../session/session-service/local-session";
import { MatDialog } from "@angular/material/dialog";
import { DemoDataGeneratingProgressDialogComponent } from "./demo-data-generating-progress-dialog.component";
-import { AppSettings } from "../app-settings";
+import { LoginStateSubject, SessionType } from "../session/session-type";
+import { environment } from "../../../environments/environment";
+import { AuthUser } from "../session/auth/auth-user";
+import { LocalAuthService } from "../session/auth/local/local-auth.service";
+import { SessionManagerService } from "../session/session-service/session-manager.service";
import { PouchDatabase } from "../database/pouch-database";
-import { Subject } from "rxjs";
-import { LoginState } from "../session/session-states/login-state.enum";
+import { AppSettings } from "../app-settings";
import { Database } from "../database/database";
-import { SessionType } from "../session/session-type";
-import { environment } from "../../../environments/environment";
-import { AuthUser } from "../session/session-service/auth-user";
+import { CurrentUserSubject } from "../user/user";
+import { LoginState } from "../session/session-states/login-state.enum";
describe("DemoDataInitializerService", () => {
+ const normalUser: AuthUser = {
+ name: DemoUserGeneratorService.DEFAULT_USERNAME,
+ roles: ["user_app"],
+ };
+ const adminUser: AuthUser = {
+ name: DemoUserGeneratorService.ADMIN_USERNAME,
+ roles: ["user_app", "admin_app", "account_manager"],
+ };
let service: DemoDataInitializerService;
let mockDemoDataService: jasmine.SpyObj;
- let mockSessionService: jasmine.SpyObj;
+ let mockLocalAuth: jasmine.SpyObj;
+ let sessionManager: jasmine.SpyObj;
let mockDialog: jasmine.SpyObj;
- let loginState: Subject;
let demoUserDBName: string;
let adminDBName: string;
@@ -32,26 +41,26 @@ describe("DemoDataInitializerService", () => {
mockDemoDataService.publishDemoData.and.resolveTo();
mockDialog = jasmine.createSpyObj(["open"]);
mockDialog.open.and.returnValue({ close: () => {} } as any);
- loginState = new Subject();
- mockSessionService = jasmine.createSpyObj(
- ["login", "saveUser", "getCurrentUser"],
- { loginState: loginState },
- );
+ mockLocalAuth = jasmine.createSpyObj(["saveUser"]);
+ sessionManager = jasmine.createSpyObj(["offlineLogin"]);
TestBed.configureTestingModule({
providers: [
DemoDataInitializerService,
+ LoginStateSubject,
+ CurrentUserSubject,
{ provide: MatDialog, useValue: mockDialog },
{ provide: Database, useClass: PouchDatabase },
{ provide: DemoDataService, useValue: mockDemoDataService },
- { provide: LocalSession, useValue: mockSessionService },
+ { provide: LocalAuthService, useValue: mockLocalAuth },
+ { provide: SessionManagerService, useValue: sessionManager },
],
});
service = TestBed.inject(DemoDataInitializerService);
});
afterEach(async () => {
- loginState.complete();
+ localStorage.clear();
const tmpDB = new PouchDatabase(undefined);
await tmpDB.initInMemoryDB(demoUserDBName).destroy();
await tmpDB.initInMemoryDB(adminDBName).destroy();
@@ -64,33 +73,14 @@ describe("DemoDataInitializerService", () => {
it("should save the default users", () => {
service.run();
- const normalUser: AuthUser = {
- name: DemoUserGeneratorService.DEFAULT_USERNAME,
- roles: ["user_app"],
- };
- const adminUser: AuthUser = {
- name: DemoUserGeneratorService.ADMIN_USERNAME,
- roles: ["user_app", "admin_app", "account_manager"],
- };
-
- expect(mockSessionService.saveUser).toHaveBeenCalledWith(
- normalUser,
- DemoUserGeneratorService.DEFAULT_PASSWORD,
- );
- expect(mockSessionService.saveUser).toHaveBeenCalledWith(
- adminUser,
- DemoUserGeneratorService.DEFAULT_PASSWORD,
- );
+ expect(mockLocalAuth.saveUser).toHaveBeenCalledWith(normalUser);
+ expect(mockLocalAuth.saveUser).toHaveBeenCalledWith(adminUser);
});
it("it should publish the demo data after logging in the default user", fakeAsync(() => {
service.run();
- expect(mockSessionService.login).toHaveBeenCalled();
- expect(mockSessionService.login).toHaveBeenCalledWith(
- DemoUserGeneratorService.DEFAULT_USERNAME,
- DemoUserGeneratorService.DEFAULT_PASSWORD,
- );
+ expect(sessionManager.offlineLogin).toHaveBeenCalledWith(normalUser);
expect(mockDemoDataService.publishDemoData).not.toHaveBeenCalled();
tick();
@@ -123,12 +113,12 @@ describe("DemoDataInitializerService", () => {
database.put(userDoc);
tick();
- mockSessionService.getCurrentUser.and.returnValue({
+ TestBed.inject(CurrentUserSubject).next({
name: DemoUserGeneratorService.ADMIN_USERNAME,
roles: [],
});
database.initInMemoryDB(adminDBName);
- loginState.next(LoginState.LOGGED_IN);
+ TestBed.inject(LoginStateSubject).next(LoginState.LOGGED_IN);
tick();
expectAsync(database.get(userDoc._id)).toBeResolved();
@@ -154,12 +144,12 @@ describe("DemoDataInitializerService", () => {
tick();
const database = TestBed.inject(Database) as PouchDatabase;
- mockSessionService.getCurrentUser.and.returnValue({
+ TestBed.inject(CurrentUserSubject).next({
name: DemoUserGeneratorService.ADMIN_USERNAME,
roles: [],
});
database.initInMemoryDB(adminDBName);
- loginState.next(LoginState.LOGGED_IN);
+ TestBed.inject(LoginStateSubject).next(LoginState.LOGGED_IN);
const adminUserDB = database.getPouchDB();
tick();
@@ -167,9 +157,9 @@ describe("DemoDataInitializerService", () => {
adminUserDB.put(syncedDoc);
tick();
- loginState.next(LoginState.LOGGED_OUT);
+ TestBed.inject(LoginStateSubject).next(LoginState.LOGGED_OUT);
- const unsyncedDoc = { _id: "unsncedDoc" };
+ const unsyncedDoc = { _id: "unsyncedDoc" };
adminUserDB.put(unsyncedDoc);
tick();
diff --git a/src/app/core/demo-data/demo-data-initializer.service.ts b/src/app/core/demo-data/demo-data-initializer.service.ts
index 2ea9a66c9c..e2beebc235 100644
--- a/src/app/core/demo-data/demo-data-initializer.service.ts
+++ b/src/app/core/demo-data/demo-data-initializer.service.ts
@@ -1,19 +1,22 @@
import { Injectable } from "@angular/core";
import { DemoDataService } from "./demo-data.service";
import { DemoUserGeneratorService } from "../user/demo-user-generator.service";
-import { LocalSession } from "../session/session-service/local-session";
import { MatDialog } from "@angular/material/dialog";
import { DemoDataGeneratingProgressDialogComponent } from "./demo-data-generating-progress-dialog.component";
+import { SessionManagerService } from "../session/session-service/session-manager.service";
+import { LocalAuthService } from "../session/auth/local/local-auth.service";
+import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
+import { AuthUser } from "../session/auth/auth-user";
import { LoggingService } from "../logging/logging.service";
-import { AppSettings } from "../app-settings";
-import { LoginState } from "../session/session-states/login-state.enum";
-import PouchDB from "pouchdb-browser";
-import { SessionType } from "../session/session-type";
-import memory from "pouchdb-adapter-memory";
import { Database } from "../database/database";
import { PouchDatabase } from "../database/pouch-database";
import { environment } from "../../../environments/environment";
-import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
+import { LoginState } from "../session/session-states/login-state.enum";
+import { AppSettings } from "../app-settings";
+import { LoginStateSubject, SessionType } from "../session/session-type";
+import memory from "pouchdb-adapter-memory";
+import { CurrentUserSubject } from "../user/user";
+import PouchDB from "pouchdb-browser";
/**
* This service handles everything related to the demo-mode
@@ -26,20 +29,29 @@ import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.serv
export class DemoDataInitializerService {
private liveSyncHandle: PouchDB.Replication.Sync;
private pouchDatabase: PouchDatabase;
-
+ private readonly normalUser: AuthUser = {
+ name: DemoUserGeneratorService.DEFAULT_USERNAME,
+ roles: ["user_app"],
+ };
+ private readonly adminUser: AuthUser = {
+ name: DemoUserGeneratorService.ADMIN_USERNAME,
+ roles: ["user_app", "admin_app", KeycloakAuthService.ACCOUNT_MANAGER_ROLE],
+ };
constructor(
private demoDataService: DemoDataService,
- private localSession: LocalSession,
+ private localAuthService: LocalAuthService,
+ private sessionManager: SessionManagerService,
private dialog: MatDialog,
private loggingService: LoggingService,
private database: Database,
+ private loginState: LoginStateSubject,
+ private currentUser: CurrentUserSubject,
) {}
async run() {
const dialogRef = this.dialog.open(
DemoDataGeneratingProgressDialogComponent,
);
-
if (this.database instanceof PouchDatabase) {
this.pouchDatabase = this.database;
} else {
@@ -47,25 +59,21 @@ export class DemoDataInitializerService {
"Cannot create demo data with session: " + environment.session_type,
);
}
- this.registerDemoUsers();
-
- await this.localSession.login(
- DemoUserGeneratorService.DEFAULT_USERNAME,
- DemoUserGeneratorService.DEFAULT_PASSWORD,
- );
+ this.localAuthService.saveUser(this.normalUser);
+ this.localAuthService.saveUser(this.adminUser);
+ await this.sessionManager.offlineLogin(this.normalUser);
await this.demoDataService.publishDemoData();
dialogRef.close();
-
this.syncDatabaseOnUserChange();
}
private syncDatabaseOnUserChange() {
- this.localSession.loginState.subscribe((state) => {
+ this.loginState.subscribe((state) => {
if (
state === LoginState.LOGGED_IN &&
- this.localSession.getCurrentUser().name !==
+ this.currentUser.value.name !==
DemoUserGeneratorService.DEFAULT_USERNAME
) {
// There is a slight race-condition with session type local
@@ -78,27 +86,6 @@ export class DemoDataInitializerService {
});
}
- private registerDemoUsers() {
- this.localSession.saveUser(
- {
- name: DemoUserGeneratorService.DEFAULT_USERNAME,
- roles: ["user_app"],
- },
- DemoUserGeneratorService.DEFAULT_PASSWORD,
- );
- this.localSession.saveUser(
- {
- name: DemoUserGeneratorService.ADMIN_USERNAME,
- roles: [
- "user_app",
- "admin_app",
- KeycloakAuthService.ACCOUNT_MANAGER_ROLE,
- ],
- },
- DemoUserGeneratorService.DEFAULT_PASSWORD,
- );
- }
-
private async syncWithDemoUserDB() {
const dbName = `${DemoUserGeneratorService.DEFAULT_USERNAME}-${AppSettings.DB_NAME}`;
let demoUserDB: PouchDB.Database;
diff --git a/src/app/core/demo-data/demo-data.module.spec.ts b/src/app/core/demo-data/demo-data.module.spec.ts
index 2b494ca3c1..24e1efe786 100644
--- a/src/app/core/demo-data/demo-data.module.spec.ts
+++ b/src/app/core/demo-data/demo-data.module.spec.ts
@@ -1,33 +1,32 @@
import { fakeAsync, flush, TestBed, tick } from "@angular/core/testing";
import { DemoDataModule } from "./demo-data.module";
import { EntityMapperService } from "../entity/entity-mapper/entity-mapper.service";
-import { PouchDatabase } from "../database/pouch-database";
-import { LocalSession } from "../session/session-service/local-session";
+import { MockedTestingModule } from "../../utils/mocked-testing.module";
import { Database } from "../database/database";
+import { PouchDatabase } from "../database/pouch-database";
describe("DemoDataModule", () => {
- let mockEntityMapper: jasmine.SpyObj;
-
beforeEach(() => {
- mockEntityMapper = jasmine.createSpyObj(["saveAll"]);
return TestBed.configureTestingModule({
- imports: [DemoDataModule],
+ imports: [DemoDataModule, MockedTestingModule.withState()],
providers: [
- { provide: EntityMapperService, useValue: mockEntityMapper },
- PouchDatabase,
- { provide: Database, useExisting: PouchDatabase },
- LocalSession,
+ {
+ provide: Database,
+ useClass: PouchDatabase,
+ },
],
}).compileComponents();
});
it("should generate the demo data once the module is loaded", fakeAsync(() => {
+ const saveAllSpy = spyOn(TestBed.inject(EntityMapperService), "saveAll");
+
TestBed.inject(DemoDataModule).publishDemoData();
- expect(mockEntityMapper.saveAll).not.toHaveBeenCalled();
+ expect(saveAllSpy).not.toHaveBeenCalled();
tick();
- expect(mockEntityMapper.saveAll).toHaveBeenCalled();
+ expect(saveAllSpy).toHaveBeenCalled();
flush();
}));
});
diff --git a/src/app/core/entity-list/duplicate-records/duplicate-records.service.spec.ts b/src/app/core/entity-list/duplicate-records/duplicate-records.service.spec.ts
index e93eb156f0..2eaa6d531f 100644
--- a/src/app/core/entity-list/duplicate-records/duplicate-records.service.spec.ts
+++ b/src/app/core/entity-list/duplicate-records/duplicate-records.service.spec.ts
@@ -7,7 +7,6 @@ import {
EntityRegistry,
} from "../../entity/database-entity.decorator";
import { Database } from "../../database/database";
-import { SessionService } from "../../session/session-service/session.service";
import { Entity } from "../../entity/model/entity";
import { DatabaseField } from "../../entity/database-field.decorator";
import { CoreModule } from "../../core.module";
@@ -38,7 +37,6 @@ describe("DuplicateRecordsService", () => {
DuplicateRecordService,
Database,
EntityMapperService,
- SessionService,
{ provide: EntityRegistry, useValue: entityRegistry },
{ provide: MatDialog, useValue: {} },
{ provide: MatSnackBar, useValue: {} },
diff --git a/src/app/core/entity/entity-mapper/entity-mapper.service.spec.ts b/src/app/core/entity/entity-mapper/entity-mapper.service.spec.ts
index f8489be4c6..510adb38d7 100644
--- a/src/app/core/entity/entity-mapper/entity-mapper.service.spec.ts
+++ b/src/app/core/entity/entity-mapper/entity-mapper.service.spec.ts
@@ -21,16 +21,14 @@ import { TestBed, waitForAsync } from "@angular/core/testing";
import { PouchDatabase } from "../../database/pouch-database";
import { DatabaseEntity } from "../database-entity.decorator";
import { Child } from "../../../child-dev-project/children/model/child";
-import { SessionService } from "../../session/session-service/session.service";
import { Database } from "../../database/database";
import { TEST_USER } from "../../../utils/mock-local-session";
-
+import { CurrentUserSubject } from "../../user/user";
import { CoreTestingModule } from "../../../utils/core-testing.module";
describe("EntityMapperService", () => {
let entityMapper: EntityMapperService;
let testDatabase: PouchDatabase;
- let mockSessionService: jasmine.SpyObj;
const existingEntity = {
_id: "Entity:existing-entity",
@@ -46,13 +44,12 @@ describe("EntityMapperService", () => {
beforeEach(waitForAsync(() => {
testDatabase = PouchDatabase.create();
- mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
TestBed.configureTestingModule({
imports: [CoreTestingModule],
providers: [
{ provide: Database, useValue: testDatabase },
- { provide: SessionService, useValue: mockSessionService },
+ CurrentUserSubject,
EntityMapperService,
],
});
@@ -286,7 +283,7 @@ describe("EntityMapperService", () => {
it("sets the entityCreated property on save if it is a new entity & entityUpdated on subsequent saves", async () => {
jasmine.clock().install();
- mockSessionService.getCurrentUser.and.returnValue({
+ TestBed.inject(CurrentUserSubject).next({
name: TEST_USER,
roles: [],
});
diff --git a/src/app/core/entity/entity-mapper/entity-mapper.service.ts b/src/app/core/entity/entity-mapper/entity-mapper.service.ts
index 29c0e54102..2216d6dc6d 100644
--- a/src/app/core/entity/entity-mapper/entity-mapper.service.ts
+++ b/src/app/core/entity/entity-mapper/entity-mapper.service.ts
@@ -24,7 +24,7 @@ import { UpdatedEntity } from "../model/entity-update";
import { EntityRegistry } from "../database-entity.decorator";
import { map } from "rxjs/operators";
import { UpdateMetadata } from "../model/update-metadata";
-import { SessionService } from "../../session/session-service/session.service";
+import { CurrentUserSubject } from "../../user/user";
/**
* Handles loading and saving of data for any higher-level feature module.
@@ -41,7 +41,7 @@ export class EntityMapperService {
constructor(
private _db: Database,
private entitySchemaService: EntitySchemaService,
- private sessionService: SessionService,
+ private currentUser: CurrentUserSubject,
private registry: EntityRegistry,
) {}
@@ -195,9 +195,7 @@ export class EntityMapperService {
}
protected setEntityMetadata(entity: Entity) {
- const newMetadata = new UpdateMetadata(
- this.sessionService.getCurrentUser()?.name,
- );
+ const newMetadata = new UpdateMetadata(this.currentUser.value?.name);
if (entity.isNew) {
entity.created = newMetadata;
}
diff --git a/src/app/core/language/accept-language.interceptor.spec.ts b/src/app/core/language/accept-language.interceptor.spec.ts
new file mode 100644
index 0000000000..b5bd871bbe
--- /dev/null
+++ b/src/app/core/language/accept-language.interceptor.spec.ts
@@ -0,0 +1,32 @@
+import { TestBed } from "@angular/core/testing";
+
+import { AcceptLanguageInterceptor } from "./accept-language.interceptor";
+import { LOCALE_ID } from "@angular/core";
+import { HttpRequest } from "@angular/common/http";
+
+describe("AcceptLanguageInterceptor", () => {
+ beforeEach(() =>
+ TestBed.configureTestingModule({
+ providers: [
+ AcceptLanguageInterceptor,
+ { provide: LOCALE_ID, useValue: "de" },
+ ],
+ }),
+ );
+
+ it("should be created", () => {
+ const interceptor = TestBed.inject(AcceptLanguageInterceptor);
+ expect(interceptor).toBeTruthy();
+ });
+
+ it("should add the Accept-Language header with the given locale", () => {
+ const handleSpy = jasmine.createSpy();
+ const request = new HttpRequest("GET", "https://some.url/");
+ const interceptor = TestBed.inject(AcceptLanguageInterceptor);
+
+ interceptor.intercept(request, { handle: handleSpy });
+ const res = handleSpy.calls.mostRecent().args[0];
+
+ expect(res.headers.get("Accept-Language")).toBe("de");
+ });
+});
diff --git a/src/app/core/language/accept-language.interceptor.ts b/src/app/core/language/accept-language.interceptor.ts
new file mode 100644
index 0000000000..8fea01cde0
--- /dev/null
+++ b/src/app/core/language/accept-language.interceptor.ts
@@ -0,0 +1,22 @@
+import { Inject, Injectable, LOCALE_ID } from "@angular/core";
+import {
+ HttpEvent,
+ HttpHandler,
+ HttpInterceptor,
+ HttpRequest,
+} from "@angular/common/http";
+import { Observable } from "rxjs";
+
+@Injectable()
+export class AcceptLanguageInterceptor implements HttpInterceptor {
+ constructor(@Inject(LOCALE_ID) private locale: string) {}
+
+ intercept(
+ request: HttpRequest,
+ next: HttpHandler,
+ ): Observable> {
+ return next.handle(
+ request.clone({ setHeaders: { "Accept-Language": this.locale } }),
+ );
+ }
+}
diff --git a/src/app/core/language/language.module.ts b/src/app/core/language/language.module.ts
index 1beaf15eaa..f08efa1d60 100644
--- a/src/app/core/language/language.module.ts
+++ b/src/app/core/language/language.module.ts
@@ -1,5 +1,7 @@
import { NgModule } from "@angular/core";
import { LanguageService } from "./language.service";
+import { HTTP_INTERCEPTORS } from "@angular/common/http";
+import { AcceptLanguageInterceptor } from "./accept-language.interceptor";
/**
* Module that aids in the management and choice of translations/languages
@@ -10,7 +12,15 @@ import { LanguageService } from "./language.service";
* The {@link LanguageSelectComponent} is used to graphically offer a way of changing
* the current language of the user
*/
-@NgModule({})
+@NgModule({
+ providers: [
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: AcceptLanguageInterceptor,
+ multi: true,
+ },
+ ],
+})
export class LanguageModule {
constructor(translationService: LanguageService) {
translationService.initDefaultLanguage();
diff --git a/src/app/core/permissions/ability/ability.service.spec.ts b/src/app/core/permissions/ability/ability.service.spec.ts
index eb0481da50..ba8d066a01 100644
--- a/src/app/core/permissions/ability/ability.service.spec.ts
+++ b/src/app/core/permissions/ability/ability.service.spec.ts
@@ -2,12 +2,11 @@ import { fakeAsync, TestBed, tick, waitForAsync } from "@angular/core/testing";
import { AbilityService } from "./ability.service";
import { Subject } from "rxjs";
-import { SessionService } from "../../session/session-service/session.service";
import { Child } from "../../../child-dev-project/children/model/child";
import { Note } from "../../../child-dev-project/notes/model/note";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { PermissionEnforcerService } from "../permission-enforcer/permission-enforcer.service";
-import { User } from "../../user/user";
+import { CurrentUserSubject, User } from "../../user/user";
import { defaultInteractionTypes } from "../../config/default-config/default-interaction-types";
import { EntityAbility } from "./entity-ability";
import { DatabaseRule, DatabaseRules } from "../permission-types";
@@ -86,7 +85,7 @@ describe("AbilityService", () => {
it("should update the ability with rules for all roles the logged in user has", () => {
spyOn(ability, "update");
- spyOn(TestBed.inject(SessionService), "getCurrentUser").and.returnValue({
+ TestBed.inject(CurrentUserSubject).next({
name: "testAdmin",
roles: ["user_app", "admin_app"],
});
@@ -116,7 +115,7 @@ describe("AbilityService", () => {
expect(ability.can("manage", new Note())).toBeFalse();
expect(ability.can("create", new Note())).toBeFalse();
- spyOn(TestBed.inject(SessionService), "getCurrentUser").and.returnValue({
+ TestBed.inject(CurrentUserSubject).next({
name: "testAdmin",
roles: ["user_app", "admin_app"],
});
@@ -218,7 +217,7 @@ describe("AbilityService", () => {
}));
it("should log a warning if no rules are found for a user", () => {
- spyOn(TestBed.inject(SessionService), "getCurrentUser").and.returnValue({
+ TestBed.inject(CurrentUserSubject).next({
name: "new-user",
roles: ["invalid_role"],
});
@@ -245,7 +244,7 @@ describe("AbilityService", () => {
expect(ability.rules).toEqual(defaultRules.concat(...rules.user_app));
- spyOn(TestBed.inject(SessionService), "getCurrentUser").and.returnValue({
+ TestBed.inject(CurrentUserSubject).next({
name: "admin",
roles: ["user_app", "admin_app"],
});
diff --git a/src/app/core/permissions/ability/ability.service.ts b/src/app/core/permissions/ability/ability.service.ts
index 61ec2726c0..8d210b5f1f 100644
--- a/src/app/core/permissions/ability/ability.service.ts
+++ b/src/app/core/permissions/ability/ability.service.ts
@@ -1,5 +1,4 @@
import { Injectable } from "@angular/core";
-import { SessionService } from "../../session/session-service/session.service";
import { shareReplay } from "rxjs/operators";
import { DatabaseRule, DatabaseRules } from "../permission-types";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
@@ -8,8 +7,9 @@ import { EntityAbility } from "./entity-ability";
import { Config } from "../../config/config";
import { LoggingService } from "../../logging/logging.service";
import { get } from "lodash-es";
-import { AuthUser } from "../../session/session-service/auth-user";
import { LatestEntityLoader } from "../../entity/latest-entity-loader";
+import { AuthUser } from "../../session/auth/auth-user";
+import { CurrentUserSubject } from "../../user/user";
/**
* This service sets up the `EntityAbility` injectable with the JSON defined rules for the currently logged in user.
@@ -24,7 +24,7 @@ export class AbilityService extends LatestEntityLoader> {
constructor(
private ability: EntityAbility,
- private sessionService: SessionService,
+ private currentUser: CurrentUserSubject,
private permissionEnforcer: PermissionEnforcerService,
entityMapper: EntityMapperService,
logger: LoggingService,
@@ -49,7 +49,7 @@ export class AbilityService extends LatestEntityLoader> {
if (userRules.length === 0) {
// No rules or only default rules defined
- const user = this.sessionService.getCurrentUser();
+ const user = this.currentUser.value;
this.logger.warn(
`no rules found for user "${user?.name}" with roles "${user?.roles}"`,
);
@@ -59,7 +59,7 @@ export class AbilityService extends LatestEntityLoader> {
}
private getRulesForUser(rules: DatabaseRules): DatabaseRule[] {
- const currentUser = this.sessionService.getCurrentUser();
+ const currentUser = this.currentUser.value;
if (!currentUser) {
return rules.public ?? [];
}
diff --git a/src/app/core/permissions/permission-enforcer/permission-enforcer.service.ts b/src/app/core/permissions/permission-enforcer/permission-enforcer.service.ts
index 822a7f054a..eca2f19fc6 100644
--- a/src/app/core/permissions/permission-enforcer/permission-enforcer.service.ts
+++ b/src/app/core/permissions/permission-enforcer/permission-enforcer.service.ts
@@ -1,6 +1,5 @@
import { Inject, Injectable } from "@angular/core";
import { DatabaseRule } from "../permission-types";
-import { SessionService } from "../../session/session-service/session.service";
import { EntityConstructor } from "../../entity/model/entity";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { Database } from "../../database/database";
@@ -10,6 +9,7 @@ import { EntityAbility } from "../ability/entity-ability";
import { EntityRegistry } from "../../entity/database-entity.decorator";
import { ConfigService } from "../../config/config.service";
import { firstValueFrom } from "rxjs";
+import { CurrentUserSubject } from "../../user/user";
/**
* This service checks whether the relevant rules for the current user changed.
@@ -25,7 +25,7 @@ export class PermissionEnforcerService {
static readonly LOCALSTORAGE_KEY = "RULES";
constructor(
- private sessionService: SessionService,
+ private currentUser: CurrentUserSubject,
private ability: EntityAbility,
private entityMapper: EntityMapperService,
private database: Database,
@@ -58,9 +58,7 @@ export class PermissionEnforcerService {
}
private getUserStorageKey() {
- return `${this.sessionService.getCurrentUser().name}-${
- PermissionEnforcerService.LOCALSTORAGE_KEY
- }`;
+ return `${this.currentUser.value.name}-${PermissionEnforcerService.LOCALSTORAGE_KEY}`;
}
private getSubjectsWithReadRestrictions(
diff --git a/src/app/core/permissions/permission-guard/user-role.guard.spec.ts b/src/app/core/permissions/permission-guard/user-role.guard.spec.ts
index b2a6554abb..2a70d6e588 100644
--- a/src/app/core/permissions/permission-guard/user-role.guard.spec.ts
+++ b/src/app/core/permissions/permission-guard/user-role.guard.spec.ts
@@ -1,16 +1,16 @@
import { TestBed } from "@angular/core/testing";
import { UserRoleGuard } from "./user-role.guard";
-import { SessionService } from "../../session/session-service/session.service";
import { RouterTestingModule } from "@angular/router/testing";
import { ActivatedRouteSnapshot, Router } from "@angular/router";
-import { AuthUser } from "../../session/session-service/auth-user";
+import { AuthUser } from "../../session/auth/auth-user";
import { ConfigService } from "../../config/config.service";
import { PREFIX_VIEW_CONFIG } from "../../config/dynamic-routing/view-config.interface";
+import { CurrentUserSubject } from "../../user/user";
describe("UserRoleGuard", () => {
let guard: UserRoleGuard;
- let mockSessionService: jasmine.SpyObj;
+ let userSubject: CurrentUserSubject;
const normalUser: AuthUser = { name: "normalUser", roles: ["user_app"] };
const adminUser: AuthUser = {
name: "admin",
@@ -19,18 +19,18 @@ describe("UserRoleGuard", () => {
let mockConfigService: jasmine.SpyObj;
beforeEach(() => {
- mockSessionService = jasmine.createSpyObj(["getCurrentUser"]);
mockConfigService = jasmine.createSpyObj(["getConfig"]);
TestBed.configureTestingModule({
imports: [RouterTestingModule],
providers: [
- { provide: SessionService, useValue: mockSessionService },
+ CurrentUserSubject,
UserRoleGuard,
{ provide: ConfigService, useValue: mockConfigService },
],
});
guard = TestBed.inject(UserRoleGuard);
+ userSubject = TestBed.inject(CurrentUserSubject);
});
it("should be created", () => {
@@ -38,7 +38,7 @@ describe("UserRoleGuard", () => {
});
it("should return true if current user is allowed", () => {
- mockSessionService.getCurrentUser.and.returnValue(adminUser);
+ userSubject.next(adminUser);
const result = guard.canActivate({
routeConfig: { path: "url" },
@@ -49,7 +49,7 @@ describe("UserRoleGuard", () => {
});
it("should return false for a user without permissions", () => {
- mockSessionService.getCurrentUser.and.returnValue(normalUser);
+ userSubject.next(normalUser);
const router = TestBed.inject(Router);
spyOn(router, "navigate");
@@ -63,7 +63,7 @@ describe("UserRoleGuard", () => {
});
it("should navigate to 404 for real navigation requests without permissions", () => {
- mockSessionService.getCurrentUser.and.returnValue(normalUser);
+ userSubject.next(normalUser);
const router = TestBed.inject(Router);
spyOn(router, "navigate");
const route = new ActivatedRouteSnapshot();
@@ -96,13 +96,16 @@ describe("UserRoleGuard", () => {
}
});
- mockSessionService.getCurrentUser.and.returnValue(normalUser);
+ userSubject.next(normalUser);
+ expect(guard.checkRoutePermissions("free")).toBeTrue();
+ expect(guard.checkRoutePermissions("/free")).toBeTrue();
expect(guard.checkRoutePermissions("restricted")).toBeFalse();
expect(guard.checkRoutePermissions("pathA")).toBeTrue();
expect(guard.checkRoutePermissions("/pathA")).toBeTrue();
expect(guard.checkRoutePermissions("pathA/1")).toBeFalse();
- mockSessionService.getCurrentUser.and.returnValue(adminUser);
+ userSubject.next(adminUser);
+ expect(guard.checkRoutePermissions("free")).toBeTrue();
expect(guard.checkRoutePermissions("restricted")).toBeTrue();
expect(guard.checkRoutePermissions("pathA")).toBeTrue();
expect(guard.checkRoutePermissions("pathA/1")).toBeTrue();
diff --git a/src/app/core/permissions/permission-guard/user-role.guard.ts b/src/app/core/permissions/permission-guard/user-role.guard.ts
index 5c64598f0f..45db58e8ba 100644
--- a/src/app/core/permissions/permission-guard/user-role.guard.ts
+++ b/src/app/core/permissions/permission-guard/user-role.guard.ts
@@ -1,13 +1,13 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
-import { SessionService } from "../../session/session-service/session.service";
import {
PREFIX_VIEW_CONFIG,
RouteData,
ViewConfig,
} from "../../config/dynamic-routing/view-config.interface";
-import { AuthUser } from "../../session/session-service/auth-user";
+import { AuthUser } from "../../session/auth/auth-user";
import { ConfigService } from "../../config/config.service";
+import { CurrentUserSubject } from "../../user/user";
/**
* A guard that checks the roles of the current user against the permissions which are saved in the route data.
@@ -15,14 +15,14 @@ import { ConfigService } from "../../config/config.service";
@Injectable()
export class UserRoleGuard implements CanActivate {
constructor(
- private sessionService: SessionService,
+ private currentUser: CurrentUserSubject,
private router: Router,
private configService: ConfigService,
) {}
canActivate(route: ActivatedRouteSnapshot): boolean {
const routeData: RouteData = route.data;
- const user = this.sessionService.getCurrentUser();
+ const user = this.currentUser.value;
if (this.canAccessRoute(routeData?.permittedUserRoles, user)) {
return true;
} else {
diff --git a/src/app/core/session/auth.guard.spec.ts b/src/app/core/session/auth.guard.spec.ts
index 4f83779c3d..0798005e32 100644
--- a/src/app/core/session/auth.guard.spec.ts
+++ b/src/app/core/session/auth.guard.spec.ts
@@ -1,18 +1,19 @@
import { TestBed } from "@angular/core/testing";
import { AuthGuard } from "./auth.guard";
-import { SessionService } from "./session-service/session.service";
import { RouterTestingModule } from "@angular/router/testing";
+import { LoginStateSubject } from "./session-type";
+import { LoginState } from "./session-states/login-state.enum";
describe("AuthGuard", () => {
- let mockSession: jasmine.SpyObj;
+ let loginState: LoginStateSubject;
beforeEach(() => {
- mockSession = jasmine.createSpyObj(["isLoggedIn"]);
TestBed.configureTestingModule({
imports: [RouterTestingModule],
- providers: [{ provide: SessionService, useValue: mockSession }],
+ providers: [LoginStateSubject],
});
+ loginState = TestBed.inject(LoginStateSubject);
});
it("should be created", () => {
@@ -20,7 +21,7 @@ describe("AuthGuard", () => {
});
it("should return true if user is logged in", () => {
- mockSession.isLoggedIn.and.returnValue(true);
+ loginState.next(LoginState.LOGGED_IN);
const res = TestBed.runInInjectionContext(() =>
AuthGuard(undefined, undefined),
@@ -29,7 +30,7 @@ describe("AuthGuard", () => {
});
it("should navigate to login page with redirect url if not logged in", () => {
- mockSession.isLoggedIn.and.returnValue(false);
+ loginState.next(LoginState.LOGGED_OUT);
const res = TestBed.runInInjectionContext(() =>
AuthGuard(undefined, { url: "/some/url" } as any),
diff --git a/src/app/core/session/auth.guard.ts b/src/app/core/session/auth.guard.ts
index 050a3c471f..688e2220c2 100644
--- a/src/app/core/session/auth.guard.ts
+++ b/src/app/core/session/auth.guard.ts
@@ -5,13 +5,14 @@ import {
Router,
RouterStateSnapshot,
} from "@angular/router";
-import { SessionService } from "./session-service/session.service";
+import { LoginStateSubject } from "./session-type";
+import { LoginState } from "./session-states/login-state.enum";
export const AuthGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
- if (inject(SessionService).isLoggedIn()) {
+ if (inject(LoginStateSubject).value === LoginState.LOGGED_IN) {
return true;
} else {
return inject(Router).createUrlTree(["/login"], {
diff --git a/src/app/core/session/auth/auth-provider.ts b/src/app/core/session/auth/auth-provider.ts
deleted file mode 100644
index 1931b02834..0000000000
--- a/src/app/core/session/auth/auth-provider.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Available authentication providers.
- */
-export enum AuthProvider {
- /**
- * Default auth provider using CouchDB's `_users` database and permission settings.
- * This is the simplest setup as no other service besides the CouchDB is required.
- * However, this provider comes with limited functionality.
- */
- CouchDB = "couchdb",
-
- /**
- * Keycloak is used to authenticate and manage users.
- * This requires keycloak and potentially other services to be running.
- * Also, the client configuration has to be placed in a file called `keycloak.json` in the `assets` folder.
- */
- Keycloak = "keycloak",
-}
diff --git a/src/app/core/session/session-service/auth-user.ts b/src/app/core/session/auth/auth-user.ts
similarity index 100%
rename from src/app/core/session/session-service/auth-user.ts
rename to src/app/core/session/auth/auth-user.ts
diff --git a/src/app/core/session/auth/auth.interceptor.spec.ts b/src/app/core/session/auth/auth.interceptor.spec.ts
deleted file mode 100644
index fae60cc037..0000000000
--- a/src/app/core/session/auth/auth.interceptor.spec.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import { TestBed } from "@angular/core/testing";
-
-import { AUTH_ENABLED, AuthInterceptor } from "./auth.interceptor";
-import { AuthService } from "./auth.service";
-import {
- HttpContext,
- HttpErrorResponse,
- HttpRequest,
- HttpResponse,
- HttpStatusCode,
-} from "@angular/common/http";
-import { EMPTY, of, throwError } from "rxjs";
-
-describe("AuthInterceptor", () => {
- let interceptor: AuthInterceptor;
- let mockAuthService: jasmine.SpyObj;
- const mockRequest = {
- clone: () => mockRequest,
- context: new HttpContext().set(AUTH_ENABLED, true),
- } as HttpRequest;
-
- beforeEach(() => {
- mockAuthService = jasmine.createSpyObj(["addAuthHeader", "autoLogin"]);
- TestBed.configureTestingModule({
- providers: [
- AuthInterceptor,
- { provide: AuthService, useValue: mockAuthService },
- ],
- });
- interceptor = TestBed.inject(AuthInterceptor);
- });
-
- it("should be created", () => {
- expect(interceptor).toBeTruthy();
- });
-
- it("should add an auth header to a request", () => {
- mockAuthService.addAuthHeader.and.callFake(
- (obj) => (obj["Authorization"] = "my-auth-header"),
- );
- spyOn(mockRequest, "clone");
-
- interceptor.intercept(mockRequest, { handle: () => EMPTY });
-
- expect(mockRequest.clone).toHaveBeenCalledWith({
- setHeaders: { Authorization: "my-auth-header" },
- });
- });
-
- it("should not add an auth header if auth is explicitly disabled", () => {
- const noAuthRequest = {
- context: new HttpContext().set(AUTH_ENABLED, false),
- } as HttpRequest;
- const handle = jasmine.createSpy().and.returnValue(EMPTY);
-
- interceptor.intercept(noAuthRequest, { handle });
-
- expect(mockAuthService.addAuthHeader).not.toHaveBeenCalled();
- expect(handle).toHaveBeenCalledWith(noAuthRequest);
- });
-
- it("should should retry request with updated auth when receiving unauthorized response", (done) => {
- const errorResponse = new HttpErrorResponse({
- status: HttpStatusCode.Unauthorized,
- });
- const expectedResponse = new HttpResponse({ status: 200 });
- const handle = jasmine.createSpy().and.returnValues(
- throwError(() => errorResponse),
- of(expectedResponse),
- );
- mockAuthService.autoLogin.and.resolveTo();
-
- interceptor.intercept(mockRequest, { handle }).subscribe((res) => {
- expect(res).toEqual(expectedResponse);
- expect(mockAuthService.autoLogin).toHaveBeenCalled();
- expect(mockAuthService.addAuthHeader).toHaveBeenCalledTimes(2);
- expect(handle).toHaveBeenCalledWith(mockRequest);
- expect(handle).toHaveBeenCalledTimes(2);
- done();
- });
- });
-
- it("should directly return error if it is not an unauthorized response", (done) => {
- const errorResponse = new HttpErrorResponse({
- status: HttpStatusCode.NotFound,
- });
- const handle = jasmine
- .createSpy()
- .and.returnValue(throwError(() => errorResponse));
-
- interceptor.intercept(mockRequest, { handle }).subscribe({
- error: (err) => {
- expect(err).toBe(errorResponse);
- expect(mockAuthService.autoLogin).not.toHaveBeenCalled();
- expect(handle).toHaveBeenCalledTimes(1);
- done();
- },
- });
- });
-
- it("should throw initial error auth attempt fails", (done) => {
- const initialError = new HttpErrorResponse({
- status: HttpStatusCode.Unauthorized,
- });
- const authError = new HttpErrorResponse({ status: 400 });
- const handle = jasmine
- .createSpy()
- .and.returnValues(throwError(() => initialError));
- mockAuthService.autoLogin.and.rejectWith(authError);
-
- interceptor.intercept(mockRequest, { handle }).subscribe({
- error: (res) => {
- expect(res).toEqual(initialError);
- expect(mockAuthService.autoLogin).toHaveBeenCalled();
- expect(handle).toHaveBeenCalledTimes(1);
- done();
- },
- });
- });
-});
diff --git a/src/app/core/session/auth/auth.interceptor.ts b/src/app/core/session/auth/auth.interceptor.ts
deleted file mode 100644
index 640992e383..0000000000
--- a/src/app/core/session/auth/auth.interceptor.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Injectable } from "@angular/core";
-import {
- HttpRequest,
- HttpHandler,
- HttpEvent,
- HttpInterceptor,
- HttpContextToken,
- HttpErrorResponse,
- HttpStatusCode,
-} from "@angular/common/http";
-import { concatMap, from, Observable } from "rxjs";
-import { AuthService } from "./auth.service";
-import { catchError } from "rxjs/operators";
-
-/**
- * This context can be used to prevent the Bearer token to be set by this interceptor.
- * This can be useful when a request goes to a 3rd party service e.g. GitHub.
- *
- * Usage:
- * ```javascript
- * this.httpClient.get(`...`, { context: new HttpContext().set(AUTH_ENABLED, false) });
- * ```
- */
-export const AUTH_ENABLED = new HttpContextToken(() => true);
-
-/**
- * This interceptor adds the required auth headers to all outgoing requests of the HttpClient service.
- * This allows other backend services to verify, whether a user is properly logged in.
- */
-@Injectable()
-export class AuthInterceptor implements HttpInterceptor {
- constructor(private authService: AuthService) {}
-
- intercept(
- request: HttpRequest,
- next: HttpHandler,
- ): Observable> {
- const authEnabled = request.context.get(AUTH_ENABLED);
- if (authEnabled) {
- request = this.getRequestWithAuth(request);
- }
- return next.handle(request).pipe(
- catchError((err: HttpErrorResponse) => {
- if (err.status === HttpStatusCode.Unauthorized && authEnabled) {
- return from(this.authService.autoLogin()).pipe(
- catchError(() => {
- // re-throw initial error
- throw err;
- }),
- concatMap(() => next.handle(this.getRequestWithAuth(request))),
- );
- } else {
- throw err;
- }
- }),
- );
- }
-
- private getRequestWithAuth(request: HttpRequest): HttpRequest {
- const headers = {} as any;
- this.authService.addAuthHeader(headers);
- // The request needs to be cloned as they are immutable
- return request.clone({ setHeaders: headers });
- }
-}
diff --git a/src/app/core/session/auth/auth.service.ts b/src/app/core/session/auth/auth.service.ts
deleted file mode 100644
index 4282f8fa60..0000000000
--- a/src/app/core/session/auth/auth.service.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { AuthUser } from "../session-service/auth-user";
-
-/**
- * Abstract class that handles user authentication and password change.
- * Implement this for different authentication providers.
- * See {@link AuthProvider} for available options.
- */
-export abstract class AuthService {
- static readonly LAST_AUTH_KEY = "LAST_REMOTE_LOGIN";
-
- /**
- * Authenticate a user with credentials.
- * @param username The username of the user
- * @param password The password of the user
- * @returns Promise that resolves with the user if the login was successful, rejects otherwise.
- */
- abstract authenticate(username: string, password: string): Promise;
-
- /**
- * Authenticate a user without credentials based on a still valid session.
- * @returns Promise that resolves with the user if the session is still valid, rejects otherwise.
- */
- abstract autoLogin(): Promise;
-
- /**
- * Add headers to requests send by PouchDB if required for authentication.
- * @param headers the object where further headers can be added
- */
- abstract addAuthHeader(headers: any);
-
- /**
- * Clear the local session of the currently logged-in user.
- */
- abstract logout(): Promise;
-
- /**
- * Log timestamp of last successful authentication
- * @protected
- */
- protected logSuccessfulAuth() {
- localStorage.setItem(AuthService.LAST_AUTH_KEY, new Date().toISOString());
- }
-}
diff --git a/src/app/core/session/auth/couchdb/couchdb-auth.service.spec.ts b/src/app/core/session/auth/couchdb/couchdb-auth.service.spec.ts
deleted file mode 100644
index b4e6d7b966..0000000000
--- a/src/app/core/session/auth/couchdb/couchdb-auth.service.spec.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { TestBed } from "@angular/core/testing";
-
-import { CouchdbAuthService } from "./couchdb-auth.service";
-import {
- HttpClient,
- HttpErrorResponse,
- HttpStatusCode,
-} from "@angular/common/http";
-import { of, throwError } from "rxjs";
-
-import { TEST_PASSWORD, TEST_USER } from "../../../../utils/mock-local-session";
-
-describe("CouchdbAuthService", () => {
- let service: CouchdbAuthService;
- let mockHttpClient: jasmine.SpyObj;
- let dbUser = { name: TEST_USER, roles: ["user_app"] };
-
- beforeEach(() => {
- mockHttpClient = jasmine.createSpyObj(["get", "post", "put"]);
- mockHttpClient.get.and.returnValue(throwError(() => new Error()));
- mockHttpClient.post.and.callFake((_url, body) => {
- if (body.name === TEST_USER && body.password === TEST_PASSWORD) {
- return of(dbUser as any);
- } else {
- return throwError(
- () =>
- new HttpErrorResponse({
- status: HttpStatusCode.Unauthorized,
- }),
- );
- }
- });
-
- TestBed.configureTestingModule({
- providers: [
- CouchdbAuthService,
- { provide: HttpClient, useValue: mockHttpClient },
- ],
- });
- service = TestBed.inject(CouchdbAuthService);
- });
-
- it("should be created", () => {
- expect(service).toBeTruthy();
- });
-
- it("should return the current user after successful login", async () => {
- const user = await service.authenticate(TEST_USER, TEST_PASSWORD);
- expect(user).toEqual(dbUser);
- });
-
- it("should login, given that CouchDB cookie is still valid", async () => {
- const responseObject = {
- ok: true,
- userCtx: dbUser,
- info: {
- authentication_handlers: ["cookie", "default"],
- authenticated: "default",
- },
- };
- mockHttpClient.get.and.returnValue(of(responseObject));
- const user = await service.autoLogin();
-
- expect(user).toEqual(responseObject.userCtx);
- });
-
- it("should not login, given that there is no valid CouchDB cookie", () => {
- const responseObject = {
- ok: true,
- userCtx: {
- name: null,
- roles: [],
- },
- info: {
- authentication_handlers: ["cookie", "default"],
- },
- };
- mockHttpClient.get.and.returnValue(of(responseObject));
- return expectAsync(service.autoLogin()).toBeRejected();
- });
-
- it("should reject if current user cant be fetched", () => {
- mockHttpClient.get.and.returnValue(throwError(() => new Error()));
-
- return expectAsync(
- service.changePassword("username", "wrongPW", ""),
- ).toBeRejected();
- });
-
- it("should report error when new Password cannot be saved", async () => {
- mockHttpClient.get.and.returnValues(of({}));
- mockHttpClient.put.and.returnValue(throwError(() => new Error()));
-
- await expectAsync(
- service.changePassword("username", "testPW", ""),
- ).toBeRejected();
- expect(mockHttpClient.get).toHaveBeenCalled();
- expect(mockHttpClient.put).toHaveBeenCalled();
- });
-
- it("should not fail if get and put requests are successful", () => {
- mockHttpClient.get.and.returnValues(of({}));
- mockHttpClient.put.and.returnValues(of({}));
-
- return expectAsync(
- service.changePassword("username", "testPW", "newPW"),
- ).not.toBeRejected();
- });
-});
diff --git a/src/app/core/session/auth/couchdb/couchdb-auth.service.ts b/src/app/core/session/auth/couchdb/couchdb-auth.service.ts
deleted file mode 100644
index 829f144e0b..0000000000
--- a/src/app/core/session/auth/couchdb/couchdb-auth.service.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { Injectable } from "@angular/core";
-import { AuthService } from "../auth.service";
-import {
- HttpClient,
- HttpErrorResponse,
- HttpHeaders,
- HttpStatusCode,
-} from "@angular/common/http";
-import { firstValueFrom } from "rxjs";
-import { AppSettings } from "../../../app-settings";
-import { AuthUser } from "../../session-service/auth-user";
-import { tap } from "rxjs/operators";
-
-@Injectable()
-export class CouchdbAuthService extends AuthService {
- private static readonly COUCHDB_USER_ENDPOINT = `${AppSettings.DB_PROXY_PREFIX}/_users/org.couchdb.user`;
-
- constructor(private http: HttpClient) {
- super();
- }
-
- addAuthHeader() {
- // auth happens through cookie
- return;
- }
-
- authenticate(username: string, password: string): Promise {
- return firstValueFrom(
- this.http
- .post(
- `${AppSettings.DB_PROXY_PREFIX}/_session`,
- { name: username, password: password },
- { withCredentials: true },
- )
- .pipe(tap(() => this.logSuccessfulAuth())),
- );
- }
-
- autoLogin(): Promise {
- return firstValueFrom(
- this.http.get<{ userCtx: AuthUser }>(
- `${AppSettings.DB_PROXY_PREFIX}/_session`,
- { withCredentials: true },
- ),
- ).then((res: any) => {
- if (res.userCtx.name) {
- this.logSuccessfulAuth();
- return res.userCtx;
- } else {
- throw new HttpErrorResponse({
- status: HttpStatusCode.Unauthorized,
- });
- }
- });
- }
-
- /**
- * Function to change the password of a user
- * @param username The username for which the password should be changed
- * @param oldPassword The current plaintext password of the user
- * @param newPassword The new plaintext password of the user
- * @return Promise that resolves once the password is changed in _user and the database
- */
- public async changePassword(
- username?: string,
- oldPassword?: string,
- newPassword?: string,
- ): Promise {
- let userResponse;
- try {
- // TODO due to cookie-auth, the old password is actually not checked
- userResponse = await this.getCouchDBUser(username, oldPassword);
- } catch (e) {
- throw new Error("Current password incorrect or server not available");
- }
-
- userResponse.password = newPassword;
- try {
- await this.saveNewPasswordToCouchDB(username, oldPassword, userResponse);
- } catch (e) {
- throw new Error(
- "Could not save new password, please contact your system administrator",
- );
- }
- }
-
- private getCouchDBUser(username: string, password: string): Promise {
- const userUrl = CouchdbAuthService.COUCHDB_USER_ENDPOINT + ":" + username;
- const headers: HttpHeaders = new HttpHeaders({
- Authorization: "Basic " + btoa(username + ":" + password),
- });
- return firstValueFrom(this.http.get(userUrl, { headers: headers }));
- }
-
- private saveNewPasswordToCouchDB(
- username: string,
- oldPassword: string,
- userObj: any,
- ): Promise {
- const userUrl = CouchdbAuthService.COUCHDB_USER_ENDPOINT + ":" + username;
- const headers: HttpHeaders = new HttpHeaders({
- Authorization: "Basic " + btoa(username + ":" + oldPassword),
- });
- return firstValueFrom(
- this.http.put(userUrl, userObj, { headers: headers }),
- );
- }
-
- logout(): Promise {
- return firstValueFrom(
- this.http.delete(`${AppSettings.DB_PROXY_PREFIX}/_session`, {
- withCredentials: true,
- }),
- );
- }
-}
diff --git a/src/app/core/session/auth/couchdb/password-form/password-form.component.html b/src/app/core/session/auth/couchdb/password-form/password-form.component.html
deleted file mode 100644
index 833eb09416..0000000000
--- a/src/app/core/session/auth/couchdb/password-form/password-form.component.html
+++ /dev/null
@@ -1,65 +0,0 @@
-
diff --git a/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts b/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts
deleted file mode 100644
index 8f6888b834..0000000000
--- a/src/app/core/session/auth/couchdb/password-form/password-form.component.spec.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import {
- ComponentFixture,
- fakeAsync,
- flush,
- TestBed,
- tick,
-} from "@angular/core/testing";
-
-import { PasswordFormComponent } from "./password-form.component";
-import { MockedTestingModule } from "../../../../../utils/mocked-testing.module";
-import { SessionService } from "../../../session-service/session.service";
-import { CouchdbAuthService } from "../couchdb-auth.service";
-import { AuthService } from "../../auth.service";
-import { NEVER } from "rxjs";
-
-describe("PasswordFormComponent", () => {
- let component: PasswordFormComponent;
- let fixture: ComponentFixture;
- let mockSessionService: jasmine.SpyObj;
- let mockCouchDBAuth: jasmine.SpyObj;
-
- beforeEach(async () => {
- mockSessionService = jasmine.createSpyObj(["login", "checkPassword"], {
- syncState: NEVER,
- loginState: NEVER,
- });
- mockCouchDBAuth = jasmine.createSpyObj(["changePassword"]);
-
- await TestBed.configureTestingModule({
- imports: [PasswordFormComponent, MockedTestingModule.withState()],
- providers: [
- { provide: SessionService, useValue: mockSessionService },
- { provide: AuthService, useValue: {} },
- ],
- }).compileComponents();
-
- fixture = TestBed.createComponent(PasswordFormComponent);
- component = fixture.componentInstance;
- component.couchdbAuthService = mockCouchDBAuth;
- component.username = "testUser";
- fixture.detectChanges();
- });
-
- it("should create", () => {
- expect(component).toBeTruthy();
- });
-
- it("should disable the form when disabled is passed to component", () => {
- component.disabled = true;
- component.ngOnInit();
- expect(component.passwordForm.disabled).toBeTrue();
- });
-
- it("should set error when password is incorrect", () => {
- component.passwordForm.get("currentPassword").setValue("wrongPW");
- mockSessionService.checkPassword.and.returnValue(false);
-
- expect(component.passwordForm.get("currentPassword")).toBeValidForm();
-
- component.changePassword();
-
- expect(component.passwordForm.get("currentPassword")).not.toBeValidForm();
- });
-
- it("should set error when password change fails", fakeAsync(() => {
- component.passwordForm.get("currentPassword").setValue("testPW");
- component.passwordForm.get("newPassword").setValue("Password1-");
- component.passwordForm.get("confirmPassword").setValue("Password1-");
- mockSessionService.checkPassword.and.returnValue(true);
- mockCouchDBAuth.changePassword.and.rejectWith(new Error("pw change error"));
-
- expectAsync(component.changePassword()).toBeRejected();
- tick();
-
- expect(mockCouchDBAuth.changePassword).toHaveBeenCalled();
- flush();
- }));
-
- it("should set success and re-login when password change worked", fakeAsync(() => {
- component.passwordForm.get("currentPassword").setValue("testPW");
- component.passwordForm.get("newPassword").setValue("Password1-");
- component.passwordForm.get("confirmPassword").setValue("Password1-");
- mockSessionService.checkPassword.and.returnValue(true);
- mockCouchDBAuth.changePassword.and.resolveTo();
- mockSessionService.login.and.resolveTo(null);
-
- component.changePassword();
- tick();
- expect(mockSessionService.login).toHaveBeenCalledWith(
- "testUser",
- "Password1-",
- );
- flush();
- }));
-});
diff --git a/src/app/core/session/auth/couchdb/password-form/password-form.component.ts b/src/app/core/session/auth/couchdb/password-form/password-form.component.ts
deleted file mode 100644
index 99075af61a..0000000000
--- a/src/app/core/session/auth/couchdb/password-form/password-form.component.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import { Component, Input, OnInit } from "@angular/core";
-import {
- FormBuilder,
- ReactiveFormsModule,
- ValidationErrors,
- Validators,
-} from "@angular/forms";
-import { SessionService } from "../../../session-service/session.service";
-import { LoggingService } from "../../../../logging/logging.service";
-import { AuthService } from "../../auth.service";
-import { CouchdbAuthService } from "../couchdb-auth.service";
-import { AlertService } from "../../../../alerts/alert.service";
-import {
- KeyValuePipe,
- NgForOf,
- NgIf,
- NgSwitch,
- NgSwitchCase,
-} from "@angular/common";
-import { MatFormFieldModule } from "@angular/material/form-field";
-import { MatInputModule } from "@angular/material/input";
-import { MatButtonModule } from "@angular/material/button";
-
-/**
- * A simple password form that enforces secure password.
- */
-@Component({
- selector: "app-password-form",
- templateUrl: "./password-form.component.html",
- imports: [
- NgIf,
- ReactiveFormsModule,
- MatFormFieldModule,
- MatInputModule,
- NgForOf,
- KeyValuePipe,
- NgSwitchCase,
- MatButtonModule,
- NgSwitch,
- ],
- standalone: true,
-})
-export class PasswordFormComponent implements OnInit {
- @Input() username: string;
- @Input() disabled = false;
-
- couchdbAuthService: CouchdbAuthService;
-
- passwordForm = this.fb.group(
- {
- currentPassword: ["", Validators.required],
- newPassword: [
- "",
- [
- Validators.required,
- Validators.minLength(8),
- Validators.pattern(/[A-Z]/),
- Validators.pattern(/[a-z]/),
- Validators.pattern(/\d/),
- Validators.pattern(/[^A-Za-z0-9]/),
- ],
- ],
- confirmPassword: ["", [Validators.required]],
- },
- { validators: () => this.passwordMatchValidator() },
- );
-
- constructor(
- private fb: FormBuilder,
- private sessionService: SessionService,
- private loggingService: LoggingService,
- private alertService: AlertService,
- authService: AuthService,
- ) {
- if (authService instanceof CouchdbAuthService) {
- this.couchdbAuthService = authService;
- }
- }
-
- ngOnInit() {
- if (this.disabled) {
- this.passwordForm.disable();
- }
- }
-
- changePassword(): Promise {
- const currentPassword = this.passwordForm.get("currentPassword").value;
-
- if (!this.sessionService.checkPassword(this.username, currentPassword)) {
- this.passwordForm
- .get("currentPassword")
- .setErrors({ incorrectPassword: true });
- return;
- }
-
- if (this.passwordForm.invalid) {
- return;
- }
-
- const newPassword = this.passwordForm.get("newPassword").value;
- return this.couchdbAuthService
- .changePassword(this.username, currentPassword, newPassword)
- .then(() => this.sessionService.login(this.username, newPassword))
- .then(() =>
- this.alertService.addInfo($localize`Password changed successfully.`),
- )
- .catch((err: Error) => {
- this.alertService.addDanger(
- $localize`Failed to change password: ${err}\nPlease try again. If the problem persists contact Aam Digital support.`,
- );
- this.loggingService.error({
- error: "password change failed",
- details: err.message,
- });
- // rethrow to properly report to sentry.io; this exception is not expected, only caught to display in UI
- throw err;
- });
- }
-
- private passwordMatchValidator(): ValidationErrors | null {
- const newPassword = this.passwordForm?.get("newPassword").value;
- const confirmPassword = this.passwordForm?.get("confirmPassword").value;
- if (newPassword !== confirmPassword) {
- this.passwordForm
- .get("confirmPassword")
- .setErrors({ passwordConfirmationMismatch: true });
- return { passwordConfirmationMismatch: true };
- }
- return null;
- }
-}
diff --git a/src/app/core/session/auth/keycloak/account-page/account-page.component.html b/src/app/core/session/auth/keycloak/account-page/account-page.component.html
index 6c5ec65db8..74ea51bcde 100644
--- a/src/app/core/session/auth/keycloak/account-page/account-page.component.html
+++ b/src/app/core/session/auth/keycloak/account-page/account-page.component.html
@@ -1,47 +1,44 @@
-
-
+
-
-
+
diff --git a/src/app/core/session/auth/keycloak/account-page/account-page.component.spec.ts b/src/app/core/session/auth/keycloak/account-page/account-page.component.spec.ts
index d89d9a7f3e..bb133e7d46 100644
--- a/src/app/core/session/auth/keycloak/account-page/account-page.component.spec.ts
+++ b/src/app/core/session/auth/keycloak/account-page/account-page.component.spec.ts
@@ -6,8 +6,7 @@ import {
} from "@angular/core/testing";
import { AccountPageComponent } from "./account-page.component";
-import { AuthService } from "../../auth.service";
-import { KeycloakAuthService } from "../keycloak-auth.service";
+import { KeycloakAuthService, KeycloakUser } from "../keycloak-auth.service";
import { of, throwError } from "rxjs";
import { MockedTestingModule } from "../../../../../utils/mocked-testing.module";
import { HttpErrorResponse } from "@angular/common/http";
@@ -24,20 +23,21 @@ describe("AccountPageComponent", () => {
"changePassword",
"getUserinfo",
"setEmail",
+ "login",
]);
- mockAuthService.getUserinfo.and.returnValue(throwError(() => new Error()));
+ mockAuthService.getUserinfo.and.rejectWith();
+ mockAuthService.login.and.rejectWith();
mockAlerts = jasmine.createSpyObj(["addInfo"]);
await TestBed.configureTestingModule({
imports: [AccountPageComponent, MockedTestingModule.withState()],
providers: [
- { provide: AuthService, useValue: {} },
+ { provide: KeycloakAuthService, useValue: mockAuthService },
{ provide: AlertService, useValue: mockAlerts },
],
}).compileComponents();
fixture = TestBed.createComponent(AccountPageComponent);
component = fixture.componentInstance;
- component.keycloakAuthService = mockAuthService;
fixture.detectChanges();
});
@@ -47,7 +47,7 @@ describe("AccountPageComponent", () => {
it("should show the email if its already set", fakeAsync(() => {
const email = "mail@exmaple.com";
- mockAuthService.getUserinfo.and.returnValue(of({ email }));
+ mockAuthService.getUserinfo.and.resolveTo({ email } as KeycloakUser);
component.ngOnInit();
tick();
@@ -55,6 +55,14 @@ describe("AccountPageComponent", () => {
expect(component.email.value).toBe(email);
}));
+ it("should disabled the email form if the disabled flag is set", () => {
+ component.disabled = true;
+
+ component.ngOnInit();
+
+ expect(component.email.disabled).toBe(true);
+ });
+
it("should not save email if form is invalid", () => {
component.email.setValue("invalid-email");
expect(component.email).not.toBeValidForm();
diff --git a/src/app/core/session/auth/keycloak/account-page/account-page.component.ts b/src/app/core/session/auth/keycloak/account-page/account-page.component.ts
index f3cdcc4aac..2d69614f40 100644
--- a/src/app/core/session/auth/keycloak/account-page/account-page.component.ts
+++ b/src/app/core/session/auth/keycloak/account-page/account-page.component.ts
@@ -1,5 +1,4 @@
import { Component, Input, OnInit } from "@angular/core";
-import { AuthService } from "../../auth.service";
import { KeycloakAuthService } from "../keycloak-auth.service";
import { FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
import { AlertService } from "../../../../alerts/alert.service";
@@ -24,25 +23,21 @@ import { MatInputModule } from "@angular/material/input";
})
export class AccountPageComponent implements OnInit {
@Input() disabled: boolean;
- keycloakAuthService: KeycloakAuthService;
email = new FormControl("", [Validators.required, Validators.email]);
constructor(
- authService: AuthService,
+ public authService: KeycloakAuthService,
private alertService: AlertService,
- ) {
- if (authService instanceof KeycloakAuthService) {
- this.keycloakAuthService = authService;
- }
- }
+ ) {}
ngOnInit() {
- if (this.keycloakAuthService) {
- this.keycloakAuthService.getUserinfo().subscribe({
- next: (res) => this.email.setValue(res.email),
- error: () => this.email.setValue(""),
- });
+ if (this.disabled) {
+ this.email.disable();
}
+ this.authService
+ .getUserinfo()
+ .then((res) => this.email.setValue(res.email))
+ .catch((err) => console.debug("user profile not available", err));
}
setEmail() {
@@ -50,7 +45,8 @@ export class AccountPageComponent implements OnInit {
return;
}
- this.keycloakAuthService.setEmail(this.email.value).subscribe({
+ // TODO can we use keycloak for this?
+ this.authService.setEmail(this.email.value).subscribe({
next: () =>
this.alertService.addInfo(
$localize`Please click the link in the email we sent you to verify your email address.`,
diff --git a/src/app/core/session/auth/keycloak/keycloak-auth.service.spec.ts b/src/app/core/session/auth/keycloak/keycloak-auth.service.spec.ts
index c9bf36b63b..907b604ec4 100644
--- a/src/app/core/session/auth/keycloak/keycloak-auth.service.spec.ts
+++ b/src/app/core/session/auth/keycloak/keycloak-auth.service.spec.ts
@@ -1,38 +1,11 @@
import { TestBed } from "@angular/core/testing";
-import {
- KeycloakAuthService,
- OIDCTokenResponse,
-} from "./keycloak-auth.service";
-import { of, throwError } from "rxjs";
-import {
- HttpClient,
- HttpErrorResponse,
- HttpStatusCode,
-} from "@angular/common/http";
-import { AuthUser } from "../../session-service/auth-user";
-import { TEST_PASSWORD, TEST_USER } from "../../../../utils/mock-local-session";
-
-function keycloakAuthHttpFake(_url, body) {
- const params = new URLSearchParams(body);
- const isValidPassword =
- params.get("username") === TEST_USER &&
- params.get("password") === TEST_PASSWORD;
- const isValidToken = params.get("refresh_token") === "test-refresh-token";
- if (isValidPassword || isValidToken) {
- return of(jwtTokenResponse as any);
- } else {
- return throwError(
- () =>
- new HttpErrorResponse({
- status: HttpStatusCode.Unauthorized,
- }),
- );
- }
-}
+import { KeycloakAuthService } from "./keycloak-auth.service";
+import { HttpClient } from "@angular/common/http";
+import { KeycloakService } from "keycloak-angular";
/**
- * Check {@link https://jwt.io} to decode the access_token.
+ * Check {@link https://jwt.io} to decode the token.
* Extract:
* ```json
* {
@@ -46,128 +19,70 @@ function keycloakAuthHttpFake(_url, body) {
* }
* ```
*/
-const jwtTokenResponse: OIDCTokenResponse = {
- access_token:
- "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJOTzU3NEpPTmoxWUM0V3VLbEtMN0R0dUdHemJTdEQ3WUFaX3FONUk0WDB3In0.eyJleHAiOjE2NTkxMTI1NjUsImlhdCI6MTY1OTExMjI2NSwianRpIjoiODYwMmJiMDQtZDA2Mi00MjcxLWFlYmMtN2I0MjY3YmY0MDNlIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay10ZXN0LmFhbS1kaWdpdGFsLmNvbTo0NDMvYXV0aC9yZWFsbXMva2V5Y2xvYWstdGVzdCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI4ODFiYTE5MS0wZDI3LTRkZmYtOWJjNC0yYzllNTYxYWM5MDAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhcHAiLCJzZXNzaW9uX3N0YXRlIjoiNTYwMmNhZDgtMjgxNS00YTY5LWFlN2YtZWY2MjVmZjE1ZGUyIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWtleWNsb2FrLXRlc3QiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYXBwIjp7InJvbGVzIjpbInVzZXJfYXBwIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6InByb2ZpbGUgZW1haWwiLCJzaWQiOiI1NjAyY2FkOC0yODE1LTRhNjktYWU3Zi1lZjYyNWZmMTVkZTIiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIl9jb3VjaGRiLnJvbGVzIjpbInVzZXJfYXBwIl0sInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QiLCJnaXZlbl9uYW1lIjoiIiwiZmFtaWx5X25hbWUiOiIiLCJ1c2VybmFtZSI6InRlc3QifQ.g0Lq8tPN9fdni-tro7xcT4g4Ju-pyFTlYY8hjy-H34jxjkFDh6eTSjmnkof8w6r5TDg7V18k3WMz5Bf4XXt9kJtrVM0nOFq7wY-BSRdvl1TtMpRRkGlEUg5CMxCoyhkpkL1dcYslKlxNw4qwavvcjqYdtL7LU7ezZfs9wcAUV0VB9frxIzhq3WW6eHPBWYdFJFY1H5kl7jI6gtrLEc25tC-8Hpsz12Ey8O1DnsTqS7cXa1gNSGY10xYO9zNhxNfYy_x4uaaVJviT-gq9Bz-LM55H9s7Nz_FT9ETHNBm479jetBwURWLR-QRTwEdgajQWUUBw3l4Ld15q1YUSVSn1Ww",
- refresh_token: "test-refresh-token",
- session_state: "test-session-state",
-};
+const keycloakToken =
+ "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJOTzU3NEpPTmoxWUM0V3VLbEtMN0R0dUdHemJTdEQ3WUFaX3FONUk0WDB3In0.eyJleHAiOjE2NTkxMTI1NjUsImlhdCI6MTY1OTExMjI2NSwianRpIjoiODYwMmJiMDQtZDA2Mi00MjcxLWFlYmMtN2I0MjY3YmY0MDNlIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay10ZXN0LmFhbS1kaWdpdGFsLmNvbTo0NDMvYXV0aC9yZWFsbXMva2V5Y2xvYWstdGVzdCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI4ODFiYTE5MS0wZDI3LTRkZmYtOWJjNC0yYzllNTYxYWM5MDAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhcHAiLCJzZXNzaW9uX3N0YXRlIjoiNTYwMmNhZDgtMjgxNS00YTY5LWFlN2YtZWY2MjVmZjE1ZGUyIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWtleWNsb2FrLXRlc3QiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYXBwIjp7InJvbGVzIjpbInVzZXJfYXBwIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6InByb2ZpbGUgZW1haWwiLCJzaWQiOiI1NjAyY2FkOC0yODE1LTRhNjktYWU3Zi1lZjYyNWZmMTVkZTIiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIl9jb3VjaGRiLnJvbGVzIjpbInVzZXJfYXBwIl0sInByZWZlcnJlZF91c2VybmFtZSI6InRlc3QiLCJnaXZlbl9uYW1lIjoiIiwiZmFtaWx5X25hbWUiOiIiLCJ1c2VybmFtZSI6InRlc3QifQ.g0Lq8tPN9fdni-tro7xcT4g4Ju-pyFTlYY8hjy-H34jxjkFDh6eTSjmnkof8w6r5TDg7V18k3WMz5Bf4XXt9kJtrVM0nOFq7wY-BSRdvl1TtMpRRkGlEUg5CMxCoyhkpkL1dcYslKlxNw4qwavvcjqYdtL7LU7ezZfs9wcAUV0VB9frxIzhq3WW6eHPBWYdFJFY1H5kl7jI6gtrLEc25tC-8Hpsz12Ey8O1DnsTqS7cXa1gNSGY10xYO9zNhxNfYy_x4uaaVJviT-gq9Bz-LM55H9s7Nz_FT9ETHNBm479jetBwURWLR-QRTwEdgajQWUUBw3l4Ld15q1YUSVSn1Ww";
describe("KeycloakAuthService", () => {
let service: KeycloakAuthService;
let mockHttpClient: jasmine.SpyObj;
- let dbUser: AuthUser;
+ let mockKeycloak: jasmine.SpyObj;
beforeEach(() => {
mockHttpClient = jasmine.createSpyObj(["post"]);
- mockHttpClient.post.and.callFake(keycloakAuthHttpFake);
+ mockKeycloak = jasmine.createSpyObj([
+ "updateToken",
+ "getToken",
+ "login",
+ "init",
+ ]);
+ mockKeycloak.updateToken.and.resolveTo();
+ mockKeycloak.getToken.and.resolveTo(keycloakToken);
TestBed.configureTestingModule({
providers: [
{ provide: HttpClient, useValue: mockHttpClient },
+ { provide: KeycloakService, useValue: mockKeycloak },
KeycloakAuthService,
],
});
- dbUser = { name: TEST_USER, roles: ["user_app"] };
service = TestBed.inject(KeycloakAuthService);
- // Mock initialization of keycloak
- service["keycloakReady"] = Promise.resolve() as any;
+ service["keycloakReady"] = Promise.resolve(true);
});
- afterEach(() =>
- window.localStorage.removeItem(KeycloakAuthService.REFRESH_TOKEN_KEY),
- );
-
it("should be created", () => {
expect(service).toBeTruthy();
});
- it("should take username and roles from jwtToken", async () => {
- const user = await service.authenticate(TEST_USER, TEST_PASSWORD);
-
- expect(user).toEqual(dbUser);
- });
-
- it("should trim whitespace from username", async () => {
- await service.authenticate(" " + TEST_USER + " ", TEST_PASSWORD);
- expect(mockHttpClient.post).toHaveBeenCalledWith(
- jasmine.anything(),
- jasmine.stringContaining(`username=${TEST_USER}&`),
- jasmine.anything(),
- );
- });
-
- it("should store access token in memory and refresh token in local storage", async () => {
- await service.authenticate(TEST_USER, TEST_PASSWORD);
-
- expect(service.accessToken).toBe(jwtTokenResponse.access_token);
- expect(
- window.localStorage.getItem(KeycloakAuthService.REFRESH_TOKEN_KEY),
- ).toBe("test-refresh-token");
+ it("should return user object after successful login check", () => {
+ return expectAsync(service.login()).toBeResolvedTo({
+ name: "test",
+ roles: ["user_app"],
+ });
});
- it("should throw a unauthorized exception if invalid_grant is returned", (done) => {
- mockHttpClient.post.and.returnValue(
- throwError(
- () =>
- new HttpErrorResponse({
- status: 400,
- error: {
- error: "invalid_grant",
- error_description: "Account disabled",
- },
- }),
- ),
- );
- service.authenticate(TEST_USER, TEST_PASSWORD).catch((err) => {
- expect(err.status).toBe(HttpStatusCode.Unauthorized);
- done();
- });
+ it("should throw an error if username claim is not available", () => {
+ const tokenWithoutUser =
+ "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJpcjdiVjZoOVkxazBzTGh2aFRxWThlMzgzV3NMY0V3cmFsaC1TM2NJUTVjIn0.eyJleHAiOjE2OTE1Njc4NzcsImlhdCI6MTY5MTU2NzU3NywianRpIjoiNzNiNGUzODEtMzk4My00ZjI1LWE1ZGYtOTRlOTYxYmU3MjgwIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5hYW0tZGlnaXRhbC5uZXQvcmVhbG1zL2RldiIsInN1YiI6IjI0YWM1Yzg5LTU3OGMtNDdmOC1hYmQ5LTE1ZjRhNmQ4M2JiNSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwcCIsInNlc3Npb25fc3RhdGUiOiIwYjVhNGQ0OS0wOTFhLTQzOGYtOTEwNi1mNmZjYmQyMDM1Y2EiLCJzY29wZSI6ImVtYWlsIiwic2lkIjoiMGI1YTRkNDktMDkxYS00MzhmLTkxMDYtZjZmY2JkMjAzNWNhIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJfY291Y2hkYi5yb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWRldiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyX2FwcCJdfQ.EvF1wc32KwdDCUGboviRYGqKv2C3yK5B1WL_hCm-IGg58DoE_XGOchVVfbFrtphXD3yQa8uAaY58jWb6SeZQt0P92qtn5ulZOqcs3q2gQfrvxkxafMWffpCsxusVLBuGJ4B4EgoRGp_puQJIJE4p5KBwcA_u0PznFDFyLzPD18AYXevGWKLP5L8Zfgitf3Lby5AtCoOKHM7u6F_hDGSvLw-YlHEZBupqJzbpsjOs2UF1_woChMm2vbllZgIaUu9bbobcWi1mZSfNP3r9Ojk2t0NauOiKXDqtG5XyBLYMTC6wZXxsoCPGhOAwDr9LffkLDl-zvZ-0f_ujTpU8M2jzsg";
+ mockKeycloak.getToken.and.resolveTo(tokenWithoutUser);
+ return expectAsync(service.login()).toBeRejected();
});
it("should call keycloak for a password reset", () => {
- const loginSpy = spyOn(service["keycloak"], "login");
-
service.changePassword();
- expect(loginSpy).toHaveBeenCalledWith(
+ expect(mockKeycloak.login).toHaveBeenCalledWith(
jasmine.objectContaining({ action: "UPDATE_PASSWORD" }),
);
});
- it("should login, if there is a valid refresh token", async () => {
- localStorage.setItem(
- KeycloakAuthService.REFRESH_TOKEN_KEY,
- "some-refresh-token",
- );
- mockHttpClient.post.and.returnValue(of(jwtTokenResponse));
- const user = await service.autoLogin();
- expect(user).toEqual(dbUser);
- });
+ it("should add the Bearer token to a request", async () => {
+ await service.login();
- it("should not login, given that there is no valid refresh token", () => {
- mockHttpClient.post.and.returnValue(
- throwError(
- () =>
- new HttpErrorResponse({
- status: 400,
- error: {
- error: "invalid_grant",
- error_description: "Token is not active",
- },
- }),
- ),
- );
- return expectAsync(service.autoLogin()).toBeRejected();
- });
+ const mapHeaders = new Map();
+ service.addAuthHeader(mapHeaders);
+ expect(mapHeaders.get("Authorization")).toBe(`Bearer ${keycloakToken}`);
- it("should throw an error if username claim is not available", () => {
- const tokenWithoutUser =
- "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJpcjdiVjZoOVkxazBzTGh2aFRxWThlMzgzV3NMY0V3cmFsaC1TM2NJUTVjIn0.eyJleHAiOjE2OTE1Njc4NzcsImlhdCI6MTY5MTU2NzU3NywianRpIjoiNzNiNGUzODEtMzk4My00ZjI1LWE1ZGYtOTRlOTYxYmU3MjgwIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5hYW0tZGlnaXRhbC5uZXQvcmVhbG1zL2RldiIsInN1YiI6IjI0YWM1Yzg5LTU3OGMtNDdmOC1hYmQ5LTE1ZjRhNmQ4M2JiNSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwcCIsInNlc3Npb25fc3RhdGUiOiIwYjVhNGQ0OS0wOTFhLTQzOGYtOTEwNi1mNmZjYmQyMDM1Y2EiLCJzY29wZSI6ImVtYWlsIiwic2lkIjoiMGI1YTRkNDktMDkxYS00MzhmLTkxMDYtZjZmY2JkMjAzNWNhIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJfY291Y2hkYi5yb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWRldiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyX2FwcCJdfQ.EvF1wc32KwdDCUGboviRYGqKv2C3yK5B1WL_hCm-IGg58DoE_XGOchVVfbFrtphXD3yQa8uAaY58jWb6SeZQt0P92qtn5ulZOqcs3q2gQfrvxkxafMWffpCsxusVLBuGJ4B4EgoRGp_puQJIJE4p5KBwcA_u0PznFDFyLzPD18AYXevGWKLP5L8Zfgitf3Lby5AtCoOKHM7u6F_hDGSvLw-YlHEZBupqJzbpsjOs2UF1_woChMm2vbllZgIaUu9bbobcWi1mZSfNP3r9Ojk2t0NauOiKXDqtG5XyBLYMTC6wZXxsoCPGhOAwDr9LffkLDl-zvZ-0f_ujTpU8M2jzsg";
- mockHttpClient.post.and.returnValue(
- of({ ...jwtTokenResponse, access_token: tokenWithoutUser }),
- );
- return expectAsync(
- service.authenticate(TEST_USER, TEST_PASSWORD),
- ).toBeRejected();
+ const objHeaders = {};
+ service.addAuthHeader(objHeaders);
+ expect(objHeaders["Authorization"]).toBe(`Bearer ${keycloakToken}`);
});
});
diff --git a/src/app/core/session/auth/keycloak/keycloak-auth.service.ts b/src/app/core/session/auth/keycloak/keycloak-auth.service.ts
index 54a6ef45e1..f7e7e63f6e 100644
--- a/src/app/core/session/auth/keycloak/keycloak-auth.service.ts
+++ b/src/app/core/session/auth/keycloak/keycloak-auth.service.ts
@@ -1,103 +1,62 @@
-import { AuthService } from "../auth.service";
import { Injectable } from "@angular/core";
-import Keycloak from "keycloak-js";
-import {
- HttpClient,
- HttpErrorResponse,
- HttpHeaders,
- HttpStatusCode,
-} from "@angular/common/http";
-import { firstValueFrom, Observable } from "rxjs";
+import { HttpClient } from "@angular/common/http";
+import { Observable } from "rxjs";
import { parseJwt } from "../../../../utils/utils";
import { environment } from "../../../../../environments/environment";
-import { AuthUser } from "../../session-service/auth-user";
-import { catchError } from "rxjs/operators";
+import { AuthUser } from "../auth-user";
+import { KeycloakService } from "keycloak-angular";
+/**
+ * Handles the remote session with keycloak
+ */
@Injectable()
-export class KeycloakAuthService extends AuthService {
+export class KeycloakAuthService {
/**
* Users with this role can create and update other accounts.
*/
static readonly ACCOUNT_MANAGER_ROLE = "account_manager";
- static readonly REFRESH_TOKEN_KEY = "REFRESH_TOKEN";
-
- public accessToken: string;
-
- private keycloak = new Keycloak("assets/keycloak.json");
- private keycloakReady = this.keycloak.init({});
-
- constructor(private httpClient: HttpClient) {
- super();
- }
+ static readonly LAST_AUTH_KEY = "LAST_REMOTE_LOGIN";
+ private keycloakInitialised = false;
+ accessToken: string;
- get realmUrl(): string {
- return `${this.keycloak.authServerUrl}realms/${this.keycloak.realm}`;
- }
+ constructor(
+ private httpClient: HttpClient,
+ private keycloak: KeycloakService,
+ ) {}
- authenticate(username: string, password: string): Promise {
- return this.keycloakReady
- .then(() => this.credentialAuth(username.trim(), password))
- .then((token) => this.processToken(token));
- }
+ /**
+ * Check for a existing session or forward to the login page.
+ */
+ async login(): Promise {
+ if (!this.keycloakInitialised) {
+ this.keycloakInitialised = true;
+ const loggedIn = await this.keycloak.init({
+ config: window.location.origin + "/assets/keycloak.json",
+ initOptions: {
+ onLoad: "check-sso",
+ silentCheckSsoRedirectUri:
+ window.location.origin + "/assets/silent-check-sso.html",
+ },
+ // GitHub API rejects if non GitHub bearer token is present
+ shouldAddToken: ({ url }) => !url.includes("api.github.com"),
+ });
+ if (!loggedIn) {
+ // Forward to the keycloak login page.
+ await this.keycloak.login({ redirectUri: location.href });
+ }
+ }
- autoLogin(): Promise {
- return this.keycloakReady
- .then(() => this.refreshTokenAuth())
+ return this.keycloak
+ .updateToken()
+ .then(() => this.keycloak.getToken())
.then((token) => this.processToken(token));
}
- private credentialAuth(
- username: string,
- password: string,
- ): Promise {
- const body = new URLSearchParams();
- body.set("username", username);
- body.set("password", password);
- body.set("grant_type", "password");
- return this.getToken(body);
- }
-
- private refreshTokenAuth(): Promise {
- const body = new URLSearchParams();
- const token = localStorage.getItem(KeycloakAuthService.REFRESH_TOKEN_KEY);
- body.set("refresh_token", token);
- body.set("grant_type", "refresh_token");
- return this.getToken(body);
- }
-
- private getToken(body: URLSearchParams): Promise {
- body.set("client_id", "app");
- const headers = new HttpHeaders().set(
- "Content-Type",
- "application/x-www-form-urlencoded",
- );
- return firstValueFrom(
- this.httpClient
- .post(
- `${this.realmUrl}/protocol/openid-connect/token`,
- body.toString(),
- { headers },
- )
- .pipe(
- catchError((err) => {
- // treat all invalid grants as unauthorized
- if (err?.error?.error === "invalid_grant") {
- const status = HttpStatusCode.Unauthorized;
- throw new HttpErrorResponse({ status });
- } else {
- throw err;
- }
- }),
- ),
- );
- }
-
- private processToken(token: OIDCTokenResponse): AuthUser {
- this.accessToken = token.access_token;
- localStorage.setItem(
- KeycloakAuthService.REFRESH_TOKEN_KEY,
- token.refresh_token,
- );
+ private processToken(token: string): AuthUser {
+ if (!token) {
+ throw new Error();
+ }
+ this.accessToken = token;
this.logSuccessfulAuth();
const parsedToken = parseJwt(this.accessToken);
if (!parsedToken.username) {
@@ -111,6 +70,10 @@ export class KeycloakAuthService extends AuthService {
};
}
+ /**
+ * Add the Bearer auth header to a existing header object.
+ * @param headers
+ */
addAuthHeader(headers: any) {
if (this.accessToken) {
if (headers.set && typeof headers.set === "function") {
@@ -123,8 +86,11 @@ export class KeycloakAuthService extends AuthService {
}
}
+ /**
+ * Forward to the keycloak logout endpoint to clear the session.
+ */
async logout() {
- window.localStorage.removeItem(KeycloakAuthService.REFRESH_TOKEN_KEY);
+ return this.keycloak.logout(location.href);
}
/**
@@ -138,10 +104,10 @@ export class KeycloakAuthService extends AuthService {
});
}
- getUserinfo(): Observable {
- return this.httpClient.get(
- `${this.realmUrl}/protocol/openid-connect/userinfo`,
- );
+ getUserinfo(): Promise {
+ return this.keycloak
+ .getKeycloakInstance()
+ .loadUserInfo() as Promise;
}
setEmail(email: string): Observable {
@@ -150,17 +116,6 @@ export class KeycloakAuthService extends AuthService {
});
}
- forgotPassword(email: string): Observable {
- return this.httpClient.post(
- `${environment.account_url}/account/forgot-password`,
- {
- email,
- realm: this.keycloak.realm,
- client: this.keycloak.clientId,
- },
- );
- }
-
createUser(user: Partial): Observable {
return this.httpClient.post(`${environment.account_url}/account`, user);
}
@@ -186,15 +141,16 @@ export class KeycloakAuthService extends AuthService {
`${environment.account_url}/account/roles`,
);
}
-}
-/**
- * Extract of openId-connect response.
- */
-export interface OIDCTokenResponse {
- access_token: string;
- refresh_token: string;
- session_state: string;
+ /**
+ * Log timestamp of last successful authentication
+ */
+ logSuccessfulAuth() {
+ localStorage.setItem(
+ KeycloakAuthService.LAST_AUTH_KEY,
+ new Date().toISOString(),
+ );
+ }
}
/**
diff --git a/src/app/core/session/auth/keycloak/password-reset/password-reset.component.html b/src/app/core/session/auth/keycloak/password-reset/password-reset.component.html
deleted file mode 100644
index f9a7f4cd4e..0000000000
--- a/src/app/core/session/auth/keycloak/password-reset/password-reset.component.html
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
- {{
- email.errors.notFound
- }}
-
- Please provide a valid email
-
-
-
-
diff --git a/src/app/core/session/auth/keycloak/password-reset/password-reset.component.spec.ts b/src/app/core/session/auth/keycloak/password-reset/password-reset.component.spec.ts
deleted file mode 100644
index b57f17648d..0000000000
--- a/src/app/core/session/auth/keycloak/password-reset/password-reset.component.spec.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import {
- ComponentFixture,
- fakeAsync,
- TestBed,
- tick,
-} from "@angular/core/testing";
-
-import { PasswordResetComponent } from "./password-reset.component";
-import { KeycloakAuthService } from "../keycloak-auth.service";
-import { MockedTestingModule } from "../../../../../utils/mocked-testing.module";
-import { of, throwError } from "rxjs";
-import { MatSnackBar } from "@angular/material/snack-bar";
-import { HttpErrorResponse } from "@angular/common/http";
-import { AuthService } from "../../auth.service";
-
-describe("PasswordResetComponent", () => {
- let component: PasswordResetComponent;
- let fixture: ComponentFixture;
- let mockAuthService: jasmine.SpyObj;
-
- beforeEach(async () => {
- mockAuthService = jasmine.createSpyObj(["forgotPassword"]);
- await TestBed.configureTestingModule({
- imports: [PasswordResetComponent, MockedTestingModule.withState()],
- providers: [{ provide: AuthService, useValue: {} }],
- }).compileComponents();
-
- fixture = TestBed.createComponent(PasswordResetComponent);
- component = fixture.componentInstance;
- component.keycloakAuth = mockAuthService;
- fixture.detectChanges();
- });
-
- it("should create", () => {
- expect(component).toBeTruthy();
- });
-
- it("should toggle the email input when clicking the button", () => {
- component.toggleEmailForm();
- expect(component.passwordResetActive).toBeTrue();
- component.toggleEmailForm();
- expect(component.passwordResetActive).toBeFalse();
- });
-
- it("should not call service when email is not valid", () => {
- component.email.setValue("invalid-email");
- expect(component.email).not.toBeValidForm();
-
- component.sendEmail();
-
- expect(mockAuthService.forgotPassword).not.toHaveBeenCalled();
- });
-
- it("should close form and show snackbar if password reset mail was sent successfully", fakeAsync(() => {
- mockAuthService.forgotPassword.and.returnValue(of({}));
- const snackbarSpy = spyOn(TestBed.inject(MatSnackBar), "open");
- component.toggleEmailForm();
- expect(component.passwordResetActive).toBeTrue();
- const validEmail = "valid@email.com";
- component.email.setValue(validEmail);
- expect(component.email).toBeValidForm();
-
- component.sendEmail();
-
- expect(mockAuthService.forgotPassword).toHaveBeenCalledWith(validEmail);
- tick();
- expect(snackbarSpy).toHaveBeenCalled();
- expect(component.passwordResetActive).toBeFalse();
- }));
-
- it("should show error message if email couldn't be found", fakeAsync(() => {
- const errorMessage = "Email not found error";
- mockAuthService.forgotPassword.and.returnValue(
- throwError(
- () => new HttpErrorResponse({ error: { message: errorMessage } }),
- ),
- );
- component.email.setValue("valid@email.com");
-
- component.sendEmail();
- tick();
-
- expect(component.email.errors).toEqual({ notFound: errorMessage });
- }));
-});
diff --git a/src/app/core/session/auth/keycloak/password-reset/password-reset.component.ts b/src/app/core/session/auth/keycloak/password-reset/password-reset.component.ts
deleted file mode 100644
index 36f05c9439..0000000000
--- a/src/app/core/session/auth/keycloak/password-reset/password-reset.component.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { Component } from "@angular/core";
-import { AuthService } from "../../auth.service";
-import { KeycloakAuthService } from "../keycloak-auth.service";
-import { FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
-import { MatSnackBar } from "@angular/material/snack-bar";
-import { NgIf } from "@angular/common";
-import { MatButtonModule } from "@angular/material/button";
-import { MatFormFieldModule } from "@angular/material/form-field";
-import { MatInputModule } from "@angular/material/input";
-import { AnalyticsService } from "../../../../analytics/analytics.service";
-
-@Component({
- selector: "app-password-reset",
- templateUrl: "./password-reset.component.html",
- imports: [
- NgIf,
- MatButtonModule,
- MatFormFieldModule,
- ReactiveFormsModule,
- MatInputModule,
- ],
- standalone: true,
-})
-export class PasswordResetComponent {
- keycloakAuth: KeycloakAuthService;
- passwordResetActive = false;
- email = new FormControl("", [Validators.required, Validators.email]);
-
- constructor(
- authService: AuthService,
- private snackbar: MatSnackBar,
- private analytics: AnalyticsService,
- ) {
- if (authService instanceof KeycloakAuthService) {
- this.keycloakAuth = authService;
- }
- }
-
- toggleEmailForm() {
- this.passwordResetActive = !this.passwordResetActive;
- }
-
- sendEmail() {
- if (this.email.invalid) {
- return;
- }
- this.analytics.eventTrack("password_reset", { category: "User" });
-
- this.keycloakAuth.forgotPassword(this.email.value).subscribe({
- next: () => {
- this.snackbar.open(
- `Password reset email sent to ${this.email.value}`,
- undefined,
- { duration: 10000 },
- );
- this.toggleEmailForm();
- },
- error: (err) => this.email.setErrors({ notFound: err.error.message }),
- });
- }
-}
diff --git a/src/app/core/session/auth/local/local-auth.service.spec.ts b/src/app/core/session/auth/local/local-auth.service.spec.ts
new file mode 100644
index 0000000000..710fc49441
--- /dev/null
+++ b/src/app/core/session/auth/local/local-auth.service.spec.ts
@@ -0,0 +1,29 @@
+import { LocalAuthService } from "./local-auth.service";
+import { AuthUser } from "../auth-user";
+import { TEST_USER } from "../../../../utils/mock-local-session";
+
+describe("LocalAuthService", () => {
+ let service: LocalAuthService;
+ let testUser: AuthUser;
+
+ beforeEach(() => {
+ service = new LocalAuthService();
+ });
+
+ it("should be created", () => {
+ expect(service).toBeDefined();
+ });
+
+ it("should return saved users", () => {
+ localStorage.clear();
+ testUser = {
+ name: TEST_USER,
+ roles: ["user_app"],
+ };
+ service.saveUser(testUser);
+
+ expect(service.getStoredUsers()).toEqual([testUser]);
+
+ localStorage.clear();
+ });
+});
diff --git a/src/app/core/session/auth/local/local-auth.service.ts b/src/app/core/session/auth/local/local-auth.service.ts
new file mode 100644
index 0000000000..3b8b87d55e
--- /dev/null
+++ b/src/app/core/session/auth/local/local-auth.service.ts
@@ -0,0 +1,32 @@
+import { Injectable } from "@angular/core";
+import { AuthUser } from "../auth-user";
+
+/**
+ * Manages the offline login.
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class LocalAuthService {
+ private readonly STORED_USER_PREFIX = "USER-";
+
+ /**
+ * Get a list of users stored in the local storage.
+ */
+ getStoredUsers(): AuthUser[] {
+ return Object.entries(localStorage)
+ .filter(([key]) => key.startsWith(this.STORED_USER_PREFIX))
+ .map(([_, user]) => JSON.parse(user));
+ }
+
+ /**
+ * Saves a user to the local storage
+ * @param user a object holding the username and the roles of the user
+ */
+ saveUser(user: AuthUser) {
+ localStorage.setItem(
+ this.STORED_USER_PREFIX + user.name,
+ JSON.stringify(user),
+ );
+ }
+}
diff --git a/src/app/core/session/login/login.component.html b/src/app/core/session/login/login.component.html
index d9e2d24842..dee49cd7ae 100644
--- a/src/app/core/session/login/login.component.html
+++ b/src/app/core/session/login/login.component.html
@@ -15,71 +15,85 @@
~ along with ndb-core. If not, see .
-->
-
-
- Please Sign In
+
+
+
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/app/core/session/login/login.component.scss b/src/app/core/session/login/login.component.scss
index 37ea48c4da..c0c4eacf1a 100644
--- a/src/app/core/session/login/login.component.scss
+++ b/src/app/core/session/login/login.component.scss
@@ -16,10 +16,12 @@
*/
@use "variables/colors";
+@use "variables/sizes";
.login-error {
color: colors.$error;
- font-weight: bold;
+ font-size: 90%;
+ text-align: justify;
}
:host {
@@ -27,3 +29,16 @@
max-width: 400px;
margin: 0 auto;
}
+
+.header {
+ justify-content: center;
+ padding-bottom: sizes.$regular;
+}
+
+.offline-title {
+ font-size: 1.1em;
+}
+
+.login-check-progressbar {
+ margin-top: 8px;
+}
diff --git a/src/app/core/session/login/login.component.spec.ts b/src/app/core/session/login/login.component.spec.ts
index 470a2d4258..5c63859dd0 100644
--- a/src/app/core/session/login/login.component.spec.ts
+++ b/src/app/core/session/login/login.component.spec.ts
@@ -24,103 +24,88 @@ import {
} from "@angular/core/testing";
import { LoginComponent } from "./login.component";
-import { LoggingService } from "../../logging/logging.service";
-import { SessionService } from "../session-service/session.service";
import { LoginState } from "../session-states/login-state.enum";
-import { MockedTestingModule } from "../../../utils/mocked-testing.module";
-import { AuthService } from "../auth/auth.service";
-import { NEVER, Subject } from "rxjs";
import { ActivatedRoute, Router } from "@angular/router";
-import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed";
-import { HarnessLoader } from "@angular/cdk/testing";
-import { MatInputHarness } from "@angular/material/input/testing";
+import { LoginStateSubject, SessionType } from "../session-type";
+import { MockedTestingModule } from "../../../utils/mocked-testing.module";
+import { SessionManagerService } from "../session-service/session-manager.service";
+import { KeycloakAuthService } from "../auth/keycloak/keycloak-auth.service";
+import { firstValueFrom, Subject } from "rxjs";
+import { AuthUser } from "../auth/auth-user";
+import { environment } from "../../../../environments/environment";
describe("LoginComponent", () => {
let component: LoginComponent;
let fixture: ComponentFixture;
- let mockSessionService: jasmine.SpyObj;
- let loginState = new Subject();
- let loader: HarnessLoader;
+ let loginState: LoginStateSubject;
beforeEach(waitForAsync(() => {
- mockSessionService = jasmine.createSpyObj(["login", "getCurrentUser"], {
- loginState,
- syncState: NEVER,
- });
- mockSessionService.getCurrentUser.and.returnValue({ name: "", roles: [] });
TestBed.configureTestingModule({
- imports: [LoginComponent, MockedTestingModule],
- providers: [
- { provide: SessionService, useValue: mockSessionService },
- { provide: AuthService, useValue: {} },
- ],
+ imports: [LoginComponent, MockedTestingModule.withState()],
}).compileComponents();
+ loginState = TestBed.inject(LoginStateSubject);
+ environment.session_type = SessionType.synced;
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
- loader = TestbedHarnessEnvironment.loader(fixture);
component = fixture.componentInstance;
- fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ environment.session_type = SessionType.mock;
});
it("should be created", () => {
expect(component).toBeTruthy();
});
- it("should show a message when login is unavailable", fakeAsync(() => {
- expectErrorMessageOnState(LoginState.UNAVAILABLE);
- }));
-
- it("should show a message when login fails", fakeAsync(() => {
- expectErrorMessageOnState(LoginState.LOGIN_FAILED);
- }));
-
- it("should show a message and call logging service on unexpected login state", fakeAsync(() => {
- const loggerSpy = spyOn(TestBed.inject(LoggingService), "error");
-
- expectErrorMessageOnState(LoginState.LOGGED_OUT);
- expect(loggerSpy).toHaveBeenCalled();
- }));
-
- it("should show a message and call logging service on error", fakeAsync(() => {
- mockSessionService.login.and.rejectWith();
- expect(component.errorMessage).toBeFalsy();
- const loggerSpy = spyOn(TestBed.inject(LoggingService), "error");
-
- component.login();
- tick();
- expect(loggerSpy).toHaveBeenCalled();
- expect(component.errorMessage).toBeTruthy();
- }));
-
- it("should focus the first input element on initialization", fakeAsync(async () => {
- component.ngAfterViewInit();
- tick();
- fixture.detectChanges();
-
- const firstInputElement = await loader.getHarness(MatInputHarness);
- await expectAsync(firstInputElement.isFocused()).toBeResolvedTo(true);
- }));
-
it("should route to redirect uri once state changes to 'logged-in'", () => {
const navigateSpy = spyOn(TestBed.inject(Router), "navigateByUrl");
TestBed.inject(ActivatedRoute).snapshot.queryParams = {
redirect_uri: "someUrl",
};
+ fixture.detectChanges();
loginState.next(LoginState.LOGGED_IN);
expect(navigateSpy).toHaveBeenCalledWith("someUrl");
});
- function expectErrorMessageOnState(loginState: LoginState) {
- mockSessionService.login.and.resolveTo(loginState);
- expect(component.errorMessage).toBeFalsy();
+ it("should show offline login if remote login fails", fakeAsync(() => {
+ const sessionManager = TestBed.inject(SessionManagerService);
+ const mockUsers = [{ name: "test", roles: [] }];
+ spyOn(sessionManager, "getOfflineUsers").and.returnValue(mockUsers);
+ spyOn(sessionManager, "remoteLoginAvailable").and.returnValue(true);
+ const remoteLoginSubject = new Subject();
+ spyOn(TestBed.inject(KeycloakAuthService), "login").and.returnValue(
+ firstValueFrom(remoteLoginSubject),
+ );
+ loginState.next(LoginState.LOGGED_OUT);
+ fixture.detectChanges();
+
+ sessionManager.remoteLogin().catch(() => undefined);
+ expect(component.enableOfflineLogin).toBeFalse();
+ expect(loginState.value).toBe(LoginState.IN_PROGRESS);
- component.login();
+ remoteLoginSubject.error("login error");
tick();
+ expect(component.enableOfflineLogin).toBeTrue();
+ expect(component.offlineUsers).toEqual(mockUsers);
+ }));
+
+ it("should show offline login after 5 seconds", fakeAsync(() => {
+ const sessionManager = TestBed.inject(SessionManagerService);
+ const mockUsers = [{ name: "test", roles: [] }];
+ spyOn(sessionManager, "getOfflineUsers").and.returnValue(mockUsers);
- expect(component.errorMessage).toBeTruthy();
- }
+ loginState.next(LoginState.LOGGED_OUT);
+ fixture.detectChanges();
+ loginState.next(LoginState.IN_PROGRESS);
+ expect(component.enableOfflineLogin).toBeFalse();
+
+ tick(10000);
+ expect(component.enableOfflineLogin).toBeTrue();
+ expect(component.offlineUsers).toEqual(mockUsers);
+ }));
});
diff --git a/src/app/core/session/login/login.component.ts b/src/app/core/session/login/login.component.ts
index fbf42e13ab..49b760d190 100644
--- a/src/app/core/session/login/login.component.ts
+++ b/src/app/core/session/login/login.component.ts
@@ -15,24 +15,26 @@
* along with ndb-core. If not, see .
*/
-import { AfterViewInit, Component, ElementRef, ViewChild } from "@angular/core";
-import { SessionService } from "../session-service/session.service";
-import { LoginState } from "../session-states/login-state.enum";
-import { LoggingService } from "../../logging/logging.service";
+import { Component, OnInit } from "@angular/core";
import { MatCardModule } from "@angular/material/card";
-import { MatFormFieldModule } from "@angular/material/form-field";
-import { MatInputModule } from "@angular/material/input";
-import { FormsModule } from "@angular/forms";
-import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
-import { MatTooltipModule } from "@angular/material/tooltip";
import { MatButtonModule } from "@angular/material/button";
-import { PasswordResetComponent } from "../auth/keycloak/password-reset/password-reset.component";
-import { ActivatedRoute, Router } from "@angular/router";
-import { filter } from "rxjs/operators";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { ActivatedRoute, Router } from "@angular/router";
+import { LoginState } from "../session-states/login-state.enum";
+import { LoginStateSubject } from "../session-type";
+import { AsyncPipe, NgForOf, NgIf } from "@angular/common";
+import { SessionManagerService } from "../session-service/session-manager.service";
+import { MatProgressBarModule } from "@angular/material/progress-bar";
+import { AuthUser } from "../auth/auth-user";
+import { SiteSettingsService } from "../../site-settings/site-settings.service";
+import { MatTooltipModule } from "@angular/material/tooltip";
+import { MatListModule } from "@angular/material/list";
+import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
+import { waitForChangeTo } from "../session-states/session-utils";
+import { race, timer } from "rxjs";
/**
- * Form to allow users to enter their credentials and log in.
+ * Allows the user to login online or offline depending on the connection status
*/
@UntilDestroy()
@Component({
@@ -41,52 +43,45 @@ import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
styleUrls: ["./login.component.scss"],
imports: [
MatCardModule,
- MatFormFieldModule,
- MatInputModule,
- FormsModule,
- FontAwesomeModule,
- MatTooltipModule,
MatButtonModule,
- PasswordResetComponent,
+ NgIf,
+ MatProgressBarModule,
+ AsyncPipe,
+ MatTooltipModule,
+ MatListModule,
+ NgForOf,
+ FontAwesomeModule,
],
standalone: true,
})
-export class LoginComponent implements AfterViewInit {
- /** true while a login is started but result is not received yet */
+export class LoginComponent implements OnInit {
+ offlineUsers: AuthUser[] = [];
+ enableOfflineLogin = !this.sessionManager.remoteLoginAvailable();
loginInProgress = false;
- /** username as entered in form */
- username: string;
-
- /** password as entered in form */
- password: string;
-
- /** whether to show or hide the password */
- passwordVisible: boolean = false;
- readonly showPasswordHint = $localize`:Tooltip text for showing the password:Show password`;
- readonly hidePasswordHint = $localize`:Tooltip text for hiding the password:Hide password`;
-
- /** errorMessage displayed in form */
- errorMessage: string;
-
- @ViewChild("usernameInput") usernameInput: ElementRef;
-
constructor(
- private _sessionService: SessionService,
- private loggingService: LoggingService,
- private route: ActivatedRoute,
private router: Router,
- ) {
- this._sessionService.loginState
- .pipe(
- untilDestroyed(this),
- filter((state) => state === LoginState.LOGGED_IN),
- )
- .subscribe(() => this.routeAfterLogin());
- }
+ private route: ActivatedRoute,
+ public sessionManager: SessionManagerService,
+ public loginState: LoginStateSubject,
+ public siteSettingsService: SiteSettingsService,
+ ) {}
+
+ ngOnInit() {
+ this.loginState.pipe(untilDestroyed(this)).subscribe((state) => {
+ this.loginInProgress = state === LoginState.IN_PROGRESS;
+ if (state === LoginState.LOGGED_IN) {
+ this.routeAfterLogin();
+ }
+ });
- ngAfterViewInit(): void {
- setTimeout(() => this.usernameInput?.nativeElement.focus());
+ this.offlineUsers = this.sessionManager.getOfflineUsers();
+ race(
+ this.loginState.pipe(waitForChangeTo(LoginState.LOGIN_FAILED)),
+ timer(10000),
+ ).subscribe(() => {
+ this.enableOfflineLogin = true;
+ });
}
private routeAfterLogin() {
@@ -94,55 +89,7 @@ export class LoginComponent implements AfterViewInit {
this.router.navigateByUrl(decodeURIComponent(redirectUri));
}
- /**
- * Do a login with the SessionService.
- */
- login() {
- this.loginInProgress = true;
- this.errorMessage = "";
-
- this._sessionService
- .login(this.username?.trim(), this.password)
- .then((loginState) => {
- switch (loginState) {
- case LoginState.LOGGED_IN:
- this.reset();
- break;
- case LoginState.UNAVAILABLE:
- this.onLoginFailure(
- $localize`:LoginError:Please connect to the internet and try again`,
- );
- break;
- case LoginState.LOGIN_FAILED:
- this.onLoginFailure(
- $localize`:LoginError:Username and/or password incorrect`,
- );
- break;
- default:
- throw new Error(`Unexpected login state: ${loginState}`);
- }
- })
- .catch((reason) => {
- this.loggingService.error(`Unexpected login error: ${reason}`);
- this.onLoginFailure($localize`:LoginError:An unexpected error occurred.
- Please reload the the page and try again.
- If you keep seeing this error message, please contact your system administrator.
- `);
- });
- }
-
- private onLoginFailure(reason: string) {
- this.reset();
- this.errorMessage = reason;
- }
-
- private reset() {
- this.errorMessage = "";
- this.password = "";
- this.loginInProgress = false;
- }
-
- togglePasswordVisible() {
- this.passwordVisible = !this.passwordVisible;
+ tryLogin() {
+ return this.sessionManager.remoteLogin();
}
}
diff --git a/src/app/core/session/login/login.stories.ts b/src/app/core/session/login/login.stories.ts
new file mode 100644
index 0000000000..d1cc8d4747
--- /dev/null
+++ b/src/app/core/session/login/login.stories.ts
@@ -0,0 +1,39 @@
+import { applicationConfig, Meta, StoryFn } from "@storybook/angular";
+import { StorybookBaseModule } from "../../../utils/storybook-base.module";
+import { importProvidersFrom } from "@angular/core";
+import { LoginComponent } from "./login.component";
+import { of } from "rxjs";
+
+export default {
+ title: "Core/> App Layout/Login",
+ component: LoginComponent,
+ decorators: [
+ applicationConfig({
+ providers: [importProvidersFrom(StorybookBaseModule)],
+ }),
+ ],
+} as Meta;
+
+const Template: StoryFn = (args: LoginComponent) => ({
+ props: {
+ ...args,
+ siteSettingsService: { siteName: of("Aam Digital - Storybook") },
+ },
+});
+
+export const LoginCheck = Template.bind({});
+LoginCheck.args = {
+ loginInProgress: true,
+ offlineUsers: [{ name: "John" }, { name: "Jane" }],
+};
+
+export const LoginCheckWithoutLocalUsers = Template.bind({});
+LoginCheckWithoutLocalUsers.args = {
+ loginInProgress: true,
+ offlineUsers: [],
+};
+
+export const Offline = Template.bind({});
+Offline.args = {
+ offlineUsers: [{ name: "John" }, { name: "Jane" }],
+};
diff --git a/src/app/core/session/session-service/local-session.spec.ts b/src/app/core/session/session-service/local-session.spec.ts
deleted file mode 100644
index 9f805085fd..0000000000
--- a/src/app/core/session/session-service/local-session.spec.ts
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * This file is part of ndb-core.
- *
- * ndb-core is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * ndb-core is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with ndb-core. If not, see .
- */
-
-import { AppSettings } from "../../app-settings";
-import { LocalSession } from "./local-session";
-import { SessionType } from "../session-type";
-import { LocalUser, passwordEqualsEncrypted } from "./local-user";
-import { LoginState } from "../session-states/login-state.enum";
-import { testSessionServiceImplementation } from "./session.service.spec";
-import { PouchDatabase } from "../../database/pouch-database";
-import { environment } from "../../../../environments/environment";
-import { AuthUser } from "./auth-user";
-import { TEST_PASSWORD, TEST_USER } from "../../../utils/mock-local-session";
-
-describe("LocalSessionService", () => {
- let userDBName;
- let deprecatedDBName;
- let localSession: LocalSession;
- let testUser: AuthUser;
- let database: jasmine.SpyObj;
-
- beforeEach(() => {
- environment.session_type = SessionType.mock;
- userDBName = `${TEST_USER}-${AppSettings.DB_NAME}`;
- deprecatedDBName = AppSettings.DB_NAME;
- database = jasmine.createSpyObj([
- "initInMemoryDB",
- "initIndexedDB",
- "isEmpty",
- ]);
- localSession = new LocalSession(database);
- });
-
- beforeEach(() => {
- testUser = {
- name: TEST_USER,
- roles: ["user_app"],
- };
- localSession.saveUser(testUser, TEST_PASSWORD);
- });
-
- afterEach(async () => {
- localSession.removeUser(TEST_USER);
- window.localStorage.removeItem(LocalSession.DEPRECATED_DB_KEY);
- const tmpDB = new PouchDatabase(undefined);
- await tmpDB.initInMemoryDB(userDBName).destroy();
- await tmpDB.initInMemoryDB(deprecatedDBName).destroy();
- });
-
- it("should be created", () => {
- expect(localSession).toBeDefined();
- });
-
- it("should save user objects to local storage", () => {
- const storedUser: LocalUser = JSON.parse(
- window.localStorage.getItem(testUser.name),
- );
- expect(storedUser.name).toBe(testUser.name);
- expect(storedUser.roles).toEqual(testUser.roles);
- expect(
- passwordEqualsEncrypted(TEST_PASSWORD, storedUser.encryptedPassword),
- ).toBeTrue();
- });
-
- it("should login a previously saved user with correct password", async () => {
- expect(localSession.loginState.value).toBe(LoginState.LOGGED_OUT);
-
- await localSession.login(TEST_USER, TEST_PASSWORD);
-
- expect(localSession.loginState.value).toBe(LoginState.LOGGED_IN);
- });
-
- it("should be case-insensitive and ignore spaces in username", async () => {
- expect(localSession.loginState.value).toBe(LoginState.LOGGED_OUT);
- const user: AuthUser = {
- name: "UserName",
- roles: [],
- };
- localSession.saveUser(user, TEST_PASSWORD);
-
- await localSession.login(" Username ", TEST_PASSWORD);
-
- expect(localSession.loginState.value).toBe(LoginState.LOGGED_IN);
- expect(localSession.getCurrentUser().name).toBe("UserName");
-
- localSession.removeUser("username");
- });
-
- it("should fail login with correct username but wrong password", async () => {
- await localSession.login(TEST_USER, "wrong password");
-
- expect(localSession.loginState.value).toBe(LoginState.LOGIN_FAILED);
- });
-
- it("should fail login with wrong username", async () => {
- await localSession.login("wrongUsername", TEST_PASSWORD);
-
- expect(localSession.loginState.value).toBe(LoginState.UNAVAILABLE);
- });
-
- it("should assign current user after successful login", async () => {
- await localSession.login(TEST_USER, TEST_PASSWORD);
-
- const currentUser = localSession.getCurrentUser();
-
- expect(currentUser.name).toBe(TEST_USER);
- expect(currentUser.roles).toEqual(testUser.roles);
- });
-
- it("should fail login after a user is removed", async () => {
- localSession.removeUser(TEST_USER);
-
- await localSession.login(TEST_USER, TEST_PASSWORD);
-
- expect(localSession.loginState.value).toBe(LoginState.UNAVAILABLE);
- expect(localSession.getCurrentUser()).toBeUndefined();
- });
-
- it("should create a pouchdb with the username of the logged in user", async () => {
- await localSession.login(TEST_USER, TEST_PASSWORD);
-
- expect(database.initInMemoryDB).toHaveBeenCalledWith(
- TEST_USER + "-" + AppSettings.DB_NAME,
- );
- expect(localSession.getDatabase()).toBe(database);
- });
-
- it("should create the database according to the session type in the AppSettings", async () => {
- async function testDatabaseCreation(
- sessionType: SessionType,
- expectedDB: "inMemory" | "indexed",
- ) {
- database.initInMemoryDB.calls.reset();
- database.initIndexedDB.calls.reset();
- environment.session_type = sessionType;
- await localSession.login(TEST_USER, TEST_PASSWORD);
- if (expectedDB === "inMemory") {
- expect(database.initInMemoryDB).toHaveBeenCalled();
- expect(database.initIndexedDB).not.toHaveBeenCalled();
- } else {
- expect(database.initInMemoryDB).not.toHaveBeenCalled();
- expect(database.initIndexedDB).toHaveBeenCalled();
- }
- }
-
- await testDatabaseCreation(SessionType.mock, "inMemory");
- await testDatabaseCreation(SessionType.local, "indexed");
- await testDatabaseCreation(SessionType.synced, "indexed");
- });
-
- it("should use current user db if database has content", async () => {
- await defineExistingDatabases(true, false);
-
- await localSession.login(TEST_USER, TEST_PASSWORD);
-
- expect(database.initInMemoryDB).toHaveBeenCalledOnceWith(userDBName);
- });
-
- it("should use and reserve a deprecated db if it exists and current db has no content", async () => {
- await defineExistingDatabases(false, true);
-
- await localSession.login(TEST_USER, TEST_PASSWORD);
-
- expect(database.initInMemoryDB).toHaveBeenCalledOnceWith(deprecatedDBName);
- const dbReservation = window.localStorage.getItem(
- LocalSession.DEPRECATED_DB_KEY,
- );
- expect(dbReservation).toBe(TEST_USER);
- });
-
- it("should open a new database if deprecated db is already in use", async () => {
- await defineExistingDatabases(false, true, "other-user");
-
- await localSession.login(TEST_USER, TEST_PASSWORD);
-
- expect(database.initInMemoryDB).toHaveBeenCalledOnceWith(userDBName);
- });
-
- it("should use the deprecated database if it is reserved by the current user", async () => {
- await defineExistingDatabases(false, true, TEST_USER);
-
- await localSession.login(TEST_USER, TEST_PASSWORD);
-
- expect(database.initInMemoryDB).toHaveBeenCalledOnceWith(deprecatedDBName);
- });
-
- async function defineExistingDatabases(
- initUserDB: boolean,
- initDeprecatedDB: boolean,
- reserved?: string,
- ) {
- if (reserved) {
- window.localStorage.setItem(LocalSession.DEPRECATED_DB_KEY, reserved);
- }
- const tmpDB = new PouchDatabase(undefined);
- if (initUserDB) {
- await tmpDB.initInMemoryDB(userDBName).put({ _id: "someDoc" });
- }
- if (initDeprecatedDB) {
- await tmpDB.initInMemoryDB(deprecatedDBName).put({ _id: "someDoc" });
- }
- }
-
- testSessionServiceImplementation(() => Promise.resolve(localSession));
-});
diff --git a/src/app/core/session/session-service/local-session.ts b/src/app/core/session/session-service/local-session.ts
deleted file mode 100644
index 141f291a08..0000000000
--- a/src/app/core/session/session-service/local-session.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * This file is part of ndb-core.
- *
- * ndb-core is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * ndb-core is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with ndb-core. If not, see .
- */
-import { Injectable } from "@angular/core";
-import { LoginState } from "../session-states/login-state.enum";
-import {
- encryptPassword,
- LocalUser,
- passwordEqualsEncrypted,
-} from "./local-user";
-import { SessionService } from "./session.service";
-import { PouchDatabase } from "../../database/pouch-database";
-import { AppSettings } from "../../app-settings";
-import { SessionType } from "../session-type";
-import { environment } from "../../../../environments/environment";
-import { AuthUser } from "./auth-user";
-
-/**
- * Responsibilities:
- * - Manage local authentication
- * - Save users in local storage
- * - Create local PouchDB according to session type and logged in user
- */
-@Injectable()
-export class LocalSession extends SessionService {
- static readonly DEPRECATED_DB_KEY = "RESERVED_FOR";
- private currentDBUser: AuthUser;
-
- constructor(private database: PouchDatabase) {
- super();
- }
-
- /**
- * Get a login at the local session by fetching the user from the local storage and validating the password.
- * Returns a Promise resolving with the loginState.
- * @param username Username
- * @param password Password
- */
- public async login(username: string, password: string): Promise {
- const user = this.getStoredUser(username);
- if (user) {
- if (passwordEqualsEncrypted(password, user.encryptedPassword)) {
- await this.handleSuccessfulLogin(user);
- } else {
- this.loginState.next(LoginState.LOGIN_FAILED);
- }
- } else {
- this.loginState.next(LoginState.UNAVAILABLE);
- }
- return this.loginState.value;
- }
-
- private getStoredUser(username: string): LocalUser {
- const stored = window.localStorage.getItem(username?.trim().toLowerCase());
- return JSON.parse(stored);
- }
-
- public async handleSuccessfulLogin(userObject: AuthUser) {
- this.currentDBUser = userObject;
- await this.initializeDatabaseForCurrentUser();
- this.loginState.next(LoginState.LOGGED_IN);
- }
-
- private async initializeDatabaseForCurrentUser() {
- const userDBName = `${this.currentDBUser.name}-${AppSettings.DB_NAME}`;
- // Work on a temporary database before initializing the real one
- const tmpDB = new PouchDatabase(undefined);
- this.initDatabase(userDBName, tmpDB);
- if (!(await tmpDB.isEmpty())) {
- // Current user has own database, we are done here
- this.initDatabase(userDBName);
- return;
- }
-
- this.initDatabase(AppSettings.DB_NAME, tmpDB);
- const dbFallback = window.localStorage.getItem(
- LocalSession.DEPRECATED_DB_KEY,
- );
- const dbAvailable = !dbFallback || dbFallback === this.currentDBUser.name;
- if (dbAvailable && !(await tmpDB.isEmpty())) {
- // Old database is available and can be used by the current user
- window.localStorage.setItem(
- LocalSession.DEPRECATED_DB_KEY,
- this.currentDBUser.name,
- );
- this.initDatabase(AppSettings.DB_NAME);
- return;
- }
-
- // Create a new database for the current user
- this.initDatabase(userDBName);
- }
-
- private initDatabase(dbName: string, db = this.database) {
- if (environment.session_type === SessionType.mock) {
- db.initInMemoryDB(dbName);
- } else {
- db.initIndexedDB(dbName);
- }
- }
-
- /**
- * Saves a user to the local storage
- * @param user a object holding the username and the roles of the user
- * @param password of the user
- * @param loginName (optional) if login also works with a username other than `user.name`. E.g. the email of the user
- */
- public saveUser(user: AuthUser, password: string, loginName = user.name) {
- const localUser: LocalUser = {
- ...user,
- encryptedPassword: encryptPassword(password),
- };
- const loginNameLower = loginName.trim().toLowerCase();
- window.localStorage.setItem(loginNameLower, JSON.stringify(localUser));
- const userNameLower = user.name.trim().toLowerCase();
- if (userNameLower !== loginNameLower) {
- window.localStorage.setItem(userNameLower, JSON.stringify(localUser));
- }
- // Update when already logged in
- if (this.getCurrentUser()?.name === localUser.name) {
- this.currentDBUser = localUser;
- }
- }
-
- /**
- * Removes the user from the local storage.
- * Method never fails, even if the user was not stored before
- * @param username
- */
- public removeUser(username: string) {
- window.localStorage.removeItem(username);
- window.localStorage.removeItem(username.trim().toLowerCase());
- }
-
- public checkPassword(username: string, password: string): boolean {
- const user = this.getStoredUser(username);
- return user && passwordEqualsEncrypted(password, user.encryptedPassword);
- }
-
- public getCurrentUser(): AuthUser {
- return this.currentDBUser;
- }
-
- /**
- * Resets the login state and current user (leaving it in local storage to allow later local login)
- */
- public logout() {
- this.currentDBUser = undefined;
- this.loginState.next(LoginState.LOGGED_OUT);
- }
-
- getDatabase(): PouchDatabase {
- return this.database;
- }
-}
diff --git a/src/app/core/session/session-service/local-user.spec.ts b/src/app/core/session/session-service/local-user.spec.ts
deleted file mode 100644
index 34863a0f05..0000000000
--- a/src/app/core/session/session-service/local-user.spec.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { passwordEqualsEncrypted, encryptPassword } from "./local-user";
-
-describe("LocalUser", () => {
- it("should match a password with its hash", () => {
- const password = "TestPassword123-";
- const encryptedPassword = encryptPassword(password);
-
- expect(passwordEqualsEncrypted(password, encryptedPassword)).toBeTrue();
- });
-});
diff --git a/src/app/core/session/session-service/local-user.ts b/src/app/core/session/session-service/local-user.ts
deleted file mode 100644
index eca30251e7..0000000000
--- a/src/app/core/session/session-service/local-user.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import CryptoES from "crypto-es";
-import { AuthUser } from "./auth-user";
-
-/**
- * User object as prepared and used by the local session.
- */
-export interface LocalUser extends AuthUser {
- encryptedPassword: EncryptedPassword;
-}
-
-export interface EncryptedPassword {
- hash: string;
- salt: string;
- iterations: number;
- keySize: number;
-}
-
-export function encryptPassword(
- password: string,
- iterations = 128,
- keySize = 256 / 32,
- salt = CryptoES.lib.WordArray.random(128 / 8).toString(),
-): EncryptedPassword {
- const hash = CryptoES.PBKDF2(password, salt, {
- keySize: keySize,
- iterations: iterations,
- }).toString();
- return {
- hash: hash,
- iterations: iterations,
- keySize: keySize,
- salt: salt,
- };
-}
-
-export function passwordEqualsEncrypted(
- password: string,
- encryptedPassword: EncryptedPassword,
-): boolean {
- const hash = CryptoES.PBKDF2(password, encryptedPassword?.salt, {
- iterations: encryptedPassword?.iterations,
- keySize: encryptedPassword?.keySize,
- }).toString();
- return hash === encryptedPassword.hash;
-}
diff --git a/src/app/core/session/session-service/remote-session.spec.ts b/src/app/core/session/session-service/remote-session.spec.ts
deleted file mode 100644
index 3d7da111c9..0000000000
--- a/src/app/core/session/session-service/remote-session.spec.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { TestBed } from "@angular/core/testing";
-import { RemoteSession } from "./remote-session";
-import { HttpErrorResponse, HttpStatusCode } from "@angular/common/http";
-import { SessionType } from "../session-type";
-import { LoggingService } from "../../logging/logging.service";
-import { testSessionServiceImplementation } from "./session.service.spec";
-import { LoginState } from "../session-states/login-state.enum";
-import { environment } from "../../../../environments/environment";
-import { AuthService } from "../auth/auth.service";
-import { AuthUser } from "./auth-user";
-import PouchDB from "pouchdb-browser";
-import { TEST_PASSWORD, TEST_USER } from "../../../utils/mock-local-session";
-
-export function mockAuth(user: AuthUser) {
- return (u: string, p: string) => {
- if (u === TEST_USER && p === TEST_PASSWORD) {
- return Promise.resolve(user);
- } else {
- return Promise.reject(
- new HttpErrorResponse({
- status: HttpStatusCode.Unauthorized,
- }),
- );
- }
- };
-}
-
-describe("RemoteSessionService", () => {
- let service: RemoteSession;
- let mockAuthService: jasmine.SpyObj;
- const testUser: AuthUser = { name: TEST_USER, roles: ["user_app"] };
-
- beforeEach(() => {
- environment.session_type = SessionType.mock;
- mockAuthService = jasmine.createSpyObj([
- "authenticate",
- "logout",
- "addAuthHeader",
- "autoLogin",
- ]);
- // Remote session allows TEST_USER and TEST_PASSWORD as valid credentials
- mockAuthService.authenticate.and.callFake(mockAuth(testUser));
-
- TestBed.configureTestingModule({
- providers: [
- RemoteSession,
- LoggingService,
- { provide: AuthService, useValue: mockAuthService },
- ],
- });
-
- service = TestBed.inject(RemoteSession);
- });
-
- it("should be unavailable if requests fails with error other than 401", async () => {
- mockAuthService.authenticate.and.rejectWith(
- new HttpErrorResponse({ status: 501 }),
- );
-
- await service.login(TEST_USER, TEST_PASSWORD);
-
- expect(service.loginState.value).toBe(LoginState.UNAVAILABLE);
- });
-
- it("should try auto-login if fetch fails and fetch again", async () => {
- const initSpy = spyOn(service["database"], "initRemoteDB");
- spyOn(PouchDB, "fetch").and.returnValues(
- Promise.resolve({
- status: HttpStatusCode.Unauthorized,
- ok: false,
- } as Response),
- Promise.resolve({ status: HttpStatusCode.Ok, ok: true } as Response),
- );
- let calls = 0;
- mockAuthService.addAuthHeader.and.callFake((headers) => {
- headers.Authorization = calls++ === 1 ? "valid" : "invalid";
- });
- mockAuthService.autoLogin.and.resolveTo();
- await service.handleSuccessfulLogin(testUser);
- const fetch = initSpy.calls.mostRecent().args[1];
-
- const url = "/db/_changes";
- const opts = { headers: {} };
- await expectAsync(fetch(url, opts)).toBeResolved();
-
- expect(PouchDB.fetch).toHaveBeenCalledTimes(2);
- expect(PouchDB.fetch).toHaveBeenCalledWith(url, opts);
- expect(opts.headers).toEqual({ Authorization: "valid" });
- expect(mockAuthService.autoLogin).toHaveBeenCalled();
- expect(mockAuthService.addAuthHeader).toHaveBeenCalledTimes(2);
- });
-
- testSessionServiceImplementation(() => Promise.resolve(service));
-});
diff --git a/src/app/core/session/session-service/remote-session.ts b/src/app/core/session/session-service/remote-session.ts
deleted file mode 100644
index 9c3d6e560e..0000000000
--- a/src/app/core/session/session-service/remote-session.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * This file is part of ndb-core.
- *
- * ndb-core is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * ndb-core is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with ndb-core. If not, see .
- */
-import { Injectable } from "@angular/core";
-import { HttpErrorResponse, HttpStatusCode } from "@angular/common/http";
-import { SessionService } from "./session.service";
-import { LoginState } from "../session-states/login-state.enum";
-import { PouchDatabase } from "../../database/pouch-database";
-import { LoggingService } from "../../logging/logging.service";
-import PouchDB from "pouchdb-browser";
-import { AppSettings } from "../../app-settings";
-import { AuthService } from "../auth/auth.service";
-import { AuthUser } from "./auth-user";
-
-/**
- * Responsibilities:
- * - Hold the remote DB
- * - Handle auth against CouchDB
- * - provide "am i online"-info
- */
-@Injectable()
-export class RemoteSession extends SessionService {
- /** remote (!) PouchDB */
- private readonly database: PouchDatabase;
- private currentDBUser: AuthUser;
-
- /**
- * Create a RemoteSession and set up connection to the remote CouchDB server with valid authentication.
- */
- constructor(
- private loggingService: LoggingService,
- private authService: AuthService,
- ) {
- super();
- this.database = new PouchDatabase(this.loggingService);
- }
-
- /**
- * Connect to the remote Database. Tries to determine from a possible error whether the login was rejected or the user is offline.
- * @param username Username
- * @param password Password
- */
- public async login(username: string, password: string): Promise {
- try {
- const user = await this.authService.authenticate(username, password);
- await this.handleSuccessfulLogin(user);
- this.loginState.next(LoginState.LOGGED_IN);
- } catch (error) {
- const httpError = error as HttpErrorResponse;
- if (httpError?.status === HttpStatusCode.Unauthorized) {
- this.loginState.next(LoginState.LOGIN_FAILED);
- } else {
- this.loggingService.error(error);
- this.loginState.next(LoginState.UNAVAILABLE);
- }
- }
- return this.loginState.value;
- }
-
- public async handleSuccessfulLogin(userObject: AuthUser) {
- this.database.initRemoteDB(
- `${AppSettings.DB_PROXY_PREFIX}/${AppSettings.DB_NAME}`,
- (url, opts: any) => {
- if (typeof url === "string") {
- const remoteUrl =
- AppSettings.DB_PROXY_PREFIX +
- url.split(AppSettings.DB_PROXY_PREFIX)[1];
- return this.sendRequest(remoteUrl, opts).then((initialRes) =>
- // retry login if request failed with unauthorized
- initialRes.status === HttpStatusCode.Unauthorized
- ? this.authService
- .autoLogin()
- .then(() => this.sendRequest(remoteUrl, opts))
- // return initial response if request failed again
- .then((newRes) => (newRes.ok ? newRes : initialRes))
- .catch(() => initialRes)
- : initialRes,
- );
- }
- },
- );
- this.currentDBUser = userObject;
- this.loginState.next(LoginState.LOGGED_IN);
- }
-
- private sendRequest(url: string, opts) {
- this.authService.addAuthHeader(opts.headers);
- return PouchDB.fetch(url, opts);
- }
-
- /**
- * Logout at the remote database.
- */
- public async logout(): Promise {
- await this.authService.logout();
- this.currentDBUser = undefined;
- this.loginState.next(LoginState.LOGGED_OUT);
- }
-
- getCurrentUser(): AuthUser {
- return this.currentDBUser;
- }
-
- checkPassword(username: string, password: string): boolean {
- // Cannot be checked against CouchDB due to cookie-auth
- throw Error("Can't check password in remote session");
- }
-
- getDatabase(): PouchDatabase {
- return this.database;
- }
-}
diff --git a/src/app/core/session/session-service/session-manager.service.spec.ts b/src/app/core/session/session-service/session-manager.service.spec.ts
new file mode 100644
index 0000000000..eccadd15b2
--- /dev/null
+++ b/src/app/core/session/session-service/session-manager.service.spec.ts
@@ -0,0 +1,242 @@
+/*
+ * This file is part of ndb-core.
+ *
+ * ndb-core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ndb-core is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with ndb-core. If not, see .
+ */
+
+import { SessionManagerService } from "./session-manager.service";
+import { LoginState } from "../session-states/login-state.enum";
+import {
+ LoginStateSubject,
+ SessionType,
+ SyncStateSubject,
+} from "../session-type";
+import { TestBed, waitForAsync } from "@angular/core/testing";
+import { PouchDatabase } from "../../database/pouch-database";
+import { environment } from "../../../../environments/environment";
+import { AuthUser } from "../auth/auth-user";
+import { TEST_USER } from "../../../utils/mock-local-session";
+import { LocalAuthService } from "../auth/local/local-auth.service";
+import { SyncService } from "../../database/sync.service";
+import { KeycloakAuthService } from "../auth/keycloak/keycloak-auth.service";
+import { Database } from "../../database/database";
+import { Router } from "@angular/router";
+import { CurrentUserSubject } from "../../user/user";
+import { AppSettings } from "../../app-settings";
+import { NAVIGATOR_TOKEN } from "../../../utils/di-tokens";
+
+describe("SessionManagerService", () => {
+ let service: SessionManagerService;
+ let loginStateSubject: LoginStateSubject;
+ let userSubject: CurrentUserSubject;
+ let mockKeycloak: jasmine.SpyObj;
+ let mockNavigator: { onLine: boolean };
+ let dbUser: AuthUser;
+ const userDBName = `${TEST_USER}-${AppSettings.DB_NAME}`;
+ const deprecatedDBName = AppSettings.DB_NAME;
+ let initInMemorySpy: jasmine.Spy;
+ let initIndexedSpy: jasmine.Spy;
+
+ beforeEach(waitForAsync(() => {
+ dbUser = { name: TEST_USER, roles: ["user_app"] };
+ mockKeycloak = jasmine.createSpyObj(["login", "logout", "addAuthHeader"]);
+ mockKeycloak.login.and.resolveTo(dbUser);
+ mockNavigator = { onLine: true };
+
+ TestBed.configureTestingModule({
+ providers: [
+ SessionManagerService,
+ SyncStateSubject,
+ LoginStateSubject,
+ CurrentUserSubject,
+ { provide: Database, useClass: PouchDatabase },
+ { provide: KeycloakAuthService, useValue: mockKeycloak },
+ { provide: NAVIGATOR_TOKEN, useValue: mockNavigator },
+ {
+ provide: Router,
+ useValue: {
+ navigate: () => Promise.resolve(),
+ routerState: { snapshot: {} },
+ },
+ },
+ ],
+ });
+ service = TestBed.inject(SessionManagerService);
+ loginStateSubject = TestBed.inject(LoginStateSubject);
+ userSubject = TestBed.inject(CurrentUserSubject);
+
+ const db = TestBed.inject(Database) as PouchDatabase;
+ initInMemorySpy = spyOn(db, "initInMemoryDB").and.callThrough();
+ initIndexedSpy = spyOn(db, "initIndexedDB").and.callThrough();
+ TestBed.inject(LocalAuthService).saveUser(dbUser);
+ environment.session_type = SessionType.mock;
+ spyOn(service, "remoteLoginAvailable").and.returnValue(true);
+ }));
+
+ afterEach(async () => {
+ localStorage.clear();
+ const tmpDB = new PouchDatabase(undefined);
+ await tmpDB.initInMemoryDB(userDBName).destroy();
+ await tmpDB.initInMemoryDB(deprecatedDBName).destroy();
+ });
+
+ it("should update the local user object once authenticated", async () => {
+ const updatedUser: AuthUser = {
+ name: TEST_USER,
+ roles: dbUser.roles.concat("admin"),
+ };
+ mockKeycloak.login.and.resolveTo(updatedUser);
+ const saveUserSpy = spyOn(TestBed.inject(LocalAuthService), "saveUser");
+ const syncSpy = spyOn(TestBed.inject(SyncService), "startSync");
+
+ await service.remoteLogin();
+
+ expect(saveUserSpy).toHaveBeenCalledWith(updatedUser);
+ expect(userSubject.value).toEqual(updatedUser);
+ expect(syncSpy).toHaveBeenCalled();
+ expect(loginStateSubject.value).toBe(LoginState.LOGGED_IN);
+ });
+
+ it("should automatically login, if the session is still valid", async () => {
+ await service.remoteLogin();
+
+ expect(loginStateSubject.value).toEqual(LoginState.LOGGED_IN);
+ expect(userSubject.value).toEqual(dbUser);
+ });
+
+ it("should trigger remote logout if remote login succeeded before", async () => {
+ await service.remoteLogin();
+
+ service.logout();
+
+ expect(mockKeycloak.logout).toHaveBeenCalled();
+ });
+
+ it("should only reset local state if remote login did not happen", async () => {
+ const navigateSpy = spyOn(TestBed.inject(Router), "navigate");
+ await service.offlineLogin(dbUser);
+ expect(loginStateSubject.value).toBe(LoginState.LOGGED_IN);
+ expect(userSubject.value).toEqual(dbUser);
+
+ service.logout();
+
+ expect(mockKeycloak.logout).not.toHaveBeenCalled();
+ expect(loginStateSubject.value).toBe(LoginState.LOGGED_OUT);
+ expect(userSubject.value).toBeUndefined();
+ expect(navigateSpy).toHaveBeenCalled();
+ });
+
+ it("should store information if remote session needs to be reset", async () => {
+ await service.remoteLogin();
+ mockNavigator.onLine = false;
+
+ await service.logout();
+
+ expect(
+ localStorage.getItem(service.RESET_REMOTE_SESSION_KEY),
+ ).toBeDefined();
+ });
+
+ it("should trigger a remote logout if reset flag has been set", async () => {
+ localStorage.setItem(service.RESET_REMOTE_SESSION_KEY, "true");
+
+ service.clearRemoteSessionIfNecessary();
+
+ expect(mockKeycloak.logout).toHaveBeenCalled();
+ });
+
+ it("should create a pouchdb with the username of the logged in user", async () => {
+ await service.remoteLogin();
+
+ expect(initInMemorySpy).toHaveBeenCalledWith(
+ TEST_USER + "-" + AppSettings.DB_NAME,
+ );
+ });
+
+ it("should create the database according to the session type in the AppSettings", async () => {
+ async function testDatabaseCreation(
+ sessionType: SessionType,
+ expectedDB: "inMemory" | "indexed",
+ ) {
+ initInMemorySpy.calls.reset();
+ initIndexedSpy.calls.reset();
+ environment.session_type = sessionType;
+ await service.remoteLogin();
+ if (expectedDB === "inMemory") {
+ expect(initInMemorySpy).toHaveBeenCalled();
+ expect(initIndexedSpy).not.toHaveBeenCalled();
+ } else {
+ expect(initInMemorySpy).not.toHaveBeenCalled();
+ expect(initIndexedSpy).toHaveBeenCalled();
+ }
+ }
+
+ await testDatabaseCreation(SessionType.mock, "inMemory");
+ await testDatabaseCreation(SessionType.local, "indexed");
+ await testDatabaseCreation(SessionType.synced, "indexed");
+ });
+
+ it("should use current user db if database has content", async () => {
+ await defineExistingDatabases(true, false);
+
+ await service.remoteLogin();
+
+ expect(initInMemorySpy).toHaveBeenCalledOnceWith(userDBName);
+ });
+
+ it("should use and reserve a deprecated db if it exists and current db has no content", async () => {
+ await defineExistingDatabases(false, true);
+
+ await service.remoteLogin();
+
+ expect(initInMemorySpy).toHaveBeenCalledOnceWith(deprecatedDBName);
+ const dbReservation = window.localStorage.getItem(
+ service.DEPRECATED_DB_KEY,
+ );
+ expect(dbReservation).toBe(TEST_USER);
+ });
+
+ it("should open a new database if deprecated db is already in use", async () => {
+ await defineExistingDatabases(false, true, "other-user");
+
+ await service.remoteLogin();
+
+ expect(initInMemorySpy).toHaveBeenCalledOnceWith(userDBName);
+ });
+
+ it("should use the deprecated database if it is reserved by the current user", async () => {
+ await defineExistingDatabases(false, true, TEST_USER);
+
+ await service.remoteLogin();
+
+ expect(initInMemorySpy).toHaveBeenCalledOnceWith(deprecatedDBName);
+ });
+
+ async function defineExistingDatabases(
+ initUserDB: boolean,
+ initDeprecatedDB: boolean,
+ reserved?: string,
+ ) {
+ if (reserved) {
+ window.localStorage.setItem(service.DEPRECATED_DB_KEY, reserved);
+ }
+ const tmpDB = new PouchDatabase(undefined);
+ if (initUserDB) {
+ await tmpDB.initInMemoryDB(userDBName).put({ _id: "someDoc" });
+ }
+ if (initDeprecatedDB) {
+ await tmpDB.initInMemoryDB(deprecatedDBName).put({ _id: "someDoc" });
+ }
+ }
+});
diff --git a/src/app/core/session/session-service/session-manager.service.ts b/src/app/core/session/session-service/session-manager.service.ts
new file mode 100644
index 0000000000..0a1d682fa9
--- /dev/null
+++ b/src/app/core/session/session-service/session-manager.service.ts
@@ -0,0 +1,171 @@
+/*
+ * This file is part of ndb-core.
+ *
+ * ndb-core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ndb-core is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with ndb-core. If not, see .
+ */
+
+import { Inject, Injectable } from "@angular/core";
+
+import { AuthUser } from "../auth/auth-user";
+import { SyncService } from "../../database/sync.service";
+import { LoginStateSubject, SessionType } from "../session-type";
+import { LoginState } from "../session-states/login-state.enum";
+import { Router } from "@angular/router";
+import { KeycloakAuthService } from "../auth/keycloak/keycloak-auth.service";
+import { LocalAuthService } from "../auth/local/local-auth.service";
+import { CurrentUserSubject } from "../../user/user";
+import { AppSettings } from "../../app-settings";
+import { PouchDatabase } from "../../database/pouch-database";
+import { environment } from "../../../../environments/environment";
+import { Database } from "../../database/database";
+import { NAVIGATOR_TOKEN } from "../../../utils/di-tokens";
+
+/**
+ * This service handles the user session.
+ * This includes a online and offline login and logout.
+ * After a successful login, the database for the current user is initialised.
+ */
+@Injectable()
+export class SessionManagerService {
+ readonly DEPRECATED_DB_KEY = "RESERVED_FOR";
+ readonly RESET_REMOTE_SESSION_KEY = "RESET_REMOTE";
+ private pouchDatabase: PouchDatabase;
+ private remoteLoggedIn = false;
+ constructor(
+ private remoteAuthService: KeycloakAuthService,
+ private localAuthService: LocalAuthService,
+ private syncService: SyncService,
+ private currentUser: CurrentUserSubject,
+ private loginStateSubject: LoginStateSubject,
+ private router: Router,
+ private database: Database,
+ @Inject(NAVIGATOR_TOKEN) private navigator: Navigator,
+ ) {
+ if (database instanceof PouchDatabase) {
+ this.pouchDatabase = database;
+ }
+ }
+
+ /**
+ * Login for a remote session and start the sync.
+ * After a user has logged in once online, this user can later also use the app offline.
+ * Should only be called if there is a internet connection
+ */
+ async remoteLogin() {
+ this.loginStateSubject.next(LoginState.IN_PROGRESS);
+ if (this.remoteLoginAvailable()) {
+ return this.remoteAuthService
+ .login()
+ .then((user) => this.handleRemoteLogin(user))
+ .catch((err) => {
+ this.loginStateSubject.next(LoginState.LOGIN_FAILED);
+ throw err;
+ });
+ }
+ this.loginStateSubject.next(LoginState.LOGIN_FAILED);
+ }
+
+ remoteLoginAvailable() {
+ return navigator.onLine && environment.session_type === SessionType.synced;
+ }
+
+ /**
+ * Login a offline session without sync.
+ * @param user
+ */
+ offlineLogin(user: AuthUser) {
+ return this.initializeUser(user);
+ }
+
+ private async initializeUser(user: AuthUser) {
+ await this.initializeDatabaseForCurrentUser(user);
+ this.currentUser.next(user);
+ this.loginStateSubject.next(LoginState.LOGGED_IN);
+ }
+
+ /**
+ * Get a list of all users that can login offline
+ */
+ getOfflineUsers(): AuthUser[] {
+ return this.localAuthService.getStoredUsers();
+ }
+
+ /**
+ * If online, clear the remote session.
+ * If offline, reset the state and forward to login page.
+ */
+ async logout() {
+ if (this.remoteLoggedIn) {
+ if (this.navigator.onLine) {
+ // This will forward to the keycloak logout page
+ await this.remoteAuthService.logout();
+ } else {
+ localStorage.setItem(this.RESET_REMOTE_SESSION_KEY, "1");
+ }
+ }
+ this.currentUser.next(undefined);
+ this.loginStateSubject.next(LoginState.LOGGED_OUT);
+ this.remoteLoggedIn = false;
+ return this.router.navigate(["/login"], {
+ queryParams: { redirect_uri: this.router.routerState.snapshot.url },
+ });
+ }
+
+ clearRemoteSessionIfNecessary() {
+ if (localStorage.getItem(this.RESET_REMOTE_SESSION_KEY)) {
+ localStorage.removeItem(this.RESET_REMOTE_SESSION_KEY);
+ return this.remoteAuthService.logout();
+ }
+ }
+
+ private async handleRemoteLogin(user: AuthUser) {
+ this.remoteLoggedIn = true;
+ await this.initializeUser(user);
+ this.syncService.startSync();
+ this.localAuthService.saveUser(user);
+ }
+
+ private async initializeDatabaseForCurrentUser(user: AuthUser) {
+ const userDBName = `${user.name}-${AppSettings.DB_NAME}`;
+ // Work on a temporary database before initializing the real one
+ const tmpDB = new PouchDatabase(undefined);
+ this.initDatabase(userDBName, tmpDB);
+ if (!(await tmpDB.isEmpty())) {
+ // Current user has own database, we are done here
+ this.initDatabase(userDBName);
+ return;
+ }
+
+ this.initDatabase(AppSettings.DB_NAME, tmpDB);
+ const dbFallback = window.localStorage.getItem(this.DEPRECATED_DB_KEY);
+ const dbAvailable = !dbFallback || dbFallback === user.name;
+ if (dbAvailable && !(await tmpDB.isEmpty())) {
+ // Old database is available and can be used by the current user
+ window.localStorage.setItem(this.DEPRECATED_DB_KEY, user.name);
+ this.initDatabase(AppSettings.DB_NAME);
+ return;
+ }
+
+ // Create a new database for the current user
+ this.initDatabase(userDBName);
+ }
+
+ private initDatabase(dbName: string, db = this.pouchDatabase) {
+ if (environment.session_type === SessionType.mock) {
+ db.initInMemoryDB(dbName);
+ } else {
+ db.initIndexedDB(dbName);
+ }
+ }
+}
diff --git a/src/app/core/session/session-service/session.service.spec.ts b/src/app/core/session/session-service/session.service.spec.ts
deleted file mode 100644
index 42cfa6a98c..0000000000
--- a/src/app/core/session/session-service/session.service.spec.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * This file is part of ndb-core.
- *
- * ndb-core is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * ndb-core is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with ndb-core. If not, see .
- */
-
-import { LoginState } from "../session-states/login-state.enum";
-import { SessionService } from "./session.service";
-import { SyncState } from "../session-states/sync-state.enum";
-import { AuthUser } from "./auth-user";
-import { TEST_PASSWORD, TEST_USER } from "../../../utils/mock-local-session";
-
-/**
- * Default tests for testing basic functionality of any SessionService implementation.
- * The session has to be setup, so TEST_USER and TEST_PASSWORD are (the only) valid credentials
- *
- * @example
- describe("TestSessionService", async () => {
- testSessionServiceImplementation(async () => {
- return new TestSessionService();
- });
-});
- *
- * @param sessionSetupFunction An async function creating a session instance to be tested
- */
-export function testSessionServiceImplementation(
- sessionSetupFunction: () => Promise,
-) {
- let sessionService: SessionService;
-
- beforeEach(async () => {
- sessionService = await sessionSetupFunction();
- });
-
- it("has the correct initial state", () => {
- expect(sessionService.syncState.value).toBe(SyncState.UNSYNCED);
- expectNotToBeLoggedIn(LoginState.LOGGED_OUT);
- });
-
- it("succeeds login", async () => {
- const loginResult = await sessionService.login(TEST_USER, TEST_PASSWORD);
-
- expect(loginResult).toEqual(LoginState.LOGGED_IN);
-
- expect(sessionService.loginState.value)
- .withContext("unexpected LoginState")
- .toEqual(LoginState.LOGGED_IN);
-
- expect(sessionService.isLoggedIn())
- .withContext("unexpected isLoggedIn")
- .toBeTrue();
- expect(sessionService.getCurrentUser().name).toBe(TEST_USER);
- });
-
- it("fails login with wrong password", async () => {
- const loginResult = await sessionService.login(TEST_USER, "");
-
- expect(loginResult).toEqual(LoginState.LOGIN_FAILED);
- expectNotToBeLoggedIn(LoginState.LOGIN_FAILED);
- });
-
- it("fails login with wrong/non-existing username", async () => {
- const loginResult = await sessionService.login("other", TEST_PASSWORD);
-
- // The LocalSession returns LoginState.UNAVAILABLE for unknown users because they might be available remote
- const failedStates = [LoginState.LOGIN_FAILED, LoginState.UNAVAILABLE];
- expect(failedStates).toContain(loginResult);
- });
-
- it("logs out and resets states", async () => {
- const loginResult = await sessionService.login(TEST_USER, TEST_PASSWORD);
- expect(loginResult).toEqual(LoginState.LOGGED_IN);
-
- await sessionService.logout();
- expectNotToBeLoggedIn(LoginState.LOGGED_OUT);
- });
-
- it("it correctly handles the necessary steps after a successful login", async () => {
- const dummyUser: AuthUser = {
- name: "Hanspeter",
- roles: ["user_app"],
- };
- await sessionService.handleSuccessfulLogin(dummyUser);
- expect(sessionService.loginState.value).toEqual(LoginState.LOGGED_IN);
- expect(sessionService.getCurrentUser()).toEqual(dummyUser);
- });
-
- /**
- * Check all states of the session to be "logged out".
- * @param expectedLoginState The expected LoginState (failed or simply logged out)
- */
- function expectNotToBeLoggedIn(
- expectedLoginState:
- | LoginState.LOGGED_OUT
- | LoginState.LOGIN_FAILED
- | LoginState.UNAVAILABLE,
- ) {
- expect(sessionService.loginState.value)
- .withContext("unexpected LoginState")
- .toEqual(expectedLoginState);
-
- expect(sessionService.isLoggedIn())
- .withContext("unexpected isLoggedIn")
- .toBeFalse();
- expect(sessionService.getCurrentUser()).toBeUndefined();
- }
-}
diff --git a/src/app/core/session/session-service/session.service.ts b/src/app/core/session/session-service/session.service.ts
deleted file mode 100644
index e8e2625435..0000000000
--- a/src/app/core/session/session-service/session.service.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * This file is part of ndb-core.
- *
- * ndb-core is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * ndb-core is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with ndb-core. If not, see .
- */
-
-import { LoginState } from "../session-states/login-state.enum";
-import { Database } from "../../database/database";
-import { SyncState } from "../session-states/sync-state.enum";
-import { BehaviorSubject } from "rxjs";
-import { AuthUser } from "./auth-user";
-
-/**
- * A session manages user authentication and database connection for the app.
- *
- * To get the current user object in any other class, inject the SessionService.
- *
- * The SessionService also sets up and provides the Database.
- * To access the database in other classes
- * you should rather inject the `Database` or the `EntityMapperService` directly and not the SessionService, however.
- *
- * This SessionService is the abstract base class for the concrete implementations like SyncedSessionService.
- * You should still use `SessionService` as the dependency injection key to get access to the functionality.
- * Providers are set up in a way that you will get the correct implementation during runtime.
- */
-export abstract class SessionService {
- /** StateHandler for login state changes */
- private _loginState = new BehaviorSubject(LoginState.LOGGED_OUT);
- /** StateHandler for sync state changes */
- private _syncState = new BehaviorSubject(SyncState.UNSYNCED);
-
- /**
- * Authenticate a user.
- * @param username
- * @param password
- */
- abstract login(username: string, password: string): Promise;
-
- /**
- * Do the necessary steps after the login has been successful.
- * i.e. set the current user and change the login state
- * @param userObject the user that is successfully loged in
- */
- abstract handleSuccessfulLogin(userObject: AuthUser): Promise;
-
- /**
- * Logout the current user.
- */
- abstract logout();
-
- /**
- * Get the current user according to the CouchDB format
- */
- abstract getCurrentUser(): AuthUser;
-
- /**
- * Check a password if its valid
- * @param username the username for which the password should be checked
- * @param password the password to be checked
- * @returns boolean true if the password is correct, false otherwise
- */
- abstract checkPassword(username: string, password: string): boolean;
-
- /**
- * Get the session status - whether a user is authenticated currently.
- */
- public isLoggedIn(): boolean {
- return this.loginState.value === LoginState.LOGGED_IN;
- }
-
- /**
- * Get the state of the session.
- */
- public get loginState(): BehaviorSubject {
- return this._loginState;
- }
-
- /**
- * Get the state of the synchronization with the remote server.
- */
- public get syncState(): BehaviorSubject {
- return this._syncState;
- }
-
- /**
- * Get the database for the current session.
- */
- abstract getDatabase(): Database;
-}
diff --git a/src/app/core/session/session-service/synced-session.service.spec.ts b/src/app/core/session/session-service/synced-session.service.spec.ts
deleted file mode 100644
index c7414e2032..0000000000
--- a/src/app/core/session/session-service/synced-session.service.spec.ts
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
- * This file is part of ndb-core.
- *
- * ndb-core is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * ndb-core is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with ndb-core. If not, see .
- */
-
-import { SyncedSessionService } from "./synced-session.service";
-import { LoginState } from "../session-states/login-state.enum";
-import { LocalSession } from "./local-session";
-import { RemoteSession } from "./remote-session";
-import { SessionType } from "../session-type";
-import {
- fakeAsync,
- flush,
- TestBed,
- tick,
- waitForAsync,
-} from "@angular/core/testing";
-import { HttpErrorResponse, HttpStatusCode } from "@angular/common/http";
-import { MockedTestingModule } from "../../../utils/mocked-testing.module";
-import { testSessionServiceImplementation } from "./session.service.spec";
-import { PouchDatabase } from "../../database/pouch-database";
-import { SessionModule } from "../session.module";
-import { LOCATION_TOKEN } from "../../../utils/di-tokens";
-import { environment } from "../../../../environments/environment";
-import { AuthService } from "../auth/auth.service";
-import { AuthUser } from "./auth-user";
-import { mockAuth } from "./remote-session.spec";
-import { TEST_PASSWORD, TEST_USER } from "../../../utils/mock-local-session";
-
-describe("SyncedSessionService", () => {
- let sessionService: SyncedSessionService;
- let localSession: LocalSession;
- let remoteSession: RemoteSession;
- let localLoginSpy: jasmine.Spy<
- (username: string, password: string) => Promise
- >;
- let remoteLoginSpy: jasmine.Spy<
- (username: string, password: string) => Promise
- >;
- let dbUser: AuthUser;
- let syncSpy: jasmine.Spy<() => Promise>;
- let liveSyncSpy: jasmine.Spy<() => any>;
- let mockAuthService: jasmine.SpyObj;
- let mockLocation: jasmine.SpyObj;
-
- beforeEach(waitForAsync(() => {
- mockLocation = jasmine.createSpyObj(["reload"]);
- mockAuthService = jasmine.createSpyObj([
- "authenticate",
- "autoLogin",
- "logout",
- ]);
- mockAuthService.autoLogin.and.rejectWith();
-
- TestBed.configureTestingModule({
- imports: [SessionModule, MockedTestingModule],
- providers: [
- PouchDatabase,
- { provide: AuthService, useValue: mockAuthService },
- { provide: LOCATION_TOKEN, useValue: mockLocation },
- ],
- });
- environment.session_type = SessionType.mock;
- sessionService = TestBed.inject(SyncedSessionService);
-
- localSession = TestBed.inject(LocalSession);
- remoteSession = TestBed.inject(RemoteSession);
-
- // Setting up local and remote session to accept TEST_USER and TEST_PASSWORD as valid credentials
- dbUser = { name: TEST_USER, roles: ["user_app"] };
- localSession.saveUser({ name: TEST_USER, roles: [] }, TEST_PASSWORD);
- mockAuthService.authenticate.and.callFake(mockAuth(dbUser));
-
- localLoginSpy = spyOn(localSession, "login").and.callThrough();
- remoteLoginSpy = spyOn(remoteSession, "login").and.callThrough();
- syncSpy = spyOn(sessionService, "sync").and.resolveTo();
- liveSyncSpy = spyOn(sessionService, "liveSyncDeferred");
- }));
-
- afterEach(() => {
- localSession.removeUser(TEST_USER);
- });
-
- it("Remote and local fail (normal login with wrong password)", fakeAsync(() => {
- const result = sessionService.login("anotherUser", "wrongPassword");
- tick();
-
- expect(localLoginSpy).toHaveBeenCalledWith("anotherUser", "wrongPassword");
- expect(remoteLoginSpy).toHaveBeenCalledWith("anotherUser", "wrongPassword");
- expect(syncSpy).not.toHaveBeenCalled();
- expectAsync(result).toBeResolvedTo(LoginState.LOGIN_FAILED);
- flush();
- }));
-
- it("Remote unavailable, local succeeds (offline)", fakeAsync(() => {
- failRemoteLogin(true);
-
- const result = sessionService.login(TEST_USER, TEST_PASSWORD);
- tick();
-
- expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
- expect(remoteLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
- expect(syncSpy).not.toHaveBeenCalled();
- expectAsync(result).toBeResolvedTo(LoginState.LOGGED_IN);
-
- sessionService.cancelLoginOfflineRetry();
- flush();
- }));
-
- it("Remote unavailable, local fails (offline, wrong password)", fakeAsync(() => {
- failRemoteLogin(true);
-
- const result = sessionService.login(TEST_USER, "wrongPassword");
- tick();
-
- expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, "wrongPassword");
- expect(remoteLoginSpy).toHaveBeenCalledWith(TEST_USER, "wrongPassword");
- expect(syncSpy).not.toHaveBeenCalled();
- expectAsync(result).toBeResolvedTo(LoginState.LOGIN_FAILED);
- tick();
- }));
-
- it("Remote succeeds, local fails (password changed and new password entered/new user)", fakeAsync(() => {
- const newUser = { name: "newUser", roles: ["user_app"] };
- passRemoteLogin(newUser);
- spyOn(localSession, "saveUser").and.callThrough();
-
- const result = sessionService.login(newUser.name, "p");
- tick();
-
- expect(localLoginSpy.calls.allArgs()).toEqual([
- [newUser.name, "p"],
- [newUser.name, "p"],
- ]);
- expect(remoteLoginSpy.calls.allArgs()).toEqual([[newUser.name, "p"]]);
- expect(syncSpy).toHaveBeenCalledTimes(1);
- expect(liveSyncSpy).toHaveBeenCalledTimes(1);
- expectAsync(result).toBeResolvedTo(LoginState.LOGGED_IN);
- expect(localSession.saveUser).toHaveBeenCalledWith(
- {
- name: newUser.name,
- roles: newUser.roles,
- },
- "p",
- newUser.name,
- );
- expect(sessionService.getCurrentUser().name).toBe("newUser");
- expect(sessionService.getCurrentUser().roles).toEqual(["user_app"]);
- tick();
- localSession.removeUser(newUser.name);
- }));
-
- it("Remote fails, local succeeds (Password changes, old password entered)", fakeAsync(() => {
- failRemoteLogin();
- spyOn(localSession, "removeUser").and.callThrough();
-
- const result = sessionService.login(TEST_USER, TEST_PASSWORD);
- tick();
-
- // The local user is removed to prohibit further offline login
- expect(localSession.removeUser).toHaveBeenCalledWith(TEST_USER);
- // Initially the user is logged in
- expectAsync(result).toBeResolvedTo(LoginState.LOGGED_IN);
- // After remote session fails the user is logged out again
- expect(sessionService.loginState.value).toBe(LoginState.LOGGED_OUT);
- flush();
- }));
-
- it("Remote and local succeed, sync fails", fakeAsync(() => {
- syncSpy.and.rejectWith();
-
- const login = sessionService.login(TEST_USER, TEST_PASSWORD);
- tick();
-
- expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
- expect(remoteLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
- expect(syncSpy).toHaveBeenCalled();
- expect(liveSyncSpy).toHaveBeenCalled();
- expectAsync(login).toBeResolvedTo(LoginState.LOGGED_IN);
-
- // clear timeouts and intervals
- sessionService.logout();
- flush();
- }));
-
- it("remote and local unavailable", fakeAsync(() => {
- failRemoteLogin(true);
-
- const result = sessionService.login("anotherUser", "anotherPassword");
- tick();
-
- expect(localLoginSpy).toHaveBeenCalledWith(
- "anotherUser",
- "anotherPassword",
- );
- expect(remoteLoginSpy).toHaveBeenCalledWith(
- "anotherUser",
- "anotherPassword",
- );
- expect(syncSpy).not.toHaveBeenCalled();
- expectAsync(result).toBeResolvedTo(LoginState.UNAVAILABLE);
-
- flush();
- }));
-
- it("should update the local user object once connected", fakeAsync(() => {
- const updatedUser: AuthUser = {
- name: TEST_USER,
- roles: dbUser.roles.concat("admin"),
- };
- passRemoteLogin(updatedUser);
-
- const result = sessionService.login(TEST_USER, TEST_PASSWORD);
- tick();
-
- expect(localLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
- expect(remoteLoginSpy).toHaveBeenCalledWith(TEST_USER, TEST_PASSWORD);
- expect(syncSpy).toHaveBeenCalledTimes(1);
- expect(liveSyncSpy).toHaveBeenCalledTimes(1);
-
- const currentUser = localSession.getCurrentUser();
- expect(currentUser.name).toEqual(TEST_USER);
- expect(currentUser.roles).toEqual(["user_app", "admin"]);
- expectAsync(result).toBeResolvedTo(LoginState.LOGGED_IN);
- tick();
- }));
-
- it("should login, if the session is still valid", fakeAsync(() => {
- mockAuthService.autoLogin.and.resolveTo(dbUser);
-
- sessionService.checkForValidSession();
- tick();
- expect(sessionService.loginState.value).toEqual(LoginState.LOGGED_IN);
- }));
-
- it("should support email instead of username for login", async () => {
- const newUser: AuthUser = { name: "test-user", roles: ["test-role"] };
- passRemoteLogin(newUser);
-
- const res = await sessionService.login("my@email.com", "test-pass");
-
- expect(res).toBe(LoginState.LOGGED_IN);
- expect(JSON.parse(localStorage.getItem("test-user"))).toEqual(
- jasmine.objectContaining(newUser),
- );
- expect(JSON.parse(localStorage.getItem("my@email.com"))).toEqual(
- jasmine.objectContaining(newUser),
- );
-
- localStorage.removeItem("test-user");
- localStorage.removeItem("my@email.com");
- });
-
- it("should correctly check the password", () => {
- localSession.saveUser({ name: "TestUser", roles: [] }, TEST_PASSWORD);
-
- expect(sessionService.checkPassword("TestUser", TEST_PASSWORD)).toBeTrue();
- expect(sessionService.checkPassword("TestUser", "wrongPW")).toBeFalse();
- });
-
- it("should restart the sync if it fails at one point", fakeAsync(() => {
- let errorCallback, pauseCallback;
- const syncHandle = {
- on: (action, callback) => {
- if (action === "error") {
- errorCallback = callback;
- }
- if (action === "paused") {
- pauseCallback = callback;
- }
- return syncHandle;
- },
- cancel: () => undefined,
- };
- syncSpy = jasmine.createSpy().and.returnValue(syncHandle);
- liveSyncSpy.and.callThrough();
- spyOn(localSession, "getDatabase").and.returnValue({
- getPouchDB: () => ({ sync: syncSpy }),
- } as any);
-
- passRemoteLogin();
- sessionService.login(TEST_USER, TEST_PASSWORD);
- flush();
-
- // error -> sync should restart
- syncSpy.calls.reset();
- errorCallback();
- expect(syncSpy).toHaveBeenCalled();
-
- // pause -> no restart required
- syncSpy.calls.reset();
- pauseCallback();
- expect(syncSpy).not.toHaveBeenCalled();
-
- // logout + error -> no restart
- syncSpy.calls.reset();
- sessionService.logout();
- tick();
- errorCallback();
- expect(syncSpy).not.toHaveBeenCalled();
- }));
-
- testSessionServiceImplementation(() => Promise.resolve(sessionService));
-
- function passRemoteLogin(response: AuthUser = { name: "", roles: [] }) {
- mockAuthService.authenticate.and.resolveTo(response);
- }
-
- function failRemoteLogin(offline = false) {
- let rejectError;
- if (!offline) {
- rejectError = new HttpErrorResponse({
- status: HttpStatusCode.Unauthorized,
- });
- }
- mockAuthService.authenticate.and.rejectWith(rejectError);
- }
-});
diff --git a/src/app/core/session/session-service/synced-session.service.ts b/src/app/core/session/session-service/synced-session.service.ts
deleted file mode 100644
index 77aa57f630..0000000000
--- a/src/app/core/session/session-service/synced-session.service.ts
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * This file is part of ndb-core.
- *
- * ndb-core is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * ndb-core is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with ndb-core. If not, see .
- */
-
-import { Inject, Injectable } from "@angular/core";
-
-import { SessionService } from "./session.service";
-import { LocalSession } from "./local-session";
-import { RemoteSession } from "./remote-session";
-import { LoginState } from "../session-states/login-state.enum";
-import { Database } from "../../database/database";
-import { SyncState } from "../session-states/sync-state.enum";
-import { LoggingService } from "../../logging/logging.service";
-import { waitForChangeTo } from "../session-states/session-utils";
-import { zip } from "rxjs";
-import { filter } from "rxjs/operators";
-import { LOCATION_TOKEN } from "../../../utils/di-tokens";
-import { AuthService } from "../auth/auth.service";
-import { AuthUser } from "./auth-user";
-
-/**
- * A synced session creates and manages a LocalSession and a RemoteSession
- * and handles the setup of synchronisation.
- *
- * also see
- * [Session Handling, Authentication & Synchronisation]{@link /additional-documentation/concepts/session-and-authentication-system.html}
- */
-@Injectable()
-export class SyncedSessionService extends SessionService {
- static readonly LAST_SYNC_KEY = "LAST_SYNC";
- private readonly LOGIN_RETRY_TIMEOUT = 60000;
- private readonly POUCHDB_SYNC_BATCH_SIZE = 500;
-
- private _liveSyncHandle: any;
- private _liveSyncScheduledHandle: any;
- private _offlineRetryLoginScheduleHandle: any;
-
- constructor(
- private loggingService: LoggingService,
- private localSession: LocalSession,
- private remoteSession: RemoteSession,
- private authService: AuthService,
- @Inject(LOCATION_TOKEN) private location: Location,
- ) {
- super();
- this.syncState
- .pipe(filter((state) => state === SyncState.COMPLETED))
- .subscribe(() =>
- localStorage.setItem(
- SyncedSessionService.LAST_SYNC_KEY,
- new Date().toISOString(),
- ),
- );
- this.checkForValidSession();
- }
-
- /**
- * Do log in automatically if there is still a valid CouchDB cookie from last login with username and password
- */
- checkForValidSession() {
- this.authService
- .autoLogin()
- .then((user) => this.handleSuccessfulLogin(user))
- .catch(() => undefined);
- }
-
- async handleSuccessfulLogin(userObject: AuthUser) {
- this.startSyncAfterLocalAndRemoteLogin();
- await this.localSession.handleSuccessfulLogin(userObject);
- // The app is ready to be used once the local session is logged in
- this.loginState.next(LoginState.LOGGED_IN);
- await this.remoteSession.handleSuccessfulLogin(userObject);
- }
-
- /**
- * Perform a login. The result will only be the login at the local DB, as we might be offline.
- * Calling this function will trigger a login in the background.
- * - If it is successful, a sync is performed in the background
- * - If it fails due to wrong credentials, yet the local login was successful somehow, we fail local login after the fact
- *
- * If the localSession is empty, the local login waits for the result of the sync triggered by the remote login (see local-session.ts).
- * If the remote login fails for some reason, this sync will never be performed, which is why it must be failed manually here
- * to abort the local login and prevent a deadlock.
- * @param username Username
- * @param password Password
- * @returns promise resolving with the local LoginState
- */
- public async login(username: string, password: string): Promise {
- this.cancelLoginOfflineRetry(); // in case this is running in the background
- this.syncState.next(SyncState.UNSYNCED);
-
- const remoteLogin = this.remoteSession
- .login(username, password)
- .then((state) => {
- this.updateLocalUser(password, username);
- return state;
- });
-
- this.startSyncAfterLocalAndRemoteLogin();
-
- const localLoginState = await this.localSession.login(username, password);
-
- if (localLoginState === LoginState.LOGGED_IN) {
- this.loginState.next(LoginState.LOGGED_IN);
- remoteLogin.then((loginState) => {
- if (loginState === LoginState.LOGIN_FAILED) {
- this.localSession.removeUser(username);
- this.logout();
- }
- if (loginState === LoginState.UNAVAILABLE) {
- this.retryLoginWhileOffline(username, password);
- }
- });
- } else {
- const remoteLoginState = await remoteLogin;
- if (remoteLoginState === LoginState.LOGGED_IN) {
- // New user or password changed
- const localLoginRetry = await this.localSession.login(
- username,
- password,
- );
- this.loginState.next(localLoginRetry);
- } else if (
- remoteLoginState === LoginState.UNAVAILABLE &&
- localLoginState === LoginState.UNAVAILABLE
- ) {
- // Offline with no local user
- this.loginState.next(LoginState.UNAVAILABLE);
- } else {
- // Password and or username wrong
- this.loginState.next(LoginState.LOGIN_FAILED);
- }
- }
- return this.loginState.value;
- }
-
- private startSyncAfterLocalAndRemoteLogin() {
- zip(
- this.localSession.loginState.pipe(waitForChangeTo(LoginState.LOGGED_IN)),
- this.remoteSession.loginState.pipe(waitForChangeTo(LoginState.LOGGED_IN)),
- ).subscribe(() => this.startSync());
- }
-
- private retryLoginWhileOffline(username: string, password: string) {
- this._offlineRetryLoginScheduleHandle = setTimeout(() => {
- this.login(username, password);
- }, this.LOGIN_RETRY_TIMEOUT);
- }
-
- private updateLocalUser(password: string, loginName: string) {
- // Update local user object
- const remoteUser = this.remoteSession.getCurrentUser();
- if (remoteUser) {
- this.localSession.saveUser(remoteUser, password, loginName);
- }
- }
-
- private startSync(): Promise {
- // Call live syn even when initial sync fails
- return this.sync()
- .catch((err) => this.loggingService.error(`Sync failed: ${err}`))
- .finally(() => this.liveSyncDeferred());
- }
-
- public getCurrentUser(): AuthUser {
- return this.localSession.getCurrentUser();
- }
-
- public checkPassword(username: string, password: string): boolean {
- // This only checks the password against locally saved users
- return this.localSession.checkPassword(username, password);
- }
-
- /** see {@link SessionService} */
- public async sync(): Promise {
- this.syncState.next(SyncState.STARTED);
- try {
- const localPouchDB = this.localSession.getDatabase().getPouchDB();
- const remotePouchDB = this.remoteSession.getDatabase().getPouchDB();
- const result = await localPouchDB.sync(remotePouchDB, {
- batch_size: this.POUCHDB_SYNC_BATCH_SIZE,
- });
- this.syncState.next(SyncState.COMPLETED);
- return result;
- } catch (error) {
- this.syncState.next(SyncState.FAILED);
- throw error; // rethrow, so later Promise-handling lands in .catch, too
- }
- }
-
- /**
- * Start live sync in background.
- */
- public liveSync() {
- this.cancelLiveSync(); // cancel any liveSync that may have been alive before
- this.syncState.next(SyncState.STARTED);
- const localPouchDB = this.localSession.getDatabase().getPouchDB();
- const remotePouchDB = this.remoteSession.getDatabase().getPouchDB();
- this._liveSyncHandle = localPouchDB.sync(remotePouchDB, {
- live: true,
- retry: true,
- });
- this._liveSyncHandle
- .on("paused", (info) => {
- // replication was paused: either because sync is finished or because of a failed sync (mostly due to lost connection). info is empty.
- if (this.remoteSession.loginState.value === LoginState.LOGGED_IN) {
- this.syncState.next(SyncState.COMPLETED);
- // We might end up here after a failed sync that is not due to offline errors.
- // It shouldn't happen too often, as we have an initial non-live sync to catch those situations, but we can't find that out here
- }
- })
- .on("active", (info) => {
- // replication was resumed: either because new things to sync or because connection is available again. info contains the direction
- this.syncState.next(SyncState.STARTED);
- })
- .on("error", this.handleFailedSync())
- .on("complete", (info) => {
- this.loggingService.info(
- `Live sync completed: ${JSON.stringify(info)}`,
- );
- this.syncState.next(SyncState.COMPLETED);
- });
- }
-
- private handleFailedSync() {
- return (info) => {
- if (this.isLoggedIn()) {
- this.syncState.next(SyncState.FAILED);
- const lastAuth = localStorage.getItem(AuthService.LAST_AUTH_KEY);
- this.loggingService.warn(
- `Live sync failed (last auth ${lastAuth}): ${JSON.stringify(info)}`,
- );
- this.liveSync();
- }
- };
- }
-
- /**
- * Schedules liveSync to be started.
- * This method should be used to start the liveSync after the initial non-live sync,
- * so the browser makes a round trip to the UI and hides the potentially visible first-sync dialog.
- * @param timeout ms to wait before starting the liveSync
- */
- public liveSyncDeferred(timeout = 1000) {
- this._liveSyncScheduledHandle = setTimeout(() => this.liveSync(), timeout);
- }
-
- /**
- * Cancels a pending login retry scheduled to start in the future.
- */
- public cancelLoginOfflineRetry() {
- if (this._offlineRetryLoginScheduleHandle) {
- clearTimeout(this._offlineRetryLoginScheduleHandle);
- }
- }
-
- /**
- * Cancels a currently running liveSync or a liveSync scheduled to start in the future.
- */
- public cancelLiveSync() {
- if (this._liveSyncScheduledHandle) {
- clearTimeout(this._liveSyncScheduledHandle);
- }
- if (this._liveSyncHandle) {
- this._liveSyncHandle.cancel();
- }
- this.syncState.next(SyncState.UNSYNCED);
- }
-
- /**
- * Get the local database instance that should be used for regular data access.
- * als see {@link SessionService}
- */
- public getDatabase(): Database {
- return this.localSession.getDatabase();
- }
-
- /**
- * Logout and stop any existing sync.
- * also see {@link SessionService}
- */
- public async logout() {
- this.cancelLoginOfflineRetry();
- this.localSession.logout();
- await this.remoteSession.logout();
- this.cancelLiveSync();
- this.location.reload();
- this.loginState.next(LoginState.LOGGED_OUT);
- }
-}
diff --git a/src/app/core/session/session-states/login-state.enum.ts b/src/app/core/session/session-states/login-state.enum.ts
index 446b108258..6bedc1622e 100644
--- a/src/app/core/session/session-states/login-state.enum.ts
+++ b/src/app/core/session/session-states/login-state.enum.ts
@@ -23,6 +23,8 @@ export enum LoginState {
LOGGED_OUT,
/** Successfully logged in */
LOGGED_IN,
+ /** Login is currently in progress */
+ IN_PROGRESS,
/** Login is not possible right now */
UNAVAILABLE,
}
diff --git a/src/app/core/session/session-type.ts b/src/app/core/session/session-type.ts
index 95c836b414..2c247957e3 100644
--- a/src/app/core/session/session-type.ts
+++ b/src/app/core/session/session-type.ts
@@ -1,3 +1,8 @@
+import { BehaviorSubject } from "rxjs";
+import { LoginState } from "./session-states/login-state.enum";
+import { Injectable } from "@angular/core";
+import { SyncState } from "./session-states/sync-state.enum";
+
/**
* Available Session types with their keys that can be used in the app-config.
*/
@@ -17,3 +22,17 @@ export enum SessionType {
*/
mock = "mock",
}
+
+@Injectable()
+export class LoginStateSubject extends BehaviorSubject {
+ constructor() {
+ super(LoginState.LOGGED_OUT);
+ }
+}
+
+@Injectable()
+export class SyncStateSubject extends BehaviorSubject {
+ constructor() {
+ super(SyncState.UNSYNCED);
+ }
+}
diff --git a/src/app/core/session/session.module.ts b/src/app/core/session/session.module.ts
index 1977f99f90..7ff25cc554 100644
--- a/src/app/core/session/session.module.ts
+++ b/src/app/core/session/session.module.ts
@@ -15,20 +15,11 @@
* along with ndb-core. If not, see .
*/
-import { Injector, NgModule } from "@angular/core";
-import { HTTP_INTERCEPTORS } from "@angular/common/http";
-import { SyncedSessionService } from "./session-service/synced-session.service";
-import { LocalSession } from "./session-service/local-session";
-import { RemoteSession } from "./session-service/remote-session";
-import { SessionService } from "./session-service/session.service";
-import { SessionType } from "./session-type";
-import { environment } from "../../../environments/environment";
-import { AuthService } from "./auth/auth.service";
+import { NgModule } from "@angular/core";
+import { SessionManagerService } from "./session-service/session-manager.service";
+import { LoginStateSubject, SyncStateSubject } from "./session-type";
import { KeycloakAuthService } from "./auth/keycloak/keycloak-auth.service";
-import { CouchdbAuthService } from "./auth/couchdb/couchdb-auth.service";
-import { AuthProvider } from "./auth/auth-provider";
-import { AuthInterceptor } from "./auth/auth.interceptor";
-import { serviceProvider } from "../../utils/utils";
+import { KeycloakAngularModule } from "keycloak-angular";
/**
* The core session logic handling user login as well as connection and synchronization with the remote database.
@@ -39,27 +30,21 @@ import { serviceProvider } from "../../utils/utils";
* [Session Handling, Authentication & Synchronisation]{@link /additional-documentation/concepts/session-and-authentication-system.html}
*/
@NgModule({
+ imports: [KeycloakAngularModule],
providers: [
- SyncedSessionService,
- LocalSession,
- RemoteSession,
- serviceProvider(SessionService, (injector: Injector) => {
- return environment.session_type === SessionType.synced
- ? injector.get(SyncedSessionService)
- : injector.get(LocalSession);
- }),
+ SessionManagerService,
KeycloakAuthService,
- CouchdbAuthService,
- serviceProvider(AuthService, (injector: Injector) => {
- return environment.authenticator === AuthProvider.Keycloak
- ? injector.get(KeycloakAuthService)
- : injector.get(CouchdbAuthService);
- }),
- {
- provide: HTTP_INTERCEPTORS,
- useClass: AuthInterceptor,
- multi: true,
- },
+ LoginStateSubject,
+ SyncStateSubject,
],
})
-export class SessionModule {}
+export class SessionModule {
+ constructor(sessionManager: SessionManagerService) {
+ this.initializeRemoteSession(sessionManager);
+ }
+
+ private async initializeRemoteSession(sessionManager: SessionManagerService) {
+ await sessionManager.remoteLogin();
+ await sessionManager.clearRemoteSessionIfNecessary();
+ }
+}
diff --git a/src/app/core/site-settings/site-settings.service.spec.ts b/src/app/core/site-settings/site-settings.service.spec.ts
index a0dba5c378..2eb01aff29 100644
--- a/src/app/core/site-settings/site-settings.service.spec.ts
+++ b/src/app/core/site-settings/site-settings.service.spec.ts
@@ -39,6 +39,10 @@ describe("SiteSettingsService", () => {
service = TestBed.inject(SiteSettingsService);
});
+ afterEach(() => {
+ localStorage.removeItem(service.SITE_SETTINGS_LOCAL_STORAGE_KEY);
+ });
+
it("should be created", () => {
expect(service).toBeTruthy();
});
@@ -46,12 +50,11 @@ describe("SiteSettingsService", () => {
it("should only publish changes if property has changed", () => {
const titleSpy = spyOn(TestBed.inject(Title), "setTitle");
const settings = new SiteSettings();
-
+ settings.siteName = undefined;
entityMapper.add(settings);
- expect(titleSpy).toHaveBeenCalled();
+ expect(titleSpy).not.toHaveBeenCalled();
- titleSpy.calls.reset();
settings.displayLanguageSelect = false;
entityMapper.add(settings);
diff --git a/src/app/core/support/support/support.component.spec.ts b/src/app/core/support/support/support.component.spec.ts
index c94c26f7cc..bad0740582 100644
--- a/src/app/core/support/support/support.component.spec.ts
+++ b/src/app/core/support/support/support.component.spec.ts
@@ -7,22 +7,22 @@ import {
} from "@angular/core/testing";
import { SupportComponent } from "./support.component";
-import { SessionService } from "../../session/session-service/session.service";
import { BehaviorSubject, of } from "rxjs";
-import { SyncState } from "../../session/session-states/sync-state.enum";
import { SwUpdate } from "@angular/service-worker";
import { LOCATION_TOKEN, WINDOW_TOKEN } from "../../../utils/di-tokens";
import { ConfirmationDialogService } from "../../common-components/confirmation-dialog/confirmation-dialog.service";
import { HttpClient } from "@angular/common/http";
-import { SyncedSessionService } from "../../session/session-service/synced-session.service";
import { MatDialogModule } from "@angular/material/dialog";
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { PouchDatabase } from "../../database/pouch-database";
import { BackupService } from "../../../features/admin/services/backup.service";
import { DownloadService } from "../../export/download-service/download.service";
-import { AuthService } from "../../session/auth/auth.service";
import { TEST_USER } from "../../../utils/mock-local-session";
+import { SyncService } from "../../database/sync.service";
+import { KeycloakAuthService } from "../../session/auth/keycloak/keycloak-auth.service";
+import { SyncStateSubject } from "../../session/session-type";
+import { CurrentUserSubject } from "../../user/user";
class MockDeleteRequest {
onsuccess: () => {};
@@ -35,7 +35,6 @@ describe("SupportComponent", () => {
let component: SupportComponent;
let fixture: ComponentFixture;
const testUser = { name: TEST_USER, roles: [] };
- let mockSessionService: jasmine.SpyObj;
const mockSW = { isEnabled: false };
let mockDB: jasmine.SpyObj;
const mockWindow = {
@@ -54,10 +53,6 @@ describe("SupportComponent", () => {
beforeEach(async () => {
localStorage.clear();
- mockSessionService = jasmine.createSpyObj(["getCurrentUser"], {
- syncState: new BehaviorSubject(SyncState.UNSYNCED),
- });
- mockSessionService.getCurrentUser.and.returnValue(testUser);
mockDB = jasmine.createSpyObj(["destroy", "getPouchDB"]);
mockDB.getPouchDB.and.returnValue({
info: () => Promise.resolve({ doc_count: 1, update_seq: 2 }),
@@ -71,13 +66,17 @@ describe("SupportComponent", () => {
NoopAnimationsModule,
],
providers: [
- { provide: SessionService, useValue: mockSessionService },
+ {
+ provide: CurrentUserSubject,
+ useValue: new BehaviorSubject(testUser),
+ },
{ provide: SwUpdate, useValue: mockSW },
{ provide: PouchDatabase, useValue: mockDB },
{ provide: WINDOW_TOKEN, useValue: mockWindow },
{ provide: LOCATION_TOKEN, useValue: mockLocation },
{ provide: BackupService, useValue: null },
{ provide: DownloadService, useValue: null },
+ SyncStateSubject,
],
}).compileComponents();
});
@@ -104,9 +103,9 @@ describe("SupportComponent", () => {
it("should correctly read sync and remote login status from local storage", async () => {
const lastSync = new Date("2022-01-01").toISOString();
- localStorage.setItem(SyncedSessionService.LAST_SYNC_KEY, lastSync);
+ localStorage.setItem(SyncService.LAST_SYNC_KEY, lastSync);
const lastRemoteLogin = new Date("2022-01-02").toISOString();
- localStorage.setItem(AuthService.LAST_AUTH_KEY, lastRemoteLogin);
+ localStorage.setItem(KeycloakAuthService.LAST_AUTH_KEY, lastRemoteLogin);
await component.ngOnInit();
diff --git a/src/app/core/support/support/support.component.ts b/src/app/core/support/support/support.component.ts
index 0f5743f03d..9f442f8465 100644
--- a/src/app/core/support/support/support.component.ts
+++ b/src/app/core/support/support/support.component.ts
@@ -1,14 +1,12 @@
import { Component, Inject, OnInit } from "@angular/core";
-import { SessionService } from "../../session/session-service/session.service";
import { LOCATION_TOKEN, WINDOW_TOKEN } from "../../../utils/di-tokens";
import { SyncState } from "../../session/session-states/sync-state.enum";
import { SwUpdate } from "@angular/service-worker";
import * as Sentry from "@sentry/browser";
import { ConfirmationDialogService } from "../../common-components/confirmation-dialog/confirmation-dialog.service";
import { HttpClient } from "@angular/common/http";
-import { SyncedSessionService } from "../../session/session-service/synced-session.service";
import { environment } from "../../../../environments/environment";
-import { AuthUser } from "../../session/session-service/auth-user";
+import { AuthUser } from "../../session/auth/auth-user";
import { firstValueFrom } from "rxjs";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatButtonModule } from "@angular/material/button";
@@ -16,7 +14,10 @@ import { PouchDatabase } from "../../database/pouch-database";
import { MatTooltipModule } from "@angular/material/tooltip";
import { BackupService } from "../../../features/admin/services/backup.service";
import { DownloadService } from "../../export/download-service/download.service";
-import { AuthService } from "../../session/auth/auth.service";
+import { SyncStateSubject } from "../../session/session-type";
+import { SyncService } from "../../database/sync.service";
+import { KeycloakAuthService } from "../../session/auth/keycloak/keycloak-auth.service";
+import { CurrentUserSubject } from "../../user/user";
@Component({
selector: "app-support",
@@ -38,7 +39,8 @@ export class SupportComponent implements OnInit {
dbInfo: string;
constructor(
- private sessionService: SessionService,
+ private syncState: SyncStateSubject,
+ private userSubject: CurrentUserSubject,
private sw: SwUpdate,
private database: PouchDatabase,
private confirmationDialog: ConfirmationDialogService,
@@ -50,7 +52,7 @@ export class SupportComponent implements OnInit {
) {}
ngOnInit() {
- this.currentUser = this.sessionService.getCurrentUser();
+ this.currentUser = this.userSubject.value;
this.appVersion = environment.appVersion;
this.initCurrentSyncState();
this.initLastSync();
@@ -61,7 +63,7 @@ export class SupportComponent implements OnInit {
}
private initCurrentSyncState() {
- switch (this.sessionService.syncState.value) {
+ switch (this.syncState.value) {
case SyncState.COMPLETED:
this.currentSyncState = "synced";
return;
@@ -74,13 +76,12 @@ export class SupportComponent implements OnInit {
}
private initLastSync() {
- this.lastSync =
- localStorage.getItem(SyncedSessionService.LAST_SYNC_KEY) || "never";
+ this.lastSync = localStorage.getItem(SyncService.LAST_SYNC_KEY) || "never";
}
private initLastRemoteLogin() {
this.lastRemoteLogin =
- localStorage.getItem(AuthService.LAST_AUTH_KEY) || "never";
+ localStorage.getItem(KeycloakAuthService.LAST_AUTH_KEY) || "never";
}
private initStorageInfo() {
diff --git a/src/app/core/ui/latest-changes/latest-changes.service.ts b/src/app/core/ui/latest-changes/latest-changes.service.ts
index d5e35b9f68..608252e35e 100644
--- a/src/app/core/ui/latest-changes/latest-changes.service.ts
+++ b/src/app/core/ui/latest-changes/latest-changes.service.ts
@@ -20,9 +20,8 @@ import { Injectable } from "@angular/core";
import { Observable, throwError } from "rxjs";
import { Changelog } from "./changelog";
import { AlertService } from "../../alerts/alert.service";
-import { HttpClient, HttpContext } from "@angular/common/http";
+import { HttpClient } from "@angular/common/http";
import { environment } from "../../../../environments/environment";
-import { AUTH_ENABLED } from "../../session/auth/auth.interceptor";
/**
* Manage the changelog information and display it to the user
@@ -111,7 +110,6 @@ export class LatestChangesService {
return this.http
.get(
`${LatestChangesService.GITHUB_API}${environment.repositoryId}/releases`,
- { context: new HttpContext().set(AUTH_ENABLED, false) },
)
.pipe(
map(excludePrereleases),
diff --git a/src/app/core/ui/sync-status/sync-status/sync-status.component.spec.ts b/src/app/core/ui/sync-status/sync-status/sync-status.component.spec.ts
index c6c560f468..ec40495cfd 100644
--- a/src/app/core/ui/sync-status/sync-status/sync-status.component.spec.ts
+++ b/src/app/core/ui/sync-status/sync-status/sync-status.component.spec.ts
@@ -19,7 +19,6 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { SyncStatusComponent } from "./sync-status.component";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
-import { SessionService } from "../../../session/session-service/session.service";
import { SyncState } from "../../../session/session-states/sync-state.enum";
import { DatabaseIndexingService } from "../../../entity/database-indexing/database-indexing.service";
import { BehaviorSubject } from "rxjs";
@@ -30,12 +29,13 @@ import {
entityRegistry,
} from "../../../entity/database-entity.decorator";
import { expectObservable } from "../../../../utils/test-utils/observable-utils";
+import { SyncStateSubject } from "../../../session/session-type";
describe("SyncStatusComponent", () => {
let component: SyncStatusComponent;
let fixture: ComponentFixture;
- let mockSessionService: jasmine.SpyObj;
+ let syncState: SyncStateSubject;
let mockIndexingService;
const DATABASE_SYNCING_STATE: BackgroundProcessState = {
@@ -48,10 +48,6 @@ describe("SyncStatusComponent", () => {
};
beforeEach(waitForAsync(() => {
- mockSessionService = jasmine.createSpyObj(["isLoggedIn"], {
- syncState: new BehaviorSubject(SyncState.UNSYNCED),
- });
- mockSessionService.isLoggedIn.and.returnValue(false);
mockIndexingService = { indicesRegistered: new BehaviorSubject([]) };
TestBed.configureTestingModule({
@@ -61,11 +57,12 @@ describe("SyncStatusComponent", () => {
FontAwesomeTestingModule,
],
providers: [
- { provide: SessionService, useValue: mockSessionService },
+ SyncStateSubject,
{ provide: DatabaseIndexingService, useValue: mockIndexingService },
{ provide: EntityRegistry, useValue: entityRegistry },
],
});
+ syncState = TestBed.inject(SyncStateSubject);
TestBed.compileComponents();
}));
@@ -81,7 +78,7 @@ describe("SyncStatusComponent", () => {
});
it("should update backgroundProcesses details on sync", async () => {
- mockSessionService.syncState.next(SyncState.STARTED);
+ syncState.next(SyncState.STARTED);
fixture.detectChanges();
await fixture.whenStable();
@@ -89,7 +86,7 @@ describe("SyncStatusComponent", () => {
DATABASE_SYNCING_STATE,
]);
- mockSessionService.syncState.next(SyncState.COMPLETED);
+ syncState.next(SyncState.COMPLETED);
fixture.detectChanges();
await fixture.whenStable();
diff --git a/src/app/core/ui/sync-status/sync-status/sync-status.component.ts b/src/app/core/ui/sync-status/sync-status/sync-status.component.ts
index af53def0c7..874f70eea9 100644
--- a/src/app/core/ui/sync-status/sync-status/sync-status.component.ts
+++ b/src/app/core/ui/sync-status/sync-status/sync-status.component.ts
@@ -16,7 +16,6 @@
*/
import { ChangeDetectionStrategy, Component } from "@angular/core";
-import { SessionService } from "../../../session/session-service/session.service";
import { SyncState } from "../../../session/session-states/sync-state.enum";
import { DatabaseIndexingService } from "../../../entity/database-indexing/database-indexing.service";
import { BackgroundProcessState } from "../background-process-state.interface";
@@ -24,6 +23,7 @@ import { BehaviorSubject } from "rxjs";
import { debounceTime } from "rxjs/operators";
import { BackgroundProcessingIndicatorComponent } from "../background-processing-indicator/background-processing-indicator.component";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { SyncStateSubject } from "../../../session/session-type";
/**
* A small indicator component that displays an icon when there is currently synchronization
@@ -52,7 +52,7 @@ export class SyncStatusComponent {
.pipe(debounceTime(1000));
constructor(
- private sessionService: SessionService,
+ private syncState: SyncStateSubject,
private dbIndexingService: DatabaseIndexingService,
) {
this.dbIndexingService.indicesRegistered
@@ -62,7 +62,7 @@ export class SyncStatusComponent {
this.updateBackgroundProcessesList();
});
- this.sessionService.syncState
+ this.syncState
.pipe(untilDestroyed(this))
.subscribe(() => this.updateBackgroundProcessesList());
}
@@ -73,7 +73,7 @@ export class SyncStatusComponent {
*/
private updateBackgroundProcessesList() {
let currentProcesses: BackgroundProcessState[] = [];
- if (this.sessionService.syncState.value === SyncState.STARTED) {
+ if (this.syncState.value === SyncState.STARTED) {
currentProcesses.push({
title: $localize`Synchronizing database`,
pending: true,
diff --git a/src/app/core/ui/ui/ui.component.ts b/src/app/core/ui/ui/ui.component.ts
index 743d98d53d..dd77edbe89 100644
--- a/src/app/core/ui/ui/ui.component.ts
+++ b/src/app/core/ui/ui/ui.component.ts
@@ -16,7 +16,6 @@
*/
import { Component, ViewChild } from "@angular/core";
-import { SessionService } from "../../session/session-service/session.service";
import { Title } from "@angular/platform-browser";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { MatDrawerMode, MatSidenavModule } from "@angular/material/sidenav";
@@ -38,6 +37,9 @@ import { PrimaryActionComponent } from "../primary-action/primary-action.compone
import { SiteSettingsService } from "../../site-settings/site-settings.service";
import { DisplayImgComponent } from "../../../features/file/display-img/display-img.component";
import { SiteSettings } from "../../site-settings/site-settings";
+import { LoginStateSubject } from "../../session/session-type";
+import { LoginState } from "../../session/session-states/login-state.enum";
+import { SessionManagerService } from "../../session/session-service/session-manager.service";
/**
* The main user interface component as root element for the app structure
@@ -77,12 +79,13 @@ export class UiComponent {
siteSettings = new SiteSettings();
constructor(
- private _sessionService: SessionService,
private titleService: Title,
private configService: ConfigService,
private screenWidthObserver: ScreenWidthObserver,
private router: Router,
private siteSettingsService: SiteSettingsService,
+ private loginState: LoginStateSubject,
+ private sessionManager: SessionManagerService,
) {
this.screenWidthObserver
.platform()
@@ -99,17 +102,14 @@ export class UiComponent {
* Check if user is logged in.
*/
isLoggedIn(): boolean {
- return this._sessionService.isLoggedIn();
+ return this.loginState.value === LoginState.LOGGED_IN;
}
/**
* Trigger logout of user.
*/
- logout() {
- this._sessionService.logout();
- this.router.navigate(["/login"], {
- queryParams: { redirect_uri: this.router.routerState.snapshot.url },
- });
+ async logout() {
+ this.sessionManager.logout();
}
closeSidenavOnMobile() {
diff --git a/src/app/core/user/demo-user-generator.service.ts b/src/app/core/user/demo-user-generator.service.ts
index 3cf31eb689..d2fa50ef98 100644
--- a/src/app/core/user/demo-user-generator.service.ts
+++ b/src/app/core/user/demo-user-generator.service.ts
@@ -11,8 +11,6 @@ export class DemoUserGeneratorService extends DemoDataGenerator {
/** the username of the basic account generated by this demo service */
static DEFAULT_USERNAME = "demo";
static ADMIN_USERNAME = "demo-admin";
- /** the password of all accounts generated by this demo service */
- static DEFAULT_PASSWORD = "pass";
/**
* This function returns a provider object to be used in an Angular Module configuration
diff --git a/src/app/core/user/user-account/user-account.component.html b/src/app/core/user/user-account/user-account.component.html
index 273da4bb37..e8c12b90ac 100644
--- a/src/app/core/user/user-account/user-account.component.html
+++ b/src/app/core/user/user-account/user-account.component.html
@@ -30,11 +30,6 @@
/>
-
-
diff --git a/src/app/core/user/user-account/user-account.component.spec.ts b/src/app/core/user/user-account/user-account.component.spec.ts
index 809de0bb63..202c139685 100644
--- a/src/app/core/user/user-account/user-account.component.spec.ts
+++ b/src/app/core/user/user-account/user-account.component.spec.ts
@@ -18,39 +18,26 @@
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { UserAccountComponent } from "./user-account.component";
-import { SessionService } from "../../session/session-service/session.service";
-import { LoggingService } from "../../logging/logging.service";
-import { AuthService } from "../../session/auth/auth.service";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
-import { NEVER } from "rxjs";
+import { KeycloakAuthService } from "../../session/auth/keycloak/keycloak-auth.service";
describe("UserAccountComponent", () => {
let component: UserAccountComponent;
let fixture: ComponentFixture;
- let mockSessionService: jasmine.SpyObj;
- let mockLoggingService: jasmine.SpyObj;
-
beforeEach(waitForAsync(() => {
- mockSessionService = jasmine.createSpyObj(
- "sessionService",
- ["getCurrentUser"],
- { syncState: NEVER, loginState: NEVER },
- );
- mockSessionService.getCurrentUser.and.returnValue({
- name: "TestUser",
- roles: [],
- });
- mockLoggingService = jasmine.createSpyObj(["error"]);
-
TestBed.configureTestingModule({
imports: [UserAccountComponent, MockedTestingModule.withState()],
providers: [
- { provide: SessionService, useValue: mockSessionService },
- { provide: AuthService, useValue: { changePassword: () => undefined } },
- { provide: LoggingService, useValue: mockLoggingService },
+ {
+ provide: KeycloakAuthService,
+ useValue: {
+ getUserinfo: () => Promise.reject(new Error()),
+ autoLogin: () => Promise.reject(new Error()),
+ },
+ },
],
- });
+ }).compileComponents();
}));
beforeEach(() => {
diff --git a/src/app/core/user/user-account/user-account.component.ts b/src/app/core/user/user-account/user-account.component.ts
index 30909b740e..c96cf4ea38 100644
--- a/src/app/core/user/user-account/user-account.component.ts
+++ b/src/app/core/user/user-account/user-account.component.ts
@@ -16,7 +16,6 @@
*/
import { Component, OnInit } from "@angular/core";
-import { SessionService } from "../../session/session-service/session.service";
import { environment } from "../../../../environments/environment";
import { SessionType } from "../../session/session-type";
import { MatTabsModule } from "@angular/material/tabs";
@@ -24,8 +23,8 @@ import { TabStateModule } from "../../../utils/tab-state/tab-state.module";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatInputModule } from "@angular/material/input";
-import { PasswordFormComponent } from "../../session/auth/couchdb/password-form/password-form.component";
import { AccountPageComponent } from "../../session/auth/keycloak/account-page/account-page.component";
+import { CurrentUserSubject } from "../user";
/**
* User account form to allow the user to view and edit information.
@@ -40,7 +39,6 @@ import { AccountPageComponent } from "../../session/auth/keycloak/account-page/a
MatFormFieldModule,
MatTooltipModule,
MatInputModule,
- PasswordFormComponent,
AccountPageComponent,
],
standalone: true,
@@ -52,11 +50,11 @@ export class UserAccountComponent implements OnInit {
passwordChangeDisabled = false;
tooltipText;
- constructor(private sessionService: SessionService) {}
+ constructor(private currentUser: CurrentUserSubject) {}
ngOnInit() {
this.checkIfPasswordChangeAllowed();
- this.username = this.sessionService.getCurrentUser()?.name;
+ this.username = this.currentUser.value?.name;
}
checkIfPasswordChangeAllowed() {
diff --git a/src/app/core/user/user-security/user-security.component.spec.ts b/src/app/core/user/user-security/user-security.component.spec.ts
index f65aca9730..4a59db4a44 100644
--- a/src/app/core/user/user-security/user-security.component.spec.ts
+++ b/src/app/core/user/user-security/user-security.component.spec.ts
@@ -9,22 +9,19 @@ import {
import { UserSecurityComponent } from "./user-security.component";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
-import { AuthService } from "../../session/auth/auth.service";
import {
KeycloakAuthService,
KeycloakUser,
Role,
} from "../../session/auth/keycloak/keycloak-auth.service";
-import { NEVER, of, throwError } from "rxjs";
-import { User } from "../user";
+import { BehaviorSubject, of, throwError } from "rxjs";
+import { CurrentUserSubject, User } from "../user";
import { AppSettings } from "../../app-settings";
-import { SessionService } from "../../session/session-service/session.service";
describe("UserSecurityComponent", () => {
let component: UserSecurityComponent;
let fixture: ComponentFixture;
let mockHttp: jasmine.SpyObj;
- let mockSession: jasmine.SpyObj;
const assignedRole: Role = {
id: "assigned-role",
name: "Assigned Role",
@@ -50,21 +47,19 @@ describe("UserSecurityComponent", () => {
mockHttp.get.and.returnValue(of([assignedRole, notAssignedRole]));
mockHttp.put.and.returnValue(of({}));
mockHttp.post.and.returnValue(of({}));
- mockSession = jasmine.createSpyObj(["getCurrentUser"], {
- syncState: NEVER,
- loginState: NEVER,
- });
- mockSession.getCurrentUser.and.returnValue({
- name: user.name,
- roles: [KeycloakAuthService.ACCOUNT_MANAGER_ROLE],
- });
await TestBed.configureTestingModule({
imports: [UserSecurityComponent, MockedTestingModule],
providers: [
- { provide: AuthService, useClass: KeycloakAuthService },
+ { provide: KeycloakAuthService, useClass: KeycloakAuthService },
{ provide: HttpClient, useValue: mockHttp },
- { provide: SessionService, useValue: mockSession },
+ {
+ provide: CurrentUserSubject,
+ useValue: new BehaviorSubject({
+ name: user.name,
+ roles: [KeycloakAuthService.ACCOUNT_MANAGER_ROLE],
+ }),
+ },
],
}).compileComponents();
diff --git a/src/app/core/user/user-security/user-security.component.ts b/src/app/core/user/user-security/user-security.component.ts
index fbf6e740bf..71034d13cf 100644
--- a/src/app/core/user/user-security/user-security.component.ts
+++ b/src/app/core/user/user-security/user-security.component.ts
@@ -11,10 +11,8 @@ import {
KeycloakUser,
Role,
} from "../../session/auth/keycloak/keycloak-auth.service";
-import { AuthService } from "../../session/auth/auth.service";
-import { User } from "../user";
+import { CurrentUserSubject, User } from "../user";
import { AlertService } from "../../alerts/alert.service";
-import { SessionService } from "../../session/session-service/session.service";
import { HttpClient } from "@angular/common/http";
import { AppSettings } from "../../app-settings";
import { NgForOf, NgIf } from "@angular/common";
@@ -50,23 +48,22 @@ export class UserSecurityComponent implements OnInit {
email: ["", [Validators.required, Validators.email]],
roles: new FormControl([]),
});
- keycloak: KeycloakAuthService;
availableRoles: Role[] = [];
user: KeycloakUser;
editing = true;
userIsPermitted = false;
constructor(
- authService: AuthService,
- sessionService: SessionService,
+ private authService: KeycloakAuthService,
+ currentUser: CurrentUserSubject,
private fb: FormBuilder,
private alertService: AlertService,
private http: HttpClient,
) {
if (
- sessionService
- .getCurrentUser()
- .roles.includes(KeycloakAuthService.ACCOUNT_MANAGER_ROLE)
+ currentUser.value?.roles.includes(
+ KeycloakAuthService.ACCOUNT_MANAGER_ROLE,
+ )
) {
this.userIsPermitted = true;
}
@@ -76,14 +73,9 @@ export class UserSecurityComponent implements OnInit {
this.form.get("email").setValue(next.email.trim());
}
});
- if (authService instanceof KeycloakAuthService) {
- this.keycloak = authService;
- this.keycloak
- .getRoles()
- .subscribe((roles) => this.initializeRoles(roles));
- } else {
- this.form.disable();
- }
+ this.authService
+ .getRoles()
+ .subscribe((roles) => this.initializeRoles(roles));
}
private initializeRoles(roles: Role[]) {
@@ -99,8 +91,8 @@ export class UserSecurityComponent implements OnInit {
ngOnInit() {
this.form.get("username").setValue(this.entity.name);
- if (this.keycloak) {
- this.keycloak.getUser(this.entity.name).subscribe({
+ if (this.authService) {
+ this.authService.getUser(this.entity.name).subscribe({
next: (res) => this.assignUser(res),
error: () => undefined,
});
@@ -152,7 +144,7 @@ export class UserSecurityComponent implements OnInit {
const user = this.getFormValues();
user.enabled = true;
if (user) {
- this.keycloak.createUser(user).subscribe({
+ this.authService.createUser(user).subscribe({
next: () => {
this.alertService.addInfo(
$localize`:Snackbar message:Account created. An email has been sent to ${
@@ -182,7 +174,7 @@ export class UserSecurityComponent implements OnInit {
}
private updateKeycloakUser(update: Partial, message: string) {
- this.keycloak.updateUser(this.user.id, update).subscribe({
+ this.authService.updateUser(this.user.id, update).subscribe({
next: () => {
this.alertService.addInfo(message);
Object.assign(this.user, update);
diff --git a/src/app/core/user/user-security/user-security.stories.ts b/src/app/core/user/user-security/user-security.stories.ts
index b7e9556ae4..03e3289731 100644
--- a/src/app/core/user/user-security/user-security.stories.ts
+++ b/src/app/core/user/user-security/user-security.stories.ts
@@ -1,27 +1,15 @@
import { applicationConfig, Meta, StoryFn } from "@storybook/angular";
import { UserSecurityComponent } from "./user-security.component";
import { StorybookBaseModule } from "../../../utils/storybook-base.module";
-import { SessionService } from "../../session/session-service/session.service";
import { User } from "../user";
import { importProvidersFrom } from "@angular/core";
-import { createLocalSession } from "../../../utils/mock-local-session";
-
export default {
title: "Core/Admin/User Security",
component: UserSecurityComponent,
decorators: [
applicationConfig({
- providers: [
- importProvidersFrom(StorybookBaseModule),
- {
- provide: SessionService,
- useValue: createLocalSession(true, {
- name: "Test",
- roles: ["account_manager"],
- }),
- },
- ],
+ providers: [importProvidersFrom(StorybookBaseModule)],
}),
],
} as Meta;
diff --git a/src/app/core/user/user.ts b/src/app/core/user/user.ts
index 4c390db113..50e19f7ed2 100644
--- a/src/app/core/user/user.ts
+++ b/src/app/core/user/user.ts
@@ -19,6 +19,9 @@ import { Entity } from "../entity/model/entity";
import { DatabaseEntity } from "../entity/database-entity.decorator";
import { DatabaseField } from "../entity/database-field.decorator";
import { IconName } from "@fortawesome/fontawesome-svg-core";
+import { BehaviorSubject } from "rxjs";
+import { AuthUser } from "../session/auth/auth-user";
+import { Injectable } from "@angular/core";
/**
* Entity representing a User object including password.
@@ -64,3 +67,13 @@ export class User extends Entity {
*/
@DatabaseField() paginatorSettingsPageSize: { [id: string]: number } = {};
}
+
+/**
+ * Use this provider to access the currently logged-in user object and subscribe to changes of user.
+ */
+@Injectable()
+export class CurrentUserSubject extends BehaviorSubject {
+ constructor() {
+ super(undefined);
+ }
+}
diff --git a/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.spec.ts b/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.spec.ts
index 560378ad25..413c60ce26 100644
--- a/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.spec.ts
+++ b/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.spec.ts
@@ -11,32 +11,27 @@ import { EntityMapperService } from "../../../../core/entity/entity-mapper/entit
import { AlertService } from "../../../../core/alerts/alert.service";
import { ProgressDashboardConfig } from "./progress-dashboard-config";
import { MatDialog } from "@angular/material/dialog";
-import { BehaviorSubject, NEVER, Subject } from "rxjs";
+import { Subject } from "rxjs";
import { take } from "rxjs/operators";
-import { SessionService } from "../../../../core/session/session-service/session.service";
import { SyncState } from "../../../../core/session/session-states/sync-state.enum";
import { MockedTestingModule } from "../../../../utils/mocked-testing.module";
+import { SyncStateSubject } from "../../../../core/session/session-type";
describe("ProgressDashboardComponent", () => {
let component: ProgressDashboardComponent;
let fixture: ComponentFixture;
let mockEntityMapper: jasmine.SpyObj;
const mockDialog = jasmine.createSpyObj("matDialog", ["open"]);
- let mockSession: jasmine.SpyObj;
- let mockSync: BehaviorSubject;
+ let mockSync: SyncStateSubject;
beforeEach(waitForAsync(() => {
- mockSync = new BehaviorSubject(SyncState.UNSYNCED);
- mockSession = jasmine.createSpyObj([], {
- syncState: mockSync,
- loginState: NEVER,
- });
+ mockSync = new SyncStateSubject();
TestBed.configureTestingModule({
imports: [ProgressDashboardComponent, MockedTestingModule.withState()],
providers: [
{ provide: MatDialog, useValue: mockDialog },
- { provide: SessionService, useValue: mockSession },
+ { provide: SyncStateSubject, useValue: mockSync },
{
provide: AlertService,
useValue: jasmine.createSpyObj(["addDebug", "addInfo", "addWarning"]),
diff --git a/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.ts b/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.ts
index 7671f6b8e6..e7a52281a1 100644
--- a/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.ts
+++ b/src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.ts
@@ -5,7 +5,6 @@ import { LoggingService } from "../../../../core/logging/logging.service";
import { MatDialog } from "@angular/material/dialog";
import { EditProgressDashboardComponent } from "../edit-progress-dashboard/edit-progress-dashboard.component";
import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator";
-import { SessionService } from "../../../../core/session/session-service/session.service";
import { waitForChangeTo } from "../../../../core/session/session-states/session-utils";
import { SyncState } from "../../../../core/session/session-states/sync-state.enum";
import { firstValueFrom } from "rxjs";
@@ -16,6 +15,7 @@ import { MatButtonModule } from "@angular/material/button";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { DashboardWidgetComponent } from "../../../../core/dashboard/dashboard-widget/dashboard-widget.component";
import { WidgetContentComponent } from "../../../../core/dashboard/dashboard-widget/widget-content/widget-content.component";
+import { SyncStateSubject } from "../../../../core/session/session-type";
@Component({
selector: "app-progress-dashboard",
@@ -41,17 +41,13 @@ export class ProgressDashboardComponent implements OnInit {
private entityMapper: EntityMapperService,
private loggingService: LoggingService,
private dialog: MatDialog,
- private sessionService: SessionService,
+ private syncState: SyncStateSubject,
) {}
async ngOnInit() {
this.data = new ProgressDashboardConfig(this.dashboardConfigId);
this.loadConfigFromDatabase().catch(() =>
- firstValueFrom(
- this.sessionService.syncState.pipe(
- waitForChangeTo(SyncState.COMPLETED),
- ),
- )
+ firstValueFrom(this.syncState.pipe(waitForChangeTo(SyncState.COMPLETED)))
.then(() => this.loadConfigFromDatabase())
.catch(() => this.createDefaultConfig()),
);
diff --git a/src/app/features/file/couchdb-file.service.spec.ts b/src/app/features/file/couchdb-file.service.spec.ts
index c4800ed4d3..8eac2a28d1 100644
--- a/src/app/features/file/couchdb-file.service.spec.ts
+++ b/src/app/features/file/couchdb-file.service.spec.ts
@@ -28,8 +28,8 @@ import {
} from "../../core/entity/database-entity.decorator";
import { AppSettings } from "../../core/app-settings";
import { FileDatatype } from "./file.datatype";
-import { SessionService } from "../../core/session/session-service/session.service";
import { SyncState } from "../../core/session/session-states/sync-state.enum";
+import { SyncStateSubject } from "../../core/session/session-type";
describe("CouchdbFileService", () => {
let service: CouchdbFileService;
@@ -61,8 +61,8 @@ describe("CouchdbFileService", () => {
},
{ provide: EntityRegistry, useValue: entityRegistry },
{
- provide: SessionService,
- useValue: { syncState: of(SyncState.COMPLETED) },
+ provide: SyncStateSubject,
+ useValue: of(SyncState.COMPLETED),
},
],
});
diff --git a/src/app/features/file/couchdb-file.service.ts b/src/app/features/file/couchdb-file.service.ts
index 7ffe0acbde..80cb6d1ab1 100644
--- a/src/app/features/file/couchdb-file.service.ts
+++ b/src/app/features/file/couchdb-file.service.ts
@@ -29,7 +29,7 @@ import { EntityRegistry } from "../../core/entity/database-entity.decorator";
import { LoggingService } from "../../core/logging/logging.service";
import { ObservableQueue } from "./observable-queue/observable-queue";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
-import { SessionService } from "../../core/session/session-service/session.service";
+import { SyncStateSubject } from "../../core/session/session-type";
/**
* Stores the files in the CouchDB.
@@ -51,9 +51,9 @@ export class CouchdbFileService extends FileService {
entityMapper: EntityMapperService,
entities: EntityRegistry,
logger: LoggingService,
- session: SessionService,
+ syncState: SyncStateSubject,
) {
- super(entityMapper, entities, logger, session);
+ super(entityMapper, entities, logger, syncState);
}
uploadFile(file: File, entity: Entity, property: string): Observable {
diff --git a/src/app/features/file/file.service.ts b/src/app/features/file/file.service.ts
index c56cd97074..db92703596 100644
--- a/src/app/features/file/file.service.ts
+++ b/src/app/features/file/file.service.ts
@@ -6,9 +6,9 @@ import { filter } from "rxjs/operators";
import { LoggingService } from "../../core/logging/logging.service";
import { SafeUrl } from "@angular/platform-browser";
import { FileDatatype } from "./file.datatype";
-import { SessionService } from "../../core/session/session-service/session.service";
import { waitForChangeTo } from "../../core/session/session-states/session-utils";
import { SyncState } from "../../core/session/session-states/sync-state.enum";
+import { SyncStateSubject } from "../../core/session/session-type";
/**
* This service allow handles the logic for files/attachments.
@@ -19,10 +19,10 @@ export abstract class FileService {
private entityMapper: EntityMapperService,
private entities: EntityRegistry,
private logger: LoggingService,
- private session: SessionService,
+ private syncState: SyncStateSubject,
) {
// TODO maybe registration is too late (only when component is rendered)
- this.session.syncState
+ this.syncState
// Only start listening to changes once the initial sync has been completed
.pipe(waitForChangeTo(SyncState.COMPLETED))
.subscribe(() => this.deleteFilesOfDeletedEntities());
diff --git a/src/app/features/file/mock-file.service.spec.ts b/src/app/features/file/mock-file.service.spec.ts
index c67a60b212..265e5006f9 100644
--- a/src/app/features/file/mock-file.service.spec.ts
+++ b/src/app/features/file/mock-file.service.spec.ts
@@ -8,7 +8,7 @@ import {
entityRegistry,
EntityRegistry,
} from "../../core/entity/database-entity.decorator";
-import { SessionService } from "../../core/session/session-service/session.service";
+import { SyncStateSubject } from "../../core/session/session-type";
import { SyncState } from "../../core/session/session-states/sync-state.enum";
describe("MockFileService", () => {
@@ -24,8 +24,8 @@ describe("MockFileService", () => {
},
{ provide: EntityRegistry, useValue: entityRegistry },
{
- provide: SessionService,
- useValue: { syncState: of(SyncState.COMPLETED) },
+ provide: SyncStateSubject,
+ useValue: of(SyncState.COMPLETED),
},
],
});
diff --git a/src/app/features/file/mock-file.service.ts b/src/app/features/file/mock-file.service.ts
index 66e8df7b4f..8e95c39020 100644
--- a/src/app/features/file/mock-file.service.ts
+++ b/src/app/features/file/mock-file.service.ts
@@ -6,7 +6,7 @@ import { EntityMapperService } from "../../core/entity/entity-mapper/entity-mapp
import { EntityRegistry } from "../../core/entity/database-entity.decorator";
import { LoggingService } from "../../core/logging/logging.service";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
-import { SessionService } from "../../core/session/session-service/session.service";
+import { SyncStateSubject } from "../../core/session/session-type";
/**
* A mock implementation of the file service which only stores the file temporarily in the browser.
@@ -21,10 +21,10 @@ export class MockFileService extends FileService {
entityMapper: EntityMapperService,
entities: EntityRegistry,
logger: LoggingService,
- session: SessionService,
+ syncState: SyncStateSubject,
private sanitizer: DomSanitizer,
) {
- super(entityMapper, entities, logger, session);
+ super(entityMapper, entities, logger, syncState);
}
removeFile(entity: Entity, property: string): Observable {
diff --git a/src/app/features/todos/todo-list/todo-list.component.ts b/src/app/features/todos/todo-list/todo-list.component.ts
index 06893502b3..e781c3fab3 100644
--- a/src/app/features/todos/todo-list/todo-list.component.ts
+++ b/src/app/features/todos/todo-list/todo-list.component.ts
@@ -7,13 +7,13 @@ import {
PrebuiltFilterConfig,
} from "../../../core/entity-list/EntityListConfig";
import { RouteData } from "../../../core/config/dynamic-routing/view-config.interface";
-import { SessionService } from "../../../core/session/session-service/session.service";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
import { TodoDetailsComponent } from "../todo-details/todo-details.component";
import { LoggingService } from "../../../core/logging/logging.service";
import moment from "moment";
import { EntityListComponent } from "../../../core/entity-list/entity-list/entity-list.component";
import { FilterSelectionOption } from "../../../core/filter/filters/filters";
+import { CurrentUserSubject } from "../../../core/user/user";
@RouteTarget("TodoList")
@Component({
@@ -39,7 +39,7 @@ export class TodoListComponent implements OnInit {
constructor(
private route: ActivatedRoute,
- private sessionService: SessionService,
+ private currentUser: CurrentUserSubject,
private formDialog: FormDialogService,
private logger: LoggingService,
) {}
@@ -89,7 +89,7 @@ export class TodoListComponent implements OnInit {
(c) => c.id === "assignedTo",
);
if (assignedToFilter && !assignedToFilter.default) {
- assignedToFilter.default = this.sessionService.getCurrentUser().name;
+ assignedToFilter.default = this.currentUser.value.name;
}
}
diff --git a/src/app/features/todos/todo.service.spec.ts b/src/app/features/todos/todo.service.spec.ts
index 1d58a3ea51..c57f8f78e8 100644
--- a/src/app/features/todos/todo.service.spec.ts
+++ b/src/app/features/todos/todo.service.spec.ts
@@ -1,9 +1,9 @@
import { TestBed } from "@angular/core/testing";
import { TodoService } from "./todo.service";
-import { SessionService } from "../../core/session/session-service/session.service";
import { AlertService } from "../../core/alerts/alert.service";
import { EntityMapperService } from "../../core/entity/entity-mapper/entity-mapper.service";
+import { CurrentUserSubject } from "../../core/user/user";
describe("TodoService", () => {
let service: TodoService;
@@ -11,7 +11,7 @@ describe("TodoService", () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
- { provide: SessionService, useValue: null },
+ CurrentUserSubject,
{ provide: AlertService, useValue: null },
{ provide: EntityMapperService, useValue: null },
],
diff --git a/src/app/features/todos/todo.service.ts b/src/app/features/todos/todo.service.ts
index 135065d42c..d8daf9f074 100644
--- a/src/app/features/todos/todo.service.ts
+++ b/src/app/features/todos/todo.service.ts
@@ -1,16 +1,16 @@
import { Injectable } from "@angular/core";
-import { SessionService } from "../../core/session/session-service/session.service";
import { AlertService } from "../../core/alerts/alert.service";
import { EntityMapperService } from "../../core/entity/entity-mapper/entity-mapper.service";
import { Todo } from "./model/todo";
import moment from "moment/moment";
+import { CurrentUserSubject } from "../../core/user/user";
@Injectable({
providedIn: "root",
})
export class TodoService {
constructor(
- private sessionService: SessionService,
+ private currentUser: CurrentUserSubject,
private alertService: AlertService,
private entityMapper: EntityMapperService,
) {}
@@ -19,7 +19,7 @@ export class TodoService {
const nextTodo = await this.createNextRepetition(todo);
todo.completed = {
- completedBy: this.sessionService.getCurrentUser().name,
+ completedBy: this.currentUser.value.name,
completedAt: new Date(),
nextRepetition: nextTodo?.getId(true),
};
diff --git a/src/app/features/todos/todos-dashboard/todos-dashboard.component.spec.ts b/src/app/features/todos/todos-dashboard/todos-dashboard.component.spec.ts
index dbdee59022..5f3ebb93e0 100644
--- a/src/app/features/todos/todos-dashboard/todos-dashboard.component.spec.ts
+++ b/src/app/features/todos/todos-dashboard/todos-dashboard.component.spec.ts
@@ -5,8 +5,8 @@ import { Todo } from "../model/todo";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
import { LoginState } from "../../../core/session/session-states/login-state.enum";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
-import { SessionService } from "../../../core/session/session-service/session.service";
import moment from "moment";
+import { CurrentUserSubject } from "../../../core/user/user";
describe("TodosDashboardComponent", () => {
let component: TodosDashboardComponent;
@@ -36,8 +36,7 @@ describe("TodosDashboardComponent", () => {
}));
beforeEach(async () => {
- testUser =
- TestBed.inject(SessionService).getCurrentUser().name;
+ testUser = TestBed.inject(CurrentUserSubject).value.name;
fixture = TestBed.createComponent(TodosDashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
diff --git a/src/app/features/todos/todos-dashboard/todos-dashboard.component.ts b/src/app/features/todos/todos-dashboard/todos-dashboard.component.ts
index fa6865aaaf..d3dc6064df 100644
--- a/src/app/features/todos/todos-dashboard/todos-dashboard.component.ts
+++ b/src/app/features/todos/todos-dashboard/todos-dashboard.component.ts
@@ -3,12 +3,12 @@ import { DynamicComponent } from "../../../core/config/dynamic-components/dynami
import { Todo } from "../model/todo";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
import { TodoDetailsComponent } from "../todo-details/todo-details.component";
-import { SessionService } from "../../../core/session/session-service/session.service";
import moment from "moment";
import { DashboardListWidgetComponent } from "../../../core/dashboard/dashboard-list-widget/dashboard-list-widget.component";
import { DatePipe, NgStyle } from "@angular/common";
import { MatTableModule } from "@angular/material/table";
import { MatTooltipModule } from "@angular/material/tooltip";
+import { CurrentUserSubject } from "../../../core/user/user";
@DynamicComponent("TodosDashboard")
@Component({
@@ -32,13 +32,13 @@ export class TodosDashboardComponent {
constructor(
private formDialog: FormDialogService,
- private sessionService: SessionService,
+ private currentUser: CurrentUserSubject,
) {}
filterEntries = (todo: Todo) => {
return (
!todo.completed &&
- todo.assignedTo.includes(this.sessionService.getCurrentUser().name) &&
+ todo.assignedTo.includes(this.currentUser.value.name) &&
moment(todo.startDate).isSameOrBefore(moment(), "days")
);
};
diff --git a/src/app/utils/database-testing.module.ts b/src/app/utils/database-testing.module.ts
index c7c0064bbe..fe0abaf986 100644
--- a/src/app/utils/database-testing.module.ts
+++ b/src/app/utils/database-testing.module.ts
@@ -1,7 +1,5 @@
import { NgModule } from "@angular/core";
import { PouchDatabase } from "../core/database/pouch-database";
-import { SessionService } from "../core/session/session-service/session.service";
-import { LocalSession } from "../core/session/session-service/local-session";
import { ConfigService } from "../core/config/config.service";
import { SessionType } from "../core/session/session-type";
import { environment } from "../../environments/environment";
@@ -26,7 +24,6 @@ import { EntityConfigService } from "../core/entity/entity-config.service";
@NgModule({
imports: [AppModule],
providers: [
- { provide: SessionService, useClass: LocalSession },
{ provide: ConfigService, useValue: createTestingConfigService() },
{
provide: ConfigurableEnumService,
diff --git a/src/app/utils/di-tokens.ts b/src/app/utils/di-tokens.ts
index 989af9c497..51f7a471c1 100644
--- a/src/app/utils/di-tokens.ts
+++ b/src/app/utils/di-tokens.ts
@@ -8,3 +8,6 @@ export const WINDOW_TOKEN = new InjectionToken("Window object");
export const LOCATION_TOKEN = new InjectionToken(
"Window location object",
);
+export const NAVIGATOR_TOKEN = new InjectionToken(
+ "Window navigator object",
+);
diff --git a/src/app/utils/mock-local-session.ts b/src/app/utils/mock-local-session.ts
index 47adba6375..3ddacd8bdc 100644
--- a/src/app/utils/mock-local-session.ts
+++ b/src/app/utils/mock-local-session.ts
@@ -1,25 +1,2 @@
-import { AuthUser } from "../core/session/session-service/auth-user";
-import { SessionService } from "../core/session/session-service/session.service";
-import { PouchDatabase } from "../core/database/pouch-database";
-import { LocalSession } from "../core/session/session-service/local-session";
-
export const TEST_USER = "test";
export const TEST_PASSWORD = "pass";
-
-export function createLocalSession(
- andLogin?: boolean,
- user: AuthUser = { name: TEST_USER, roles: ["user_app"] },
-): SessionService {
- const databaseMock: Partial = {
- isEmpty: () => Promise.resolve(false),
- initIndexedDB: () => undefined,
- initInMemoryDB: () => undefined,
- destroy: () => Promise.resolve(),
- };
- const localSession = new LocalSession(databaseMock as PouchDatabase);
- localSession.saveUser(user, TEST_PASSWORD);
- if (andLogin === true) {
- localSession.login(TEST_USER, TEST_PASSWORD);
- }
- return localSession;
-}
diff --git a/src/app/utils/mocked-testing.module.ts b/src/app/utils/mocked-testing.module.ts
index 6e1a8b7ff8..a09f3e8bc2 100644
--- a/src/app/utils/mocked-testing.module.ts
+++ b/src/app/utils/mocked-testing.module.ts
@@ -1,13 +1,11 @@
import { ModuleWithProviders, NgModule } from "@angular/core";
-import { SessionService } from "../core/session/session-service/session.service";
import { LoginState } from "../core/session/session-states/login-state.enum";
import { EntityMapperService } from "../core/entity/entity-mapper/entity-mapper.service";
import { mockEntityMapper } from "../core/entity/entity-mapper/mock-entity-mapper-service";
-import { User } from "../core/user/user";
+import { CurrentUserSubject, User } from "../core/user/user";
import { AnalyticsService } from "../core/analytics/analytics.service";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { RouterTestingModule } from "@angular/router/testing";
-import { Database } from "../core/database/database";
import { SessionType } from "../core/session/session-type";
import { Entity } from "../core/entity/model/entity";
import { DatabaseIndexingService } from "../core/entity/database-indexing/database-indexing.service";
@@ -21,7 +19,8 @@ import { ComponentRegistry } from "../dynamic-components";
import { ConfigurableEnumService } from "../core/basic-datatypes/configurable-enum/configurable-enum.service";
import { createTestingConfigurableEnumService } from "../core/basic-datatypes/configurable-enum/configurable-enum-testing";
import { SwRegistrationOptions } from "@angular/service-worker";
-import { createLocalSession, TEST_USER } from "./mock-local-session";
+import { TEST_USER } from "./mock-local-session";
+import { BehaviorSubject } from "rxjs";
/**
* Utility module that can be imported in test files or stories to have mock implementations of the SessionService
@@ -71,18 +70,22 @@ export class MockedTestingModule {
): ModuleWithProviders {
environment.session_type = SessionType.mock;
const mockedEntityMapper = mockEntityMapper([...data]);
- const session = createLocalSession(loginState === LoginState.LOGGED_IN);
return {
ngModule: MockedTestingModule,
providers: [
- { provide: SessionService, useValue: session },
{ provide: EntityMapperService, useValue: mockedEntityMapper },
{ provide: ConfigService, useValue: createTestingConfigService() },
{
provide: ConfigurableEnumService,
useValue: createTestingConfigurableEnumService(),
},
- { provide: Database, useValue: session.getDatabase() },
+ {
+ provide: CurrentUserSubject,
+ useValue: new BehaviorSubject({
+ name: TEST_USER,
+ roles: ["user_app"],
+ }),
+ },
],
};
}
diff --git a/src/app/utils/storybook-base.module.ts b/src/app/utils/storybook-base.module.ts
index 7ab847a729..c36e96db9f 100644
--- a/src/app/utils/storybook-base.module.ts
+++ b/src/app/utils/storybook-base.module.ts
@@ -8,7 +8,6 @@ import { AbilityService } from "../core/permissions/ability/ability.service";
import { EMPTY, Subject } from "rxjs";
import { EntityAbility } from "../core/permissions/ability/entity-ability";
import { defineAbility } from "@casl/ability";
-import { SessionService } from "../core/session/session-service/session.service";
import { createTestingConfigService } from "../core/config/testing-config-service";
import { componentRegistry } from "../dynamic-components";
import { AppModule } from "../app.module";
@@ -22,7 +21,7 @@ import {
} from "../core/entity/entity-mapper/mock-entity-mapper-service";
import { EntityMapperService } from "../core/entity/entity-mapper/entity-mapper.service";
import { DatabaseIndexingService } from "../core/entity/database-indexing/database-indexing.service";
-import { createLocalSession, TEST_USER } from "./mock-local-session";
+import { TEST_USER } from "./mock-local-session";
componentRegistry.allowDuplicates();
entityRegistry.allowDuplicates();
@@ -56,10 +55,6 @@ export const entityFormStorybookDefaultParameters = {
provide: EntityAbility,
useValue: defineAbility((can) => can("manage", "all")),
},
- {
- provide: SessionService,
- useValue: createLocalSession(true),
- },
{
provide: DatabaseIndexingService,
useValue: {
diff --git a/src/assets/locale/messages.de.xlf b/src/assets/locale/messages.de.xlf
index 4870e0ee75..03bc6ca2a3 100644
--- a/src/assets/locale/messages.de.xlf
+++ b/src/assets/locale/messages.de.xlf
@@ -1532,7 +1532,7 @@
The progress, e.g. of a certain activity
src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.ts
- 70
+ 66
@@ -2230,7 +2230,7 @@
src/app/core/user/user.ts
- 34
+ 37
@@ -5435,7 +5435,7 @@
Neuste รnderungen konnten nicht geladen werden:
src/app/core/ui/latest-changes/latest-changes.service.ts
- 124
+ 122
@@ -5485,10 +5485,6 @@
Benutzername
Username placeholder
-
- src/app/core/session/login/login.component.html
- 37
-
src/app/core/user/user-account/user-account.component.html
24
@@ -5498,75 +5494,6 @@
93
-
-
- Passwort
- password placeholder
-
- src/app/core/session/login/login.component.html
- 52
-
-
-
-
- Login
- Login button
-
- src/app/core/session/login/login.component.html
- 77
-
-
-
-
- Passwort anzeigen
- Tooltip text for showing the password
-
- src/app/core/session/login/login.component.ts
- 66
-
-
-
-
- Passwort verbergen
- Tooltip text for hiding the password
-
- src/app/core/session/login/login.component.ts
- 67
-
-
-
-
- Bitte stellen Sie sicher das Sie eine Internetverbindung haben und versuchen Sie es erneut.
- LoginError
-
- src/app/core/session/login/login.component.ts
- 113
-
-
-
-
- Benutzername und/oder Password inkorrekt
- LoginError
-
- src/app/core/session/login/login.component.ts
- 118
-
-
-
-
- Ein unerwarteter Fehler ist aufgetreten.
- Bitte laden Sie die Seite neu und versuchen Sie es erneut.
- Wenn Sie weiterhin diese Fehlermeldung sehen, kontaktieren Sie bitte Ihren Systemadministrator.
-
- LoginError
-
- src/app/core/session/login/login.component.ts
- 127
-
-
App Name
@@ -5662,7 +5589,7 @@
Title user feedback dialog
src/app/core/support/support/support.component.ts
- 142
+ 148
@@ -5671,7 +5598,7 @@
Subtitle user feedback dialog
src/app/core/support/support/support.component.ts
- 143
+ 149
@@ -5753,7 +5680,11 @@
-
+
Profil
Navigate to user profile page
@@ -5762,7 +5693,9 @@
-
+
Abmelden
Sign out of the app
@@ -5785,7 +5718,7 @@
Password reset disabled tooltip
src/app/core/user/user-account/user-account.component.ts
- 68
+ 66
@@ -5794,7 +5727,7 @@
Password reset disabled tooltip
src/app/core/user/user-account/user-account.component.ts
- 70
+ 68
@@ -5940,7 +5873,7 @@
Snackbar message
src/app/core/user/user-security/user-security.component.ts
- 131
+ 123
@@ -5949,7 +5882,7 @@
Snackbar message
src/app/core/user/user-security/user-security.component.ts
- 133
+ 125
@@ -5958,7 +5891,7 @@
Snackbar message
src/app/core/user/user-security/user-security.component.ts
- 158
+ 150
@@ -5967,7 +5900,7 @@
Snackbar message
src/app/core/user/user-security/user-security.component.ts
- 179
+ 171
@@ -5976,7 +5909,7 @@
label for entity
src/app/core/user/user.ts
- 33
+ 36
@@ -5985,7 +5918,7 @@
Label of username
src/app/core/user/user.ts
- 39
+ 42
@@ -5995,7 +5928,7 @@
Error message when trying to change the username
src/app/core/user/user.ts
- 47
+ 50
@@ -6976,179 +6909,131 @@
41
-
-
- Aktuelles Passwort
+
+
+ Passwort รคndern
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 7
+ src/app/core/session/auth/keycloak/account-page/account-page.component.html
+ 9,11
+ Button which opens password reset page
-
-
- Bitte das korrekte Passwort zur Bestรคtigung eingeben
- An incorrect password was entered trying to change
- the password
- Password Confirmation
+
+
+ Email Adresse
+ Email address input field
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 15
+ src/app/core/session/auth/keycloak/account-page/account-page.component.html
+ 19
-
-
- Neues Passwort
+
+
+ Bitte klicke auf den Link in der E-Mail, welche wir an die angegebene Adresse gesendet haben um diese zu bestรคtigen.
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 21
+ src/app/core/session/auth/keycloak/account-page/account-page.component.ts
+ 52
-
-
- Bitte ein neues Passwort eingeben.
- Password validation
+
+
+ Willkommen
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 27
+ src/app/core/session/login/login.component.html
+ 20,22
+ Sign in title
-
-
- Muss mindestend 8 Zeichen lang sein.
- Password validation
+
+
+ zu ihrer Anwendung
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 30
+ src/app/core/session/login/login.component.html
+ 23,26
+ Sign in subtitle
-
-
- Muss Kleinbuchstaben, Groรbuchstaben, Symbole und Nummern enthalten um sicher zu sein.
+
+
+ Sie mรผssen sich einloggen um Ihre Daten mit anderen Nutzer:innen zu synchronisieren. Wir รผberprรผfen, ob Sie noch eingeloggt sind oder leiten Sie zum Anmelde-Fenster weiter. Sie kรถnnen die Anwendung weiterhin ohne Login nutzen, falls Sie derzeit keine aktive Internetverbindung haben.
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 33,36
+ src/app/core/session/login/login.component.html
+ 33
- Illegal password pattern
+ online login tooltip
-
-
- Neues Passwort bestรคtigen
+
+
+ Online Login wird รผberprรผft ...
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 41
+ src/app/core/session/login/login.component.html
+ 37
+ login progess bar title
-
-
- Die Passwรถrter stimmen nicht รผberein.
- Illegal new password
+
+
+ Die Verbindung mit dem Server ist fehlgeschlagen. Sie kรถnnen die Anwendung ohne Login nutzen, falls Sie sich zuvor schon einmal auf diesem Gerรคt angemeldet hatten.
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 49
+ src/app/core/session/login/login.component.html
+ 48,52
+ online login failed text
-
-
- Passwort รคndern
- Change password button
-
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 62
-
+
+
+ Online login erneut versuchen
- src/app/core/session/auth/keycloak/account-page/account-page.component.html
- 10
+ src/app/core/session/login/login.component.html
+ 59,61
+ Login button
-
-
- Passwort erfolgreich geรคndert.
+
+
+ Sie kรถnnen die komplette Anwendung ohne Internetverbindung nutzen. Dabei werden Ihre Daten aber nicht mit anderen Nutzer:innen synchronisiert. Wenn Sie eine Internetverbindung haben, sollten Sie sich immer Online einloggen.
- src/app/core/session/auth/couchdb/password-form/password-form.component.ts
- 105
+ src/app/core/session/login/login.component.html
+ 72
+ offline login tooltip
-
-
- Password konnte nicht geรคndert werden:
-Bitte versuchen Sie es erneut. Wenn das Problem weiterhin besteht, kontaktieren Sie das Support-Team.
+
+
+ Offline nutzen
- src/app/core/session/auth/couchdb/password-form/password-form.component.ts
- 109
+ src/app/core/session/login/login.component.html
+ 75,77
+ offline section title
-
-
- Email Adresse
+
+
+ Einloggen als Nutzer:in ...
- src/app/core/session/auth/keycloak/account-page/account-page.component.html
- 20
+ src/app/core/session/login/login.component.html
+ 79
- Email address input field
+ Select user for offline login title
-
+
- Bitte eine gรผltige Email Adresse eingeben
+ Bitte eine gรผltige Email Adresse angeben
+ Error message if email format is invalid
src/app/core/session/auth/keycloak/account-page/account-page.component.html
- 26,28
+ 25
- Error message if email format is invalid
-
+
Email speichern
- Button which updates the user's email
src/app/core/session/auth/keycloak/account-page/account-page.component.html
- 42
-
-
-
-
- Bitte klicke auf den Link in der E-Mail, welche wir an die angegebene Adresse gesendet haben um diese zu bestรคtigen.
-
- src/app/core/session/auth/keycloak/account-page/account-page.component.ts
- 56
-
-
-
-
- Passwort vergessen?
-
- Login button
-
- src/app/core/session/auth/keycloak/password-reset/password-reset.component.html
- 8
-
-
-
-
- Please provide a valid email
- Invalid email error message
-
- src/app/core/session/auth/keycloak/password-reset/password-reset.component.html
- 27
-
-
-
-
- Bitte einloggen
- Sign in title
-
- src/app/core/session/login/login.component.html
- 23
+ 40,42
+ Button which updates the user's email
diff --git a/src/assets/locale/messages.fr.xlf b/src/assets/locale/messages.fr.xlf
index aeb78da414..329159f3fa 100644
--- a/src/assets/locale/messages.fr.xlf
+++ b/src/assets/locale/messages.fr.xlf
@@ -2761,7 +2761,7 @@
The progress, e.g. of a certain activity
src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.ts
- 70
+ 66
@@ -3091,10 +3091,6 @@
Nom d'utilisateur
Username placeholder
-
- src/app/core/session/login/login.component.html
- 37
-
src/app/core/user/user-account/user-account.component.html
24
@@ -3227,7 +3223,7 @@
src/app/core/user/user.ts
- 34
+ 37
@@ -5058,7 +5054,7 @@
Les derniรจres modifications n'ont pas pu รชtre chargรฉes:
src/app/core/ui/latest-changes/latest-changes.service.ts
- 124
+ 122
@@ -5104,75 +5100,6 @@
37
-
-
- Mot de passe
- password placeholder
-
- src/app/core/session/login/login.component.html
- 52
-
-
-
-
- S'identifier
- Login button
-
- src/app/core/session/login/login.component.html
- 77
-
-
-
-
- Show password
- Tooltip text for showing the password
-
- src/app/core/session/login/login.component.ts
- 66
-
-
-
-
- Hide password
- Tooltip text for hiding the password
-
- src/app/core/session/login/login.component.ts
- 67
-
-
-
-
- Veuillez vous connecter ร internet et rรฉessayer
- LoginError
-
- src/app/core/session/login/login.component.ts
- 113
-
-
-
-
- Nom d'utilisateur et/ou mot de passe incorrect
- LoginError
-
- src/app/core/session/login/login.component.ts
- 118
-
-
-
-
- Une erreur inattendue s'est produite.
- Veuillez recharger la page et rรฉessayer.
- Si vous continuez ร voir ce message d'erreur, veuillez contacter votre administrateur systรจme.
-
- LoginError
-
- src/app/core/session/login/login.component.ts
- 127
-
-
Site name
@@ -5268,7 +5195,7 @@
Title user feedback dialog
src/app/core/support/support/support.component.ts
- 142
+ 148
@@ -5277,7 +5204,7 @@
Subtitle user feedback dialog
src/app/core/support/support/support.component.ts
- 143
+ 149
@@ -5424,7 +5351,11 @@
-
+
Profil
Navigate to user profile page
@@ -5433,7 +5364,9 @@
-
+
Se dรฉconnecter
Sign out of the app
@@ -5456,7 +5389,7 @@
Password reset disabled tooltip
src/app/core/user/user-account/user-account.component.ts
- 68
+ 66
@@ -5465,7 +5398,7 @@
Password reset disabled tooltip
src/app/core/user/user-account/user-account.component.ts
- 70
+ 68
@@ -5611,7 +5544,7 @@
Snackbar message
src/app/core/user/user-security/user-security.component.ts
- 131
+ 123
@@ -5620,7 +5553,7 @@
Snackbar message
src/app/core/user/user-security/user-security.component.ts
- 133
+ 125
@@ -5629,7 +5562,7 @@
Snackbar message
src/app/core/user/user-security/user-security.component.ts
- 158
+ 150
@@ -5638,7 +5571,7 @@
Snackbar message
src/app/core/user/user-security/user-security.component.ts
- 179
+ 171
@@ -5647,7 +5580,7 @@
label for entity
src/app/core/user/user.ts
- 33
+ 36
@@ -5656,7 +5589,7 @@
Label of username
src/app/core/user/user.ts
- 39
+ 42
@@ -5666,7 +5599,7 @@
Error message when trying to change the username
src/app/core/user/user.ts
- 47
+ 50
@@ -7038,179 +6971,131 @@
41
-
-
- Mot de passe actuel
+
+
+ Modifier le mot de passe
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 7
+ src/app/core/session/auth/keycloak/account-page/account-page.component.html
+ 9,11
+ Button which opens password reset page
-
-
- Veuillez fournir votre mot de passe actuel correct pour confirmer.
- An incorrect password was entered trying to change
- the password
- Password Confirmation
+
+
+ Your email address
+ Email address input field
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 15
+ src/app/core/session/auth/keycloak/account-page/account-page.component.html
+ 19
-
-
- Nouveau mot de passe
+
+
+ Please click the link in the email we sent you to verify your email address.
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 21
+ src/app/core/session/auth/keycloak/account-page/account-page.component.ts
+ 52
-
-
- Please enter a new password.
- Password validation
+
+
+ Welcome
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 27
+ src/app/core/session/login/login.component.html
+ 20,22
+ Sign in title
-
-
- Le mot de passe doit comporter au moins 8 caractรจres.
- Password validation
+
+
+ to your system
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 30
+ src/app/core/session/login/login.component.html
+ 23,26
+ Sign in subtitle
-
-
- Le mot de passe doit contenir des lettres minuscules, majuscules, et des chiffres pour รชtre sรฉcurisรฉ.
+
+
+ It is necessary to log in to the server so that your data can be synchronized with other team members. We are checking whether you are still logged in or otherwise forward you to provide your credentials. You can still use the application offline, if you currently have no internet connection.
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 33,36
+ src/app/core/session/login/login.component.html
+ 33
- Illegal password pattern
+ online login tooltip
-
-
- Confirmez votre nouveau mot de passe
+
+
+ Checking online login ...
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 41
+ src/app/core/session/login/login.component.html
+ 37
+ login progess bar title
-
-
- Passwords don't match.
- Illegal new password
+
+
+ We couldn't connect to the server currently. You can still use the application offline, if you have logged in on this device previously.
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 49
+ src/app/core/session/login/login.component.html
+ 48,52
+ online login failed text
-
-
- Modifier le mot de passe
- Change password button
-
- src/app/core/session/auth/couchdb/password-form/password-form.component.html
- 62
-
+
+
+ Retry online login
- src/app/core/session/auth/keycloak/account-page/account-page.component.html
- 10
+ src/app/core/session/login/login.component.html
+ 59,61
+ Login button
-
-
- Mot de passe modifiรฉ avec succรจs.
+
+
+ You can use the application completely offline. However, your changes cannot be synchronized with other team members in this mode. When available, you should always use the online login.
- src/app/core/session/auth/couchdb/password-form/password-form.component.ts
- 105
+ src/app/core/session/login/login.component.html
+ 72
+ offline login tooltip
-
-
- Failed to change password:
-Please try again. If the problem persists contact Aam Digital support.
+
+
+ Offline Login
- src/app/core/session/auth/couchdb/password-form/password-form.component.ts
- 109
+ src/app/core/session/login/login.component.html
+ 75,77
+ offline section title
-
-
- Your email address
+
+
+ Log in as user ...
- src/app/core/session/auth/keycloak/account-page/account-page.component.html
- 20
+ src/app/core/session/login/login.component.html
+ 79
- Email address input field
+ Select user for offline login title
-
+
- Please provide a valid email
+ Please provide a valid email
+ Error message if email format is invalid
src/app/core/session/auth/keycloak/account-page/account-page.component.html
- 26,28
+ 25
- Error message if email format is invalid
-
+
- Update email
- Button which updates the user's email
+ Update email
src/app/core/session/auth/keycloak/account-page/account-page.component.html
- 42
-
-
-
-
- Please click the link in the email we sent you to verify your email address.
-
- src/app/core/session/auth/keycloak/account-page/account-page.component.ts
- 56
-
-
-
-
- Forgot password?
-
- Login button
-
- src/app/core/session/auth/keycloak/password-reset/password-reset.component.html
- 8
-
-
-
-
- Please provide a valid email
- Invalid email error message
-
- src/app/core/session/auth/keycloak/password-reset/password-reset.component.html
- 27
-
-
-
-
- Veuillez vous identifier
- Sign in title
-
- src/app/core/session/login/login.component.html
- 23
+ 40,42
+ Button which updates the user's email