This commit is contained in:
admin 2025-08-20 22:35:19 +02:00
commit 70c35f6d2b
37 changed files with 1374 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.vscode
.idea
dist
node_modules

6
.npmignore Normal file
View File

@ -0,0 +1,6 @@
.vscode
.idea
node_modules
src
.gitignore
tsconfig.json

21
LICENSE Normal file
View File

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

94
README.md Normal file
View File

@ -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<Account> | a Service that handles Session Get, Save, Delete Operations |
| Group Repository | @apihub24/group_repository | Repository<Group> | a Service that handles Session Get, Save, Delete Operations |
| Right Repository | @apihub24/right_repository | Repository<Right> | a Service that handles Session Get, Save, Delete Operations |
| Organization Repository | @apihub24/organization_repository | Repository<Organization> | 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<Organization>,
},
{
provide: "@apihub24/account_repository",
useClass: InMemoryRepository<Account>,
},
{
provide: "@apihub24/group_repository",
useClass: InMemoryRepository<Group>,
},
{
provide: "@apihub24/right_repository",
useClass: InMemoryRepository<Right>,
},
]),
],
controllers: [LoginController, LogoutController],
providers: [],
// required exports for the TokenGuard
exports: [TokenAuthenticationModule, jwtModule],
})
export class AuthenticationModule {}
```

454
package-lock.json generated Normal file
View File

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

30
package.json Normal file
View File

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

12
src/contracts/index.ts Normal file
View File

@ -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";

View File

@ -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[];
}

View File

@ -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[];
}

View File

@ -0,0 +1,9 @@
import { IsNotEmpty, MinLength } from 'class-validator';
export class Organization {
id: string;
@IsNotEmpty()
@MinLength(3)
name: string;
}

View File

@ -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[];
}

View File

@ -0,0 +1,8 @@
import { MinLength } from 'class-validator';
export class Right {
id: string;
@MinLength(3)
name: string;
}

View File

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

View File

@ -0,0 +1,8 @@
import { MinLength } from 'class-validator';
export class SignIn {
@MinLength(3)
accountName: string;
@MinLength(3)
password: string;
}

View File

@ -0,0 +1,6 @@
import { Account } from "../models/account";
export interface MailVerificationService {
sendVerificationMail(account: Account): Promise<void>;
verify(email: string, code: string): Promise<boolean>;
}

View File

@ -0,0 +1,4 @@
export interface PasswordService {
hash(plainTextPassword: string): Promise<string>;
verify(plainTextPassword: string, passwordHash: string): Promise<boolean>;
}

View File

@ -0,0 +1,9 @@
import { Account } from "../models/account";
import { Session } from "../models/session";
export interface SessionService {
create(account: Account): Promise<Session>;
getBy(filter: (account: Account) => boolean): Promise<Session[]>;
getById(sessionId: string): Promise<Session | null>;
remove(sessionId: string): Promise<void>;
}

View File

@ -0,0 +1,8 @@
import { Account } from "../models/account";
import { Session } from "../models/session";
export interface TokenService {
generate(session: Session): Promise<string>;
validate(token: string): Promise<boolean>;
getAccount(token: string): Promise<Account | null>;
}

4
src/guards/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from "./organization.decorator";
export * from "./rights.decorator";
export * from "./roles.decorator";
export * from "./token.guard";

View File

@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
export const ORGANIZATIONS_KEY = 'organizations';
export const Organizations = (...organizations: string[]) =>
SetMetadata(ORGANIZATIONS_KEY, organizations);

View File

@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const RIGHTS_KEY = 'rights';
export const Rights = (...rights: string[]) => SetMetadata(RIGHTS_KEY, rights);

View File

@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

78
src/guards/token.guard.ts Normal file
View File

@ -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<boolean> {
const requiredRights = this.reflector.getAllAndOverride<string[]>(
RIGHTS_KEY,
[context.getHandler(), context.getClass()],
);
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
const requiredOrganizations = this.reflector.getAllAndOverride<string[]>(
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;
}
}

30
src/index.ts Normal file
View File

@ -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";

View File

@ -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<contracts.Account> {
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;
}
}

View File

@ -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<Account>,
@Inject("@apihub24/group_repository")
private readonly groupRepository: repository.Repository<Group>
) {}
async getBy(filter: (account: Account) => boolean): Promise<Account[]> {
return await this.accountRepository.getBy(filter);
}
async save(account: Account): Promise<Account | null> {
const accounts = await this.accountRepository.save([account]);
return accounts?.length ? accounts[0] : null;
}
async delete(filter: (account: Account) => boolean): Promise<boolean> {
return await this.accountRepository.deleteBy(filter);
}
async addAccountToGroup(
accountId: string,
groupId: string
): Promise<Account> {
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<Account> {
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]];
}
}

View File

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

View File

@ -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<Group>,
@Inject("@apihub24/right_repository")
private readonly rightRepository: repository.Repository<Right>
) {}
async getBy(filter: (group: Group) => boolean): Promise<Group[]> {
return await this.groupRepository.getBy(filter);
}
async save(group: Group): Promise<Group | null> {
const groups = await this.groupRepository.save([group]);
return groups?.length ? groups[0] : null;
}
async delete(filter: (group: Group) => boolean): Promise<boolean> {
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]];
}
}

10
src/services/index.ts Normal file
View File

@ -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";

View File

@ -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<string> {
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<void> {
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);
}
}

View File

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

View File

@ -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<Organization>,
) {}
async getBy(
filter: (organization: Organization) => boolean,
): Promise<Organization[]> {
return await this.organizationRepository.getBy(filter);
}
async save(organization: Organization): Promise<Organization> {
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<boolean> {
return await this.organizationRepository.deleteBy(filter);
}
}

View File

@ -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<Account | null> {
const newAccount = await this.accountFactory.createFromRegistration(
registration
);
return this.accountService.save(newAccount);
}
async verify(email: string, code: string): Promise<boolean> {
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<boolean> {
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<boolean> {
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<void> {
const accounts = await this.accountService.getBy((x) => x.id === accountId);
if (!accounts?.length) {
return;
}
void this.accountService.delete((x) => x.id === accountId);
}
}

View File

@ -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<Right> {
const right = new Right();
right.name = name;
const validationErrors = await validate(right);
if (validationErrors?.length) {
throw new Error(validationErrors[0].toString());
}
return right;
}
}

View File

@ -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<Right>,
) {}
async getBy(filter: (right: Right) => boolean): Promise<Right[]> {
return await this.rightRepository.getBy(filter);
}
async save(right: Right): Promise<Right | null> {
const rights = await this.rightRepository.save([right]);
return rights?.length ? rights[0] : null;
}
async delete(filter: (right: Right) => boolean): Promise<boolean> {
return await this.rightRepository.deleteBy(filter);
}
}

View File

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

26
tsconfig.json Normal file
View File

@ -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"]
}