Compare commits

8 Commits

Author SHA1 Message Date
Roland Schneider
5e5a4fc505 login/logout 2025-11-18 14:51:54 +01:00
Roland Schneider
ce76bf75c9 create admin layout 2025-11-18 13:42:58 +01:00
Schneider Roland
e2211e2de7 add new angular lib 2025-11-18 11:18:57 +01:00
Roland Schneider
f1f2fefdab logout works 2025-11-14 17:20:13 +01:00
Roland Schneider
d11b0c65e0 add refresh token 2025-11-14 17:20:13 +01:00
Roland Schneider
96af8e564b add refresh token 2025-11-14 17:20:11 +01:00
Roland Schneider
42158d1fd4 add refresh token 2025-11-14 17:19:52 +01:00
Roland Schneider
f4c0bb0b76 add refresh token 2025-11-14 17:19:07 +01:00
48 changed files with 1377 additions and 65 deletions

2
.gitignore vendored
View File

@@ -54,3 +54,5 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
tmp

View File

@@ -80,6 +80,32 @@
}
}
}
},
"@rschneider/ng-daisyui": {
"projectType": "library",
"root": "projects/rschneider/ng-daisyui",
"sourceRoot": "projects/rschneider/ng-daisyui/src",
"prefix": "rs-daisy",
"architect": {
"build": {
"builder": "@angular/build:ng-packagr",
"configurations": {
"production": {
"tsConfig": "projects/rschneider/ng-daisyui/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/rschneider/ng-daisyui/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "projects/rschneider/ng-daisyui/tsconfig.spec.json"
}
}
}
}
}
}

433
admin/package-lock.json generated
View File

