Florian Eisenmenger 2 лет назад
Родитель
Сommit
50f2324602
61 измененных файлов: 1317 добавлений и 874 удалений
  1. +3
    -0
      README.md
  2. +162
    -673
      matsen-tool/package-lock.json
  3. +2
    -1
      matsen-tool/package.json
  4. +4
    -0
      matsen-tool/src/app/_components/alert.component.html
  5. +96
    -0
      matsen-tool/src/app/_components/alert.component.ts
  6. +1
    -0
      matsen-tool/src/app/_components/index.ts
  7. +24
    -0
      matsen-tool/src/app/_helpers/auth.guard.ts
  8. +24
    -0
      matsen-tool/src/app/_helpers/error.interceptor.ts
  9. +138
    -0
      matsen-tool/src/app/_helpers/fake-backend.ts
  10. +4
    -0
      matsen-tool/src/app/_helpers/index.ts
  11. +27
    -0
      matsen-tool/src/app/_helpers/jwt.interceptor.ts
  12. +25
    -0
      matsen-tool/src/app/_models/alert.ts
  13. +2
    -0
      matsen-tool/src/app/_models/index.ts
  14. +8
    -0
      matsen-tool/src/app/_models/user.ts
  15. +82
    -0
      matsen-tool/src/app/_services/account.service.ts
  16. +44
    -0
      matsen-tool/src/app/_services/alert.service.ts
  17. +2
    -0
      matsen-tool/src/app/_services/index.ts
  18. +22
    -0
      matsen-tool/src/app/account/account-routing.module.ts
  19. +22
    -0
      matsen-tool/src/app/account/account.module.ts
  20. +3
    -0
      matsen-tool/src/app/account/layout.component.html
  21. +17
    -0
      matsen-tool/src/app/account/layout.component.ts
  22. +28
    -0
      matsen-tool/src/app/account/login.component.html
  23. +58
    -0
      matsen-tool/src/app/account/login.component.ts
  24. +43
    -0
      matsen-tool/src/app/account/register.component.html
  25. +59
    -0
      matsen-tool/src/app/account/register.component.ts
  26. +18
    -5
      matsen-tool/src/app/app-routing.module.ts
  27. +22
    -10
      matsen-tool/src/app/app.component.html
  28. +12
    -30
      matsen-tool/src/app/app.component.ts
  29. +11
    -0
      matsen-tool/src/app/app.config.server.ts
  30. +10
    -0
      matsen-tool/src/app/app.config.ts
  31. +30
    -30
      matsen-tool/src/app/app.module.ts
  32. +3
    -0
      matsen-tool/src/app/app.routes.ts
  33. +7
    -0
      matsen-tool/src/app/home/home.component.html
  34. +13
    -0
      matsen-tool/src/app/home/home.component.ts
  35. +1
    -0
      matsen-tool/src/app/home/index.ts
  36. +0
    -18
      matsen-tool/src/app/login/login.module.ts
  37. +0
    -1
      matsen-tool/src/app/login/login/login.component.html
  38. +0
    -0
      matsen-tool/src/app/login/login/login.component.scss
  39. +0
    -23
      matsen-tool/src/app/login/login/login.component.spec.ts
  40. +0
    -10
      matsen-tool/src/app/login/login/login.component.ts
  41. +0
    -11
      matsen-tool/src/app/test/test-routing.module.ts
  42. +0
    -1
      matsen-tool/src/app/test/test.component.html
  43. +0
    -0
      matsen-tool/src/app/test/test.component.scss
  44. +0
    -23
      matsen-tool/src/app/test/test.component.spec.ts
  45. +0
    -10
      matsen-tool/src/app/test/test.component.ts
  46. +0
    -17
      matsen-tool/src/app/test/test.module.ts
  47. +49
    -0
      matsen-tool/src/app/users/add-edit.component.html
  48. +86
    -0
      matsen-tool/src/app/users/add-edit.component.ts
  49. +5
    -0
      matsen-tool/src/app/users/layout.component.html
  50. +4
    -0
      matsen-tool/src/app/users/layout.component.ts
  51. +31
    -0
      matsen-tool/src/app/users/list.component.html
  52. +25
    -0
      matsen-tool/src/app/users/list.component.ts
  53. +23
    -0
      matsen-tool/src/app/users/users-routing.module.ts
  54. +22
    -0
      matsen-tool/src/app/users/users.module.ts
  55. +0
    -4
      matsen-tool/src/environment.prod.ts
  56. +0
    -5
      matsen-tool/src/environment.ts
  57. +4
    -0
      matsen-tool/src/environments/environment.prod.ts
  58. +17
    -0
      matsen-tool/src/environments/environment.ts
  59. +5
    -0
      matsen-tool/src/main.ts
  60. +11
    -0
      matsen-tool/src/styles.scss
  61. +8
    -2
      matsen-tool/tsconfig.json

