diff --git a/README.md b/README.md
index f7c09f5..d9db8d4 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,9 @@
- Java must be installed
- cd matsen-tool
- npm run generate:api
+ - Wenn es nicht geht: brew install java
+ - sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk
+ - java -version
## Module anlegen
- cd app
diff --git a/matsen-tool/package-lock.json b/matsen-tool/package-lock.json
index 25bada7..f97d88a 100644
--- a/matsen-tool/package-lock.json
+++ b/matsen-tool/package-lock.json
@@ -41,6 +41,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
+ "openapi-typescript": "^7.0.0-next.5",
"typescript": "~5.2.2"
}
},
@@ -2326,54 +2327,6 @@
"node": ">=10.0.0"
}
},
- "node_modules/@esbuild/android-arm": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz",
- "integrity": "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz",
- "integrity": "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.5.tgz",
- "integrity": "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@esbuild/darwin-arm64": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz",
@@ -2390,294 +2343,6 @@
"node": ">=12"
}
},
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz",
- "integrity": "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz",
- "integrity": "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz",
- "integrity": "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz",
- "integrity": "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz",
- "integrity": "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz",
- "integrity": "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz",
- "integrity": "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz",
- "integrity": "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz",
- "integrity": "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz",
- "integrity": "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz",
- "integrity": "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz",
- "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz",
- "integrity": "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz",
- "integrity": "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz",
- "integrity": "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz",
- "integrity": "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz",
- "integrity": "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.19.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz",
- "integrity": "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@fastify/busboy": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
@@ -4384,6 +4049,95 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@redocly/ajv": {
+ "version": "8.11.0",
+ "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.0.tgz",
+ "integrity": "sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@redocly/openapi-core": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.5.0.tgz",
+ "integrity": "sha512-AnDLoDl1+a7mZO4+lx0KG8zH04BQx4ez6yh403PuNl9/0ygbicPPc9QG/y0/0OImChOA+knKLpJazNFjzhOAeg==",
+ "dev": true,
+ "dependencies": {
+ "@redocly/ajv": "^8.11.0",
+ "@types/node": "^14.11.8",
+ "colorette": "^1.2.0",
+ "js-levenshtein": "^1.1.6",
+ "js-yaml": "^4.1.0",
+ "lodash.isequal": "^4.5.0",
+ "minimatch": "^5.0.1",
+ "node-fetch": "^2.6.1",
+ "pluralize": "^8.0.0",
+ "yaml-ast-parser": "0.0.43"
+ },
+ "engines": {
+ "node": ">=14.19.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/@redocly/openapi-core/node_modules/@types/node": {
+ "version": "14.18.63",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
+ "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
+ "dev": true
+ },
+ "node_modules/@redocly/openapi-core/node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/@redocly/openapi-core/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@redocly/openapi-core/node_modules/colorette": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
+ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
+ "dev": true
+ },
+ "node_modules/@redocly/openapi-core/node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@redocly/openapi-core/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@schematics/angular": {
"version": "17.0.6",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.6.tgz",
@@ -9030,6 +8784,15 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/js-levenshtein": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
+ "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -9730,6 +9493,12 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true
},
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "dev": true
+ },
"node_modules/lodash.isfinite": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz",
@@ -10778,7 +10547,48 @@
"node": ">=12"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/openapi-typescript": {
+ "version": "7.0.0-next.5",
+ "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.0.0-next.5.tgz",
+ "integrity": "sha512-zqEDw/FZkT0ndOCd8EybkDVwEYgaOh+ryWm6OCON70DmY9YqUnNSIVyRFVjN8hesa0bxOs9QOMzXAasczNdHbQ==",
+ "dev": true,
+ "dependencies": {
+ "@redocly/openapi-core": "^1.4.1",
+ "ansi-colors": "^4.1.3",
+ "supports-color": "^9.4.0",
+ "typescript": "^5.3.2",
+ "yargs-parser": "^21.1.1"
+ },
+ "bin": {
+ "openapi-typescript": "bin/cli.js"
+ }
+ },
+ "node_modules/openapi-typescript/node_modules/supports-color": {
+ "version": "9.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz",
+ "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/openapi-typescript/node_modules/typescript": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
+ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
}
},
"node_modules/openurl": {
@@ -11316,6 +11126,15 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
+ "node_modules/pluralize": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/portscanner": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz",
@@ -13577,54 +13396,6 @@
}
}
},
- "node_modules/vite/node_modules/@esbuild/android-arm": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
- "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
- "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
- "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/vite/node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
@@ -13641,294 +13412,6 @@
"node": ">=12"
}
},
- "node_modules/vite/node_modules/@esbuild/darwin-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
- "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
- "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
- "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-arm": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
- "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
- "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-ia32": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
- "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-loong64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
- "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
- "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
- "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
- "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-s390x": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
- "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
- "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
- "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
- "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/sunos-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
- "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
- "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-ia32": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
- "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
- "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/vite/node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
@@ -14509,6 +13992,12 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
+ "node_modules/yaml-ast-parser": {
+ "version": "0.0.43",
+ "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
+ "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
+ "dev": true
+ },
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
diff --git a/matsen-tool/package.json b/matsen-tool/package.json
index 8304d6e..07ae70c 100644
--- a/matsen-tool/package.json
+++ b/matsen-tool/package.json
@@ -8,7 +8,7 @@
"watch": "ng build --watch --configuration development",
"test": "ng test",
"serve:ssr:matsen-tool": "node dist/matsen-tool/server/server.mjs",
- "generate:api": "openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o src/app/core/api/v1 -p=removeOperationIdPrefix=true"
+ "generate:api": "openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o src/app/core/api/v1 -p=removeOperationIdPrefix=true --additional-properties=supportsES6=true,typescriptThreePlus=true"
},
"private": true,
"dependencies": {
@@ -45,6 +45,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
+ "openapi-typescript": "^7.0.0-next.5",
"typescript": "~5.2.2"
}
}
diff --git a/matsen-tool/src/app/_components/alert.component.html b/matsen-tool/src/app/_components/alert.component.html
new file mode 100644
index 0000000..cc6be70
--- /dev/null
+++ b/matsen-tool/src/app/_components/alert.component.html
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/matsen-tool/src/app/_components/alert.component.ts b/matsen-tool/src/app/_components/alert.component.ts
new file mode 100644
index 0000000..a6f30ba
--- /dev/null
+++ b/matsen-tool/src/app/_components/alert.component.ts
@@ -0,0 +1,96 @@
+import { Component, OnInit, OnDestroy, Input } from '@angular/core';
+import { Router, NavigationStart } from '@angular/router';
+import { Subscription } from 'rxjs';
+
+import { Alert, AlertType } from '@app/_models';
+import { AlertService } from '@app/_services';
+
+@Component({ selector: 'alert', templateUrl: 'alert.component.html' })
+export class AlertComponent implements OnInit, OnDestroy {
+ @Input() id = 'default-alert';
+ @Input() fade = true;
+
+ alerts: Alert[] = [];
+ alertSubscription!: Subscription;
+ routeSubscription!: Subscription;
+
+ constructor(private router: Router, private alertService: AlertService) { }
+
+ ngOnInit() {
+ // subscribe to new alert notifications
+ this.alertSubscription = this.alertService.onAlert(this.id)
+ .subscribe(alert => {
+ // clear alerts when an empty alert is received
+ if (!alert.message) {
+ // filter out alerts without 'keepAfterRouteChange' flag
+ this.alerts = this.alerts.filter(x => x.keepAfterRouteChange);
+
+ // remove 'keepAfterRouteChange' flag on the rest
+ this.alerts.forEach(x => delete x.keepAfterRouteChange);
+ return;
+ }
+
+ // add alert to array
+ this.alerts.push(alert);
+
+ // auto close alert if required
+ if (alert.autoClose) {
+ setTimeout(() => this.removeAlert(alert), 3000);
+ }
+ });
+
+ // clear alerts on location change
+ this.routeSubscription = this.router.events.subscribe(event => {
+ if (event instanceof NavigationStart) {
+ this.alertService.clear(this.id);
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ // unsubscribe to avoid memory leaks
+ this.alertSubscription.unsubscribe();
+ this.routeSubscription.unsubscribe();
+ }
+
+ removeAlert(alert: Alert) {
+ // check if already removed to prevent error on auto close
+ if (!this.alerts.includes(alert)) return;
+
+ if (this.fade) {
+ // fade out alert
+ alert.fade = true;
+
+ // remove alert after faded out
+ setTimeout(() => {
+ this.alerts = this.alerts.filter(x => x !== alert);
+ }, 250);
+ } else {
+ // remove alert
+ this.alerts = this.alerts.filter(x => x !== alert);
+ }
+ }
+
+ cssClass(alert: Alert) {
+ if (!alert) return;
+
+ const classes = ['alert', 'alert-dismissible', 'mt-4', 'container'];
+
+ const alertTypeClass = {
+ [AlertType.Success]: 'alert-success',
+ [AlertType.Error]: 'alert-danger',
+ [AlertType.Info]: 'alert-info',
+ [AlertType.Warning]: 'alert-warning'
+ }
+
+ if (alert.type !== undefined) {
+ classes.push(alertTypeClass[alert.type]);
+ }
+
+ if (alert.fade) {
+ classes.push('fade');
+ }
+
+ return classes.join(' ');
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/_components/index.ts b/matsen-tool/src/app/_components/index.ts
new file mode 100644
index 0000000..d221c1b
--- /dev/null
+++ b/matsen-tool/src/app/_components/index.ts
@@ -0,0 +1 @@
+export * from './alert.component';
diff --git a/matsen-tool/src/app/_helpers/auth.guard.ts b/matsen-tool/src/app/_helpers/auth.guard.ts
new file mode 100644
index 0000000..6f26abc
--- /dev/null
+++ b/matsen-tool/src/app/_helpers/auth.guard.ts
@@ -0,0 +1,24 @@
+import { Injectable } from '@angular/core';
+import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
+
+import { AccountService } from '@app/_services';
+
+@Injectable({ providedIn: 'root' })
+export class AuthGuard implements CanActivate {
+ constructor(
+ private router: Router,
+ private accountService: AccountService
+ ) {}
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+ const user = this.accountService.userValue;
+ if (user) {
+ // authorised so return true
+ return true;
+ }
+
+ // not logged in so redirect to login page with the return url
+ this.router.navigate(['/account/login'], { queryParams: { returnUrl: state.url }});
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/_helpers/error.interceptor.ts b/matsen-tool/src/app/_helpers/error.interceptor.ts
new file mode 100644
index 0000000..06cfbf1
--- /dev/null
+++ b/matsen-tool/src/app/_helpers/error.interceptor.ts
@@ -0,0 +1,24 @@
+import { Injectable } from '@angular/core';
+import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
+import { Observable, throwError } from 'rxjs';
+import { catchError } from 'rxjs/operators';
+
+import { AccountService } from '@app/_services';
+
+@Injectable()
+export class ErrorInterceptor implements HttpInterceptor {
+ constructor(private accountService: AccountService) {}
+
+ intercept(request: HttpRequest, next: HttpHandler): Observable> {
+ return next.handle(request).pipe(catchError(err => {
+ if ([401, 403].includes(err.status) && this.accountService.userValue) {
+ // auto logout if 401 or 403 response returned from api
+ this.accountService.logout();
+ }
+
+ const error = err.error?.message || err.statusText;
+ console.error(err);
+ return throwError(() => error);
+ }))
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/_helpers/fake-backend.ts b/matsen-tool/src/app/_helpers/fake-backend.ts
new file mode 100644
index 0000000..e74bb06
--- /dev/null
+++ b/matsen-tool/src/app/_helpers/fake-backend.ts
@@ -0,0 +1,138 @@
+import { Injectable } from '@angular/core';
+import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
+import { Observable, of, throwError } from 'rxjs';
+import { delay, materialize, dematerialize } from 'rxjs/operators';
+
+// array in local storage for registered users
+const usersKey = 'angular-14-registration-login-example-users';
+let users: any[] = JSON.parse(localStorage.getItem(usersKey)!) || [];
+
+@Injectable()
+export class FakeBackendInterceptor implements HttpInterceptor {
+ intercept(request: HttpRequest, next: HttpHandler): Observable> {
+ const { url, method, headers, body } = request;
+
+ return handleRoute();
+
+ function handleRoute() {
+ switch (true) {
+ case url.endsWith('/users/authenticate') && method === 'POST':
+ return authenticate();
+ case url.endsWith('/users/register') && method === 'POST':
+ return register();
+ case url.endsWith('/users') && method === 'GET':
+ return getUsers();
+ case url.match(/\/users\/\d+$/) && method === 'GET':
+ return getUserById();
+ case url.match(/\/users\/\d+$/) && method === 'PUT':
+ return updateUser();
+ case url.match(/\/users\/\d+$/) && method === 'DELETE':
+ return deleteUser();
+ default:
+ // pass through any requests not handled above
+ return next.handle(request);
+ }
+ }
+
+ // route functions
+
+ function authenticate() {
+ const { username, password } = body;
+ const user = users.find(x => x.username === username && x.password === password);
+ if (!user) return error('Username or password is incorrect');
+ return ok({
+ ...basicDetails(user),
+ token: 'fake-jwt-token'
+ })
+ }
+
+ function register() {
+ const user = body
+
+ if (users.find(x => x.username === user.username)) {
+ return error('Username "' + user.username + '" is already taken')
+ }
+
+ user.id = users.length ? Math.max(...users.map(x => x.id)) + 1 : 1;
+ users.push(user);
+ localStorage.setItem(usersKey, JSON.stringify(users));
+ return ok();
+ }
+
+ function getUsers() {
+ if (!isLoggedIn()) return unauthorized();
+ return ok(users.map(x => basicDetails(x)));
+ }
+
+ function getUserById() {
+ if (!isLoggedIn()) return unauthorized();
+
+ const user = users.find(x => x.id === idFromUrl());
+ return ok(basicDetails(user));
+ }
+
+ function updateUser() {
+ if (!isLoggedIn()) return unauthorized();
+
+ let params = body;
+ let user = users.find(x => x.id === idFromUrl());
+
+ // only update password if entered
+ if (!params.password) {
+ delete params.password;
+ }
+
+ // update and save user
+ Object.assign(user, params);
+ localStorage.setItem(usersKey, JSON.stringify(users));
+
+ return ok();
+ }
+
+ function deleteUser() {
+ if (!isLoggedIn()) return unauthorized();
+
+ users = users.filter(x => x.id !== idFromUrl());
+ localStorage.setItem(usersKey, JSON.stringify(users));
+ return ok();
+ }
+
+ // helper functions
+
+ function ok(body?: any) {
+ return of(new HttpResponse({ status: 200, body }))
+ .pipe(delay(500)); // delay observable to simulate server api call
+ }
+
+ function error(message: string) {
+ return throwError(() => ({ error: { message } }))
+ .pipe(materialize(), delay(500), dematerialize()); // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648);
+ }
+
+ function unauthorized() {
+ return throwError(() => ({ status: 401, error: { message: 'Unauthorized' } }))
+ .pipe(materialize(), delay(500), dematerialize());
+ }
+
+ function basicDetails(user: any) {
+ const { id, username, firstName, lastName } = user;
+ return { id, username, firstName, lastName };
+ }
+
+ function isLoggedIn() {
+ return headers.get('Authorization') === 'Bearer fake-jwt-token';
+ }
+
+ function idFromUrl() {
+ const urlParts = url.split('/');
+ return parseInt(urlParts[urlParts.length - 1]);
+ }
+ }
+}
+
+export const fakeBackendProvider = {
+ // use fake backend in place of Http service for backend-less development
+ provide: HTTP_INTERCEPTORS,
+ useClass: FakeBackendInterceptor,
+ multi: true
+};
diff --git a/matsen-tool/src/app/_helpers/index.ts b/matsen-tool/src/app/_helpers/index.ts
new file mode 100644
index 0000000..d4ea538
--- /dev/null
+++ b/matsen-tool/src/app/_helpers/index.ts
@@ -0,0 +1,4 @@
+export * from './auth.guard';
+export * from './error.interceptor';
+export * from './jwt.interceptor';
+export * from './fake-backend';
diff --git a/matsen-tool/src/app/_helpers/jwt.interceptor.ts b/matsen-tool/src/app/_helpers/jwt.interceptor.ts
new file mode 100644
index 0000000..85a2550
--- /dev/null
+++ b/matsen-tool/src/app/_helpers/jwt.interceptor.ts
@@ -0,0 +1,27 @@
+import { Injectable } from '@angular/core';
+import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+import { environment } from '@environments/environment';
+import { AccountService } from '@app/_services';
+
+@Injectable()
+export class JwtInterceptor implements HttpInterceptor {
+ constructor(private accountService: AccountService) { }
+
+ intercept(request: HttpRequest, next: HttpHandler): Observable> {
+ // add auth header with jwt if user is logged in and request is to the api url
+ const user = this.accountService.userValue;
+ const isLoggedIn = user && user.token;
+ const isApiUrl = request.url.startsWith(environment.apiUrl);
+ if (isLoggedIn && isApiUrl) {
+ request = request.clone({
+ setHeaders: {
+ Authorization: `Bearer ${user.token}`
+ }
+ });
+ }
+
+ return next.handle(request);
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/_models/alert.ts b/matsen-tool/src/app/_models/alert.ts
new file mode 100644
index 0000000..e623372
--- /dev/null
+++ b/matsen-tool/src/app/_models/alert.ts
@@ -0,0 +1,25 @@
+export class Alert {
+ id?: string;
+ type?: AlertType;
+ message?: string;
+ autoClose?: boolean;
+ keepAfterRouteChange?: boolean;
+ fade?: boolean;
+
+ constructor(init?:Partial) {
+ Object.assign(this, init);
+ }
+}
+
+export enum AlertType {
+ Success,
+ Error,
+ Info,
+ Warning
+}
+
+export class AlertOptions {
+ id?: string;
+ autoClose?: boolean;
+ keepAfterRouteChange?: boolean;
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/_models/index.ts b/matsen-tool/src/app/_models/index.ts
new file mode 100644
index 0000000..a039b12
--- /dev/null
+++ b/matsen-tool/src/app/_models/index.ts
@@ -0,0 +1,2 @@
+export * from './alert';
+export * from './user';
\ No newline at end of file
diff --git a/matsen-tool/src/app/_models/user.ts b/matsen-tool/src/app/_models/user.ts
new file mode 100644
index 0000000..abb5c40
--- /dev/null
+++ b/matsen-tool/src/app/_models/user.ts
@@ -0,0 +1,8 @@
+export class User {
+ id?: string;
+ username?: string;
+ password?: string;
+ firstName?: string;
+ lastName?: string;
+ token?: string;
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/_services/account.service.ts b/matsen-tool/src/app/_services/account.service.ts
new file mode 100644
index 0000000..e1fb715
--- /dev/null
+++ b/matsen-tool/src/app/_services/account.service.ts
@@ -0,0 +1,82 @@
+import { Injectable } from '@angular/core';
+import { Router } from '@angular/router';
+import { HttpClient } from '@angular/common/http';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { environment } from '@environments/environment';
+import { User } from '@app/_models';
+
+@Injectable({ providedIn: 'root' })
+export class AccountService {
+ private userSubject: BehaviorSubject;
+ public user: Observable;
+
+ constructor(
+ private router: Router,
+ private http: HttpClient
+ ) {
+ this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!));
+ this.user = this.userSubject.asObservable();
+ }
+
+ public get userValue() {
+ return this.userSubject.value;
+ }
+
+ login(username: string, password: string) {
+ return this.http.post(`${environment.apiUrl}/login`, { username, password })
+ .pipe(map(user => {
+ // store user details and jwt token in local storage to keep user logged in between page refreshes
+ localStorage.setItem('user', JSON.stringify(user));
+ this.userSubject.next(user);
+ return user;
+ }));
+ }
+
+ logout() {
+ // remove user from local storage and set current user to null
+ localStorage.removeItem('user');
+ this.userSubject.next(null);
+ this.router.navigate(['/account/login']);
+ }
+
+ register(user: User) {
+ return this.http.post(`${environment.apiUrl}/users/register`, user);
+ }
+
+ getAll() {
+ return this.http.get(`${environment.apiUrl}/users`);
+ }
+
+ getById(id: string) {
+ return this.http.get(`${environment.apiUrl}/users/${id}`);
+ }
+
+ update(id: string, params: any) {
+ return this.http.put(`${environment.apiUrl}/users/${id}`, params)
+ .pipe(map(x => {
+ // update stored user if the logged in user updated their own record
+ if (id == this.userValue?.id) {
+ // update local storage
+ const user = { ...this.userValue, ...params };
+ localStorage.setItem('user', JSON.stringify(user));
+
+ // publish updated user to subscribers
+ this.userSubject.next(user);
+ }
+ return x;
+ }));
+ }
+
+ delete(id: string) {
+ return this.http.delete(`${environment.apiUrl}/users/${id}`)
+ .pipe(map(x => {
+ // auto logout if the logged in user deleted their own record
+ if (id == this.userValue?.id) {
+ this.logout();
+ }
+ return x;
+ }));
+ }
+}
diff --git a/matsen-tool/src/app/_services/alert.service.ts b/matsen-tool/src/app/_services/alert.service.ts
new file mode 100644
index 0000000..9ecd8c4
--- /dev/null
+++ b/matsen-tool/src/app/_services/alert.service.ts
@@ -0,0 +1,44 @@
+import { Injectable } from '@angular/core';
+import { Observable, Subject } from 'rxjs';
+import { filter } from 'rxjs/operators';
+
+import { Alert, AlertType, AlertOptions } from '@app/_models';
+
+@Injectable({ providedIn: 'root' })
+export class AlertService {
+ private subject = new Subject();
+ private defaultId = 'default-alert';
+
+ // enable subscribing to alerts observable
+ onAlert(id = this.defaultId): Observable {
+ return this.subject.asObservable().pipe(filter(x => x && x.id === id));
+ }
+
+ // convenience methods
+ success(message: string, options?: AlertOptions) {
+ this.alert(new Alert({ ...options, type: AlertType.Success, message }));
+ }
+
+ error(message: string, options?: AlertOptions) {
+ this.alert(new Alert({ ...options, type: AlertType.Error, message }));
+ }
+
+ info(message: string, options?: AlertOptions) {
+ this.alert(new Alert({ ...options, type: AlertType.Info, message }));
+ }
+
+ warn(message: string, options?: AlertOptions) {
+ this.alert(new Alert({ ...options, type: AlertType.Warning, message }));
+ }
+
+ // main alert method
+ alert(alert: Alert) {
+ alert.id = alert.id || this.defaultId;
+ this.subject.next(alert);
+ }
+
+ // clear alerts
+ clear(id = this.defaultId) {
+ this.subject.next(new Alert({ id }));
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/_services/index.ts b/matsen-tool/src/app/_services/index.ts
new file mode 100644
index 0000000..a2b99f0
--- /dev/null
+++ b/matsen-tool/src/app/_services/index.ts
@@ -0,0 +1,2 @@
+export * from './account.service';
+export * from './alert.service';
diff --git a/matsen-tool/src/app/account/account-routing.module.ts b/matsen-tool/src/app/account/account-routing.module.ts
new file mode 100644
index 0000000..05b795b
--- /dev/null
+++ b/matsen-tool/src/app/account/account-routing.module.ts
@@ -0,0 +1,22 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { LayoutComponent } from './layout.component';
+import { LoginComponent } from './login.component';
+import { RegisterComponent } from './register.component';
+
+const routes: Routes = [
+ {
+ path: '', component: LayoutComponent,
+ children: [
+ { path: 'login', component: LoginComponent },
+ { path: 'register', component: RegisterComponent }
+ ]
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class AccountRoutingModule { }
\ No newline at end of file
diff --git a/matsen-tool/src/app/account/account.module.ts b/matsen-tool/src/app/account/account.module.ts
new file mode 100644
index 0000000..1276469
--- /dev/null
+++ b/matsen-tool/src/app/account/account.module.ts
@@ -0,0 +1,22 @@
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+
+import { AccountRoutingModule } from './account-routing.module';
+import { LayoutComponent } from './layout.component';
+import { LoginComponent } from './login.component';
+import { RegisterComponent } from './register.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ AccountRoutingModule
+ ],
+ declarations: [
+ LayoutComponent,
+ LoginComponent,
+ RegisterComponent
+ ]
+})
+export class AccountModule { }
\ No newline at end of file
diff --git a/matsen-tool/src/app/account/layout.component.html b/matsen-tool/src/app/account/layout.component.html
new file mode 100644
index 0000000..134a1de
--- /dev/null
+++ b/matsen-tool/src/app/account/layout.component.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/matsen-tool/src/app/account/layout.component.ts b/matsen-tool/src/app/account/layout.component.ts
new file mode 100644
index 0000000..f94aaf8
--- /dev/null
+++ b/matsen-tool/src/app/account/layout.component.ts
@@ -0,0 +1,17 @@
+import { Component } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { AccountService } from '@app/_services';
+
+@Component({ templateUrl: 'layout.component.html' })
+export class LayoutComponent {
+ constructor(
+ private router: Router,
+ private accountService: AccountService
+ ) {
+ // redirect to home if already logged in
+ if (this.accountService.userValue) {
+ this.router.navigate(['/']);
+ }
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/account/login.component.html b/matsen-tool/src/app/account/login.component.html
new file mode 100644
index 0000000..04f5089
--- /dev/null
+++ b/matsen-tool/src/app/account/login.component.html
@@ -0,0 +1,28 @@
+
diff --git a/matsen-tool/src/app/account/login.component.ts b/matsen-tool/src/app/account/login.component.ts
new file mode 100644
index 0000000..8f5a526
--- /dev/null
+++ b/matsen-tool/src/app/account/login.component.ts
@@ -0,0 +1,58 @@
+import { Component, OnInit } from '@angular/core';
+import { Router, ActivatedRoute } from '@angular/router';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { first } from 'rxjs/operators';
+
+import { AccountService, AlertService } from '@app/_services';
+
+@Component({ templateUrl: 'login.component.html' })
+export class LoginComponent implements OnInit {
+ form!: FormGroup;
+ loading = false;
+ submitted = false;
+
+ constructor(
+ private formBuilder: FormBuilder,
+ private route: ActivatedRoute,
+ private router: Router,
+ private accountService: AccountService,
+ private alertService: AlertService
+ ) { }
+
+ ngOnInit() {
+ this.form = this.formBuilder.group({
+ username: ['', Validators.required],
+ password: ['', Validators.required]
+ });
+ }
+
+ // convenience getter for easy access to form fields
+ get f() { return this.form.controls; }
+
+ onSubmit() {
+ this.submitted = true;
+
+ // reset alerts on submit
+ this.alertService.clear();
+
+ // stop here if form is invalid
+ if (this.form.invalid) {
+ return;
+ }
+
+ this.loading = true;
+ this.accountService.login(this.f['username'].value, this.f['password'].value)
+ .pipe(first())
+ .subscribe({
+ next: () => {
+ // get return url from query parameters or default to home page
+ const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
+ this.router.navigateByUrl(returnUrl);
+ },
+ error: error => {
+ this.alertService.error(error);
+ this.loading = false;
+ }
+ });
+ }
+}
diff --git a/matsen-tool/src/app/account/register.component.html b/matsen-tool/src/app/account/register.component.html
new file mode 100644
index 0000000..3c31d7b
--- /dev/null
+++ b/matsen-tool/src/app/account/register.component.html
@@ -0,0 +1,43 @@
+
diff --git a/matsen-tool/src/app/account/register.component.ts b/matsen-tool/src/app/account/register.component.ts
new file mode 100644
index 0000000..1b24209
--- /dev/null
+++ b/matsen-tool/src/app/account/register.component.ts
@@ -0,0 +1,59 @@
+import { Component, OnInit } from '@angular/core';
+import { Router, ActivatedRoute } from '@angular/router';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { first } from 'rxjs/operators';
+
+import { AccountService, AlertService } from '@app/_services';
+
+@Component({ templateUrl: 'register.component.html' })
+export class RegisterComponent implements OnInit {
+ form!: FormGroup;
+ loading = false;
+ submitted = false;
+
+ constructor(
+ private formBuilder: FormBuilder,
+ private route: ActivatedRoute,
+ private router: Router,
+ private accountService: AccountService,
+ private alertService: AlertService
+ ) { }
+
+ ngOnInit() {
+ this.form = this.formBuilder.group({
+ firstName: ['', Validators.required],
+ lastName: ['', Validators.required],
+ username: ['', Validators.required],
+ password: ['', [Validators.required, Validators.minLength(6)]]
+ });
+ }
+
+ // convenience getter for easy access to form fields
+ get f() { return this.form.controls; }
+
+ onSubmit() {
+ this.submitted = true;
+
+ // reset alerts on submit
+ this.alertService.clear();
+
+ // stop here if form is invalid
+ if (this.form.invalid) {
+ return;
+ }
+
+ this.loading = true;
+ this.accountService.register(this.form.value)
+ .pipe(first())
+ .subscribe({
+ next: () => {
+ this.alertService.success('Registration successful', { keepAfterRouteChange: true });
+ this.router.navigate(['../login'], { relativeTo: this.route });
+ },
+ error: error => {
+ this.alertService.error(error);
+ this.loading = false;
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/app-routing.module.ts b/matsen-tool/src/app/app-routing.module.ts
index 8d4dcf3..902fb58 100644
--- a/matsen-tool/src/app/app-routing.module.ts
+++ b/matsen-tool/src/app/app-routing.module.ts
@@ -1,10 +1,23 @@
import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
+import { Routes, RouterModule } from '@angular/router';
-const routes: Routes = [{ path: 'test', loadChildren: () => import('./test/test.module').then(m => m.TestModule) }];
+import { HomeComponent } from './home';
+import { AuthGuard } from './_helpers';
+
+const accountModule = () => import('./account/account.module').then(x => x.AccountModule);
+const usersModule = () => import('./users/users.module').then(x => x.UsersModule);
+
+const routes: Routes = [
+ { path: '', component: HomeComponent, canActivate: [AuthGuard] },
+ { path: 'users', loadChildren: usersModule, canActivate: [AuthGuard] },
+ { path: 'account', loadChildren: accountModule },
+
+ // otherwise redirect to home
+ { path: '**', redirectTo: '' }
+];
@NgModule({
- imports: [RouterModule.forRoot(routes)],
- exports: [RouterModule]
+ imports: [RouterModule.forRoot(routes)],
+ exports: [RouterModule]
})
-export class AppRoutingModule { }
+export class AppRoutingModule { }
\ No newline at end of file
diff --git a/matsen-tool/src/app/app.component.html b/matsen-tool/src/app/app.component.html
index 853e557..21ed9b7 100644
--- a/matsen-tool/src/app/app.component.html
+++ b/matsen-tool/src/app/app.component.html
@@ -1,12 +1,24 @@
+
+
+
+
-
-
- Posts
-
+
+
- {{post.id}} - {{post.owner}}
- {{post.message}}
-
-
-
-
+
+
\ No newline at end of file
diff --git a/matsen-tool/src/app/app.component.ts b/matsen-tool/src/app/app.component.ts
index d0ecd35..0f6ca9a 100644
--- a/matsen-tool/src/app/app.component.ts
+++ b/matsen-tool/src/app/app.component.ts
@@ -1,35 +1,17 @@
-import {Component, OnInit} from '@angular/core';
-import {ApiPostsGetCollection200Response, PostJsonld, PostService} from "./core/api/v1";
-import {Observable, Subscription} from "rxjs";
+import { Component } from '@angular/core';
-@Component({
- selector: 'app-root',
- templateUrl: './app.component.html',
- styleUrl: './app.component.scss'
-})
-export class AppComponent implements OnInit{
+import { AccountService } from './_services';
+import { User } from './_models';
- title = 'matsen-tool';
- protected postSub: Subscription;
- protected posts: Array;
-
- constructor(private postService: PostService) {
- this.postSub = new Subscription();
- this.posts = [];
- }
-
- ngOnInit(): void {
-
- this.postSub = this.postService.postsGetCollection().subscribe(
- data => {
-
- console.log(data);
- //myVariable['my:prop:name' as keyof MyModel]
- this.posts = data["hydra:member"];
- console.log(this.posts);
- }
- );
+@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
+export class AppComponent {
+ user?: User | null;
+ constructor(private accountService: AccountService) {
+ this.accountService.user.subscribe(x => this.user = x);
}
-}
+ logout() {
+ this.accountService.logout();
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/app.config.server.ts b/matsen-tool/src/app/app.config.server.ts
new file mode 100644
index 0000000..b4d57c9
--- /dev/null
+++ b/matsen-tool/src/app/app.config.server.ts
@@ -0,0 +1,11 @@
+import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
+import { provideServerRendering } from '@angular/platform-server';
+import { appConfig } from './app.config';
+
+const serverConfig: ApplicationConfig = {
+ providers: [
+ provideServerRendering()
+ ]
+};
+
+export const config = mergeApplicationConfig(appConfig, serverConfig);
diff --git a/matsen-tool/src/app/app.config.ts b/matsen-tool/src/app/app.config.ts
new file mode 100644
index 0000000..3ea3d51
--- /dev/null
+++ b/matsen-tool/src/app/app.config.ts
@@ -0,0 +1,10 @@
+import { ApplicationConfig } from '@angular/core';
+import { provideRouter } from '@angular/router';
+
+import { routes } from './app.routes';
+import { provideClientHydration } from '@angular/platform-browser';
+import { provideAnimations } from '@angular/platform-browser/animations';
+
+export const appConfig: ApplicationConfig = {
+ providers: [provideRouter(routes), provideClientHydration(), provideAnimations()]
+};
diff --git a/matsen-tool/src/app/app.module.ts b/matsen-tool/src/app/app.module.ts
index facae96..55baeb7 100644
--- a/matsen-tool/src/app/app.module.ts
+++ b/matsen-tool/src/app/app.module.ts
@@ -1,36 +1,36 @@
-import { NgModule } from '@angular/core';
-import { BrowserModule, provideClientHydration } from '@angular/platform-browser';
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { ReactiveFormsModule } from '@angular/forms';
+import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
+
+// used to create fake backend
+import { fakeBackendProvider } from './_helpers';
import { AppRoutingModule } from './app-routing.module';
+import { JwtInterceptor, ErrorInterceptor } from './_helpers';
import { AppComponent } from './app.component';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-import {ApiModule, Configuration, ConfigurationParameters} from "./core/api/v1";
-import {HttpClientModule} from "@angular/common/http";
-import {environment} from "../environment";
-import { LoginModule } from './login/login.module';
-
-export function apiConfigFactory(): Configuration {
- const params: ConfigurationParameters = {
- basePath: environment.basePath,
- };
- return new Configuration(params);
-}
+import { AlertComponent } from './_components';
+import { HomeComponent } from './home';
@NgModule({
- declarations: [
- AppComponent
- ],
- imports: [
- BrowserModule,
- ApiModule.forRoot(apiConfigFactory),
- HttpClientModule,
- AppRoutingModule,
- BrowserAnimationsModule,
- LoginModule
- ],
- providers: [
- provideClientHydration()
- ],
- bootstrap: [AppComponent]
+ imports: [
+ BrowserModule,
+ ReactiveFormsModule,
+ HttpClientModule,
+ AppRoutingModule
+ ],
+ declarations: [
+ AppComponent,
+ AlertComponent,
+ HomeComponent
+ ],
+ providers: [
+ { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
+ { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
+
+ // provider used to create fake backend
+ fakeBackendProvider
+ ],
+ bootstrap: [AppComponent]
})
-export class AppModule { }
+export class AppModule { };
diff --git a/matsen-tool/src/app/app.routes.ts b/matsen-tool/src/app/app.routes.ts
new file mode 100644
index 0000000..dc39edb
--- /dev/null
+++ b/matsen-tool/src/app/app.routes.ts
@@ -0,0 +1,3 @@
+import { Routes } from '@angular/router';
+
+export const routes: Routes = [];
diff --git a/matsen-tool/src/app/home/home.component.html b/matsen-tool/src/app/home/home.component.html
new file mode 100644
index 0000000..0516e3f
--- /dev/null
+++ b/matsen-tool/src/app/home/home.component.html
@@ -0,0 +1,7 @@
+
+
+
Hi {{user?.firstName}}!
+
You're logged in with Angular 14!!
+
Manage Users
+
+
\ No newline at end of file
diff --git a/matsen-tool/src/app/home/home.component.ts b/matsen-tool/src/app/home/home.component.ts
new file mode 100644
index 0000000..6dcd2ff
--- /dev/null
+++ b/matsen-tool/src/app/home/home.component.ts
@@ -0,0 +1,13 @@
+import { Component } from '@angular/core';
+
+import { User } from '@app/_models';
+import { AccountService } from '@app/_services';
+
+@Component({ templateUrl: 'home.component.html' })
+export class HomeComponent {
+ user: User | null;
+
+ constructor(private accountService: AccountService) {
+ this.user = this.accountService.userValue;
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/home/index.ts b/matsen-tool/src/app/home/index.ts
new file mode 100644
index 0000000..1c212fb
--- /dev/null
+++ b/matsen-tool/src/app/home/index.ts
@@ -0,0 +1 @@
+export * from './home.component';
\ No newline at end of file
diff --git a/matsen-tool/src/app/login/login.module.ts b/matsen-tool/src/app/login/login.module.ts
deleted file mode 100644
index bea0794..0000000
--- a/matsen-tool/src/app/login/login.module.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { NgModule } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { LoginComponent } from './login/login.component';
-
-
-
-@NgModule({
- declarations: [
- LoginComponent
- ],
- exports: [
- LoginComponent
- ],
- imports: [
- CommonModule
- ]
-})
-export class LoginModule { }
diff --git a/matsen-tool/src/app/login/login/login.component.html b/matsen-tool/src/app/login/login/login.component.html
deleted file mode 100644
index 147cfc4..0000000
--- a/matsen-tool/src/app/login/login/login.component.html
+++ /dev/null
@@ -1 +0,0 @@
-login works!
diff --git a/matsen-tool/src/app/login/login/login.component.scss b/matsen-tool/src/app/login/login/login.component.scss
deleted file mode 100644
index e69de29..0000000
diff --git a/matsen-tool/src/app/login/login/login.component.spec.ts b/matsen-tool/src/app/login/login/login.component.spec.ts
deleted file mode 100644
index f1c475b..0000000
--- a/matsen-tool/src/app/login/login/login.component.spec.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { LoginComponent } from './login.component';
-
-describe('LoginComponent', () => {
- let component: LoginComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- declarations: [LoginComponent]
- })
- .compileComponents();
-
- fixture = TestBed.createComponent(LoginComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/matsen-tool/src/app/login/login/login.component.ts b/matsen-tool/src/app/login/login/login.component.ts
deleted file mode 100644
index c178092..0000000
--- a/matsen-tool/src/app/login/login/login.component.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
- selector: 'app-login',
- templateUrl: './login.component.html',
- styleUrl: './login.component.scss'
-})
-export class LoginComponent {
-
-}
diff --git a/matsen-tool/src/app/test/test-routing.module.ts b/matsen-tool/src/app/test/test-routing.module.ts
deleted file mode 100644
index 9f42520..0000000
--- a/matsen-tool/src/app/test/test-routing.module.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { TestComponent } from './test.component';
-
-const routes: Routes = [{ path: '', component: TestComponent }];
-
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class TestRoutingModule { }
diff --git a/matsen-tool/src/app/test/test.component.html b/matsen-tool/src/app/test/test.component.html
deleted file mode 100644
index 941f267..0000000
--- a/matsen-tool/src/app/test/test.component.html
+++ /dev/null
@@ -1 +0,0 @@
-test works!
diff --git a/matsen-tool/src/app/test/test.component.scss b/matsen-tool/src/app/test/test.component.scss
deleted file mode 100644
index e69de29..0000000
diff --git a/matsen-tool/src/app/test/test.component.spec.ts b/matsen-tool/src/app/test/test.component.spec.ts
deleted file mode 100644
index 2d65b88..0000000
--- a/matsen-tool/src/app/test/test.component.spec.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TestComponent } from './test.component';
-
-describe('TestComponent', () => {
- let component: TestComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- declarations: [TestComponent]
- })
- .compileComponents();
-
- fixture = TestBed.createComponent(TestComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/matsen-tool/src/app/test/test.component.ts b/matsen-tool/src/app/test/test.component.ts
deleted file mode 100644
index 4f123a4..0000000
--- a/matsen-tool/src/app/test/test.component.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
- selector: 'app-test',
- templateUrl: './test.component.html',
- styleUrl: './test.component.scss'
-})
-export class TestComponent {
-
-}
diff --git a/matsen-tool/src/app/test/test.module.ts b/matsen-tool/src/app/test/test.module.ts
deleted file mode 100644
index f1a2a29..0000000
--- a/matsen-tool/src/app/test/test.module.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { NgModule } from '@angular/core';
-import { CommonModule } from '@angular/common';
-
-import { TestRoutingModule } from './test-routing.module';
-import { TestComponent } from './test.component';
-
-
-@NgModule({
- declarations: [
- TestComponent
- ],
- imports: [
- CommonModule,
- TestRoutingModule
- ]
-})
-export class TestModule { }
diff --git a/matsen-tool/src/app/users/add-edit.component.html b/matsen-tool/src/app/users/add-edit.component.html
new file mode 100644
index 0000000..cef6e48
--- /dev/null
+++ b/matsen-tool/src/app/users/add-edit.component.html
@@ -0,0 +1,49 @@
+{{title}}
+
+
+
+
diff --git a/matsen-tool/src/app/users/add-edit.component.ts b/matsen-tool/src/app/users/add-edit.component.ts
new file mode 100644
index 0000000..af636de
--- /dev/null
+++ b/matsen-tool/src/app/users/add-edit.component.ts
@@ -0,0 +1,86 @@
+import { Component, OnInit } from '@angular/core';
+import { Router, ActivatedRoute } from '@angular/router';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { first } from 'rxjs/operators';
+
+import { AccountService, AlertService } from '@app/_services';
+
+@Component({ templateUrl: 'add-edit.component.html' })
+export class AddEditComponent implements OnInit {
+ form!: FormGroup;
+ id?: string;
+ title!: string;
+ loading = false;
+ submitting = false;
+ submitted = false;
+
+ constructor(
+ private formBuilder: FormBuilder,
+ private route: ActivatedRoute,
+ private router: Router,
+ private accountService: AccountService,
+ private alertService: AlertService
+ ) { }
+
+ ngOnInit() {
+ this.id = this.route.snapshot.params['id'];
+
+ // form with validation rules
+ this.form = this.formBuilder.group({
+ firstName: ['', Validators.required],
+ lastName: ['', Validators.required],
+ username: ['', Validators.required],
+ // password only required in add mode
+ password: ['', [Validators.minLength(6), ...(!this.id ? [Validators.required] : [])]]
+ });
+
+ this.title = 'Add User';
+ if (this.id) {
+ // edit mode
+ this.title = 'Edit User';
+ this.loading = true;
+ this.accountService.getById(this.id)
+ .pipe(first())
+ .subscribe(x => {
+ this.form.patchValue(x);
+ this.loading = false;
+ });
+ }
+ }
+
+ // convenience getter for easy access to form fields
+ get f() { return this.form.controls; }
+
+ onSubmit() {
+ this.submitted = true;
+
+ // reset alerts on submit
+ this.alertService.clear();
+
+ // stop here if form is invalid
+ if (this.form.invalid) {
+ return;
+ }
+
+ this.submitting = true;
+ this.saveUser()
+ .pipe(first())
+ .subscribe({
+ next: () => {
+ this.alertService.success('User saved', { keepAfterRouteChange: true });
+ this.router.navigateByUrl('/users');
+ },
+ error: error => {
+ this.alertService.error(error);
+ this.submitting = false;
+ }
+ })
+ }
+
+ private saveUser() {
+ // create or update user based on id param
+ return this.id
+ ? this.accountService.update(this.id!, this.form.value)
+ : this.accountService.register(this.form.value);
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/users/layout.component.html b/matsen-tool/src/app/users/layout.component.html
new file mode 100644
index 0000000..f33ebd4
--- /dev/null
+++ b/matsen-tool/src/app/users/layout.component.html
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/matsen-tool/src/app/users/layout.component.ts b/matsen-tool/src/app/users/layout.component.ts
new file mode 100644
index 0000000..8720c08
--- /dev/null
+++ b/matsen-tool/src/app/users/layout.component.ts
@@ -0,0 +1,4 @@
+import { Component } from '@angular/core';
+
+@Component({ templateUrl: 'layout.component.html' })
+export class LayoutComponent { }
\ No newline at end of file
diff --git a/matsen-tool/src/app/users/list.component.html b/matsen-tool/src/app/users/list.component.html
new file mode 100644
index 0000000..a3a5165
--- /dev/null
+++ b/matsen-tool/src/app/users/list.component.html
@@ -0,0 +1,31 @@
+Users
+Add User
+
+
+
+ First Name
+ Last Name
+ Username
+
+
+
+
+
+ {{user.firstName}}
+ {{user.lastName}}
+ {{user.username}}
+
+ Edit
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matsen-tool/src/app/users/list.component.ts b/matsen-tool/src/app/users/list.component.ts
new file mode 100644
index 0000000..df4f6ea
--- /dev/null
+++ b/matsen-tool/src/app/users/list.component.ts
@@ -0,0 +1,25 @@
+import { Component, OnInit } from '@angular/core';
+import { first } from 'rxjs/operators';
+
+import { AccountService } from '@app/_services';
+
+@Component({ templateUrl: 'list.component.html' })
+export class ListComponent implements OnInit {
+ users?: any[];
+
+ constructor(private accountService: AccountService) {}
+
+ ngOnInit() {
+ this.accountService.getAll()
+ .pipe(first())
+ .subscribe(users => this.users = users);
+ }
+
+ deleteUser(id: string) {
+ const user = this.users!.find(x => x.id === id);
+ user.isDeleting = true;
+ this.accountService.delete(id)
+ .pipe(first())
+ .subscribe(() => this.users = this.users!.filter(x => x.id !== id));
+ }
+}
\ No newline at end of file
diff --git a/matsen-tool/src/app/users/users-routing.module.ts b/matsen-tool/src/app/users/users-routing.module.ts
new file mode 100644
index 0000000..d450bb3
--- /dev/null
+++ b/matsen-tool/src/app/users/users-routing.module.ts
@@ -0,0 +1,23 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { LayoutComponent } from './layout.component';
+import { ListComponent } from './list.component';
+import { AddEditComponent } from './add-edit.component';
+
+const routes: Routes = [
+ {
+ path: '', component: LayoutComponent,
+ children: [
+ { path: '', component: ListComponent },
+ { path: 'add', component: AddEditComponent },
+ { path: 'edit/:id', component: AddEditComponent }
+ ]
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class UsersRoutingModule { }
\ No newline at end of file
diff --git a/matsen-tool/src/app/users/users.module.ts b/matsen-tool/src/app/users/users.module.ts
new file mode 100644
index 0000000..17ed24b
--- /dev/null
+++ b/matsen-tool/src/app/users/users.module.ts
@@ -0,0 +1,22 @@
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+
+import { UsersRoutingModule } from './users-routing.module';
+import { LayoutComponent } from './layout.component';
+import { ListComponent } from './list.component';
+import { AddEditComponent } from './add-edit.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ UsersRoutingModule
+ ],
+ declarations: [
+ LayoutComponent,
+ ListComponent,
+ AddEditComponent
+ ]
+})
+export class UsersModule { }
\ No newline at end of file
diff --git a/matsen-tool/src/environment.prod.ts b/matsen-tool/src/environment.prod.ts
deleted file mode 100644
index 23acecc..0000000
--- a/matsen-tool/src/environment.prod.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const environment = {
- production: true,
- basePath: 'https://jsonplaceholder.typicode.com',
-};
diff --git a/matsen-tool/src/environment.ts b/matsen-tool/src/environment.ts
deleted file mode 100644
index d9530a5..0000000
--- a/matsen-tool/src/environment.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const environment = {
- production: false,
- // basePath: 'https://matsen-tool-be.ddev.site:8443',
- basePath: 'https://matsen-tool-be.ddev.site:8443',
-};
diff --git a/matsen-tool/src/environments/environment.prod.ts b/matsen-tool/src/environments/environment.prod.ts
new file mode 100644
index 0000000..9ce4927
--- /dev/null
+++ b/matsen-tool/src/environments/environment.prod.ts
@@ -0,0 +1,4 @@
+export const environment = {
+ production: true,
+ apiUrl: 'http://localhost:4000'
+};
diff --git a/matsen-tool/src/environments/environment.ts b/matsen-tool/src/environments/environment.ts
new file mode 100644
index 0000000..e335573
--- /dev/null
+++ b/matsen-tool/src/environments/environment.ts
@@ -0,0 +1,17 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+
+export const environment = {
+ production: false,
+ apiUrl: 'https://matsen-tool-be.ddev.site:8443'
+};
+
+/*
+ * For easier debugging in development mode, you can import the following file
+ * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
+ *
+ * This import should be commented out in production mode because it will have a negative impact
+ * on performance if an error is thrown.
+ */
+// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
diff --git a/matsen-tool/src/main.ts b/matsen-tool/src/main.ts
index c58dc05..c7b673c 100644
--- a/matsen-tool/src/main.ts
+++ b/matsen-tool/src/main.ts
@@ -1,7 +1,12 @@
+import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+if (environment.production) {
+ enableProdMode();
+}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
diff --git a/matsen-tool/src/styles.scss b/matsen-tool/src/styles.scss
index 7e7239a..50803c2 100644
--- a/matsen-tool/src/styles.scss
+++ b/matsen-tool/src/styles.scss
@@ -2,3 +2,14 @@
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
+
+.app-container {
+ min-height: 320px;
+ overflow: hidden;
+}
+
+.btn-delete-user {
+ width: 40px;
+ text-align: center;
+ box-sizing: content-box;
+}
diff --git a/matsen-tool/tsconfig.json b/matsen-tool/tsconfig.json
index f37b67f..c335b6e 100644
--- a/matsen-tool/tsconfig.json
+++ b/matsen-tool/tsconfig.json
@@ -2,17 +2,19 @@
{
"compileOnSave": false,
"compilerOptions": {
+ "baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
- "noPropertyAccessFromIndexSignature": true,
+ "noPropertyAccessFromIndexSignature": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
+ "downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
@@ -22,7 +24,11 @@
"lib": [
"ES2022",
"dom"
- ]
+ ],
+ "paths": {
+ "@app/*": ["src/app/*"],
+ "@environments/*": ["src/environments/*"]
+ }
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,