@@ -32,6 +32,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"ng-packagr": "^20.3.0",
"typescript": "~5.9.2"
}
},
@@ -3067,6 +3068,50 @@
"node": ">=14"
}
},
"node_modules/@rollup/plugin-json": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
@@ -3375,6 +3420,26 @@
"win32"
]
},
"node_modules/@rollup/wasm-node": {
"version": "4.53.2",
"resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.53.2.tgz",
"integrity": "sha512-oPSy4fH0C66muvPr/HU13K8X9QFO74Em+JUegHUpEwD61M3lihIlfrLpilhrEiiReFOfG00Qyhf7NGFuwkX2yA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/@schematics/angular": {
"version": "20.3.9",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.9.tgz",
@@ -3943,6 +4008,16 @@
"node": ">= 14.0.0"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-escapes": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
@@ -4517,6 +4592,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/common-path-prefix": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
"integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
"dev": true,
"license": "ISC"
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4659,6 +4751,19 @@
"node": ">=6.6.0"
}
},
"node_modules/copy-anything": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-what": "^3.14.1"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@@ -4772,6 +4877,16 @@
"node": ">= 0.8"
}
},
"node_modules/dependency-graph": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz",
"integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -5117,6 +5232,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/errno": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"prr": "~1.0.1"
},
"bin": {
"errno": "cli.js"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -5209,6 +5338,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -5402,6 +5538,36 @@
"node": ">= 0.8"
}
},
"node_modules/find-cache-directory": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/find-cache-directory/-/find-cache-directory-6.0.0.tgz",
"integrity": "sha512-CvFd5ivA6HcSHbD+59P7CyzINHXzwhuQK8RY7CxJZtgDSAtRlHiCaQpZQ2lMR/WRyUIEmzUvL6G2AGurMfegZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"common-path-prefix": "^3.0.0",
"pkg-dir": "^8.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-up-simple": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
"integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
@@ -5898,6 +6064,20 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/image-size": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
"dev": true,
"license": "MIT",
"optional": true,
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/immutable": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
@@ -5944,6 +6124,16 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/injection-js": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.6.1.tgz",
"integrity": "sha512-dbR5bdhi7TWDoCye9cByZqeg/gAfamm8Vu3G1KZOTYkOif8WkuM8CD0oeDPtZYMzT5YH76JAFB7bkmyY9OJi2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
}
},
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
@@ -6091,6 +6281,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-what": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
"dev": true,
"license": "MIT"
},
"node_modules/isbinaryfile": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
@@ -6768,6 +6965,84 @@
"node": ">=10"
}
},
"node_modules/less": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/less/-/less-4.4.2.tgz",
"integrity": "sha512-j1n1IuTX1VQjIy3tT7cyGbX7nvQOsFLoIqobZv4ttI5axP923gA44zUj6miiA6R5Aoms4sEGVIIcucXUbRI14g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",
"tslib": "^2.3.0"
},
"bin": {
"lessc": "bin/lessc"
},
"engines": {
"node": ">=14"
},
"optionalDependencies": {
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"needle": "^3.1.0",
"source-map": "~0.6.0"
}
},
"node_modules/less/node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/less/node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"license": "MIT",
"optional": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/less/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/less/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@@ -7651,6 +7926,38 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/needle": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.3",
"sax": "^1.2.4"
},
"bin": {
"needle": "bin/needle"
},
"engines": {
"node": ">= 4.4.x"
}
},
"node_modules/needle/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@@ -7661,6 +7968,56 @@
"node": ">= 0.6"
}
},
"node_modules/ng-packagr": {
"version": "20.3.2",
"resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-20.3.2.tgz",
"integrity": "sha512-yW5ME0hqTz38r/th/7zVwX5oSIw1FviSA2PUlGZdVjghDme/KX6iiwmOBmlt9E9whNmwijEC6Gn3KKbrsBx8ig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/wasm-node": "^4.24.0",
"ajv": "^8.17.1",
"ansi-colors": "^4.1.3",
"browserslist": "^4.22.1",
"chokidar": "^4.0.1",
"commander": "^14.0.0",
"dependency-graph": "^1.0.0",
"esbuild": "^0.25.0",
"find-cache-directory": "^6.0.0",
"injection-js": "^2.4.0",
"jsonc-parser": "^3.3.1",
"less": "^4.2.0",
"ora": "^8.2.0",
"piscina": "^5.0.0",
"postcss": "^8.4.47",
"rollup-plugin-dts": "^6.2.0",
"rxjs": "^7.8.1",
"sass": "^1.81.0",
"tinyglobby": "^0.2.12"
},
"bin": {
"ng-packagr": "src/cli/main.js"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"optionalDependencies": {
"rollup": "^4.24.0"
},
"peerDependencies": {
"@angular/compiler-cli": "^20.0.0",
"tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0",
"tslib": "^2.3.0",
"typescript": ">=5.8 <6.0"
},
"peerDependenciesMeta": {
"tailwindcss": {
"optional": true
}
}
},
"node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
@@ -8185,6 +8542,16 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/parse-node-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/parse5": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
@@ -8343,6 +8710,17 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/piscina": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.3.tgz",
@@ -8366,6 +8744,22 @@
"node": ">=16.20.0"
}
},
"node_modules/pkg-dir": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-8.0.0.tgz",
"integrity": "sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"find-up-simple": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -8439,6 +8833,14 @@
"node": ">= 0.10"
}
},
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
@@ -8660,6 +9062,29 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-dts": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.2.3.tgz",
"integrity": "sha512-UgnEsfciXSPpASuOelix7m4DrmyQgiaWBnvI0TM4GxuDh5FkqW8E5hu57bCxXB90VvR1WNfLV80yEDN18UogSA==",
"dev": true,
"license": "LGPL-3.0-only",
"dependencies": {
"magic-string": "^0.30.17"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/Swatinem"
},
"optionalDependencies": {
"@babel/code-frame": "^7.27.1"
},
"peerDependencies": {
"rollup": "^3.29.4 || ^4",
"typescript": "^4.5 || ^5.0"
}
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -8753,6 +9178,14 @@
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/sax": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"optional": true
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",

View File

@@ -46,6 +46,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"ng-packagr": "^20.3.0",
"typescript": "~5.9.2"
}
}

View File

@@ -0,0 +1,63 @@
# NgDaisyui
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.0.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the library, run:
```bash
ng build ng-daisyui
```
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
### Publishing the Library
Once the project is built, you can publish your library by following these steps:
1. Navigate to the `dist` directory:
```bash
cd dist/ng-daisyui
```
2. Run the `npm publish` command to publish your library to the npm registry:
```bash
npm publish
```
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/rschneider/ng-daisyui",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,12 @@
{
"name": "@rschneider/ng-daisyui",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^20.3.0",
"@angular/core": "^20.3.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"sideEffects": false
}