+ 3
- 0
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


+ 162
- 673
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",


+ 2
- 1
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"
}
}

+ 4
- 0
matsen-tool/src/app/_components/alert.component.html Просмотреть файл

@@ -0,0 +1,4 @@
<div *ngFor="let alert of alerts" class="{{cssClass(alert)}}">
<span [innerHTML]="alert.message"></span>
<button class="btn-close" (click)="removeAlert(alert)"></button>
</div>

+ 96
- 0
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(' ');
}
}

+ 1
- 0
matsen-tool/src/app/_components/index.ts Просмотреть файл

@@ -0,0 +1 @@
export * from './alert.component';

+ 24
- 0
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;
}
}

+ 24
- 0
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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
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);
}))
}
}

+ 138
- 0
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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
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
};

+ 4
- 0
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';

+ 27
- 0
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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 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);
}
}

+ 25
- 0
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<Alert>) {
Object.assign(this, init);
}
}

export enum AlertType {
Success,
Error,
Info,
Warning
}

export class AlertOptions {
id?: string;
autoClose?: boolean;
keepAfterRouteChange?: boolean;
}

+ 2
- 0
matsen-tool/src/app/_models/index.ts Просмотреть файл

@@ -0,0 +1,2 @@
export * from './alert';
export * from './user';

+ 8
- 0
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;
}

+ 82
- 0
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<User | null>;
public user: Observable<User | null>;

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<User>(`${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<User[]>(`${environment.apiUrl}/users`);
}

getById(id: string) {
return this.http.get<User>(`${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;
}));
}
}

+ 44
- 0
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<Alert>();
private defaultId = 'default-alert';

// enable subscribing to alerts observable
onAlert(id = this.defaultId): Observable<Alert> {
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 }));
}
}

+ 2
- 0
matsen-tool/src/app/_services/index.ts Просмотреть файл

@@ -0,0 +1,2 @@
export * from './account.service';
export * from './alert.service';

+ 22
- 0
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 { }

+ 22
- 0
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 { }

+ 3
- 0
matsen-tool/src/app/account/layout.component.html Просмотреть файл

@@ -0,0 +1,3 @@
<div class="container col-md-6 offset-md-3 mt-5">
<router-outlet></router-outlet>
</div>

+ 17
- 0
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(['/']);
}
}
}

+ 28
- 0
matsen-tool/src/app/account/login.component.html Просмотреть файл

@@ -0,0 +1,28 @@
<div class="card">
<h4 class="card-header">Login</h4>
<div class="card-body">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['username'].errors }" />
<div *ngIf="submitted && f['username'].errors" class="invalid-feedback">
<div *ngIf="f['username'].hasError('required')">Username is required</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['password'].errors }" />
<div *ngIf="submitted && f['password'].errors" class="invalid-feedback">
<div *ngIf="f['password'].hasError('required')">Password is required</div>
</div>
</div>
<div>
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
Login
</button>
<a routerLink="../register" class="btn btn-link">Register</a>
</div>
</form>
</div>
</div>

+ 58
- 0
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;
}
});
}
}

+ 43
- 0
matsen-tool/src/app/account/register.component.html Просмотреть файл

@@ -0,0 +1,43 @@
<div class="card">
<h4 class="card-header">Register</h4>
<div class="card-body">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="mb-3">
<label class="form-label">First Name</label>
<input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['firstName'].errors }" />
<div *ngIf="submitted && f['firstName'].errors" class="invalid-feedback">
<div *ngIf="f['firstName'].hasError('required')">First Name is required</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Last Name</label>
<input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['lastName'].errors }" />
<div *ngIf="submitted && f['lastName'].errors" class="invalid-feedback">
<div *ngIf="f['lastName'].hasError('required')">Last Name is required</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['username'].errors }" />
<div *ngIf="submitted && f['username'].errors" class="invalid-feedback">
<div *ngIf="f['username'].hasError('required')">Username is required</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['password'].errors }" />
<div *ngIf="submitted && f['password'].errors" class="invalid-feedback">
<div *ngIf="f['password'].hasError('required')">Password is required</div>
<div *ngIf="f['password'].hasError('minlength')">Password must be at least 6 characters</div>
</div>
</div>
<div>
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
Register
</button>
<a routerLink="../login" class="btn btn-link">Cancel</a>
</div>
</form>
</div>
</div>

