commit 70c35f6d2b6a78690887596fb89fbfb8f6005726 Author: admin Date: Wed Aug 20 22:35:19 2025 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4c6c48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vscode +.idea +dist +node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..fc59ab5 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +.vscode +.idea +node_modules +src +.gitignore +tsconfig.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2efc0b9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Markus Morgenstern + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..51ef8b6 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Token Authentication + +This Package contains a Token Authentication Module for NestJs with Organizations UserAccounts Groups and Rights access. + +## Required external Services + +You have to implement these Interfaces and give the Implementations into the Dynamic Module. + +| Service | Injection Token | Interface in Contracts | Description | +| ----------------------- | ----------------------------------- | ------------------------ | -------------------------------------------------------------- | +| Token Service | @apihub24/token_service | TokenService | a Service that generates the Token for authentication | +| Password Service | @apihub24/password_service | PasswordService | a Service that hash the Password | +| Session Service | @apihub24/session_service | SessionService | a Service that handles Session Get, Save, Delete Operations | +| Mail Service | @apihub24/mail_verification_service | MailVerificationService | a Service that handles Send Verify Mail and the verify Request | +| Account Repository | @apihub24/account_repository | Repository | a Service that handles Session Get, Save, Delete Operations | +| Group Repository | @apihub24/group_repository | Repository | a Service that handles Session Get, Save, Delete Operations | +| Right Repository | @apihub24/right_repository | Repository | a Service that handles Session Get, Save, Delete Operations | +| Organization Repository | @apihub24/organization_repository | Repository | a Service that handles Session Get, Save, Delete Operations | + +If you want to use a InMemory Repository you not have to implement itself you can use the package [@apihub24/in-memory-repository](https://www.npmjs.com/package/@apihub24/in-memory-repository). + +## Example + +```typescript +import { Module } from "@nestjs/common"; +import { + Account, + Group, + Organization, + Right, + TokenAuthenticationModule, +} from "@apihub24/token-authentication"; +import { LoginController } from "./controllers/login.controller"; +import { LogoutController } from "./controllers/logout.controller"; +import { ConfigModule } from "@nestjs/config"; +// a implementation of the EmailService +import { EmailVerificationModule } from "@apihub24/email-verification"; +// a implementation of the InMemoryRepository used for Repositories +import { InMemoryRepository } from "@apihub24/in-memory-repository"; +// a implementation of the SessionService +import { InMemorySessionsModule } from "@apihub24/in-memory-sessions"; +// a implementation of the TokenService +import { JwtGeneratorModule } from "@apihub24/jwt-generator"; +// a implementation of the PasswordService +import { PasswordHasherModule } from "@apihub24/password-hasher"; +import { takeExportedProviders } from "@apihub24/nestjs-helper"; + +// use the Dynamic Modules from the Implementations +const emailVerificationModule = EmailVerificationModule.forRoot(); +const sessionModule = InMemorySessionsModule.forRoot(); +// JwtGeneratorModule requires the SessionModule so give them the Providers +const jwtModule = JwtGeneratorModule.forRoot([ + ...takeExportedProviders(sessionModule), +]); +const passwordModule = PasswordHasherModule.forRoot(); + +@Module({ + imports: [ + ConfigModule.forRoot(), + emailVerificationModule, + sessionModule, + jwtModule, + passwordModule, + TokenAuthenticationModule.forRoot([ + ...takeExportedProviders(emailVerificationModule), + ...takeExportedProviders(sessionModule), + ...takeExportedProviders(jwtModule), + ...takeExportedProviders(passwordModule), + // register the Repositories with InMemoryRepository + { + provide: "@apihub24/organization_repository", + useClass: InMemoryRepository, + }, + { + provide: "@apihub24/account_repository", + useClass: InMemoryRepository, + }, + { + provide: "@apihub24/group_repository", + useClass: InMemoryRepository, + }, + { + provide: "@apihub24/right_repository", + useClass: InMemoryRepository, + }, + ]), + ], + controllers: [LoginController, LogoutController], + providers: [], + // required exports for the TokenGuard + exports: [TokenAuthenticationModule, jwtModule], +}) +export class AuthenticationModule {} +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..14be4af --- /dev/null +++ b/package-lock.json @@ -0,0 +1,454 @@ +{ + "name": "@apihub24/token-authentication", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@apihub24/token-authentication", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@apihub24/repository": "^1.0.1", + "@nestjs/common": "^11.1.6", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.1.6", + "class-validator": "^0.14.2" + }, + "devDependencies": { + "typescript": "^5.9.2" + } + }, + "node_modules/@apihub24/repository": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@apihub24/repository/-/repository-1.0.1.tgz", + "integrity": "sha512-ex3Z+lxsHtVKDTolJQqLHswq9SKfXzM/hWv17zsrhKqJwuGxO7CeBFM60aiuApZX9NqBhGAkPGGj9jt+F/Y9HQ==", + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", + "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", + "license": "MIT", + "dependencies": { + "file-type": "21.0.0", + "iterare": "1.2.1", + "load-esm": "1.0.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz", + "integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.2.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", + "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.13", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.13.tgz", + "integrity": "sha512-QZXnR/OGiDcBjF4hGk0wwVrPcZvbSSyzlvkjXv5LFfktj7O2VZDrt4Xs8SgR/vOFco+qk1i8J43ikMXZoTrtPw==", + "license": "MIT" + }, + "node_modules/load-esm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", + "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/token-types": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.1.0", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.1.tgz", + "integrity": "sha512-+NWHrac9dvilNgme+gP4YrBSumsaMZP0fNBtXXFIf33RLLKEcBUKaQZ7ULUbS0sBfcjxIZ4V96OTRkCbM7hxpw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad94d3d --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "@apihub24/token-authentication", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@nestjs/common": "^11.1.6", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.1.6", + "@apihub24/repository": "^1.0.1", + "class-validator": "^0.14.2" + }, + "devDependencies": { + "typescript": "^5.9.2" + }, + "keywords": [], + "author": { + "email": "markusmorgenstern87@outlook.de", + "name": "Markus Morgenstern", + "url": "https://git.apihub24.de/" + }, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/src/contracts/index.ts b/src/contracts/index.ts new file mode 100644 index 0000000..378a316 --- /dev/null +++ b/src/contracts/index.ts @@ -0,0 +1,12 @@ +export * from "./models/account"; +export * from "./models/group"; +export * from "./models/organization"; +export * from "./models/registration"; +export * from "./models/right"; +export * from "./models/session"; +export * from "./models/sign.in"; + +export * from "./services/mail.verification.service"; +export * from "./services/password.service"; +export * from "./services/session.service"; +export * from "./services/token.service"; diff --git a/src/contracts/models/account.ts b/src/contracts/models/account.ts new file mode 100644 index 0000000..8562b5c --- /dev/null +++ b/src/contracts/models/account.ts @@ -0,0 +1,28 @@ +import { Group } from './group'; +import { + IsEmail, + IsNotEmpty, + MinLength, + ValidateNested, +} from 'class-validator'; + +export class Account { + id: string; + + @MinLength(3) + accountName: string; + + @MinLength(3) + passwordHash: string; + + @IsEmail() + email: string; + + emailVerified: boolean; + + active: boolean; + + @IsNotEmpty({ each: true }) + @ValidateNested({ each: true }) + groups: Group[]; +} diff --git a/src/contracts/models/group.ts b/src/contracts/models/group.ts new file mode 100644 index 0000000..0f905ca --- /dev/null +++ b/src/contracts/models/group.ts @@ -0,0 +1,18 @@ +import { Organization } from './organization'; +import { Right } from './right'; +import { IsNotEmpty, MinLength, ValidateNested } from 'class-validator'; + +export class Group { + id: string; + + @MinLength(3) + name: string; + + @IsNotEmpty() + @ValidateNested() + organization: Organization; + + @IsNotEmpty({ each: true }) + @ValidateNested({ each: true }) + rights: Right[]; +} diff --git a/src/contracts/models/organization.ts b/src/contracts/models/organization.ts new file mode 100644 index 0000000..49c2e3a --- /dev/null +++ b/src/contracts/models/organization.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty, MinLength } from 'class-validator'; + +export class Organization { + id: string; + + @IsNotEmpty() + @MinLength(3) + name: string; +} diff --git a/src/contracts/models/registration.ts b/src/contracts/models/registration.ts new file mode 100644 index 0000000..00a0bf3 --- /dev/null +++ b/src/contracts/models/registration.ts @@ -0,0 +1,22 @@ +import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; + +export class Registration { + // @IsNotEmpty() + @MinLength(3) + accountName: string; + + // @IsNotEmpty() + @MinLength(3) + password: string; + + // @IsNotEmpty() + @MinLength(3) + passwordComparer: string; + + // @IsNotEmpty() + @IsEmail() + email: string; + + @IsNotEmpty({ each: true }) + groupIds: string[]; +} diff --git a/src/contracts/models/right.ts b/src/contracts/models/right.ts new file mode 100644 index 0000000..6b7f559 --- /dev/null +++ b/src/contracts/models/right.ts @@ -0,0 +1,8 @@ +import { MinLength } from 'class-validator'; + +export class Right { + id: string; + + @MinLength(3) + name: string; +} diff --git a/src/contracts/models/session.ts b/src/contracts/models/session.ts new file mode 100644 index 0000000..61a1a8d --- /dev/null +++ b/src/contracts/models/session.ts @@ -0,0 +1,12 @@ +import { IsNotEmpty, ValidateNested } from "class-validator"; +import { Account } from "./account"; + +export class Session { + id: string; + + @IsNotEmpty() + @ValidateNested() + account: Account; + + metaData: Record; +} diff --git a/src/contracts/models/sign.in.ts b/src/contracts/models/sign.in.ts new file mode 100644 index 0000000..4946aec --- /dev/null +++ b/src/contracts/models/sign.in.ts @@ -0,0 +1,8 @@ +import { MinLength } from 'class-validator'; + +export class SignIn { + @MinLength(3) + accountName: string; + @MinLength(3) + password: string; +} diff --git a/src/contracts/services/mail.verification.service.ts b/src/contracts/services/mail.verification.service.ts new file mode 100644 index 0000000..7545056 --- /dev/null +++ b/src/contracts/services/mail.verification.service.ts @@ -0,0 +1,6 @@ +import { Account } from "../models/account"; + +export interface MailVerificationService { + sendVerificationMail(account: Account): Promise; + verify(email: string, code: string): Promise; +} diff --git a/src/contracts/services/password.service.ts b/src/contracts/services/password.service.ts new file mode 100644 index 0000000..5d5c0ef --- /dev/null +++ b/src/contracts/services/password.service.ts @@ -0,0 +1,4 @@ +export interface PasswordService { + hash(plainTextPassword: string): Promise; + verify(plainTextPassword: string, passwordHash: string): Promise; +} diff --git a/src/contracts/services/session.service.ts b/src/contracts/services/session.service.ts new file mode 100644 index 0000000..9c53baa --- /dev/null +++ b/src/contracts/services/session.service.ts @@ -0,0 +1,9 @@ +import { Account } from "../models/account"; +import { Session } from "../models/session"; + +export interface SessionService { + create(account: Account): Promise; + getBy(filter: (account: Account) => boolean): Promise; + getById(sessionId: string): Promise; + remove(sessionId: string): Promise; +} diff --git a/src/contracts/services/token.service.ts b/src/contracts/services/token.service.ts new file mode 100644 index 0000000..5593c94 --- /dev/null +++ b/src/contracts/services/token.service.ts @@ -0,0 +1,8 @@ +import { Account } from "../models/account"; +import { Session } from "../models/session"; + +export interface TokenService { + generate(session: Session): Promise; + validate(token: string): Promise; + getAccount(token: string): Promise; +} diff --git a/src/guards/index.ts b/src/guards/index.ts new file mode 100644 index 0000000..942b42b --- /dev/null +++ b/src/guards/index.ts @@ -0,0 +1,4 @@ +export * from "./organization.decorator"; +export * from "./rights.decorator"; +export * from "./roles.decorator"; +export * from "./token.guard"; diff --git a/src/guards/organization.decorator.ts b/src/guards/organization.decorator.ts new file mode 100644 index 0000000..051e039 --- /dev/null +++ b/src/guards/organization.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; + +export const ORGANIZATIONS_KEY = 'organizations'; +export const Organizations = (...organizations: string[]) => + SetMetadata(ORGANIZATIONS_KEY, organizations); diff --git a/src/guards/rights.decorator.ts b/src/guards/rights.decorator.ts new file mode 100644 index 0000000..2f9f0a6 --- /dev/null +++ b/src/guards/rights.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const RIGHTS_KEY = 'rights'; +export const Rights = (...rights: string[]) => SetMetadata(RIGHTS_KEY, rights); diff --git a/src/guards/roles.decorator.ts b/src/guards/roles.decorator.ts new file mode 100644 index 0000000..e038e16 --- /dev/null +++ b/src/guards/roles.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/src/guards/token.guard.ts b/src/guards/token.guard.ts new file mode 100644 index 0000000..2b9b81f --- /dev/null +++ b/src/guards/token.guard.ts @@ -0,0 +1,78 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { RIGHTS_KEY } from './rights.decorator'; +import { ROLES_KEY } from './roles.decorator'; +import * as tokenService from '../contracts/services/token.service'; +import { ORGANIZATIONS_KEY } from './organization.decorator'; + +@Injectable() +export class TokenGuard implements CanActivate { + constructor( + private reflector: Reflector, + @Inject('@apihub24/token_service') + private readonly tokenService: tokenService.TokenService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const requiredRights = this.reflector.getAllAndOverride( + RIGHTS_KEY, + [context.getHandler(), context.getClass()], + ); + const requiredRoles = this.reflector.getAllAndOverride( + ROLES_KEY, + [context.getHandler(), context.getClass()], + ); + const requiredOrganizations = this.reflector.getAllAndOverride( + ORGANIZATIONS_KEY, + [context.getHandler(), context.getClass()], + ); + if (!requiredRights && !requiredRoles && !requiredOrganizations) { + return true; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { + headers: { authorization }, + } = context.switchToHttp().getRequest(); + if ( + !authorization || + typeof authorization !== 'string' || + !authorization.includes('Bearer ') + ) { + return false; + } + const token = authorization.slice(7); + const account = await this.tokenService.getAccount(token); + if (!account) { + return false; + } + if (requiredOrganizations) { + for (const orga of requiredOrganizations) { + if (!account.groups.find((x) => x.organization.name === orga)) { + return false; + } + } + } + if (requiredRoles) { + for (const role of requiredRoles) { + if (!account.groups.find((x) => x.name === role)) { + return false; + } + } + } + if (requiredRights) { + for (const right of requiredRights) { + if ( + !account.groups.find((x) => x.rights.find((y) => y.name === right)) + ) { + return false; + } + } + } + return true; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d8b0ce0 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,30 @@ +export * from "./token-authentication.module"; + +export * from "./contracts/models/account"; +export * from "./contracts/models/group"; +export * from "./contracts/models/organization"; +export * from "./contracts/models/registration"; +export * from "./contracts/models/right"; +export * from "./contracts/models/session"; +export * from "./contracts/models/sign.in"; + +export * from "./contracts/services/mail.verification.service"; +export * from "./contracts/services/password.service"; +export * from "./contracts/services/session.service"; +export * from "./contracts/services/token.service"; + +export * from "./guards/organization.decorator"; +export * from "./guards/rights.decorator"; +export * from "./guards/roles.decorator"; +export * from "./guards/token.guard"; + +export * from "./services/account.factory.service"; +export * from "./services/account.service"; +export * from "./services/group.factory.service"; +export * from "./services/group.service"; +export * from "./services/login.service"; +export * from "./services/organization.factory.service"; +export * from "./services/organization.service"; +export * from "./services/registration.service"; +export * from "./services/right.factory.service"; +export * from "./services/right.service"; diff --git a/src/services/account.factory.service.ts b/src/services/account.factory.service.ts new file mode 100644 index 0000000..620c22d --- /dev/null +++ b/src/services/account.factory.service.ts @@ -0,0 +1,40 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { validate } from "class-validator"; +import * as contracts from "src/contracts"; +import { GroupService } from "./group.service"; + +@Injectable() +export class AccountFactoryService { + constructor( + @Inject("@apihub24/password_service") + private readonly passwordService: contracts.PasswordService, + @Inject(GroupService) + private readonly groupService: GroupService + ) {} + + async createFromRegistration( + registration: contracts.Registration + ): Promise { + let validationErrors = await validate(registration); + if (validationErrors?.length) { + throw new Error(validationErrors[0].toString()); + } + const groups = await this.groupService.getBy((x) => + registration.groupIds.includes(x.id) + ); + const account = new contracts.Account(); + account.accountName = registration.accountName; + account.email = registration.email; + account.emailVerified = false; + account.passwordHash = await this.passwordService.hash( + registration.password + ); + account.active = false; + account.groups = groups; + validationErrors = await validate(account); + if (validationErrors?.length) { + throw new Error(validationErrors[0].toString()); + } + return account; + } +} diff --git a/src/services/account.service.ts b/src/services/account.service.ts new file mode 100644 index 0000000..382d3d3 --- /dev/null +++ b/src/services/account.service.ts @@ -0,0 +1,71 @@ +import { Account } from "../contracts/models/account"; +import { Inject, Injectable } from "@nestjs/common"; +import * as repository from "@apihub24/repository"; +import { Group } from "src/contracts"; + +@Injectable() +export class AccountService { + constructor( + @Inject("@apihub24/account_repository") + private readonly accountRepository: repository.Repository, + @Inject("@apihub24/group_repository") + private readonly groupRepository: repository.Repository + ) {} + + async getBy(filter: (account: Account) => boolean): Promise { + return await this.accountRepository.getBy(filter); + } + + async save(account: Account): Promise { + const accounts = await this.accountRepository.save([account]); + return accounts?.length ? accounts[0] : null; + } + + async delete(filter: (account: Account) => boolean): Promise { + return await this.accountRepository.deleteBy(filter); + } + + async addAccountToGroup( + accountId: string, + groupId: string + ): Promise { + const [account, group] = await this.getAccountAndGroup(accountId, groupId); + account.groups = account.groups.filter((x) => x.id !== groupId); + account.groups.push(group); + const accountsSaved = await this.accountRepository.save([account]); + if (!accountsSaved.length) { + throw new Error(`account ${account.accountName} can not be saved`); + } + return accountsSaved[0]; + } + + async removeAccountFromGroup( + accountId: string, + groupId: string + ): Promise { + const [account] = await this.getAccountAndGroup(accountId, groupId); + account.groups = account.groups.filter((x) => x.id !== groupId); + const accountsSaved = await this.accountRepository.save([account]); + if (!accountsSaved.length) { + throw new Error(`account ${account.accountName} can not be saved`); + } + return accountsSaved[0]; + } + + private async getAccountAndGroup( + accountId: string, + groupId: string + ): Promise<[Account, Group]> { + const accounts = await this.accountRepository.getBy( + (x) => x.id === accountId + ); + if (!accounts.length) { + throw new Error(`account with id ${accountId} not found`); + } + const groups = await this.groupRepository.getBy((x) => x.id === groupId); + if (!groups.length) { + throw new Error(`group with id ${groupId} not found`); + } + return [accounts[0], groups[0]]; + } +} diff --git a/src/services/group.factory.service.ts b/src/services/group.factory.service.ts new file mode 100644 index 0000000..b6b038a --- /dev/null +++ b/src/services/group.factory.service.ts @@ -0,0 +1,40 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { validate } from "class-validator"; +import { OrganizationService } from "./organization.service"; +import { RightService } from "./right.service"; +import { Group } from "src/contracts"; + +@Injectable() +export class GroupFactoryService { + constructor( + @Inject(RightService) + private readonly rightService: RightService, + @Inject(OrganizationService) + private readonly organizationService: OrganizationService + ) {} + + async createGroup( + name: string, + organizationId: string, + rightIds: string[] + ): Promise { + const rights = await this.rightService.getBy((x) => + rightIds.includes(x.id) + ); + const organizations = await this.organizationService.getBy( + (x) => x.id === organizationId + ); + if (!organizations?.length) { + throw new Error(`organization with id (${organizationId}) not exists`); + } + const group = new Group(); + group.name = name; + group.organization = organizations[0]; + group.rights = rights; + const validationErrors = await validate(group); + if (validationErrors?.length) { + throw new Error(validationErrors[0].toString()); + } + return group; + } +} diff --git a/src/services/group.service.ts b/src/services/group.service.ts new file mode 100644 index 0000000..985e6f8 --- /dev/null +++ b/src/services/group.service.ts @@ -0,0 +1,64 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { Group } from "../contracts/models/group"; +import * as repository from "@apihub24/repository"; +import { Right } from "src/contracts"; + +@Injectable() +export class GroupService { + constructor( + @Inject("@apihub24/group_repository") + private readonly groupRepository: repository.Repository, + @Inject("@apihub24/right_repository") + private readonly rightRepository: repository.Repository + ) {} + + async getBy(filter: (group: Group) => boolean): Promise { + return await this.groupRepository.getBy(filter); + } + + async save(group: Group): Promise { + const groups = await this.groupRepository.save([group]); + return groups?.length ? groups[0] : null; + } + + async delete(filter: (group: Group) => boolean): Promise { + return this.groupRepository.deleteBy(filter); + } + + async addRightToGroup(groupId: string, rightId: string) { + const [group, right] = await this.getGroupAndRight(groupId, rightId); + group.rights = group.rights.filter((x) => x.id !== rightId); + group.rights.push(right); + const savedGroups = await this.groupRepository.save([group]); + if (!savedGroups.length) { + throw new Error(`group ${group.name} can not be saved`); + } + return savedGroups[0]; + } + + async removeRightFromGroup(groupId: string, rightId: string) { + const [group] = await this.getGroupAndRight(groupId, rightId); + group.rights = group.rights.filter((x) => x.id !== rightId); + const savedGroups = await this.groupRepository.save([group]); + if (!savedGroups.length) { + throw new Error(`group ${group.name} can not be saved`); + } + return savedGroups[0]; + } + + private async getGroupAndRight( + groupId: string, + rightId: string + ): Promise<[Group, Right]> { + const groups = await this.groupRepository.getBy((x) => x.id === groupId); + if (!groups.length) { + throw new Error(`group with id ${groupId} not found`); + } + const rights = await this.rightRepository.getBy((x) => x.id === rightId); + if (!rights.length) { + throw new Error(`right with id ${rightId} not found`); + } + + return [groups[0], rights[0]]; + } +} diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..8a77039 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,10 @@ +export * from "./account.factory.service"; +export * from "./account.service"; +export * from "./group.factory.service"; +export * from "./group.service"; +export * from "./login.service"; +export * from "./organization.factory.service"; +export * from "./organization.service"; +export * from "./registration.service"; +export * from "./right.factory.service"; +export * from "./right.service"; diff --git a/src/services/login.service.ts b/src/services/login.service.ts new file mode 100644 index 0000000..a8ac8de --- /dev/null +++ b/src/services/login.service.ts @@ -0,0 +1,52 @@ +import { Inject, Injectable } from "@nestjs/common"; +import * as contracts from "src/contracts"; +import { AccountService } from "./account.service"; + +@Injectable() +export class LoginService { + constructor( + @Inject("@apihub24/token_service") + private readonly tokenService: contracts.TokenService, + @Inject("@apihub24/password_service") + private readonly passwordService: contracts.PasswordService, + @Inject("@apihub24/session_service") + private readonly sessionService: contracts.SessionService, + @Inject(AccountService) + private readonly accountService: AccountService + ) {} + + async signIn(signIn: contracts.SignIn): Promise { + const accounts = await this.accountService.getBy( + (x) => + x.accountName === signIn.accountName && + x.active && + !!x.passwordHash?.length + ); + if ( + !accounts.length || + !(await this.passwordService.verify( + signIn.password, + accounts[0].passwordHash + )) + ) { + return ""; + } + const session = await this.sessionService.create(accounts[0]); + const token = await this.tokenService.generate(session); + return token; + } + + async signOut(accountId: string): Promise { + const accounts = await this.accountService.getBy((x) => x.id === accountId); + if (!accounts.length) { + return; + } + const sessions = await this.sessionService.getBy( + (x) => x.id === accounts[0].id + ); + if (!sessions.length) { + return; + } + await this.sessionService.remove(sessions[0].id); + } +} diff --git a/src/services/organization.factory.service.ts b/src/services/organization.factory.service.ts new file mode 100644 index 0000000..e6a9146 --- /dev/null +++ b/src/services/organization.factory.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { Organization } from '../contracts/models/organization'; + +@Injectable() +export class OrganizationFactoryService { + createFromName(name: string): Organization { + const organization = new Organization(); + organization.name = name; + return organization; + } +} diff --git a/src/services/organization.service.ts b/src/services/organization.service.ts new file mode 100644 index 0000000..049055e --- /dev/null +++ b/src/services/organization.service.ts @@ -0,0 +1,33 @@ +import { Inject, Injectable } from '@nestjs/common'; +import * as repository from '@apihub24/repository'; +import { Organization } from '../contracts/models/organization'; + +@Injectable() +export class OrganizationService { + constructor( + @Inject('@apihub24/organization_repository') + private readonly organizationRepository: repository.Repository, + ) {} + + async getBy( + filter: (organization: Organization) => boolean, + ): Promise { + return await this.organizationRepository.getBy(filter); + } + + async save(organization: Organization): Promise { + const savedOrganizations = await this.organizationRepository.save([ + organization, + ]); + if (!savedOrganizations?.length) { + throw new Error(`organization (${organization.name}) not saved`); + } + return savedOrganizations[0]; + } + + async deleteBy( + filter: (organization: Organization) => boolean, + ): Promise { + return await this.organizationRepository.deleteBy(filter); + } +} diff --git a/src/services/registration.service.ts b/src/services/registration.service.ts new file mode 100644 index 0000000..66c95e9 --- /dev/null +++ b/src/services/registration.service.ts @@ -0,0 +1,68 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { Account } from "../contracts/models/account"; +import { Registration } from "../contracts/models/registration"; +import * as mailVerificationService from "../contracts/services/mail.verification.service"; +import { AccountService } from "./account.service"; +import { AccountFactoryService } from "./account.factory.service"; + +@Injectable() +export class RegistrationService { + constructor( + @Inject(AccountService) + private readonly accountService: AccountService, + @Inject(AccountFactoryService) + private readonly accountFactory: AccountFactoryService, + @Inject("@apihub24/mail_verification_service") + private readonly mailVerificationService: mailVerificationService.MailVerificationService + ) {} + + async registerAccount(registration: Registration): Promise { + const newAccount = await this.accountFactory.createFromRegistration( + registration + ); + return this.accountService.save(newAccount); + } + + async verify(email: string, code: string): Promise { + const verified = await this.mailVerificationService.verify(email, code); + if (!verified) { + return false; + } + const unverifiedAccounts = await this.accountService.getBy( + (x) => x.email === email && !x.emailVerified + ); + for (const account of unverifiedAccounts) { + account.emailVerified = true; + await this.accountService.save(account); + } + return verified; + } + + async activateAccount(accountId: string): Promise { + const accounts = await this.accountService.getBy((x) => x.id === accountId); + if (!accounts?.length) { + return false; + } + accounts[0].active = true; + const savedAccount = await this.accountService.save(accounts[0]); + return savedAccount?.active === true; + } + + async deactivateAccount(accountId: string): Promise { + const accounts = await this.accountService.getBy((x) => x.id === accountId); + if (!accounts?.length) { + return true; + } + accounts[0].active = false; + const savedAccount = await this.accountService.save(accounts[0]); + return savedAccount?.active === false; + } + + async unregisterAccount(accountId: string): Promise { + const accounts = await this.accountService.getBy((x) => x.id === accountId); + if (!accounts?.length) { + return; + } + void this.accountService.delete((x) => x.id === accountId); + } +} diff --git a/src/services/right.factory.service.ts b/src/services/right.factory.service.ts new file mode 100644 index 0000000..e4026d6 --- /dev/null +++ b/src/services/right.factory.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from "@nestjs/common"; +import { Right } from "../contracts/models/right"; +import { validate } from "class-validator"; + +@Injectable() +export class RightFactoryService { + async createRight(name: string): Promise { + const right = new Right(); + right.name = name; + const validationErrors = await validate(right); + if (validationErrors?.length) { + throw new Error(validationErrors[0].toString()); + } + return right; + } +} diff --git a/src/services/right.service.ts b/src/services/right.service.ts new file mode 100644 index 0000000..0876283 --- /dev/null +++ b/src/services/right.service.ts @@ -0,0 +1,22 @@ +import { Right } from '../contracts/models/right'; +import { Inject, Injectable } from '@nestjs/common'; +import * as repository from '@apihub24/repository'; + +@Injectable() +export class RightService { + constructor( + @Inject('@apihub24/right_repository') + private readonly rightRepository: repository.Repository, + ) {} + + async getBy(filter: (right: Right) => boolean): Promise { + return await this.rightRepository.getBy(filter); + } + async save(right: Right): Promise { + const rights = await this.rightRepository.save([right]); + return rights?.length ? rights[0] : null; + } + async delete(filter: (right: Right) => boolean): Promise { + return await this.rightRepository.deleteBy(filter); + } +} diff --git a/src/token-authentication.module.ts b/src/token-authentication.module.ts new file mode 100644 index 0000000..6865ba3 --- /dev/null +++ b/src/token-authentication.module.ts @@ -0,0 +1,43 @@ +import { DynamicModule, Module, Provider } from "@nestjs/common"; +import { RightService } from "./services/right.service"; +import { GroupService } from "./services/group.service"; +import { AccountService } from "./services/account.service"; +import { LoginService } from "./services/login.service"; +import { RegistrationService } from "./services/registration.service"; +import { AccountFactoryService } from "./services/account.factory.service"; +import { ConfigModule } from "@nestjs/config"; +import { GroupFactoryService } from "./services/group.factory.service"; +import { RightFactoryService } from "./services/right.factory.service"; +import { OrganizationService } from "./services/organization.service"; +import { OrganizationFactoryService } from "./services/organization.factory.service"; + +@Module({}) +export class TokenAuthenticationModule { + static forRoot(providers: Provider[]): DynamicModule { + return { + module: TokenAuthenticationModule, + imports: [ConfigModule.forRoot()], + providers: [ + RightService, + GroupService, + AccountService, + OrganizationService, + LoginService, + RegistrationService, + OrganizationFactoryService, + AccountFactoryService, + GroupFactoryService, + RightFactoryService, + ...providers, + ], + exports: [ + RightService, + GroupService, + AccountService, + LoginService, + RegistrationService, + AccountFactoryService, + ], + }; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..99c400b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "resolvePackageJsonExports": true, + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false + }, + "include": ["./src"] +}