View File

@@ -0,0 +1,38 @@
<button
[aria-disabled]="disabled()"
role="button"
[ngClass]="{
'btn': true,
'btn-primary': variant() === 'primary',
'btn-secondary': variant() === 'secondary',
'btn-accent': variant() === 'accent',
'btn-info': variant() === 'info',
'btn-success': variant() === 'success',
'btn-warning': variant() === 'warning',
'btn-error': variant() === 'error',
'btn-ghost': variant() === 'ghost',
'btn-link': variant() === 'link',
'btn-lg': size() === 'lg',
'btn-md': size() === 'md',
'btn-sm': size() === 'sm',
'btn-xs': size() === 'xs',
'btn-outline': outline(),
'btn-active': active(),
'btn-glass': glass(),
'btn-square': square(),
'btn-circle': circle(),
'btn-wide': wide(),
'btn-block': block(),
'no-animation': noAnimation(),
'loading': loading(),
'btn-responsive': responsive(),
'btn-disabled': disabled()
}"
[class]="customClass() || ''"
(click)="onClick($event)"
>
<ng-content />
</button>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Button } from './button';
describe('Button', () => {
let component: Button;
let fixture: ComponentFixture<Button>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Button]
})
.compileComponents();
fixture = TestBed.createComponent(Button);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,60 @@
// rs-daisy-button.component.ts
import { Component, input, HostBinding, output } from '@angular/core';
import { NgClass } from '@angular/common';
@Component({
selector: 'rs-daisy-button',
standalone: true,
templateUrl: 'button.html',
imports: [NgClass]
})
export class Button {
// Daisy UI Button Props as Angular Inputs
// Sizes: lg, md, sm, xs
size = input<'lg' | 'md' | 'sm' | 'xs' | null>(null);
// States: primary, secondary, accent, info, success, warning, error, ghost, link
variant = input<'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'ghost' | 'link' | null>(null);
// Outlined
outline = input<boolean>(false);
// Active
active = input<boolean>(false);
// Disabled
disabled = input<boolean>(false);
// Glass
glass = input<boolean>(false);
// Square
square = input<boolean>(false);
// Circle
circle = input<boolean>(false);
// Wide
wide = input<boolean>(false);
// Block
block = input<boolean>(false);
// No animation
noAnimation = input<boolean>(false);
// Loading
loading = input<boolean>(false);
// Responsive
responsive = input<boolean>(false);
// You can also add a custom class input if needed
customClass = input<string | null>(null);
@HostBinding('attr.disabled') get isDisabled() {
return this.disabled() ? true : null;
}
// New Output for the click event
// By default, it emits 'void' (no specific data), but you could specify a type if needed.
readonly clickEvent = output<MouseEvent>();
// Event handler method
onClick(event: MouseEvent): void {
// Prevent default browser behavior for buttons if needed (e.g., form submission)
// event.preventDefault();
// Emit the click event
this.clickEvent.emit(event);
}
}

View File

@@ -0,0 +1,32 @@
<footer class="footer sm:footer-horizontal bg-neutral text-neutral-content p-10">
@if (config()) {
@for ( footerNav of config()?.navs; let i = $index; track i){
<nav>
<h6 class="footer-title">{{ footerNav.label }}</h6>
@for ( link of footerNav.links; let j = $index; track j){
<a class="link link-hover" [routerLink]="link.href" >{{link.text}}</a>
}
</nav>
}
}
<nav>
<h6 class="footer-title">Services</h6>
<a class="link link-hover">Branding</a>
<a class="link link-hover">Design</a>
<a class="link link-hover">Marketing</a>
<a class="link link-hover">Advertisement</a>
</nav>
<nav>
<h6 class="footer-title">Company</h6>
<a class="link link-hover">About us</a>
<a class="link link-hover">Contact</a>
<a class="link link-hover">Jobs</a>
<a class="link link-hover">Press kit</a>
</nav>
<nav>
<h6 class="footer-title">Legal</h6>
<a class="link link-hover">Terms of use</a>
<a class="link link-hover">Privacy policy</a>
<a class="link link-hover">Cookie policy</a>
</nav>
</footer>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Footer } from './footer';
describe('Footer', () => {
let component: Footer;
let fixture: ComponentFixture<Footer>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Footer]
})
.compileComponents();
fixture = TestBed.createComponent(Footer);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,17 @@
import { Component, input } from '@angular/core';
import { FooterConfig } from '../../daisy.types';
import { RouterLink } from '@angular/router';
@Component({
selector: 'rs-daisy-footer',
imports: [
RouterLink,
],
templateUrl: './footer.html',
styleUrl: './footer.css',
})
export class Footer {
config = input<FooterConfig>()
}