+ 59
- 0
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;
}
});
}
}

+ 18
- 5
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 { }

+ 22
- 10
matsen-tool/src/app/app.component.html Просмотреть файл

@@ -1,12 +1,24 @@
<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark px-3" *ngIf="user">
<div class="navbar-nav">
<a class="nav-item nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
<a class="nav-item nav-link" routerLink="/users" routerLinkActive="active">Users</a>
<button class="btn btn-link nav-item nav-link" (click)="logout()">Logout</button>
</div>
</nav>

<app-login></app-login>
<ul>
<li>Posts</li>
<li *ngFor="let post of posts">
<!-- main app container -->
<div class="app-container" [ngClass]="{ 'bg-light': user }">
<alert></alert>
<router-outlet></router-outlet>
</div>

<h2>{{post.id}} - {{post.owner}}</h2>
<p>{{post.message}}</p>
</li>
</ul>

<router-outlet></router-outlet>
<!-- credits -->
<div class="text-center mt-4">
<p>
<a href="https://jasonwatmore.com/post/2022/11/29/angular-14-user-registration-and-login-example-tutorial" target="_top">Angular 14 - User Registration and Login Example & Tutorial</a>
</p>
<p>
<a href="https://jasonwatmore.com" target="_top">JasonWatmore.com</a>
</p>
</div>

+ 12
- 30
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<PostJsonld>;

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();
}
}

+ 11
- 0
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);

+ 10
- 0
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()]
};

+ 30
- 30
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 { };

+ 3
- 0
matsen-tool/src/app/app.routes.ts Просмотреть файл

@@ -0,0 +1,3 @@
import { Routes } from '@angular/router';

export const routes: Routes = [];

+ 7
- 0
matsen-tool/src/app/home/home.component.html Просмотреть файл

@@ -0,0 +1,7 @@
<div class="p-4">
<div class="container">
<h1>Hi {{user?.firstName}}!</h1>
<p>You're logged in with Angular 14!!</p>
<p><a routerLink="/users">Manage Users</a></p>
</div>
</div>

+ 13
- 0
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;
}
}

+ 1
- 0
matsen-tool/src/app/home/index.ts Просмотреть файл

@@ -0,0 +1 @@
export * from './home.component';

+ 0
- 18
matsen-tool/src/app/login/login.module.ts Просмотреть файл

@@ -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 { }

+ 0
- 1
matsen-tool/src/app/login/login/login.component.html Просмотреть файл

@@ -1 +0,0 @@
<p>login works!</p>

+ 0
- 0
matsen-tool/src/app/login/login/login.component.scss Просмотреть файл


+ 0
- 23
matsen-tool/src/app/login/login/login.component.spec.ts Просмотреть файл

@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { LoginComponent } from './login.component';

describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LoginComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 0
- 10
matsen-tool/src/app/login/login/login.component.ts Просмотреть файл

@@ -1,10 +0,0 @@
import { Component } from '@angular/core';

@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
export class LoginComponent {

}

+ 0
- 11
matsen-tool/src/app/test/test-routing.module.ts Просмотреть файл

@@ -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 { }

+ 0
- 1
matsen-tool/src/app/test/test.component.html Просмотреть файл

@@ -1 +0,0 @@
<p>test works!</p>

+ 0
- 0
matsen-tool/src/app/test/test.component.scss Просмотреть файл


+ 0
- 23
matsen-tool/src/app/test/test.component.spec.ts Просмотреть файл

@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { TestComponent } from './test.component';

describe('TestComponent', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TestComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 0
- 10
matsen-tool/src/app/test/test.component.ts Просмотреть файл

@@ -1,10 +0,0 @@
import { Component } from '@angular/core';

@Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrl: './test.component.scss'
})
export class TestComponent {

}

+ 0
- 17
matsen-tool/src/app/test/test.module.ts Просмотреть файл

