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 @@ +
+

Login

+
+
+
+ + +
+
Username is required
+
+
+
+ + +
+
Password is required
+
+
+
+ + Register +
+
+
+
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 @@ +
+

Register

+
+
+
+ + +
+
First Name is required
+
+
+
+ + +
+
Last Name is required
+
+
+
+ + +
+
Username is required
+
+
+
+ + +
+
Password is required
+
Password must be at least 6 characters
+
+
+
+ + Cancel +
+
+
+
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}}

+
+
+
+ + +
+
First Name is required
+
+
+
+ + +
+
Last Name is required
+
+
+
+
+
+ + +
+
Username is required
+
+
+
+ + +
+
Password is required
+
Password must be at least 6 characters
+
+
+
+
+ + Cancel +
+
+
+ +
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 NameLast NameUsername
{{user.firstName}}{{user.lastName}}{{user.username}} + Edit + +
+ +
\ 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,