View File

@@ -0,0 +1,13 @@
export interface Link{
href: string;
text: string;
}
export interface FooterNav{
label: string;
links: Link[];
}
export interface FooterConfig{
navs: FooterNav[]
}

View File

@@ -0,0 +1,83 @@
<div class="drawer lg:drawer-open">
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<!-- Page Content -->
<div class="drawer-content flex flex-col h-screen">
<div class="navbar bg-base-200 p-4 shadow-md z-10">
<div class="flex-1">
<label for="my-drawer-2" class="btn btn-square btn-ghost drawer-button lg:hidden">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="inline-block w-6 h-6 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</label>
<a class="btn btn-ghost text-xl">daisyUI</a>
</div>
@if (loggedIn()) {
<div class="flex-none">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full">
<img
alt="Tailwind CSS Navbar component"
src="https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp" />
</div>
</div>
<ul
tabindex="-1"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
<li>
<a class="justify-between">
Profile
<span class="badge">New</span>
</a>
</li>
<li><a>Settings</a></li>
<li><a (click)="onClick($event,'logout')">Logout</a></li>
</ul>
</div>
</div>
}
</div>
<!-- Main Content -->
<main class="flex-1 p-6 bg-base-100 overflow-y-auto">
<ng-content></ng-content>
</main>
<!-- Footer -->
<footer class="bg-base-200 p-4 text-center">
<p>Copyright © 2025 - All right reserved</p>
</footer>
</div>
<!-- Sidebar -->
<div class="drawer-side">
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu p-4 w-80 min-h-full bg-base-300 text-base-content">
<!-- Sidebar content here -->
<li class="text-lg font-bold p-4">My App</li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
Dashboard</a></li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Analytics</a></li>
<li><a>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Reports</a></li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminLayoutRs1 } from './admin-layout-rs1';
describe('AdminLayoutRs1', () => {
let component: AdminLayoutRs1;
let fixture: ComponentFixture<AdminLayoutRs1>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AdminLayoutRs1]
})
.compileComponents();
fixture = TestBed.createComponent(AdminLayoutRs1);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,28 @@
import { Component, input, output } from '@angular/core';
@Component({
selector: 'rs-daisy-admin-layout-rs1',
imports: [],
templateUrl: './admin-layout-rs1.html',
styleUrl: './admin-layout-rs1.css',
})
export class AdminLayoutRs1 {
readonly loggedIn = input<boolean>(false)
// New Output for the click event
// By default, it emits 'void' (no specific data), but you could specify a type if needed.
readonly clickEvent = output<MouseEvent>();
// Event handler method
onClick(event: MouseEvent, command: string): void {
// Prevent default browser behavior for buttons if needed (e.g., form submission)
// event.preventDefault();
// Emit the click event
this.clickEvent.emit(event);
}
}

View File

@@ -0,0 +1,3 @@
export * from './admin-layout-rs1/admin-layout-rs1';

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NgDaisyui } from './ng-daisyui';
describe('NgDaisyui', () => {
let component: NgDaisyui;
let fixture: ComponentFixture<NgDaisyui>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NgDaisyui]
})
.compileComponents();
fixture = TestBed.createComponent(NgDaisyui);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
@Component({
selector: 'rs-daisy-ng-daisyui',
imports: [],
template: `
<p>
ng-daisyui works!
</p>
`,
styles: ``,
})
export class NgDaisyui {
}