@@ -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 { }

+ 49
- 0
matsen-tool/src/app/users/add-edit.component.html Просмотреть файл

@@ -0,0 +1,49 @@
<h1>{{title}}</h1>
<form *ngIf="!loading" [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="row">
<div class="mb-3 col">
<label class="form-label">First Name</label>
<input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['firstName'].errors }" />
<div *ngIf="submitted && f['firstName'].errors" class="invalid-feedback">
<div *ngIf="f['firstName'].hasError('required')">First Name is required</div>
</div>
</div>
<div class="mb-3 col">
<label class="form-label">Last Name</label>
<input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['lastName'].errors }" />
<div *ngIf="submitted && f['lastName'].errors" class="invalid-feedback">
<div *ngIf="f['lastName'].hasError('required')">Last Name is required</div>
</div>
</div>
</div>
<div class="row">
<div class="mb-3 col">
<label class="form-label">Username</label>
<input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['username'].errors }" />
<div *ngIf="submitted && f['username'].errors" class="invalid-feedback">
<div *ngIf="f['username'].hasError('required')">Username is required</div>
</div>
</div>
<div class="mb-3 col">
<label class="form-label">
Password
<em *ngIf="id">(Leave blank to keep the same password)</em>
</label>
<input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['password'].errors }" />
<div *ngIf="submitted && f['password'].errors" class="invalid-feedback">
<div *ngIf="f['password'].hasError('required')">Password is required</div>
<div *ngIf="f['password'].hasError('minlength')">Password must be at least 6 characters</div>
</div>
</div>
</div>
<div class="mb-3">
<button [disabled]="submitting" class="btn btn-primary">
<span *ngIf="submitting" class="spinner-border spinner-border-sm me-1"></span>
Save
</button>
<a routerLink="/users" class="btn btn-link">Cancel</a>
</div>
</form>
<div *ngIf="loading" class="text-center m-5">
<span class="spinner-border spinner-border-lg align-center"></span>
</div>

+ 86
- 0
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);
}
}

+ 5
- 0
matsen-tool/src/app/users/layout.component.html Просмотреть файл

@@ -0,0 +1,5 @@
<div class="p-4">
<div class="container">
<router-outlet></router-outlet>
</div>
</div>

+ 4
- 0
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 { }

+ 31
- 0
matsen-tool/src/app/users/list.component.html Просмотреть файл

@@ -0,0 +1,31 @@
<h1>Users</h1>
<a routerLink="add" class="btn btn-sm btn-success mb-2">Add User</a>
<table class="table table-striped">
<thead>
<tr>
<th style="width: 30%">First Name</th>
<th style="width: 30%">Last Name</th>
<th style="width: 30%">Username</th>
<th style="width: 10%"></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users">
<td>{{user.firstName}}</td>
<td>{{user.lastName}}</td>
<td>{{user.username}}</td>
<td style="white-space: nowrap">
<a routerLink="edit/{{user.id}}" class="btn btn-sm btn-primary me-1">Edit</a>
<button (click)="deleteUser(user.id)" class="btn btn-sm btn-danger btn-delete-user" [disabled]="user.isDeleting">
<span *ngIf="user.isDeleting" class="spinner-border spinner-border-sm"></span>
<span *ngIf="!user.isDeleting">Delete</span>
</button>
</td>
</tr>
<tr *ngIf="!users">
<td colspan="4" class="text-center">
<span class="spinner-border spinner-border-lg align-center"></span>
</td>
</tr>
</tbody>
</table>

+ 25
- 0
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));
}
}

+ 23
- 0
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 { }

+ 22
- 0
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 { }

+ 0
- 4
matsen-tool/src/environment.prod.ts Просмотреть файл

@@ -1,4 +0,0 @@
export const environment = {
production: true,
basePath: 'https://jsonplaceholder.typicode.com',
};

+ 0
- 5
matsen-tool/src/environment.ts Просмотреть файл

@@ -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',
};

+ 4
- 0
matsen-tool/src/environments/environment.prod.ts Просмотреть файл

@@ -0,0 +1,4 @@
export const environment = {
production: true,
apiUrl: 'http://localhost:4000'
};

+ 17
- 0
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.

+ 5
- 0
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));

+ 11
- 0
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;
}

+ 8
- 2
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,


Загрузка…
Отмена
Сохранить