View File

@@ -0,0 +1,9 @@
/*
* Public API Surface of ng-daisyui
*/
export * from './lib/ng-daisyui';
export * from './lib/components/button/button';
export * from './lib/components/footer/footer';
export * from './lib/daisy.types';
export * from './lib/layout/';

View File

@@ -0,0 +1,18 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,11 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@@ -0,0 +1,14 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.ts"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,5 +1,3 @@
<div>
<button (click)="logout()">Logout</button>
</div>
<app-main-menu></app-main-menu>
<router-outlet />
<rs-daisy-admin-layout-rs1 (clickEvent)="logout()" [loggedIn]="loggedIn()">
<router-outlet />
</rs-daisy-admin-layout-rs1>

View File

@@ -1,22 +1,41 @@
import { Component, signal } from '@angular/core';
import { Component, inject, signal } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { MainMenu } from './components/main-menu/main-menu';
import { AuthService } from './auth/auth.service';
import { AdminLayout } from './layout/admin-layout/admin-layout';
import { finalize } from 'rxjs/operators';
import {Button} from '@rschneider/ng-daisyui';
import { AdminLayoutRs1 } from '../../projects/rschneider/ng-daisyui/src/lib/layout';
@Component({
selector: 'app-root',
imports: [RouterOutlet, AdminLayout,MainMenu],
imports: [RouterOutlet, MainMenu, Button, AdminLayoutRs1],
templateUrl: './app.html',
styleUrl: './app.css'
styleUrl: './app.css',
})
export class App {
protected readonly title = signal('admin');
constructor(private authService: AuthService, private router: Router) {}
logout(): void {
this.authService.logout();
this.router.navigate(['/login']);
// With the interceptor fixed, this is now the correct and robust way.
// The error from a failed server logout will propagate here.
this.authService.serverSideLogout().subscribe({
next: () => {
console.log('Server-side logout successful.');
this.authService.clientSideLogout();
},
error: (err) => {
console.error('Server-side logout failed, logging out client-side anyway.', err);
this.authService.clientSideLogout();
},
});
}
loggedIn(){
return this.authService.isLoggedIn()
}
}

View File

@@ -1,36 +1,76 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Observable, of, throwError } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private readonly TOKEN_KEY = 'access_token';
private readonly ACCESS_TOKEN_KEY = 'accessToken';
private readonly REFRESH_TOKEN_KEY = 'refreshToken';
private apiUrl = 'http://localhost:4200/api/auth'; // Adjust if your server URL is different
constructor(private http: HttpClient) {}
constructor(private http: HttpClient, private router: Router) {}
login(credentials: { username: string; password: string }): Observable<any> {
return this.http.post<{ access_token: string }>(`${this.apiUrl}/login`, credentials).pipe(
tap((response) => this.setToken(response.access_token))
return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/login`, credentials).pipe(
tap((response) => this.setTokens(response.accessToken, response.refreshToken))
);
}
logout(): void {
localStorage.removeItem(this.TOKEN_KEY);
/**
* Makes a best-effort call to the server to invalidate the refresh token.
*/
serverSideLogout(): Observable<any> {
return this.http.post(`${this.apiUrl}/logout`, {});
}
getToken(): string | null {
return localStorage.getItem(this.TOKEN_KEY);
/**
* Performs the client-side cleanup, removing tokens and redirecting to login.
* This is the definitive logout action from the user's perspective.
*/
clientSideLogout(): void {
console.info("clientSideLogout")
this.removeTokens();
this.router.navigate(['/login']);
}
refreshToken(): Observable<any> {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
// If no refresh token is present, logout and return an error.
this.clientSideLogout();
return throwError(() => new Error('No refresh token available'));
}
return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/refresh`, {}, {
headers: { Authorization: `Bearer ${refreshToken}` }
}).pipe(
tap((response) => this.setTokens(response.accessToken, response.refreshToken))
);
}
getAccessToken(): string | null {
return localStorage.getItem(this.ACCESS_TOKEN_KEY);
}
getRefreshToken(): string | null {
return localStorage.getItem(this.REFRESH_TOKEN_KEY);
}
isLoggedIn(): boolean {
return this.getToken() !== null;
return this.getAccessToken() !== null;
}
private setToken(token: string): void {
localStorage.setItem(this.TOKEN_KEY, token);
private setTokens(accessToken: string, refreshToken: string): void {
localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken);
localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
}
private removeTokens(): void {
localStorage.removeItem(this.ACCESS_TOKEN_KEY);
localStorage.removeItem(this.REFRESH_TOKEN_KEY);
}
}

View File

@@ -4,26 +4,81 @@ import {
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpErrorResponse,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, switchMap, filter, take, finalize } from 'rxjs/operators'; // Import finalize
import { AuthService } from './auth.service';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
private isRefreshing = false;
// Initialize refreshTokenSubject with null
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private authService: AuthService) {}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
const token = this.authService.getToken();
if (token) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
if (request.url.includes('/auth/refresh')) {
return next.handle(request);
}
return next.handle(request);
const accessToken = this.authService.getAccessToken();
if (accessToken) {
request = this.addToken(request, accessToken);
}
return next.handle(request).pipe(
catchError((error) => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(request, next);
}
return throwError(() => error);
})
);
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
if (!this.isRefreshing) {
this.isRefreshing = true;
// Reset the refreshTokenSubject to null so that subsequent requests will wait
// this.refreshTokenSubject.next(null);
this.refreshTokenSubject = new BehaviorSubject<any>(null);
return this.authService.refreshToken().pipe(
switchMap((token: any) => {
this.refreshTokenSubject.next(token.accessToken);
return next.handle(this.addToken(request, token.accessToken));
}),
catchError((err) => {
// If refresh fails, logout the user
this.authService.clientSideLogout();
return throwError(() => err);
}),
finalize(() => {
// When the refresh attempt completes, set isRefreshing to false
this.isRefreshing = false;
})
);
} else {
// If a refresh is already in progress, wait for it to complete
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap(jwt => next.handle(this.addToken(request, jwt)))
);
}
}
private addToken(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
}
}

View File

@@ -1,15 +1,17 @@
<div>
<h2>Login</h2>
<div class="bg-base-200 rounded-2xl p-4">
<h2 class="">Login</h2>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div>
<label for="username">Username:</label>
<input id="username" formControlName="username" />
<div class="pt-4">
<label class="label pe-2" for="username">Username:</label>
<input id="username" class="input " formControlName="username" />
</div>
<div>
<label for="password">Password:</label>
<input id="password" type="password" formControlName="password" />
<div class="pt-4">
<label class="label pe-2" for="password">Password:</label>
<input id="password" class="input " type="password" formControlName="password" />
</div>
<div class="button-container pt-4">
<rs-daisy-button type="submit" [disabled]="loginForm.invalid">Log In</rs-daisy-button>
</div>
<button type="submit" [disabled]="loginForm.invalid">Log In</button>
</form>
<p *ngIf="errorMessage">{{ errorMessage }}</p>
</div>

View File

@@ -3,12 +3,14 @@ import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angula
import { Router } from '@angular/router';
import { AuthService } from '../../auth/auth.service';
import { CommonModule } from '@angular/common';
import {Button} from '@rschneider/ng-daisyui';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
imports: [ReactiveFormsModule, CommonModule, Button],
})
export class LoginComponent {
loginForm: FormGroup;

View File

@@ -2,4 +2,5 @@
@import "tailwindcss";
@import "./styles/grid.css";
@source "../projects/rschneider/ng-daisyui/src";
@plugin "daisyui";

View File

@@ -3,6 +3,11 @@
{
"compileOnSave": false,
"compilerOptions": {
"paths": {
"@rschneider/ng-daisyui": [
"./dist/rschneider/ng-daisyui"
]
},
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
@@ -29,6 +34,12 @@
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./projects/rschneider/ng-daisyui/tsconfig.lib.json"
},
{
"path": "./projects/rschneider/ng-daisyui/tsconfig.spec.json"
}
]
}

View File

@@ -1,4 +1,4 @@
POST http://localhost:3000/auth/login
POST {{apiBaseUrl}}/auth/login
Content-Type: application/json
{
@@ -6,10 +6,17 @@ Content-Type: application/json
"password": "123456"
}
> {% client.global.set("auth_token", response.body.access_token); %}
> {% client.global.set("auth_token", response.body.accessToken); %}
### GET request with parameter
GET http://localhost:3000/users
GET {{apiBaseUrl}}/users
Accept: application/json
Authorization: Bearer {{auth_token}}
### GET request with parameter
POST {{apiBaseUrl}}/auth/logout
Accept: application/json
Authorization: Bearer {{auth_token}}

View File

@@ -0,0 +1,5 @@
{
"dev": {
"apiBaseUrl": "http://localhost:3000/api"
}
}

View File

@@ -1,6 +1,16 @@
import { Controller, Post, Body, ValidationPipe } from '@nestjs/common';
import { AuthService } from './auth.service';
import {
Controller,
Post,
Body,
ValidationPipe,
UseGuards,
Req,
} from '@nestjs/common';
import { LoginRequestDto } from './dto/login-request.dto';
import { JwtAuthGuard } from './jwt-auth.guard';
import { JwtRefreshAuthGuard } from './jwt-refresh-auth.guard';
import express from 'express';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
@@ -10,4 +20,18 @@ export class AuthController {
async login(@Body(new ValidationPipe()) body: LoginRequestDto) {
return await this.authService.login(body);
}
@UseGuards(JwtAuthGuard)
@Post('logout')
async logout(@Req() req: express.Request) {
const user = req.user as { sub: number };
return await this.authService.logout(user.sub);
}
@UseGuards(JwtRefreshAuthGuard)
@Post('refresh')
async refresh(@Req() req: express.Request) {
const user = req.user as { sub: number; refreshToken: string };
return await this.authService.refreshToken(user.sub, user.refreshToken);
}
}

View File

@@ -6,22 +6,27 @@ import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtRefreshTokenStrategy } from './jwt-refresh.strategy';
import { StringValue } from 'ms';
@Module({
imports: [
ConfigModule, // <--- Import ConfigModule here
ConfigModule,
UserModule,
PassportModule,
// Restore the correct async registration for JwtModule
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '60m' },
signOptions: {
expiresIn: configService.get<StringValue>('JWT_EXPIRATION_TIME'),
},
}),
}),
],
providers: [AuthService, JwtStrategy],
providers: [AuthService, JwtStrategy, JwtRefreshTokenStrategy],
controllers: [AuthController],
})
export class AuthModule {}

View File

@@ -4,12 +4,15 @@ import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { User } from '../entity/user';
import { LoginRequest, LoginResponse } from '../types';
import { ConfigService } from '@nestjs/config';
import type { StringValue } from 'ms';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
private configService: ConfigService,
) {}
async validateUser(username: string, pass: string): Promise<User | null> {
@@ -34,19 +37,84 @@ export class AuthService {
throw new UnauthorizedException();
}
const tokens = await this.getTokens(user);
await this.userService.setRefreshToken(user.id, tokens.refreshToken);
return tokens;
}
async logout(userId: number): Promise<void> {
await this.userService.setRefreshToken(userId, null);
}
async refreshToken(
userId: number,
refreshToken: string,
): Promise<LoginResponse> {
const user = await this.userService.findOne(userId);
if (!user || !user.hashedRefreshToken) {
throw new UnauthorizedException('Access Denied');
}
const refreshTokenMatches = await bcrypt.compare(
refreshToken,
user.hashedRefreshToken,
);
if (!refreshTokenMatches) {
throw new UnauthorizedException('Access Denied');
}
const tokens = await this.getTokens(user);
await this.userService.setRefreshToken(user.id, tokens.refreshToken);
return tokens;
}
private async getTokens(user: User): Promise<LoginResponse> {
const roles: Set<string> = new Set<string>();
for (const group of user.groups ?? []) {
for (const role of group.roles ?? []) {
roles.add(role.name);
}
}
const payload = {
username: user.username,
sub: user.id,
roles: Array.from(roles),
};
const jwtSecret = this.configService.get<string>('JWT_SECRET');
const jwtexpirationtime = this.configService.get<string>(
'JWT_EXPIRATION_TIME',
);
console.info(
'creating. jwt secret is: ' + jwtSecret + ',' + jwtexpirationtime,
);
// let accessToken: string, refreshToken: string;
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(
{
username: user.username,
sub: user.id,
roles: Array.from(roles),
},
{
secret: this.configService.get<string>('JWT_SECRET'),
expiresIn: +this.configService.get<StringValue>('JWT_EXPIRATION_TIME')!,
},
),
this.jwtService.signAsync(
{
sub: user.id,
},
{
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
expiresIn: this.configService.get<StringValue>(
'JWT_REFRESH_EXPIRATION_TIME',
),
},
),
]);
return {
access_token: this.jwtService.sign(payload),
accessToken,
refreshToken,
};
}
}

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtRefreshAuthGuard extends AuthGuard('jwt-refresh') {}

View File

@@ -0,0 +1,47 @@
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy, StrategyOptionsWithRequest } from 'passport-jwt';
import { Request } from 'express';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
// Define the shape of the JWT payload
interface JwtPayload {
sub: number;
// iat and exp are automatically added by passport-jwt
iat: number;
exp: number;
}
// Define the shape of the object returned by the validate function
interface JwtPayloadWithRefreshToken extends JwtPayload {
refreshToken: string;
}
@Injectable()
export class JwtRefreshTokenStrategy extends PassportStrategy(
Strategy,
'jwt-refresh',
) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET'),
passReqToCallback: true,
} as StrategyOptionsWithRequest);
}
validate(req: Request, payload: JwtPayload): JwtPayloadWithRefreshToken {
const refreshToken = req.get('Authorization')?.replace('Bearer', '').trim();
// Ensure refreshToken is not undefined before returning
if (!refreshToken) {
// This case should be rare given the guard is used, but it's good practice
// to handle it. Depending on strictness, you might throw an error.
// For now, we'll proceed, but in a real-world scenario, logging or an error
// might be better.
return { ...payload, refreshToken: '' };
}
return { ...payload, refreshToken };
}
}

View File

@@ -3,18 +3,32 @@ import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { Role } from './role.enum';
import { Request } from 'express';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET') as string,
// DO NOT use secretOrKey here. It causes a race condition with ConfigService.
// Instead, use secretOrKeyProvider to look up the secret dynamically
// at request time, ensuring ConfigService is ready.
secretOrKeyProvider: (
request: Request,
rawJwtToken: any,
done: (err: any, secretOrKey?: string | Buffer) => void,
) => {
const secretKey = this.configService.get<string>('JWT_SECRET');
console.info('secretKey', secretKey);
done(null, secretKey);
},
});
}
validate(payload: { sub: string; username: string; roles: Role[] }) {
// The payload is already validated by passport-jwt at this point,
// so we can trust its contents.
validate(payload: { sub: number; username: string; roles: Role[] }) {
return {
userId: payload.sub,
username: payload.username,

View File

@@ -24,4 +24,7 @@ export class User {
@ManyToMany(() => UserGroup)
@JoinTable()
groups: UserGroup[];
@Column({ type: 'varchar', nullable: true })
hashedRefreshToken: string | null;
}

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddRefreshTokenToUserObject1763106308120 implements MigrationInterface {
name = 'AddRefreshTokenToUserObject1763106308120'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" ADD "hashedRefreshToken" character varying`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hashedRefreshToken"`);
}
}

View File

@@ -4,8 +4,7 @@ export interface LoginRequest{
password: string;
}
export interface LoginResponse{
access_token: string;
export interface LoginResponse {
accessToken: string;
refreshToken: string;
}

View File

@@ -54,4 +54,20 @@ export class UserService {
this.logger.log(`Removing user with id: ${id}`, 'UserService');
await this.usersRepository.delete(id);
}
async setRefreshToken(
id: number,
refreshToken: string | null,
): Promise<void> {
this.logger.log(
`Updating refresh token for user with id: ${id}`,
'UserService',
);
if (refreshToken) {
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
await this.usersRepository.update(id, { hashedRefreshToken });
} else {
await this.usersRepository.update(id, { hashedRefreshToken: null });
}
}
}