From 9064224a85c84d15cf06b87a1c54695c190449f4 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 16:58:40 +0100 Subject: [PATCH 01/62] refactor: create services files for further SoC --- src/services/auth.service.ts | 0 src/services/mail.service.ts | 0 src/services/oauth.service.ts | 0 src/services/password-reset.service.ts | 0 src/services/permissions.service.ts | 0 src/services/roles.service.ts | 0 src/services/token.service.ts | 0 src/services/users.service.ts | 0 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/services/auth.service.ts create mode 100644 src/services/mail.service.ts create mode 100644 src/services/oauth.service.ts create mode 100644 src/services/password-reset.service.ts create mode 100644 src/services/permissions.service.ts create mode 100644 src/services/roles.service.ts create mode 100644 src/services/token.service.ts create mode 100644 src/services/users.service.ts diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/mail.service.ts b/src/services/mail.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/oauth.service.ts b/src/services/oauth.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/password-reset.service.ts b/src/services/password-reset.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/permissions.service.ts b/src/services/permissions.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/roles.service.ts b/src/services/roles.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/token.service.ts b/src/services/token.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/users.service.ts b/src/services/users.service.ts new file mode 100644 index 0000000..e69de29 From 1c674fbbe111f4da12a5b6740ca1da07133adde7 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 16:59:16 +0100 Subject: [PATCH 02/62] refactor: update authKit Module to not ever call any db, and delete client model --- src/auth-kit.module.ts | 14 ++------------ src/models/client.model.ts | 35 ----------------------------------- 2 files changed, 2 insertions(+), 47 deletions(-) delete mode 100644 src/models/client.model.ts diff --git a/src/auth-kit.module.ts b/src/auth-kit.module.ts index 5105c37..7e095c3 100644 --- a/src/auth-kit.module.ts +++ b/src/auth-kit.module.ts @@ -1,10 +1,8 @@ import 'dotenv/config'; -import { MiddlewareConsumer, Module, NestModule, OnModuleDestroy, OnModuleInit, RequestMethod } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; import passport from './config/passport.config'; import cookieParser from 'cookie-parser'; -import mongoose from 'mongoose'; -import { connectDB } from './config/db.config'; import { AuthController } from './controllers/auth.controller'; import { PasswordResetController } from './controllers/password-reset.controller'; import { UsersController } from './controllers/users.controller'; @@ -22,15 +20,7 @@ import { AdminController } from './controllers/admin.controller'; AdminController, ], }) -export class AuthKitModule implements NestModule, OnModuleInit, OnModuleDestroy { - async onModuleInit(): Promise { - await connectDB(); - } - - async onModuleDestroy(): Promise { - await mongoose.disconnect(); - } - +export class AuthKitModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(cookieParser(), passport.initialize()) diff --git a/src/models/client.model.ts b/src/models/client.model.ts deleted file mode 100644 index 566c0f1..0000000 --- a/src/models/client.model.ts +++ /dev/null @@ -1,35 +0,0 @@ -import mongoose from 'mongoose'; -import mongoosePaginate from 'mongoose-paginate-v2'; - -const ClientSchema = new mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true - }, - password: { - type: String, - required: function () { - return !this.microsoftId && !this.googleId && !this.facebookId; - } - }, - name: { type: String }, - microsoftId: { type: String, index: true }, - googleId: { type: String, index: true }, - facebookId: { type: String, index: true }, - roles: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Role' }], - resetPasswordToken: { type: String }, - resetPasswordExpires: { type: Date }, - refreshToken: { type: String }, - createdAt: { type: Date, default: Date.now } - }, - { timestamps: true } -); - -ClientSchema.plugin(mongoosePaginate); - -const Client = mongoose.models.Client || mongoose.model('Client', ClientSchema); - -export { ClientSchema }; -export default Client; From 072c1b5158eecd6652e61188c64bd131832023a9 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 16:59:25 +0100 Subject: [PATCH 03/62] refactor: create repositories files for further SoC --- src/repositories/client.repository.ts | 0 src/repositories/permission.repository.ts | 0 src/repositories/role.repository.ts | 0 src/repositories/user.repository.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/repositories/client.repository.ts create mode 100644 src/repositories/permission.repository.ts create mode 100644 src/repositories/role.repository.ts create mode 100644 src/repositories/user.repository.ts diff --git a/src/repositories/client.repository.ts b/src/repositories/client.repository.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/repositories/permission.repository.ts b/src/repositories/permission.repository.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/repositories/role.repository.ts b/src/repositories/role.repository.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts new file mode 100644 index 0000000..e69de29 From 7e26f307298cb133b44084b65ce52645a77984cb Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 17:34:42 +0100 Subject: [PATCH 04/62] refactor: create proper package json file #deleted unnecessary matters --- package.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index a634f84..cb79740 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,7 @@ { "name": "@ciscode/authentication-kit", + "version": "1.2.0", + "description": "NestJS auth kit with local + OAuth, JWT, RBAC, password reset.", "publishConfig": { "access": "public" }, @@ -7,8 +9,6 @@ "type": "git", "url": "git+https://github.com/CISCODE-MA/AuthKit.git" }, - "version": "1.1.3", - "description": "A login library with local login, Microsoft Entra ID authentication, password reset, and roles/permissions.", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ @@ -24,13 +24,12 @@ "release": "semantic-release" }, "keywords": [ - "login", "authentication", - "microsoft-oauth", - "password-reset", - "roles", - "permissions", - "users" + "nest", + "oauth", + "jwt", + "rbac", + "password-reset" ], "author": "Ciscode", "license": "MIT", @@ -38,17 +37,18 @@ "@nestjs/common": "^10.4.0", "@nestjs/core": "^10.4.0", "@nestjs/platform-express": "^10.4.0", + "@nestjs/mongoose": "^10.0.2", "axios": "^1.7.7", "bcryptjs": "^2.4.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", - "dotenv": "^16.0.3", - "express": "^4.18.2", - "jsonwebtoken": "^9.0.0", + "dotenv": "^16.4.5", + "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "mongoose": "^7.0.0", - "mongoose-paginate-v2": "^1.7.1", - "nodemailer": "^7.0.12", - "passport": "^0.6.0", + "mongoose": "^7.6.4", + "nodemailer": "^6.9.15", + "passport": "^0.7.0", "passport-azure-ad-oauth2": "^0.0.4", "passport-facebook": "^3.0.0", "passport-google-oauth20": "^2.0.0", @@ -68,4 +68,4 @@ "ts-node": "^10.9.2", "typescript": "^5.6.2" } -} +} \ No newline at end of file From 0d4e6918ad43e05aea8177de57164c0cfe8955fb Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 17:42:13 +0100 Subject: [PATCH 05/62] refactor: update database models and packages configs --- package-lock.json | 765 ++++++++++++++++++--------------- src/models/permission.model.ts | 22 +- src/models/role.model.ts | 25 +- src/models/tenant.model.ts | 12 - src/models/user.model.ts | 102 +++-- tsconfig.json | 16 +- 6 files changed, 513 insertions(+), 429 deletions(-) delete mode 100644 src/models/tenant.model.ts diff --git a/package-lock.json b/package-lock.json index c9a35f8..946fe73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,28 +1,29 @@ { "name": "@ciscode/authentication-kit", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ciscode/authentication-kit", - "version": "1.1.1", + "version": "1.2.0", "license": "MIT", "dependencies": { "@nestjs/common": "^10.4.0", "@nestjs/core": "^10.4.0", + "@nestjs/mongoose": "^10.0.2", "@nestjs/platform-express": "^10.4.0", "axios": "^1.7.7", "bcryptjs": "^2.4.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", - "dotenv": "^16.0.3", - "express": "^4.18.2", - "jsonwebtoken": "^9.0.0", + "dotenv": "^16.4.5", + "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "mongoose": "^7.0.0", - "mongoose-paginate-v2": "^1.7.1", - "nodemailer": "^7.0.12", - "passport": "^0.6.0", + "mongoose": "^7.6.4", + "nodemailer": "^6.9.15", + "passport": "^0.7.0", "passport-azure-ad-oauth2": "^0.0.4", "passport-facebook": "^3.0.0", "passport-google-oauth20": "^2.0.0", @@ -44,14 +45,14 @@ } }, "node_modules/@actions/core": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.1.tgz", - "integrity": "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.2.tgz", + "integrity": "sha512-Ast1V7yHbGAhplAsuVlnb/5J8Mtr/Zl6byPPL+Qjq3lmfIgWF1ak1iYfF/079cRERiuTALTXkSuEUdZeDCfGtA==", "dev": true, "license": "MIT", "dependencies": { "@actions/exec": "^2.0.0", - "@actions/http-client": "^3.0.0" + "@actions/http-client": "^3.0.1" } }, "node_modules/@actions/exec": { @@ -65,9 +66,9 @@ } }, "node_modules/@actions/http-client": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz", - "integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.1.tgz", + "integrity": "sha512-SbGS8c/vySbNO3kjFgSW77n83C4MQx/Yoe+b1hAdpuvfHxnkHzDq2pWljUpAA56Si1Gae/7zjeZsV0CYjmLo/w==", "dev": true, "license": "MIT", "dependencies": { @@ -96,13 +97,13 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -202,9 +203,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.4.tgz", - "integrity": "sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz", + "integrity": "sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==", "license": "MIT", "optional": true, "dependencies": { @@ -212,9 +213,9 @@ } }, "node_modules/@nestjs/common": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.20.tgz", - "integrity": "sha512-hxJxZF7jcKGuUzM9EYbuES80Z/36piJbiqmPy86mk8qOn5gglFebBTvcx7PWVbRNSb4gngASYnefBj/Y2HAzpQ==", + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", + "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", "license": "MIT", "dependencies": { "file-type": "20.4.1", @@ -242,9 +243,9 @@ } }, "node_modules/@nestjs/core": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.20.tgz", - "integrity": "sha512-kRdtyKA3+Tu70N3RQ4JgmO1E3LzAMs/eppj7SfjabC7TgqNWoS4RLhWl4BqmsNVmjj6D5jgfPVtHtgYkU3AfpQ==", + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", + "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -279,19 +280,25 @@ } } }, - "node_modules/@nestjs/core/node_modules/path-to-regexp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", - "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", - "license": "MIT" + "node_modules/@nestjs/mongoose": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-10.1.0.tgz", + "integrity": "sha512-1ExAnZUfh2QffEaGjqYGgVPy/sYBQCVLCLqVgkcClKx/BCd0QNgND8MB70lwyobp3nm/+nbGQqBpu9F3/hgOCw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "mongoose": "^6.0.2 || ^7.0.0 || ^8.0.0", + "rxjs": "^7.0.0" + } }, "node_modules/@nestjs/platform-express": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.21.tgz", - "integrity": "sha512-xIsa4h+oKf4zrHpTWN2i0gYkGaXewDqv4+KCatI1+aWoZKScFdoI82MFfuzq+z/EBpnVP2ABGqvPJWu+ZhKYvQ==", + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", "license": "MIT", "dependencies": { - "body-parser": "1.20.3", + "body-parser": "1.20.4", "cors": "2.8.5", "express": "4.22.1", "multer": "2.0.2", @@ -324,37 +331,6 @@ "npm": ">=5.0.0" } }, - "node_modules/@nuxtjs/opencollective/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@nuxtjs/opencollective/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -542,9 +518,9 @@ "license": "ISC" }, "node_modules/@pnpm/npm-conf": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", - "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", "dev": true, "license": "MIT", "dependencies": { @@ -671,22 +647,6 @@ } } }, - "node_modules/@semantic-release/github/node_modules/mime": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", - "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa" - ], - "license": "MIT", - "bin": { - "mime": "bin/cli.js" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/@semantic-release/github/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1009,6 +969,7 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -1019,6 +980,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1038,6 +1000,7 @@ "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -1047,9 +1010,10 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -1062,6 +1026,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, "license": "MIT" }, "node_modules/@types/jsonwebtoken": { @@ -1078,6 +1043,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -1087,9 +1053,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", - "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1185,18 +1151,21 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1206,6 +1175,7 @@ "version": "1.15.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -1217,12 +1187,19 @@ "version": "0.17.6", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1335,13 +1312,15 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -1434,23 +1413,23 @@ "license": "Apache-2.0" }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -1558,13 +1537,16 @@ } }, "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -1580,6 +1562,23 @@ "node": ">=10" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, "node_modules/clean-stack": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.3.0.tgz", @@ -1618,39 +1617,6 @@ "npm": ">=5.0.0" } }, - "node_modules/cli-highlight/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/cli-highlight/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -1838,20 +1804,6 @@ "typedarray": "^0.0.6" } }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -2135,9 +2087,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2206,6 +2158,39 @@ "readable-stream": "^2.0.2" } }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2566,20 +2551,11 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/fast-content-type-parse": { "version": "3.0.0", @@ -2658,17 +2634,17 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -2783,10 +2759,43 @@ "readable-stream": "^2.0.0" } }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", "dev": true, "license": "MIT", "dependencies": { @@ -3033,20 +3042,34 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-proxy-agent": { @@ -3542,12 +3565,11 @@ } }, "node_modules/jwks-rsa": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", - "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.1.tgz", + "integrity": "sha512-r7QdN9TdqI6aFDFZt+GpAqj5yRtMUv23rL2I01i7B8P2/g8F0ioEN6VeSObKgTLs4GmmNJwP9J7Fyp/AYDBGRg==", "license": "MIT", "dependencies": { - "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", "jose": "^4.15.4", @@ -3600,6 +3622,12 @@ "node": ">=12.0.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.34", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.34.tgz", + "integrity": "sha512-v/Ip8k8eYdp7bINpzqDh46V/PaQ8sK+qi97nMQgjZzFlb166YFqlR/HVI+MzsI9JqcyyVWCOipmmretiaSyQyw==", + "license": "MIT" + }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -3733,13 +3761,15 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": "20 || >=22" + "node": ">=10" } }, "node_modules/lru-memoizer": { @@ -3752,18 +3782,6 @@ "lru-cache": "6.0.0" } }, - "node_modules/lru-memoizer/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-asynchronous": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.0.1.tgz", @@ -3837,6 +3855,19 @@ "marked": ">=1 <16" } }, + "node_modules/marked-terminal/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3915,15 +3946,19 @@ } }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], "license": "MIT", "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4" + "node": ">=16" } }, "node_modules/mime-db": { @@ -4054,42 +4089,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose-lean-virtuals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/mongoose-lean-virtuals/-/mongoose-lean-virtuals-1.1.1.tgz", - "integrity": "sha512-8chOqpVE3bcoWT2pIgcJeIZlXaOfQCavZgQZF4qytUtjRBqsNMyzUoR16qdw9XL2kC478N8iA8z0AA+NSS0d1A==", - "license": "Apache 2.0", - "dependencies": { - "mpath": "^0.8.4" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "mongoose": ">=5.11.10" - } - }, - "node_modules/mongoose-lean-virtuals/node_modules/mpath": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz", - "integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mongoose-paginate-v2": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/mongoose-paginate-v2/-/mongoose-paginate-v2-1.9.1.tgz", - "integrity": "sha512-DgURdWkPCeY6cuzkONk0snkFFG/l6ABABDkV3PyW/M2XGvfK/yP4i6Y/PuXkqhfVxKHFLvZ/qBnQxkGzzvo43Q==", - "license": "MIT", - "dependencies": { - "mongoose-lean-virtuals": "^1.1.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/mongoose/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4118,9 +4117,9 @@ } }, "node_modules/mquery/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4258,9 +4257,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", - "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -4282,9 +4281,9 @@ } }, "node_modules/normalize-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", - "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", "dev": true, "license": "MIT", "engines": { @@ -6726,9 +6725,9 @@ } }, "node_modules/passport": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", - "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", "dependencies": { "passport-strategy": "1.x.x", @@ -6871,9 +6870,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", "license": "MIT" }, "node_modules/path-type": { @@ -6994,12 +6993,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -7018,15 +7017,15 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -7118,28 +7117,19 @@ } }, "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -7147,13 +7137,13 @@ "license": "Apache-2.0" }, "node_modules/registry-auth-token": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", - "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", "dev": true, "license": "MIT", "dependencies": { - "@pnpm/npm-conf": "^2.1.0" + "@pnpm/npm-conf": "^3.0.2" }, "engines": { "node": ">=14" @@ -7326,36 +7316,39 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", + "bin": { + "mime": "cli.js" + }, "engines": { - "node": ">= 0.8" + "node": ">=4" } }, "node_modules/send/node_modules/ms": { @@ -7365,15 +7358,15 @@ "license": "MIT" }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.19.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" @@ -7716,9 +7709,9 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -7735,6 +7728,39 @@ "readable-stream": "^2.0.2" } }, + "node_modules/stream-combiner2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-combiner2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-combiner2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -7744,20 +7770,14 @@ } }, "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7916,9 +7936,9 @@ } }, "node_modules/tempy": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", - "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.1.tgz", + "integrity": "sha512-ozXJ+Z2YduKpJuuM07LNcIxpX+r8W4J84HrgqB/ay4skWfa5MhjsVn6e2fw+bRDa8cYO5jRJWnEMWL1HqCc2sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7994,6 +8014,39 @@ "xtend": "~4.0.1" } }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/time-span": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", @@ -8184,9 +8237,9 @@ } }, "node_modules/type-fest": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz", - "integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.1.tgz", + "integrity": "sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ==", "dev": true, "license": "(MIT OR CC0-1.0)", "dependencies": { @@ -8277,9 +8330,9 @@ } }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.0.tgz", + "integrity": "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==", "dev": true, "license": "MIT", "engines": { @@ -8400,6 +8453,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8479,6 +8541,19 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", diff --git a/src/models/permission.model.ts b/src/models/permission.model.ts index 89c4ed0..dc488e4 100644 --- a/src/models/permission.model.ts +++ b/src/models/permission.model.ts @@ -1,15 +1,15 @@ -import mongoose from 'mongoose'; -import mongoosePaginate from 'mongoose-paginate-v2'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; -const PermissionSchema = new mongoose.Schema({ - name: { type: String, required: true, unique: true }, - category: { type: String }, - description: { type: String } -}); +export type PermissionDocument = Permission & Document; -PermissionSchema.plugin(mongoosePaginate); +@Schema({ timestamps: true }) +export class Permission { + @Prop({ required: true, unique: true, trim: true }) + name!: string; -const Permission = mongoose.models.Permission || mongoose.model('Permission', PermissionSchema); + @Prop({ trim: true }) + description?: string; +} -export { PermissionSchema }; -export default Permission; +export const PermissionSchema = SchemaFactory.createForClass(Permission); diff --git a/src/models/role.model.ts b/src/models/role.model.ts index 1ea23ca..d813808 100644 --- a/src/models/role.model.ts +++ b/src/models/role.model.ts @@ -1,18 +1,15 @@ -import mongoose from 'mongoose'; -import mongoosePaginate from 'mongoose-paginate-v2'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document, Types } from 'mongoose'; -const RoleSchema = new mongoose.Schema( - { - name: { type: String, required: true, unique: true }, - description: { type: String }, - permissions: [{ type: String }] - }, - { timestamps: true } -); +export type RoleDocument = Role & Document; -RoleSchema.plugin(mongoosePaginate); +@Schema({ timestamps: true }) +export class Role { + @Prop({ required: true, unique: true, trim: true }) + name!: string; -const Role = mongoose.models.Role || mongoose.model('Role', RoleSchema); + @Prop({ type: [{ type: Types.ObjectId, ref: 'Permission' }], default: [] }) + permissions!: Types.ObjectId[]; +} -export { RoleSchema }; -export default Role; +export const RoleSchema = SchemaFactory.createForClass(Role); diff --git a/src/models/tenant.model.ts b/src/models/tenant.model.ts deleted file mode 100644 index 3247c8b..0000000 --- a/src/models/tenant.model.ts +++ /dev/null @@ -1,12 +0,0 @@ -import mongoose from 'mongoose'; - -const TenantSchema = new mongoose.Schema({ - _id: String, - name: String, - plan: String -}); - -const Tenant = mongoose.models.Tenant || mongoose.model('Tenant', TenantSchema); - -export { TenantSchema }; -export default Tenant; diff --git a/src/models/user.model.ts b/src/models/user.model.ts index 080a2c1..0907705 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -1,43 +1,59 @@ -import mongoose from 'mongoose'; -import mongoosePaginate from 'mongoose-paginate-v2'; - -const UserSchema = new mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true - }, - password: { - type: String, - required: function () { - return !this.microsoftId && !this.googleId && !this.facebookId; - } - }, - name: { type: String }, - jobTitle: { type: String }, - company: { type: String }, - microsoftId: { type: String, index: true }, - googleId: { type: String, index: true }, - facebookId: { type: String, index: true }, - roles: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Role' }], - resetPasswordToken: { type: String }, - resetPasswordExpires: { type: Date }, - status: { - type: String, - enum: ['pending', 'active', 'suspended', 'deactivated'], - default: 'pending' - }, - refreshToken: { type: String }, - failedLoginAttempts: { type: Number, default: 0 }, - lockUntil: { type: Date } - }, - { timestamps: true } -); - -UserSchema.plugin(mongoosePaginate); - -const User = mongoose.models.User || mongoose.model('User', UserSchema); - -export { UserSchema }; -export default User; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document, Types } from 'mongoose'; + +export type UserDocument = User & Document; + +class FullName { + @Prop({ required: true, trim: true }) + fname!: string; + + @Prop({ required: true, trim: true }) + lname!: string; +} + + +@Schema({ timestamps: true }) +export class User { + @Prop({ type: FullName, required: true }) + fullname!: FullName; + + @Prop({ required: true, unique: true, trim: true, minlength: 3, maxlength: 30 }) + username!: string; + + @Prop({ + required: true, + unique: true, + lowercase: true, + trim: true, + match: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, + }) + email!: string; + + @Prop({ default: 'default.jpg' }) + avatar?: string; + + @Prop({ + unique: true, + trim: true, + match: /^[0-9]{10,14}$/, + }) + phoneNumber?: string; + + @Prop({ required: true, minlength: 6, select: false }) + password?: string; + + @Prop({ required: false }) + passwordChangedAt: Date; + + @Prop({ type: [{ type: Types.ObjectId, ref: 'Role' }], required: true }) + roles!: Types.ObjectId[]; + + @Prop({ default: false }) + isVerified!: boolean; + + @Prop({ default: false }) + isBanned!: boolean; + +} + +export const UserSchema = SchemaFactory.createForClass(User); diff --git a/tsconfig.json b/tsconfig.json index 2824b65..b94aacb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,16 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "skipLibCheck": true, - "types": ["node"] + "types": [ + "node" + ] }, - "include": ["src/**/*.ts", "src/**/*.d.ts"], - "exclude": ["node_modules", "dist"] -} + "include": [ + "src/**/*.ts", + "src/**/*.d.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file From cebc953576538690edbd0818aa01aae17606d33e Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 17:46:10 +0100 Subject: [PATCH 06/62] refactor: create DTOs files --- src/dtos/forgot-password.dto.ts | 6 ++++++ src/dtos/fullname.dto.ts | 11 +++++++++++ src/dtos/login.dto.ts | 9 +++++++++ src/dtos/refresh-token.dto.ts | 7 +++++++ src/dtos/register-user.dto.ts | 24 ++++++++++++++++++++++++ src/dtos/resend-verification.dto.ts | 6 ++++++ src/dtos/reset-password.dto.ts | 10 ++++++++++ src/dtos/verify-email.dto.ts | 6 ++++++ 8 files changed, 79 insertions(+) create mode 100644 src/dtos/forgot-password.dto.ts create mode 100644 src/dtos/fullname.dto.ts create mode 100644 src/dtos/login.dto.ts create mode 100644 src/dtos/refresh-token.dto.ts create mode 100644 src/dtos/register-user.dto.ts create mode 100644 src/dtos/resend-verification.dto.ts create mode 100644 src/dtos/reset-password.dto.ts create mode 100644 src/dtos/verify-email.dto.ts diff --git a/src/dtos/forgot-password.dto.ts b/src/dtos/forgot-password.dto.ts new file mode 100644 index 0000000..d1cf28e --- /dev/null +++ b/src/dtos/forgot-password.dto.ts @@ -0,0 +1,6 @@ +import { IsEmail } from 'class-validator'; + +export class ForgotPasswordDto { + @IsEmail() + email!: string; +} diff --git a/src/dtos/fullname.dto.ts b/src/dtos/fullname.dto.ts new file mode 100644 index 0000000..9822331 --- /dev/null +++ b/src/dtos/fullname.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString } from "class-validator"; + +export class FullnameDto { + @IsString() + @IsNotEmpty() + fname!: string; + + @IsString() + @IsNotEmpty() + lname!: string; +} \ No newline at end of file diff --git a/src/dtos/login.dto.ts b/src/dtos/login.dto.ts new file mode 100644 index 0000000..6708675 --- /dev/null +++ b/src/dtos/login.dto.ts @@ -0,0 +1,9 @@ +import { IsEmail, IsString } from 'class-validator'; + +export class LoginDto { + @IsEmail() + email!: string; + + @IsString() + password!: string; +} diff --git a/src/dtos/refresh-token.dto.ts b/src/dtos/refresh-token.dto.ts new file mode 100644 index 0000000..afe13d2 --- /dev/null +++ b/src/dtos/refresh-token.dto.ts @@ -0,0 +1,7 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class RefreshTokenDto { + @IsOptional() + @IsString() + refreshToken?: string; +} diff --git a/src/dtos/register-user.dto.ts b/src/dtos/register-user.dto.ts new file mode 100644 index 0000000..73e5db4 --- /dev/null +++ b/src/dtos/register-user.dto.ts @@ -0,0 +1,24 @@ +import { IsEmail, IsNotEmpty, IsOptional, IsString, MinLength, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { FullnameDto } from './fullname.dto'; + +export class RegisterUserDto { + @ValidateNested() + @Type(() => FullnameDto) + fullname!: FullnameDto; + + @IsString() + @IsNotEmpty() + username!: string; + + @IsEmail() + email!: string; + + @IsOptional() + @IsString() + phoneNumber?: string; + + @IsString() + @MinLength(6) + password!: string; +} diff --git a/src/dtos/resend-verification.dto.ts b/src/dtos/resend-verification.dto.ts new file mode 100644 index 0000000..a2b6903 --- /dev/null +++ b/src/dtos/resend-verification.dto.ts @@ -0,0 +1,6 @@ +import { IsEmail } from 'class-validator'; + +export class ResendVerificationDto { + @IsEmail() + email!: string; +} diff --git a/src/dtos/reset-password.dto.ts b/src/dtos/reset-password.dto.ts new file mode 100644 index 0000000..732f45c --- /dev/null +++ b/src/dtos/reset-password.dto.ts @@ -0,0 +1,10 @@ +import { IsString, MinLength } from 'class-validator'; + +export class ResetPasswordDto { + @IsString() + token!: string; + + @IsString() + @MinLength(6) + newPassword!: string; +} diff --git a/src/dtos/verify-email.dto.ts b/src/dtos/verify-email.dto.ts new file mode 100644 index 0000000..4e7525c --- /dev/null +++ b/src/dtos/verify-email.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class VerifyEmailDto { + @IsString() + token!: string; +} From 466ed8f8bed89d16ff675d9772c3b23232933f58 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 18:02:11 +0100 Subject: [PATCH 07/62] refactor: update register dto --- src/dtos/fullname.dto.ts | 11 ----------- src/dtos/register-user.dto.ts | 24 ------------------------ src/dtos/register.dto.ts | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 35 deletions(-) delete mode 100644 src/dtos/fullname.dto.ts delete mode 100644 src/dtos/register-user.dto.ts create mode 100644 src/dtos/register.dto.ts diff --git a/src/dtos/fullname.dto.ts b/src/dtos/fullname.dto.ts deleted file mode 100644 index 9822331..0000000 --- a/src/dtos/fullname.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsNotEmpty, IsString } from "class-validator"; - -export class FullnameDto { - @IsString() - @IsNotEmpty() - fname!: string; - - @IsString() - @IsNotEmpty() - lname!: string; -} \ No newline at end of file diff --git a/src/dtos/register-user.dto.ts b/src/dtos/register-user.dto.ts deleted file mode 100644 index 73e5db4..0000000 --- a/src/dtos/register-user.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IsEmail, IsNotEmpty, IsOptional, IsString, MinLength, ValidateNested } from 'class-validator'; -import { Type } from 'class-transformer'; -import { FullnameDto } from './fullname.dto'; - -export class RegisterUserDto { - @ValidateNested() - @Type(() => FullnameDto) - fullname!: FullnameDto; - - @IsString() - @IsNotEmpty() - username!: string; - - @IsEmail() - email!: string; - - @IsOptional() - @IsString() - phoneNumber?: string; - - @IsString() - @MinLength(6) - password!: string; -} diff --git a/src/dtos/register.dto.ts b/src/dtos/register.dto.ts new file mode 100644 index 0000000..62ff572 --- /dev/null +++ b/src/dtos/register.dto.ts @@ -0,0 +1,32 @@ +import { IsEmail, IsOptional, IsString, MinLength, ValidateNested, IsArray } from 'class-validator'; +import { Type } from 'class-transformer'; + +class FullNameDto { + @IsString() fname!: string; + @IsString() lname!: string; +} + +export class RegisterDto { + @ValidateNested() + @Type(() => FullNameDto) + fullname!: FullNameDto; + + @IsString() + @MinLength(3) + username!: string; + + @IsEmail() + email!: string; + + @IsString() + @MinLength(6) + password!: string; + + @IsOptional() + @IsString() + phoneNumber?: string; + + @IsOptional() + @IsString() + avatar?: string; +} From 8d99d8df3504705cbe44a8f83b3b212ac372eae5 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 18:04:05 +0100 Subject: [PATCH 08/62] refactor: remove business logic from auth controller --- src/controllers/auth.controller.ts | 516 +++-------------------------- 1 file changed, 53 insertions(+), 463 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 6210854..da4d116 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,78 +1,43 @@ -import { Controller, Get, Next, Post, Req, Res } from '@nestjs/common'; -import type { Request, Response, NextFunction } from 'express'; -import passport from '../config/passport.config'; -import jwt from 'jsonwebtoken'; -import type { SignOptions } from 'jsonwebtoken'; -import bcrypt from 'bcryptjs'; -import jwksClient from 'jwks-rsa'; -import axios from 'axios'; -import User from '../models/user.model'; -import Client from '../models/client.model'; -import Role from '../models/role.model'; -import { getMillisecondsFromExpiry } from '../utils/helper'; - -const MSAL_MOBILE_CLIENT_ID = process.env.MSAL_MOBILE_CLIENT_ID; -type JwtExpiry = SignOptions['expiresIn']; - -const resolveJwtExpiry = (value: string | undefined, fallback: JwtExpiry): JwtExpiry => - (value || fallback) as JwtExpiry; - -const msJwks = jwksClient({ - jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys', - cache: true, - rateLimit: true, - jwksRequestsPerMinute: 5, -}); - -function verifyMicrosoftIdToken(idToken: string): Promise { - return new Promise((resolve, reject) => { - const getKey = (header: any, cb: (err: any, key?: string) => void) => { - msJwks - .getSigningKey(header.kid) - .then((k) => cb(null, k.getPublicKey())) - .catch(cb); - }; - - jwt.verify( - idToken, - getKey as any, - { algorithms: ['RS256'], audience: MSAL_MOBILE_CLIENT_ID }, - (err, payload) => (err ? reject(err) : resolve(payload)) - ); - }); -} +import { Body, Controller, Delete, Post, Req, Res } from '@nestjs/common'; +import type { Request, Response } from 'express'; +import { AuthService } from '@services/auth.service'; +import { LoginDto } from '@dtos/login.dto'; +import { RegisterDto } from '@dtos/register.dto'; +import { RefreshTokenDto } from '@dtos/refresh-token.dto'; +import { VerifyEmailDto } from '@dtos/verify-email.dto'; +import { ResendVerificationDto } from '@dtos/resend-verification.dto'; +import { ForgotPasswordDto } from '@dtos/forgot-password.dto'; +import { ResetPasswordDto } from '@dtos/reset-password.dto'; +import { getMillisecondsFromExpiry } from '@utils/helper'; @Controller('api/auth') export class AuthController { - private async issueTokensAndRespond(principal: any, res: Response) { - const roleDocs = await Role.find({ _id: { $in: principal.roles } }) - .select('name permissions -_id') - .lean(); + constructor(private readonly auth: AuthService) { } - const roles = roleDocs.map((r: any) => r.name); - const permissions = Array.from(new Set(roleDocs.flatMap((r: any) => r.permissions))); - - const accessTTL = resolveJwtExpiry(process.env.JWT_ACCESS_TOKEN_EXPIRES_IN, '15m'); - const refreshTTL = resolveJwtExpiry(process.env.JWT_REFRESH_TOKEN_EXPIRES_IN, '7d'); - - const payload = { - id: principal._id, - email: principal.email, - roles, - permissions, - }; + @Post('register') + async register(@Body() dto: RegisterDto, @Res() res: Response) { + const result = await this.auth.register(dto); + return res.status(201).json(result); + } - const accessToken = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: accessTTL }); - const refreshToken = jwt.sign({ id: principal._id }, process.env.JWT_REFRESH_SECRET as string, { expiresIn: refreshTTL }); + @Post('verify-email') + async verifyEmail(@Body() dto: VerifyEmailDto, @Res() res: Response) { + const result = await this.auth.verifyEmail(dto.token); + return res.status(200).json(result); + } - principal.refreshToken = refreshToken; - try { - await principal.save(); - } catch (e) { - console.error('Error saving refreshToken:', e); - } + @Post('resend-verification') + async resendVerification(@Body() dto: ResendVerificationDto, @Res() res: Response) { + const result = await this.auth.resendVerification(dto.email); + return res.status(200).json(result); + } + @Post('login') + async login(@Body() dto: LoginDto, @Res() res: Response) { + const { accessToken, refreshToken } = await this.auth.login(dto); + const refreshTTL = process.env.JWT_REFRESH_TOKEN_EXPIRES_IN || '7d'; const isProd = process.env.NODE_ENV === 'production'; + res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: isProd, @@ -84,52 +49,15 @@ export class AuthController { return res.status(200).json({ accessToken, refreshToken }); } - private async respondWebOrMobile(req: Request, res: Response, principal: any) { - const roleDocs = await Role.find({ _id: { $in: principal.roles } }) - .select('name permissions -_id') - .lean(); - - const roles = roleDocs.map((r: any) => r.name); - const permissions = Array.from(new Set(roleDocs.flatMap((r: any) => r.permissions))); - - const accessTTL = resolveJwtExpiry(process.env.JWT_ACCESS_TOKEN_EXPIRES_IN, '15m'); - const refreshTTL = resolveJwtExpiry(process.env.JWT_REFRESH_TOKEN_EXPIRES_IN, '7d'); - - const payload = { - id: principal._id, - email: principal.email, - roles, - permissions, - }; - - const accessToken = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: accessTTL }); - const refreshToken = jwt.sign({ id: principal._id }, process.env.JWT_REFRESH_SECRET as string, { expiresIn: refreshTTL }); - - principal.refreshToken = refreshToken; - try { - await principal.save(); - } catch (e) { - console.error('Saving refreshToken failed:', e); - } - - let mobileRedirect: string | undefined; - if (req.query.state) { - try { - const decoded = JSON.parse(Buffer.from(req.query.state as string, 'base64url').toString('utf8')); - mobileRedirect = decoded.redirect; - } catch (_) { - // ignore - } - } - - if (mobileRedirect) { - const url = new URL(mobileRedirect); - url.searchParams.set('accessToken', accessToken); - url.searchParams.set('refreshToken', refreshToken); - return res.redirect(302, url.toString()); - } + @Post('refresh-token') + async refresh(@Body() dto: RefreshTokenDto, @Req() req: Request, @Res() res: Response) { + const token = dto.refreshToken || (req as any).cookies?.refreshToken; + if (!token) return res.status(401).json({ message: 'Refresh token missing.' }); + const { accessToken, refreshToken } = await this.auth.refresh(token); + const refreshTTL = process.env.JWT_REFRESH_TOKEN_EXPIRES_IN || '7d'; const isProd = process.env.NODE_ENV === 'production'; + res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: isProd, @@ -141,361 +69,23 @@ export class AuthController { return res.status(200).json({ accessToken, refreshToken }); } - @Post('clients/register') - async registerClient(@Req() req: Request, @Res() res: Response) { - try { - const { email, password, name, roles = [] } = req.body; - if (!email || !password) { - return res.status(400).json({ message: 'Email and password are required.' }); - } - if (await Client.findOne({ email })) { - return res.status(409).json({ message: 'Email already in use.' }); - } - const salt = await bcrypt.genSalt(10); - const hashed = await bcrypt.hash(password, salt); - const client = new Client({ email, password: hashed, name, roles }); - await client.save(); - return res.status(201).json({ - id: client._id, - email: client.email, - name: client.name, - roles: client.roles, - }); - } catch (err) { - console.error('registerClient error:', err); - return res.status(500).json({ message: 'Server error.' }); - } - } - - @Post('clients/login') - async clientLogin(@Req() req: Request, @Res() res: Response) { - try { - const { email, password } = req.body; - if (!email || !password) { - return res.status(400).json({ message: 'Email and password are required.' }); - } - const client = await Client.findOne({ email }) - .select('+password') - .populate('roles', 'name permissions'); - if (!client) { - return res.status(400).json({ message: 'Incorrect email.' }); - } - const match = await bcrypt.compare(password, client.password); - if (!match) { - return res.status(400).json({ message: 'Incorrect password.' }); - } - - return this.issueTokensAndRespond(client, res); - } catch (err) { - console.error('clientLogin error:', err); - return res.status(500).json({ message: 'Server error.' }); - } - } - - @Post('login') - localLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - return passport.authenticate('local', { session: false }, async (err: any, user: any, info: any) => { - if (err) return next(err); - if (!user) return res.status(400).json({ message: info?.message || 'Invalid credentials.' }); - - try { - return this.issueTokensAndRespond(user, res); - } catch (e) { - console.error('localLogin error:', e); - return res.status(500).json({ message: 'Server error.' }); - } - })(req, res, next); - } - - @Get('microsoft') - microsoftLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - const redirect = req.query.redirect as string | undefined; - const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined; - - return passport.authenticate('azure_ad_oauth2', { - session: false, - state, - })(req, res, next); - } - - @Get('microsoft/callback') - microsoftCallback(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - passport.authenticate('azure_ad_oauth2', { session: false }, async (err: any, user: any) => { - if (err) return next(err); - if (!user) return res.status(400).json({ message: 'Microsoft authentication failed.' }); - return this.respondWebOrMobile(req, res, user); - })(req, res, next); - } - - @Post('microsoft/exchange') - async microsoftExchange(@Req() req: Request, @Res() res: Response) { - try { - if (!MSAL_MOBILE_CLIENT_ID) { - console.error('MSAL_MOBILE_CLIENT_ID is not set in environment.'); - return res.status(500).json({ message: 'Server misconfiguration.' }); - } - - const { idToken } = req.body || {}; - if (!idToken) { - return res.status(400).json({ message: 'idToken is required.' }); - } - - let ms: any; - try { - ms = await verifyMicrosoftIdToken(idToken); - } catch (e: any) { - console.error('ID token verify failed:', e.message || e); - return res.status(401).json({ message: 'Invalid Microsoft ID token.' }); - } - - const email = ms.preferred_username || ms.email; - const name = ms.name; - if (!email) { - return res.status(400).json({ message: 'Email claim missing in Microsoft ID token.' }); - } - - const microsoftId = ms.oid || ms.sub; - let user = await User.findOne({ email }); - - if (!user) { - user = new User({ - email, - name, - microsoftId, - roles: [], - status: 'active', - }); - await user.save(); - } else { - let changed = false; - if (!user.microsoftId) { user.microsoftId = microsoftId; changed = true; } - if (changed) await user.save(); - } - - return this.issueTokensAndRespond(user, res); - } catch (e) { - console.error('microsoftExchange error:', e); - return res.status(500).json({ message: 'Server error.' }); - } - } - - @Get('google') - googleUserLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - const redirect = req.query.redirect as string | undefined; - const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined; - return passport.authenticate('google-user', { session: false, scope: ['profile', 'email'], state })(req, res, next); + @Post('forgot-password') + async forgotPassword(@Body() dto: ForgotPasswordDto, @Res() res: Response) { + const result = await this.auth.forgotPassword(dto.email); + return res.status(200).json(result); } - @Get('google/callback') - googleUserCallback(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - passport.authenticate('google-user', { session: false }, async (err: any, user: any) => { - if (err) return next(err); - if (!user) return res.status(400).json({ message: 'Google authentication failed.' }); - return this.respondWebOrMobile(req, res, user); - })(req, res, next); - } - - @Get('client/google') - googleClientLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - const redirect = req.query.redirect as string | undefined; - const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined; - return passport.authenticate('google-client', { session: false, scope: ['profile', 'email'], state })(req, res, next); - } - - @Get('client/google/callback') - googleClientCallback(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - passport.authenticate('google-client', { session: false }, async (err: any, client: any) => { - if (err) return next(err); - if (!client) return res.status(400).json({ message: 'Google authentication failed.' }); - return this.respondWebOrMobile(req, res, client); - })(req, res, next); - } - - @Post('google/exchange') - async googleExchange(@Req() req: Request, @Res() res: Response) { - try { - let { code, idToken, type = 'user' } = req.body || {}; - if (!['user', 'client'].includes(type)) { - return res.status(400).json({ message: 'invalid type; must be "user" or "client"' }); - } - - let email: string | undefined; - let name: string | undefined; - let googleId: string | undefined; - - if (code) { - const tokenResp = await axios.post('https://oauth2.googleapis.com/token', { - code, - client_id: process.env.GOOGLE_CLIENT_ID, - client_secret: process.env.GOOGLE_CLIENT_SECRET, - redirect_uri: 'postmessage', - grant_type: 'authorization_code', - }); - - const { access_token } = tokenResp.data || {}; - if (!access_token) { - return res.status(401).json({ message: 'Failed to exchange code with Google.' }); - } - - const profileResp = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { - headers: { Authorization: `Bearer ${access_token}` }, - }); - email = profileResp.data?.email; - name = profileResp.data?.name || profileResp.data?.given_name || ''; - googleId = profileResp.data?.id; - } else if (idToken) { - const verifyResp = await axios.get('https://oauth2.googleapis.com/tokeninfo', { - params: { id_token: idToken }, - }); - email = verifyResp.data?.email; - name = verifyResp.data?.name || ''; - googleId = verifyResp.data?.sub; - } else { - return res.status(400).json({ message: 'code or idToken is required' }); - } - - if (!email) return res.status(400).json({ message: 'Google profile missing email.' }); - - const Model: any = type === 'user' ? User : Client; - - let principal = await Model.findOne({ $or: [{ email }, { googleId }] }); - if (!principal) { - principal = new Model( - type === 'user' - ? { email, name, googleId, roles: [], status: 'active' } - : { email, name, googleId, roles: [] } - ); - await principal.save(); - } else if (!principal.googleId) { - principal.googleId = googleId; - await principal.save(); - } - - const roleDocs = await Role.find({ _id: { $in: principal.roles } }) - .select('name permissions -_id').lean(); - const roles = roleDocs.map((r: any) => r.name); - const permissions = Array.from(new Set(roleDocs.flatMap((r: any) => r.permissions))); - const accessTTL = resolveJwtExpiry(process.env.JWT_ACCESS_TOKEN_EXPIRES_IN, '15m'); - const refreshTTL = resolveJwtExpiry(process.env.JWT_REFRESH_TOKEN_EXPIRES_IN, '7d'); - - const payload = { id: principal._id, email: principal.email, roles, permissions }; - const accessToken = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: accessTTL }); - const refreshToken = jwt.sign({ id: principal._id }, process.env.JWT_REFRESH_SECRET as string, { expiresIn: refreshTTL }); - - principal.refreshToken = refreshToken; - try { await principal.save(); } catch (e) { console.error('Saving refreshToken failed:', e); } - - const isProd = process.env.NODE_ENV === 'production'; - res.cookie('refreshToken', refreshToken, { - httpOnly: true, - secure: isProd, - sameSite: isProd ? 'none' : 'lax', - path: '/', - maxAge: getMillisecondsFromExpiry(refreshTTL), - }); - - return res.status(200).json({ accessToken, refreshToken }); - } catch (err: any) { - console.error('googleExchange error:', err?.response?.data || err.message || err); - return res.status(500).json({ message: 'Server error during Google exchange.' }); - } + @Post('reset-password') + async resetPassword(@Body() dto: ResetPasswordDto, @Res() res: Response) { + const result = await this.auth.resetPassword(dto.token, dto.newPassword); + return res.status(200).json(result); } - @Get('facebook') - facebookUserLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - const redirect = req.query.redirect as string | undefined; - const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined; - return passport.authenticate('facebook-user', { session: false, scope: ['email'], state })(req, res, next); - } - - @Get('facebook/callback') - facebookUserCallback(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - passport.authenticate('facebook-user', { session: false }, async (err: any, user: any) => { - if (err) return next(err); - if (!user) return res.status(400).json({ message: 'Facebook authentication failed.' }); - return this.respondWebOrMobile(req, res, user); - })(req, res, next); - } - - @Get('client/facebook') - facebookClientLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - const redirect = req.query.redirect as string | undefined; - const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined; - return passport.authenticate('facebook-client', { session: false, scope: ['email'], state })(req, res, next); - } - - @Get('client/facebook/callback') - facebookClientCallback(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - passport.authenticate('facebook-client', { session: false }, async (err: any, client: any) => { - if (err) return next(err); - if (!client) return res.status(400).json({ message: 'Facebook authentication failed.' }); - return this.respondWebOrMobile(req, res, client); - })(req, res, next); - } - - @Get('client/microsoft') - microsoftClientLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - const redirect = req.query.redirect as string | undefined; - const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined; - return passport.authenticate('azure_ad_oauth2_client', { session: false, state })(req, res, next); - } - - @Get('client/microsoft/callback') - microsoftClientCallback(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - passport.authenticate('azure_ad_oauth2_client', { session: false }, async (err: any, client: any) => { - if (err) return next(err); - if (!client) return res.status(400).json({ message: 'Microsoft authentication failed.' }); - return this.respondWebOrMobile(req, res, client); - })(req, res, next); - } - - @Post('refresh-token') - async refreshToken(@Req() req: Request, @Res() res: Response) { - try { - const refreshToken = (req as any).cookies?.refreshToken || req.body.refreshToken; - if (!refreshToken) { - return res.status(401).json({ message: 'Refresh token missing.' }); - } - - let decoded: any; - try { - decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET as string); - } catch (err: any) { - const msg = err.name === 'TokenExpiredError' ? 'Refresh token expired.' : 'Invalid refresh token.'; - return res.status(401).json({ message: msg }); - } - - let principal = await User.findById(decoded.id); - let principalType = 'user'; - if (!principal) { - principal = await Client.findById(decoded.id); - principalType = 'client'; - } - if (!principal) return res.status(401).json({ message: 'Account not found.' }); - - if (principal.refreshToken !== refreshToken) { - return res.status(401).json({ message: 'Refresh token mismatch.' }); - } - - const roleDocs = await Role.find({ _id: { $in: principal.roles } }) - .select('name permissions -_id').lean(); - const roles = roleDocs.map((r: any) => r.name); - const permissions = Array.from(new Set(roleDocs.flatMap((r: any) => r.permissions))); - - const payload = { - id: principal._id, - email: principal.email, - roles, - permissions, - }; - - const accessTokenExpiresIn = resolveJwtExpiry(process.env.JWT_ACCESS_TOKEN_EXPIRES_IN, '15m'); - const accessToken = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: accessTokenExpiresIn }); - - return res.status(200).json({ accessToken, type: principalType }); - } catch (error) { - console.error('[Refresh] Unexpected error:', error); - return res.status(500).json({ message: 'Server error during token refresh.' }); - } + @Delete('account') + async deleteAccount(@Req() req: Request, @Res() res: Response) { + const userId = (req as any).user?.sub; + if (!userId) return res.status(401).json({ message: 'Unauthorized.' }); + const result = await this.auth.deleteAccount(userId); + return res.status(200).json(result); } } From a68656e084dead7cb39702831a8ee8900a9822b2 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 18:04:14 +0100 Subject: [PATCH 09/62] refactor: update user model --- src/models/user.model.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/user.model.ts b/src/models/user.model.ts index 0907705..a259300 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -40,11 +40,11 @@ export class User { phoneNumber?: string; @Prop({ required: true, minlength: 6, select: false }) - password?: string; - - @Prop({ required: false }) - passwordChangedAt: Date; + password!: string; + @Prop({ default: Date.now }) + passwordChangedAt!: Date; + @Prop({ type: [{ type: Types.ObjectId, ref: 'Role' }], required: true }) roles!: Types.ObjectId[]; From 901b9bc16f6c51d4c74c08d781ee027bfc1d8078 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 18:04:32 +0100 Subject: [PATCH 10/62] refactor: create an auth business logic service file --- src/services/auth.service.ts | 169 +++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index e69de29..5ad3320 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -0,0 +1,169 @@ +import { Injectable } from '@nestjs/common'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +import { UserRepository } from '@repos/user.repository'; +import { RegisterDto } from '@dtos/register.dto'; +import { LoginDto } from '@dtos/login.dto'; +import { MailService } from '@services/mail.service'; + +type JwtExpiry = string | number; + +@Injectable() +export class AuthService { + constructor( + private readonly users: UserRepository, + private readonly mail: MailService + ) { } + + private resolveExpiry(value: string | undefined, fallback: JwtExpiry): JwtExpiry { + return (value || fallback) as JwtExpiry; + } + + private signAccessToken(payload: any) { + const expiresIn = this.resolveExpiry(process.env.JWT_ACCESS_TOKEN_EXPIRES_IN, '15m'); + return jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn }); + } + + private signRefreshToken(payload: any) { + const expiresIn = this.resolveExpiry(process.env.JWT_REFRESH_TOKEN_EXPIRES_IN, '7d'); + return jwt.sign(payload, process.env.JWT_REFRESH_SECRET as string, { expiresIn }); + } + + private signEmailToken(payload: any) { + const expiresIn = this.resolveExpiry(process.env.JWT_EMAIL_TOKEN_EXPIRES_IN, '1d'); + return jwt.sign(payload, process.env.JWT_EMAIL_SECRET as string, { expiresIn }); + } + + private signResetToken(payload: any) { + const expiresIn = this.resolveExpiry(process.env.JWT_RESET_TOKEN_EXPIRES_IN, '1h'); + return jwt.sign(payload, process.env.JWT_RESET_SECRET as string, { expiresIn }); + } + + private async buildTokenPayload(userId: string) { + const user = await this.users.findByIdWithRolesAndPermissions(userId); + if (!user) throw new Error('User not found.'); + + const roles = (user.roles || []).map((r: any) => r._id.toString()); + const permissions = (user.roles || []) + .flatMap((r: any) => (r.permissions || []).map((p: any) => p.name)) + .filter(Boolean); + + return { sub: user._id.toString(), roles, permissions }; + } + + async register(dto: RegisterDto) { + if (await this.users.findByEmail(dto.email)) throw new Error('Email already in use.'); + if (await this.users.findByUsername(dto.username)) throw new Error('Username already in use.'); + if (dto.phoneNumber && (await this.users.findByPhone(dto.phoneNumber))) { + throw new Error('Phone already in use.'); + } + + const salt = await bcrypt.genSalt(10); + const hashed = await bcrypt.hash(dto.password, salt); + + const user = await this.users.create({ + fullname: dto.fullname, + username: dto.username, + email: dto.email, + phoneNumber: dto.phoneNumber, + avatar: dto.avatar, + password: hashed, + roles: [], // assign default role in admin setup + isVerified: false, + isBanned: false, + passwordChangedAt: new Date() + }); + + const emailToken = this.signEmailToken({ sub: user._id.toString(), purpose: 'verify' }); + await this.mail.sendVerificationEmail(user.email, emailToken); + + return { id: user._id, email: user.email }; + } + + async verifyEmail(token: string) { + const decoded: any = jwt.verify(token, process.env.JWT_EMAIL_SECRET as string); + if (decoded.purpose !== 'verify') throw new Error('Invalid token purpose.'); + + const user = await this.users.findById(decoded.sub); + if (!user) throw new Error('User not found.'); + if (user.isVerified) return { ok: true }; + + user.isVerified = true; + await user.save(); + return { ok: true }; + } + + async resendVerification(email: string) { + const user = await this.users.findByEmail(email); + if (!user || user.isVerified) return { ok: true }; + + const emailToken = this.signEmailToken({ sub: user._id.toString(), purpose: 'verify' }); + await this.mail.sendVerificationEmail(user.email, emailToken); + return { ok: true }; + } + + async login(dto: LoginDto) { + const user = await this.users.findByEmail(dto.email); + if (!user) throw new Error('Invalid credentials.'); + if (user.isBanned) throw new Error('Account banned.'); + if (!user.isVerified) throw new Error('Email not verified.'); + + const ok = await bcrypt.compare(dto.password, user.password as string); + if (!ok) throw new Error('Invalid credentials.'); + + const payload = await this.buildTokenPayload(user._id.toString()); + const accessToken = this.signAccessToken(payload); + const refreshToken = this.signRefreshToken({ sub: user._id.toString(), purpose: 'refresh' }); + + return { accessToken, refreshToken }; + } + + async refresh(refreshToken: string) { + const decoded: any = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET as string); + if (decoded.purpose !== 'refresh') throw new Error('Invalid token purpose.'); + + const user = await this.users.findById(decoded.sub); + if (!user) throw new Error('User not found.'); + if (user.isBanned) throw new Error('Account banned.'); + if (!user.isVerified) throw new Error('Email not verified.'); + + if (user.passwordChangedAt && decoded.iat * 1000 < user.passwordChangedAt.getTime()) { + throw new Error('Token expired.'); + } + + const payload = await this.buildTokenPayload(user._id.toString()); + const accessToken = this.signAccessToken(payload); + const newRefreshToken = this.signRefreshToken({ sub: user._id.toString(), purpose: 'refresh' }); + + return { accessToken, refreshToken: newRefreshToken }; + } + + async forgotPassword(email: string) { + const user = await this.users.findByEmail(email); + if (!user) return { ok: true }; + + const resetToken = this.signResetToken({ sub: user._id.toString(), purpose: 'reset' }); + await this.mail.sendPasswordResetEmail(user.email, resetToken); + return { ok: true }; + } + + async resetPassword(token: string, newPassword: string) { + const decoded: any = jwt.verify(token, process.env.JWT_RESET_SECRET as string); + if (decoded.purpose !== 'reset') throw new Error('Invalid token purpose.'); + + const user = await this.users.findById(decoded.sub); + if (!user) throw new Error('User not found.'); + + const salt = await bcrypt.genSalt(10); + user.password = await bcrypt.hash(newPassword, salt); + user.passwordChangedAt = new Date(); + await user.save(); + + return { ok: true }; + } + + async deleteAccount(userId: string) { + await this.users.deleteById(userId); + return { ok: true }; + } +} From 99356dd59c3d36823f506f8aa0d2d5fc12b958ec Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 18:05:03 +0100 Subject: [PATCH 11/62] refactor: create user repository file, for db interaction; --- src/repositories/user.repository.ts | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index e69de29..203880a 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import type { Model, Types } from 'mongoose'; +import { User, UserDocument } from '@models/user.model'; + +@Injectable() +export class UserRepository { + constructor(@InjectModel(User.name) private readonly userModel: Model) { } + + create(data: Partial) { + return this.userModel.create(data); + } + + findById(id: string | Types.ObjectId) { + return this.userModel.findById(id); + } + + findByEmail(email: string) { + return this.userModel.findOne({ email }); + } + + findByUsername(username: string) { + return this.userModel.findOne({ username }); + } + + findByPhone(phoneNumber: string) { + return this.userModel.findOne({ phoneNumber }); + } + + updateById(id: string | Types.ObjectId, data: Partial) { + return this.userModel.findByIdAndUpdate(id, data, { new: true }); + } + + deleteById(id: string | Types.ObjectId) { + return this.userModel.findByIdAndDelete(id); + } + + findByIdWithRolesAndPermissions(id: string | Types.ObjectId) { + return this.userModel.findById(id).populate({ + path: 'roles', + populate: { path: 'permissions', select: 'name' }, + select: 'name permissions' + }); + } +} From fe24559bf6a0709635d51166bf7d02bf76f26314 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 18:05:14 +0100 Subject: [PATCH 12/62] refactor: add alias paths to tsconfig --- tsconfig.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index b94aacb..7495a09 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,13 +6,40 @@ "outDir": "dist", "rootDir": "src", "strict": false, + "baseUrl": ".", "esModuleInterop": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "skipLibCheck": true, "types": [ "node" - ] + ], + "paths": { + "@models/*": [ + "src/models/*" + ], + "@dtos/*": [ + "src/dtos/*" + ], + "@repos/*": [ + "src/repositories/*" + ], + "@services/*": [ + "src/services/*" + ], + "@controllers/*": [ + "src/controllers/*" + ], + "@config/*": [ + "src/config/*" + ], + "@middleware/*": [ + "src/middleware/*" + ], + "@utils/*": [ + "src/utils/*" + ] + } }, "include": [ "src/**/*.ts", From 6dfb8848917445f9368a749be0e32a65d6680e55 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 18:07:29 +0100 Subject: [PATCH 13/62] refactor: create mail service --- src/services/mail.service.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/services/mail.service.ts b/src/services/mail.service.ts index e69de29..b771a90 100644 --- a/src/services/mail.service.ts +++ b/src/services/mail.service.ts @@ -0,0 +1,33 @@ +import nodemailer from 'nodemailer'; + +export class MailService { + private transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: parseInt(process.env.SMTP_PORT as string, 10), + secure: process.env.SMTP_SECURE === 'true', + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS + } + }); + + async sendVerificationEmail(email: string, token: string) { + const url = `${process.env.FRONTEND_URL}/confirm-email?token=${token}`; + await this.transporter.sendMail({ + from: process.env.FROM_EMAIL, + to: email, + subject: 'Verify your email', + text: `Click to verify your email: ${url}` + }); + } + + async sendPasswordResetEmail(email: string, token: string) { + const url = `${process.env.FRONTEND_URL}/reset-password?token=${token}`; + await this.transporter.sendMail({ + from: process.env.FROM_EMAIL, + to: email, + subject: 'Reset your password', + text: `Reset your password: ${url}` + }); + } +} From 80134e1329f6e17b426e229c382c5eec33debf99 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 21 Jan 2026 18:10:32 +0100 Subject: [PATCH 14/62] refactor: update authservice and remove all client references from the password reset files --- src/config/passport.config.ts | 15 +++++++-------- src/controllers/password-reset.controller.ts | 5 ++--- src/services/auth.service.ts | 5 +++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/config/passport.config.ts b/src/config/passport.config.ts index 2aa4b4a..7863381 100644 --- a/src/config/passport.config.ts +++ b/src/config/passport.config.ts @@ -6,7 +6,6 @@ import { Strategy as FacebookStrategy } from 'passport-facebook'; import bcrypt from 'bcryptjs'; import { decode as jwtDecode } from 'jsonwebtoken'; import User from '../models/user.model'; -import Client from '../models/client.model'; import 'dotenv/config'; const MAX_FAILED = parseInt(process.env.MAX_FAILED_LOGIN_ATTEMPTS || '', 10) || 3; @@ -90,9 +89,9 @@ passport.use( const email = decoded.preferred_username; const name = decoded.name; - let client = await Client.findOne({ $or: [{ microsoftId }, { email }] }); + let client = await User.findOne({ $or: [{ microsoftId }, { email }] }); if (!client) { - client = new Client({ email, name, microsoftId, roles: [] }); + client = new User({ email, name, microsoftId, roles: [] }); await client.save(); } else if (!client.microsoftId) { client.microsoftId = microsoftId; @@ -158,9 +157,9 @@ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET && process. const email = profile.emails && profile.emails[0]?.value; if (!email) return done(null, false); - let client = await Client.findOne({ email }); + let client = await User.findOne({ email }); if (!client) { - client = new Client({ + client = new User({ email, name: profile.displayName, googleId: profile.id, @@ -234,9 +233,9 @@ if (process.env.FB_CLIENT_ID && process.env.FB_CLIENT_SECRET && process.env.FB_C const email = profile.emails && profile.emails[0]?.value; if (!email) return done(null, false); - let client = await Client.findOne({ email }); + let client = await User.findOne({ email }); if (!client) { - client = new Client({ + client = new User({ email, name: profile.displayName, facebookId: profile.id, @@ -260,7 +259,7 @@ passport.serializeUser((principal: any, done: any) => done(null, principal.id)); passport.deserializeUser(async (id: string, done: any) => { try { let principal = await User.findById(id); - if (!principal) principal = await Client.findById(id); + if (!principal) principal = await User.findById(id); done(null, principal); } catch (err) { done(err); diff --git a/src/controllers/password-reset.controller.ts b/src/controllers/password-reset.controller.ts index 97c722d..cd69c9c 100644 --- a/src/controllers/password-reset.controller.ts +++ b/src/controllers/password-reset.controller.ts @@ -4,14 +4,13 @@ import { randomBytes } from 'node:crypto'; import bcrypt from 'bcryptjs'; import nodemailer from 'nodemailer'; import User from '../models/user.model'; -import Client from '../models/client.model'; -const ACCOUNT_TYPES = ['user', 'client'] as const; +const ACCOUNT_TYPES = ['user'] as const; type AccountType = (typeof ACCOUNT_TYPES)[number]; const isAccountType = (value: unknown): value is AccountType => ACCOUNT_TYPES.includes(value as AccountType); -const getModel = (type: AccountType) => (type === 'user' ? User : Client); +const getModel = (type: AccountType) => (type === 'user' ? User : User); @Controller('api/auth') export class PasswordResetController { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 5ad3320..ff256b4 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import type { SignOptions } from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; +import * as jwt from 'jsonwebtoken'; import { UserRepository } from '@repos/user.repository'; import { RegisterDto } from '@dtos/register.dto'; import { LoginDto } from '@dtos/login.dto'; import { MailService } from '@services/mail.service'; -type JwtExpiry = string | number; +type JwtExpiry = SignOptions['expiresIn']; @Injectable() export class AuthService { From b18bf55b08dd058b1f78586b6634aea33d02d06e Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 10:59:09 +0100 Subject: [PATCH 15/62] refactor: Update the authentication guard, and wiring new implementations into Kit module --- src/auth-kit.module.ts | 44 +++++++++++++++++++++------- src/middleware/authenticate.guard.ts | 33 ++++++++++++++++----- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/auth-kit.module.ts b/src/auth-kit.module.ts index 7e095c3..a386e16 100644 --- a/src/auth-kit.module.ts +++ b/src/auth-kit.module.ts @@ -1,29 +1,53 @@ import 'dotenv/config'; import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; -import passport from './config/passport.config'; +import { MongooseModule } from '@nestjs/mongoose'; import cookieParser from 'cookie-parser'; -import { AuthController } from './controllers/auth.controller'; -import { PasswordResetController } from './controllers/password-reset.controller'; -import { UsersController } from './controllers/users.controller'; -import { RolesController } from './controllers/roles.controller'; -import { PermissionsController } from './controllers/permissions.controller'; -import { AdminController } from './controllers/admin.controller'; +import { AuthController } from '@controllers/auth.controller'; +import { UsersController } from '@controllers/users.controller'; +import { RolesController } from '@controllers/roles.controller'; +import { PermissionsController } from '@controllers/permissions.controller'; + +import { User, UserSchema } from '@models/user.model'; +import { Role, RoleSchema } from '@models/role.model'; +import { Permission, PermissionSchema } from '@models/permission.model'; + +import { AuthService } from '@services/auth.service'; +import { MailService } from '@services/mail.service'; +import { UserRepository } from '@repos/user.repository'; + +import { AuthenticateGuard } from '@middleware/authenticate.guard'; @Module({ + imports: [ + MongooseModule.forFeature([ + { name: User.name, schema: UserSchema }, + { name: Role.name, schema: RoleSchema }, + { name: Permission.name, schema: PermissionSchema }, + ]), + ], controllers: [ AuthController, - PasswordResetController, UsersController, RolesController, PermissionsController, - AdminController, + ], + providers: [ + AuthService, + MailService, + UserRepository, + AuthenticateGuard, + ], + exports: [ + AuthenticateGuard, + AuthService, + UserRepository, ], }) export class AuthKitModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer - .apply(cookieParser(), passport.initialize()) + .apply(cookieParser()) .forRoutes({ path: '*', method: RequestMethod.ALL }); } } diff --git a/src/middleware/authenticate.guard.ts b/src/middleware/authenticate.guard.ts index f633440..fb7b637 100644 --- a/src/middleware/authenticate.guard.ts +++ b/src/middleware/authenticate.guard.ts @@ -1,12 +1,16 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import jwt from 'jsonwebtoken'; +import { UserRepository } from '@repos/user.repository'; @Injectable() export class AuthenticateGuard implements CanActivate { - canActivate(context: ExecutionContext): boolean { + constructor(private readonly users: UserRepository) { } + + async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest(); const res = context.switchToHttp().getResponse(); const authHeader = req.headers?.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { res.status(401).json({ message: 'Missing or invalid Authorization header.' }); return false; @@ -14,14 +18,29 @@ export class AuthenticateGuard implements CanActivate { const token = authHeader.split(' ')[1]; try { - const decoded = jwt.verify(token, process.env.JWT_SECRET as string); - req.user = decoded; - return true; - } catch (err: any) { - if (err?.name === 'TokenExpiredError') { - res.status(401).json({ message: 'Access token expired.' }); + const decoded: any = jwt.verify(token, process.env.JWT_SECRET as string); + const user = await this.users.findById(decoded.sub); + + if (!user) { + res.status(401).json({ message: 'User not found.' }); + return false; + } + if (!user.isVerified) { + res.status(403).json({ message: 'Email not verified.' }); return false; } + if (user.isBanned) { + res.status(403).json({ message: 'Account banned.' }); + return false; + } + if (user.passwordChangedAt && decoded.iat * 1000 < user.passwordChangedAt.getTime()) { + res.status(401).json({ message: 'Token expired.' }); + return false; + } + + req.user = decoded; + return true; + } catch { res.status(401).json({ message: 'Invalid access token.' }); return false; } From afd7672f8d5a4ae2146aaa25a4da2fe90de8a02b Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 12:06:54 +0100 Subject: [PATCH 16/62] refactor: create admin user-management controller --- src/controllers/users.controller.ts | 232 ++++------------------------ 1 file changed, 26 insertions(+), 206 deletions(-) diff --git a/src/controllers/users.controller.ts b/src/controllers/users.controller.ts index 53a7b09..03a25da 100644 --- a/src/controllers/users.controller.ts +++ b/src/controllers/users.controller.ts @@ -1,219 +1,39 @@ -import { Controller, Delete, Get, Post, Put, Req, Res } from '@nestjs/common'; -import type { Request, Response } from 'express'; -import User from '../models/user.model'; -import nodemailer from 'nodemailer'; -import bcrypt from 'bcryptjs'; -import { randomBytes } from 'node:crypto'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Res } from '@nestjs/common'; +import type { Response } from 'express'; +import { UsersService } from '@services/users.service'; +import { RegisterDto } from '@dtos/register.dto'; -@Controller('api/users') +@Controller('api/admin/users') export class UsersController { - @Post() - async createUser(@Req() req: Request, @Res() res: Response) { - try { - const { email, password, name, microsoftId, roles, jobTitle, company } = req.body; - - if (!email) { - return res.status(400).json({ message: 'Email is required.' }); - } - if (!microsoftId && !password) { - return res.status(400).json({ message: 'Password is required for local login.' }); - } - - const existingUser = await User.findOne({ email }); - if (existingUser) { - return res.status(400).json({ message: 'User with this email already exists.' }); - } - - const salt = await bcrypt.genSalt(10); - const hashedPassword = password ? await bcrypt.hash(password, salt) : undefined; - - const status = 'pending'; - const tokenExpiryHours = parseFloat(process.env.EMAIL_TOKEN_EXPIRATION_HOURS || '0') || 24; - const tokenExpiration = Date.now() + tokenExpiryHours * 60 * 60 * 1000; - const confirmationToken = randomBytes(20).toString('hex'); - - const newUser = new User({ - email, - password: hashedPassword, - name, - microsoftId, - roles, - jobTitle, - company, - status, - resetPasswordToken: confirmationToken, - resetPasswordExpires: tokenExpiration - }); - - await newUser.save(); - - const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: parseInt(process.env.SMTP_PORT as string, 10), - secure: process.env.SMTP_SECURE === 'true', - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS - } - }); - - const confirmationUrl = `${process.env.FRONTEND_URL}/confirm-email?token=${confirmationToken}&email=${encodeURIComponent(email)}`; - - const mailOptions = { - from: process.env.FROM_EMAIL, - to: email, - subject: 'Confirm Your Email Address', - text: `Hello, + constructor(private readonly users: UsersService) { } -Thank you for registering. Please confirm your account by clicking the link below: - -${confirmationUrl} - -This link will expire in ${tokenExpiryHours} hour(s). - -If you did not initiate this registration, please ignore this email. - -Thank you.` - }; - - await transporter.sendMail(mailOptions); - - return res.status(201).json({ message: 'User created and confirmation email sent successfully.', user: newUser }); - } catch (error: any) { - console.error('Error creating user:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + @Post() + async create(@Body() dto: RegisterDto, @Res() res: Response) { + const result = await this.users.create(dto); + return res.status(201).json(result); } - @Put(':id') - async updateUser(@Req() req: Request, @Res() res: Response) { - try { - const userId = req.params.id; - - if (req.body.password) { - const salt = await bcrypt.genSalt(10); - req.body.password = await bcrypt.hash(req.body.password, salt); - } - - const updatedUser = await User.findByIdAndUpdate(userId, req.body, { new: true }); - if (!updatedUser) { - return res.status(404).json({ message: 'User not found.' }); - } - return res.status(200).json(updatedUser); - } catch (error: any) { - console.error('Error updating user:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + @Get() + async list(@Query() query: { email?: string; username?: string }, @Res() res: Response) { + const result = await this.users.list(query); + return res.status(200).json(result); } - @Delete(':id') - async deleteUser(@Req() req: Request, @Res() res: Response) { - try { - const userId = req.params.id; - const deletedUser = await User.findByIdAndDelete(userId); - if (!deletedUser) { - return res.status(404).json({ message: 'User not found.' }); - } - return res.status(200).json({ message: 'User deleted successfully.' }); - } catch (error: any) { - console.error('Error deleting user:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + @Patch(':id/ban') + async ban(@Param('id') id: string, @Res() res: Response) { + const result = await this.users.setBan(id, true); + return res.status(200).json(result); } - @Post('invite') - async createUserInvitation(@Req() req: Request, @Res() res: Response) { - try { - const { email, name } = req.body; - - if (!email) { - return res.status(400).json({ message: 'Email is required.' }); - } - - const existingUser = await User.findOne({ email }); - if (existingUser) { - return res.status(400).json({ message: 'User with this email already exists.' }); - } - - const token = randomBytes(20).toString('hex'); - const tokenExpiration = Date.now() + 24 * 60 * 60 * 1000; - - const newUser = new User({ - email, - name, - resetPasswordToken: token, - resetPasswordExpires: tokenExpiration - }); - await newUser.save(); - - const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: parseInt(process.env.SMTP_PORT as string, 10), - secure: process.env.SMTP_SECURE === 'true', - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS - } - }); - - const invitationUrl = `${process.env.FRONTEND_URL}/set-password?token=${token}&email=${encodeURIComponent(email)}`; - - const mailOptions = { - from: process.env.FROM_EMAIL, - to: email, - subject: "You're invited: Set up your password", - text: `Hello, - -You have been invited to join our platform. Please click on the link below to set your password and complete your registration: - -${invitationUrl} - -This link will expire in 24 hours. If you did not request this or believe it to be in error, please ignore this email. - -Thank you!` - }; - - await transporter.sendMail(mailOptions); - return res.status(201).json({ message: 'Invitation sent successfully. Please check your email.' }); - } catch (error: any) { - console.error('Error in createUserInvitation:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + @Patch(':id/unban') + async unban(@Param('id') id: string, @Res() res: Response) { + const result = await this.users.setBan(id, false); + return res.status(200).json(result); } - @Get() - async getAllUsers(@Req() req: Request, @Res() res: Response) { - try { - const filter: any = {}; - if (req.query.email) filter.email = req.query.email; - - const page = Math.max(parseInt(req.query.page as string, 10) || 1, 1); - const limit = Math.min(parseInt(req.query.limit as string, 10) || 20, 100); - const skip = (page - 1) * limit; - - const [totalItems, users] = await Promise.all([ - User.countDocuments(filter), - User.find(filter) - .populate({ path: 'roles', select: '-__v' }) - .skip(skip) - .limit(limit) - .lean() - ]); - - return res.status(200).json({ - data: users, - pagination: { - totalItems, - limit, - totalPages: Math.ceil(totalItems / limit) || 1, - currentPage: page, - hasNextPage: page * limit < totalItems, - hasPrevPage: page > 1 - } - }); - } catch (error: any) { - console.error('Error fetching users:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + @Delete(':id') + async delete(@Param('id') id: string, @Res() res: Response) { + const result = await this.users.delete(id); + return res.status(200).json(result); } } From b71395bf081640b69fe0ce6b01f6e7f2aa22e50b Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 12:07:24 +0100 Subject: [PATCH 17/62] refactor: delete duplicated auth middleware --- src/middleware/auth.guard.ts | 24 ------------------------ src/middleware/permission.guard.ts | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 src/middleware/auth.guard.ts diff --git a/src/middleware/auth.guard.ts b/src/middleware/auth.guard.ts deleted file mode 100644 index 4f20cee..0000000 --- a/src/middleware/auth.guard.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; -import jwt from 'jsonwebtoken'; - -@Injectable() -export class AuthGuard implements CanActivate { - canActivate(context: ExecutionContext): boolean { - const req = context.switchToHttp().getRequest(); - const res = context.switchToHttp().getResponse(); - const token = req.headers?.authorization?.split(' ')[1]; - if (!token) { - res.status(401).json({ error: 'Unauthorized' }); - return false; - } - - try { - const decoded = jwt.verify(token, process.env.JWT_SECRET as string); - req.user = decoded; - return true; - } catch (error) { - res.status(403).json({ error: 'Invalid token' }); - return false; - } - } -} diff --git a/src/middleware/permission.guard.ts b/src/middleware/permission.guard.ts index a62fe66..9b0bc42 100644 --- a/src/middleware/permission.guard.ts +++ b/src/middleware/permission.guard.ts @@ -1,5 +1,5 @@ import { CanActivate, ExecutionContext, Injectable, mixin } from '@nestjs/common'; -import Role from '../models/role.model'; +import { Role } from '@models/role.model'; export const hasPermission = (requiredPermission: string) => { @Injectable() From cf3fc2f07093489315b935d57854cac6d7d65030 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 12:07:38 +0100 Subject: [PATCH 18/62] refactor: create user-management repository --- src/repositories/user.repository.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index 203880a..b6920cf 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -42,4 +42,16 @@ export class UserRepository { select: 'name permissions' }); } + + list(filter: { email?: string; username?: string }) { + const query: any = {}; + if (filter.email) query.email = filter.email; + if (filter.username) query.username = filter.username; + + return this.userModel + .find(query) + .populate({ path: 'roles', select: 'name' }) + .lean(); + } + } From 3b3f6841f46ec2abf8c9b69fd2eb4710587aae12 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 12:07:52 +0100 Subject: [PATCH 19/62] refactor: create user-management admin servie --- src/services/users.service.ts | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/services/users.service.ts b/src/services/users.service.ts index e69de29..60736d7 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@nestjs/common'; +import bcrypt from 'bcryptjs'; +import { UserRepository } from '@repos/user.repository'; +import { RegisterDto } from '@dtos/register.dto'; + +@Injectable() +export class UsersService { + constructor(private readonly users: UserRepository) { } + + async create(dto: RegisterDto) { + if (await this.users.findByEmail(dto.email)) throw new Error('Email already in use.'); + if (await this.users.findByUsername(dto.username)) throw new Error('Username already in use.'); + if (dto.phoneNumber && (await this.users.findByPhone(dto.phoneNumber))) { + throw new Error('Phone already in use.'); + } + + const salt = await bcrypt.genSalt(10); + const hashed = await bcrypt.hash(dto.password, salt); + + const user = await this.users.create({ + fullname: dto.fullname, + username: dto.username, + email: dto.email, + phoneNumber: dto.phoneNumber, + avatar: dto.avatar, + password: hashed, + roles: [], // default role can be assigned here later + isVerified: true, + isBanned: false, + passwordChangedAt: new Date() + }); + + return { id: user._id, email: user.email }; + } + + async list(filter: { email?: string; username?: string }) { + return this.users.list(filter); + } + + async setBan(id: string, banned: boolean) { + const user = await this.users.updateById(id, { isBanned: banned }); + if (!user) throw new Error('User not found.'); + return { id: user._id, isBanned: user.isBanned }; + } + + async delete(id: string) { + const user = await this.users.deleteById(id); + if (!user) throw new Error('User not found.'); + return { ok: true }; + } + + + +} From b82473490e557cb40a8348670a0ad18550a1ba30 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 12:08:08 +0100 Subject: [PATCH 20/62] refactor: create role-update dto ; --- src/dtos/update-user-role.dto.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/dtos/update-user-role.dto.ts diff --git a/src/dtos/update-user-role.dto.ts b/src/dtos/update-user-role.dto.ts new file mode 100644 index 0000000..b271e3f --- /dev/null +++ b/src/dtos/update-user-role.dto.ts @@ -0,0 +1,7 @@ +import { IsArray, IsString } from 'class-validator'; + +export class UpdateUserRolesDto { + @IsArray() + @IsString({ each: true }) + roles!: string[]; +} From 4c6289e109562752641b5da6fac839a998d899e2 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 12:16:25 +0100 Subject: [PATCH 21/62] refactor: separating DTOs folder, create roles&permissions DTOs --- src/controllers/auth.controller.ts | 14 +++++++------- src/controllers/users.controller.ts | 2 +- src/dtos/{ => auth}/forgot-password.dto.ts | 0 src/dtos/{ => auth}/login.dto.ts | 0 src/dtos/{ => auth}/refresh-token.dto.ts | 0 src/dtos/{ => auth}/register.dto.ts | 0 src/dtos/{ => auth}/resend-verification.dto.ts | 0 src/dtos/{ => auth}/reset-password.dto.ts | 0 src/dtos/{ => auth}/update-user-role.dto.ts | 0 src/dtos/{ => auth}/verify-email.dto.ts | 0 src/dtos/permission/create-permission.dto.ts | 10 ++++++++++ src/dtos/permission/update-permission.dto.ts | 11 +++++++++++ src/dtos/role/create-role.dto.ts | 11 +++++++++++ src/dtos/role/update-role.dto.ts | 12 ++++++++++++ src/services/auth.service.ts | 4 ++-- src/services/users.service.ts | 4 ++-- 16 files changed, 56 insertions(+), 12 deletions(-) rename src/dtos/{ => auth}/forgot-password.dto.ts (100%) rename src/dtos/{ => auth}/login.dto.ts (100%) rename src/dtos/{ => auth}/refresh-token.dto.ts (100%) rename src/dtos/{ => auth}/register.dto.ts (100%) rename src/dtos/{ => auth}/resend-verification.dto.ts (100%) rename src/dtos/{ => auth}/reset-password.dto.ts (100%) rename src/dtos/{ => auth}/update-user-role.dto.ts (100%) rename src/dtos/{ => auth}/verify-email.dto.ts (100%) create mode 100644 src/dtos/permission/create-permission.dto.ts create mode 100644 src/dtos/permission/update-permission.dto.ts create mode 100644 src/dtos/role/create-role.dto.ts create mode 100644 src/dtos/role/update-role.dto.ts diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index da4d116..240cbef 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,13 +1,13 @@ import { Body, Controller, Delete, Post, Req, Res } from '@nestjs/common'; import type { Request, Response } from 'express'; import { AuthService } from '@services/auth.service'; -import { LoginDto } from '@dtos/login.dto'; -import { RegisterDto } from '@dtos/register.dto'; -import { RefreshTokenDto } from '@dtos/refresh-token.dto'; -import { VerifyEmailDto } from '@dtos/verify-email.dto'; -import { ResendVerificationDto } from '@dtos/resend-verification.dto'; -import { ForgotPasswordDto } from '@dtos/forgot-password.dto'; -import { ResetPasswordDto } from '@dtos/reset-password.dto'; +import { LoginDto } from '@dtos/auth/login.dto'; +import { RegisterDto } from '@dtos/auth/register.dto'; +import { RefreshTokenDto } from '@dtos/auth/refresh-token.dto'; +import { VerifyEmailDto } from '@dtos/auth/verify-email.dto'; +import { ResendVerificationDto } from '@dtos/auth/resend-verification.dto'; +import { ForgotPasswordDto } from '@dtos/auth/forgot-password.dto'; +import { ResetPasswordDto } from '@dtos/auth/reset-password.dto'; import { getMillisecondsFromExpiry } from '@utils/helper'; @Controller('api/auth') diff --git a/src/controllers/users.controller.ts b/src/controllers/users.controller.ts index 03a25da..ff433d7 100644 --- a/src/controllers/users.controller.ts +++ b/src/controllers/users.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Res } from '@nestjs/common'; import type { Response } from 'express'; import { UsersService } from '@services/users.service'; -import { RegisterDto } from '@dtos/register.dto'; +import { RegisterDto } from '@dtos/auth/register.dto'; @Controller('api/admin/users') export class UsersController { diff --git a/src/dtos/forgot-password.dto.ts b/src/dtos/auth/forgot-password.dto.ts similarity index 100% rename from src/dtos/forgot-password.dto.ts rename to src/dtos/auth/forgot-password.dto.ts diff --git a/src/dtos/login.dto.ts b/src/dtos/auth/login.dto.ts similarity index 100% rename from src/dtos/login.dto.ts rename to src/dtos/auth/login.dto.ts diff --git a/src/dtos/refresh-token.dto.ts b/src/dtos/auth/refresh-token.dto.ts similarity index 100% rename from src/dtos/refresh-token.dto.ts rename to src/dtos/auth/refresh-token.dto.ts diff --git a/src/dtos/register.dto.ts b/src/dtos/auth/register.dto.ts similarity index 100% rename from src/dtos/register.dto.ts rename to src/dtos/auth/register.dto.ts diff --git a/src/dtos/resend-verification.dto.ts b/src/dtos/auth/resend-verification.dto.ts similarity index 100% rename from src/dtos/resend-verification.dto.ts rename to src/dtos/auth/resend-verification.dto.ts diff --git a/src/dtos/reset-password.dto.ts b/src/dtos/auth/reset-password.dto.ts similarity index 100% rename from src/dtos/reset-password.dto.ts rename to src/dtos/auth/reset-password.dto.ts diff --git a/src/dtos/update-user-role.dto.ts b/src/dtos/auth/update-user-role.dto.ts similarity index 100% rename from src/dtos/update-user-role.dto.ts rename to src/dtos/auth/update-user-role.dto.ts diff --git a/src/dtos/verify-email.dto.ts b/src/dtos/auth/verify-email.dto.ts similarity index 100% rename from src/dtos/verify-email.dto.ts rename to src/dtos/auth/verify-email.dto.ts diff --git a/src/dtos/permission/create-permission.dto.ts b/src/dtos/permission/create-permission.dto.ts new file mode 100644 index 0000000..f54c2b4 --- /dev/null +++ b/src/dtos/permission/create-permission.dto.ts @@ -0,0 +1,10 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class CreatePermissionDto { + @IsString() + name!: string; + + @IsOptional() + @IsString() + description?: string; +} diff --git a/src/dtos/permission/update-permission.dto.ts b/src/dtos/permission/update-permission.dto.ts new file mode 100644 index 0000000..c1420d7 --- /dev/null +++ b/src/dtos/permission/update-permission.dto.ts @@ -0,0 +1,11 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class UpdatePermissionDto { + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsString() + description?: string; +} diff --git a/src/dtos/role/create-role.dto.ts b/src/dtos/role/create-role.dto.ts new file mode 100644 index 0000000..12e35f3 --- /dev/null +++ b/src/dtos/role/create-role.dto.ts @@ -0,0 +1,11 @@ +import { IsArray, IsOptional, IsString } from 'class-validator'; + +export class CreateRoleDto { + @IsString() + name!: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + permissions?: string[]; +} diff --git a/src/dtos/role/update-role.dto.ts b/src/dtos/role/update-role.dto.ts new file mode 100644 index 0000000..2d90627 --- /dev/null +++ b/src/dtos/role/update-role.dto.ts @@ -0,0 +1,12 @@ +import { IsArray, IsOptional, IsString } from 'class-validator'; + +export class UpdateRoleDto { + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + permissions?: string[]; +} diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index ff256b4..dd41651 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -3,8 +3,8 @@ import type { SignOptions } from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import * as jwt from 'jsonwebtoken'; import { UserRepository } from '@repos/user.repository'; -import { RegisterDto } from '@dtos/register.dto'; -import { LoginDto } from '@dtos/login.dto'; +import { RegisterDto } from '@dtos/auth/register.dto'; +import { LoginDto } from '@dtos/auth/login.dto'; import { MailService } from '@services/mail.service'; type JwtExpiry = SignOptions['expiresIn']; diff --git a/src/services/users.service.ts b/src/services/users.service.ts index 60736d7..923befa 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { UserRepository } from '@repos/user.repository'; -import { RegisterDto } from '@dtos/register.dto'; +import { RegisterDto } from '@dtos/auth/register.dto'; @Injectable() export class UsersService { @@ -49,6 +49,6 @@ export class UsersService { return { ok: true }; } - + } From be490a71937a004ef5975f91edd023785bbba431 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 12:21:57 +0100 Subject: [PATCH 22/62] refactor: create roles & permissions HTTP controllers --- src/controllers/permissions.controller.ts | 80 +++++++---------------- src/controllers/roles.controller.ts | 67 ++++++------------- 2 files changed, 43 insertions(+), 104 deletions(-) diff --git a/src/controllers/permissions.controller.ts b/src/controllers/permissions.controller.ts index b473144..f65bd5a 100644 --- a/src/controllers/permissions.controller.ts +++ b/src/controllers/permissions.controller.ts @@ -1,68 +1,34 @@ -import { Controller, Delete, Get, Post, Put, Req, Res } from '@nestjs/common'; -import type { Request, Response } from 'express'; -import Permission from '../models/permission.model'; +import { Body, Controller, Delete, Get, Param, Post, Put, Res } from '@nestjs/common'; +import type { Response } from 'express'; +import { PermissionsService } from '@services/permissions.service'; +import { CreatePermissionDto } from '@dtos/permission/create-permission.dto'; +import { UpdatePermissionDto } from '@dtos/permission/update-permission.dto'; -@Controller('api/auth/permissions') +@Controller('api/admin/permissions') export class PermissionsController { - @Post('add-permission') - async createPermission(@Req() req: Request, @Res() res: Response) { - try { - const { name, description } = req.body; - if (!name) { - return res.status(400).json({ message: 'Permission name is required.' }); - } + constructor(private readonly perms: PermissionsService) { } - const newPermission = new Permission({ name, description }); - await newPermission.save(); - return res.status(201).json(newPermission); - } catch (error: any) { - console.error('Error creating permission:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + @Post() + async create(@Body() dto: CreatePermissionDto, @Res() res: Response) { + const result = await this.perms.create(dto); + return res.status(201).json(result); } - @Get('get-permission') - async getPermissions(@Req() req: Request, @Res() res: Response) { - try { - const { page, limit } = req.query; - const permissions = await (Permission as any).paginate({}, { - page: parseInt(page as string, 10) || 1, - limit: parseInt(limit as string, 10) || 10 - }); - return res.status(200).json(permissions); - } catch (error: any) { - console.error('Error retrieving permissions:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + @Get() + async list(@Res() res: Response) { + const result = await this.perms.list(); + return res.status(200).json(result); } - @Put('update-permission/:id') - async updatePermission(@Req() req: Request, @Res() res: Response) { - try { - const { id } = req.params; - const updatedPermission = await Permission.findByIdAndUpdate(id, req.body, { new: true }); - if (!updatedPermission) { - return res.status(404).json({ message: 'Permission not found.' }); - } - return res.status(200).json(updatedPermission); - } catch (error: any) { - console.error('Error updating permission:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + @Put(':id') + async update(@Param('id') id: string, @Body() dto: UpdatePermissionDto, @Res() res: Response) { + const result = await this.perms.update(id, dto); + return res.status(200).json(result); } - @Delete('delete-permission/:id') - async deletePermission(@Req() req: Request, @Res() res: Response) { - try { - const { id } = req.params; - const deletedPermission = await Permission.findByIdAndDelete(id); - if (!deletedPermission) { - return res.status(404).json({ message: 'Permission not found.' }); - } - return res.status(200).json({ message: 'Permission deleted successfully.' }); - } catch (error: any) { - console.error('Error deleting permission:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + @Delete(':id') + async delete(@Param('id') id: string, @Res() res: Response) { + const result = await this.perms.delete(id); + return res.status(200).json(result); } } diff --git a/src/controllers/roles.controller.ts b/src/controllers/roles.controller.ts index 1a0652f..b3f5cee 100644 --- a/src/controllers/roles.controller.ts +++ b/src/controllers/roles.controller.ts @@ -1,61 +1,34 @@ -import { Controller, Delete, Get, Post, Put, Req, Res } from '@nestjs/common'; -import type { Request, Response } from 'express'; -import Role from '../models/role.model'; +import { Body, Controller, Delete, Get, Param, Post, Put, Res } from '@nestjs/common'; +import type { Response } from 'express'; +import { RolesService } from '@services/roles.service'; +import { CreateRoleDto } from '@dtos/role/create-role.dto'; +import { UpdateRoleDto } from '@dtos/role/update-role.dto'; -@Controller('api/auth/roles') +@Controller('api/admin/roles') export class RolesController { + constructor(private readonly roles: RolesService) { } + @Post() - async createRole(@Req() req: Request, @Res() res: Response) { - try { - const { name, description, permissions } = req.body; - if (!name) { - return res.status(400).json({ message: 'Role name is required.' }); - } - const newRole = new Role({ name, description, permissions }); - await newRole.save(); - return res.status(201).json(newRole); - } catch (error: any) { - console.error('Error creating role:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + async create(@Body() dto: CreateRoleDto, @Res() res: Response) { + const result = await this.roles.create(dto); + return res.status(201).json(result); } @Get() - async getRoles(@Req() req: Request, @Res() res: Response) { - try { - const roles = await (Role as any).paginate({}, { page: 1, limit: 100 }); - return res.status(200).json(roles); - } catch (error: any) { - console.error('Error retrieving roles:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + async list(@Res() res: Response) { + const result = await this.roles.list(); + return res.status(200).json(result); } @Put(':id') - async updateRole(@Req() req: Request, @Res() res: Response) { - try { - const updatedRole = await Role.findByIdAndUpdate(req.params.id, req.body, { new: true }); - if (!updatedRole) { - return res.status(404).json({ message: 'Role not found.' }); - } - return res.status(200).json(updatedRole); - } catch (error: any) { - console.error('Error updating role:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + async update(@Param('id') id: string, @Body() dto: UpdateRoleDto, @Res() res: Response) { + const result = await this.roles.update(id, dto); + return res.status(200).json(result); } @Delete(':id') - async deleteRole(@Req() req: Request, @Res() res: Response) { - try { - const deletedRole = await Role.findByIdAndDelete(req.params.id); - if (!deletedRole) { - return res.status(404).json({ message: 'Role not found.' }); - } - return res.status(200).json({ message: 'Role deleted successfully.' }); - } catch (error: any) { - console.error('Error deleting role:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } + async delete(@Param('id') id: string, @Res() res: Response) { + const result = await this.roles.delete(id); + return res.status(200).json(result); } } From 6fdc267ec2915d254b9c7bd0f078467670ad4219 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 12:22:12 +0100 Subject: [PATCH 23/62] refactor: create roles & permissions Repositories --- src/repositories/permission.repository.ts | 33 +++++++++++++++++++++++ src/repositories/role.repository.ts | 33 +++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/repositories/permission.repository.ts b/src/repositories/permission.repository.ts index e69de29..dc0538b 100644 --- a/src/repositories/permission.repository.ts +++ b/src/repositories/permission.repository.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import type { Model, Types } from 'mongoose'; +import { Permission, PermissionDocument } from '@models/permission.model'; + +@Injectable() +export class PermissionRepository { + constructor(@InjectModel(Permission.name) private readonly permModel: Model) { } + + create(data: Partial) { + return this.permModel.create(data); + } + + findById(id: string | Types.ObjectId) { + return this.permModel.findById(id); + } + + findByName(name: string) { + return this.permModel.findOne({ name }); + } + + list() { + return this.permModel.find().lean(); + } + + updateById(id: string | Types.ObjectId, data: Partial) { + return this.permModel.findByIdAndUpdate(id, data, { new: true }); + } + + deleteById(id: string | Types.ObjectId) { + return this.permModel.findByIdAndDelete(id); + } +} diff --git a/src/repositories/role.repository.ts b/src/repositories/role.repository.ts index e69de29..a022cd0 100644 --- a/src/repositories/role.repository.ts +++ b/src/repositories/role.repository.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import type { Model, Types } from 'mongoose'; +import { Role, RoleDocument } from '@models/role.model'; + +@Injectable() +export class RoleRepository { + constructor(@InjectModel(Role.name) private readonly roleModel: Model) { } + + create(data: Partial) { + return this.roleModel.create(data); + } + + findById(id: string | Types.ObjectId) { + return this.roleModel.findById(id); + } + + findByName(name: string) { + return this.roleModel.findOne({ name }); + } + + list() { + return this.roleModel.find().populate('permissions').lean(); + } + + updateById(id: string | Types.ObjectId, data: Partial) { + return this.roleModel.findByIdAndUpdate(id, data, { new: true }); + } + + deleteById(id: string | Types.ObjectId) { + return this.roleModel.findByIdAndDelete(id); + } +} From 2ab5999f4d497a32501f7501148e75bd6ca8a270 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 13:34:22 +0100 Subject: [PATCH 24/62] refactor: delete unnecessary unused files --- src/controllers/admin.controller.ts | 36 ---------------------- src/middleware/permission.guard.ts | 41 -------------------------- src/repositories/client.repository.ts | 0 src/services/oauth.service.ts | 0 src/services/password-reset.service.ts | 0 src/services/permissions.service.ts | 30 +++++++++++++++++++ src/services/roles.service.ts | 30 +++++++++++++++++++ src/services/token.service.ts | 0 8 files changed, 60 insertions(+), 77 deletions(-) delete mode 100644 src/controllers/admin.controller.ts delete mode 100644 src/middleware/permission.guard.ts delete mode 100644 src/repositories/client.repository.ts delete mode 100644 src/services/oauth.service.ts delete mode 100644 src/services/password-reset.service.ts delete mode 100644 src/services/token.service.ts diff --git a/src/controllers/admin.controller.ts b/src/controllers/admin.controller.ts deleted file mode 100644 index 4049fca..0000000 --- a/src/controllers/admin.controller.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Controller, Put, Req, Res, UseGuards } from '@nestjs/common'; -import type { Request, Response } from 'express'; -import User from '../models/user.model'; -import { AuthenticateGuard } from '../middleware/authenticate.guard'; - -@UseGuards(AuthenticateGuard) -@Controller('api/admin') -export class AdminController { - @Put(':id/suspend') - async suspendUser(@Req() req: Request, @Res() res: Response) { - try { - if (!req.user || !(req.user as any).roles) { - return res.status(403).json({ message: 'Access denied. Superadmin privileges required.' }); - } - - if (!(req.user as any).roles.includes('superadmin')) { - return res.status(403).json({ message: 'Access denied. Superadmin privileges required.' }); - } - - const { id } = req.params; - if (!id) { - return res.status(400).json({ message: 'User ID is required in the URL.' }); - } - - const updatedUser = await User.findByIdAndUpdate(id, { status: 'suspended' }, { new: true }); - if (!updatedUser) { - return res.status(404).json({ message: 'User not found.' }); - } - - return res.status(200).json({ message: 'User suspended successfully.', user: updatedUser }); - } catch (error: any) { - console.error('Error suspending user:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } - } -} diff --git a/src/middleware/permission.guard.ts b/src/middleware/permission.guard.ts deleted file mode 100644 index 9b0bc42..0000000 --- a/src/middleware/permission.guard.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { CanActivate, ExecutionContext, Injectable, mixin } from '@nestjs/common'; -import { Role } from '@models/role.model'; - -export const hasPermission = (requiredPermission: string) => { - @Injectable() - class PermissionGuard implements CanActivate { - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - const res = context.switchToHttp().getResponse(); - try { - const { roleIds, roles, permissions } = req.user || {}; - const tokenPermissions = Array.isArray(permissions) ? permissions : []; - - if (tokenPermissions.includes(requiredPermission)) { - return true; - } - - let resolvedPermissions: string[] = []; - if (Array.isArray(roleIds) && roleIds.length > 0) { - const roleDocs = await Role.find({ _id: { $in: roleIds } }); - resolvedPermissions = roleDocs.flatMap((role: any) => role.permissions); - } else if (Array.isArray(roles) && roles.length > 0) { - const roleDocs = await Role.find({ name: { $in: roles } }); - resolvedPermissions = roleDocs.flatMap((role: any) => role.permissions); - } - - if (resolvedPermissions.includes(requiredPermission)) { - return true; - } - - res.status(403).json({ error: 'Forbidden: Insufficient permissions' }); - return false; - } catch (error) { - res.status(500).json({ error: 'Authorization error' }); - return false; - } - } - } - - return mixin(PermissionGuard); -}; diff --git a/src/repositories/client.repository.ts b/src/repositories/client.repository.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/services/oauth.service.ts b/src/services/oauth.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/services/password-reset.service.ts b/src/services/password-reset.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/services/permissions.service.ts b/src/services/permissions.service.ts index e69de29..41893e3 100644 --- a/src/services/permissions.service.ts +++ b/src/services/permissions.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { PermissionRepository } from '@repos/permission.repository'; +import { CreatePermissionDto } from '@dtos/permission/create-permission.dto'; +import { UpdatePermissionDto } from '@dtos/permission/update-permission.dto'; + +@Injectable() +export class PermissionsService { + constructor(private readonly perms: PermissionRepository) { } + + async create(dto: CreatePermissionDto) { + if (await this.perms.findByName(dto.name)) throw new Error('Permission already exists.'); + return this.perms.create(dto); + } + + async list() { + return this.perms.list(); + } + + async update(id: string, dto: UpdatePermissionDto) { + const perm = await this.perms.updateById(id, dto); + if (!perm) throw new Error('Permission not found.'); + return perm; + } + + async delete(id: string) { + const perm = await this.perms.deleteById(id); + if (!perm) throw new Error('Permission not found.'); + return { ok: true }; + } +} diff --git a/src/services/roles.service.ts b/src/services/roles.service.ts index e69de29..4c9dd2f 100644 --- a/src/services/roles.service.ts +++ b/src/services/roles.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { RoleRepository } from '@repos/role.repository'; +import { CreateRoleDto } from '@dtos/role/create-role.dto'; +import { UpdateRoleDto } from '@dtos/role/update-role.dto'; + +@Injectable() +export class RolesService { + constructor(private readonly roles: RoleRepository) { } + + async create(dto: CreateRoleDto) { + if (await this.roles.findByName(dto.name)) throw new Error('Role already exists.'); + return this.roles.create({ name: dto.name, permissions: dto.permissions || [] }); + } + + async list() { + return this.roles.list(); + } + + async update(id: string, dto: UpdateRoleDto) { + const role = await this.roles.updateById(id, dto); + if (!role) throw new Error('Role not found.'); + return role; + } + + async delete(id: string) { + const role = await this.roles.deleteById(id); + if (!role) throw new Error('Role not found.'); + return { ok: true }; + } +} diff --git a/src/services/token.service.ts b/src/services/token.service.ts deleted file mode 100644 index e69de29..0000000 From 0d243aac684024112c8cea64ba288c9d68f44fb7 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 13:35:11 +0100 Subject: [PATCH 25/62] refactor: create role middleware and admin decorator, alongside default roles and permissions seeder --- src/middleware/admin.decorator.ts | 8 ++++++++ src/middleware/role.guard.ts | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/middleware/admin.decorator.ts create mode 100644 src/middleware/role.guard.ts diff --git a/src/middleware/admin.decorator.ts b/src/middleware/admin.decorator.ts new file mode 100644 index 0000000..ff8ca03 --- /dev/null +++ b/src/middleware/admin.decorator.ts @@ -0,0 +1,8 @@ +import { applyDecorators, UseGuards } from '@nestjs/common'; +import { AuthenticateGuard } from '@middleware/authenticate.guard'; +import { hasRole } from '@middleware/role.guard'; + +export const Admin = () => + applyDecorators( + UseGuards(AuthenticateGuard, hasRole(process.env.ADMIN_ROLE_ID as string)) + ); diff --git a/src/middleware/role.guard.ts b/src/middleware/role.guard.ts new file mode 100644 index 0000000..220978d --- /dev/null +++ b/src/middleware/role.guard.ts @@ -0,0 +1,19 @@ +import { CanActivate, ExecutionContext, Injectable, mixin } from '@nestjs/common'; + +export const hasRole = (requiredRoleId: string) => { + @Injectable() + class RoleGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const req = context.switchToHttp().getRequest(); + const res = context.switchToHttp().getResponse(); + const roles = Array.isArray(req.user?.roles) ? req.user.roles : []; + + if (roles.includes(requiredRoleId)) return true; + + res.status(403).json({ message: 'Forbidden: role required.' }); + return false; + } + } + + return mixin(RoleGuard); +}; From bd13dfa45a6526c8e455e66f33c1f08644fd04bb Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 13:36:10 +0100 Subject: [PATCH 26/62] refactor: create roles & seed services, and update user roles --- src/services/roles.service.ts | 24 ++++++++++++++++++++++-- src/services/seed.service.ts | 35 +++++++++++++++++++++++++++++++++++ src/services/users.service.ts | 17 +++++++++++++++-- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 src/services/seed.service.ts diff --git a/src/services/roles.service.ts b/src/services/roles.service.ts index 4c9dd2f..74eec05 100644 --- a/src/services/roles.service.ts +++ b/src/services/roles.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { RoleRepository } from '@repos/role.repository'; import { CreateRoleDto } from '@dtos/role/create-role.dto'; import { UpdateRoleDto } from '@dtos/role/update-role.dto'; +import { Types } from 'mongoose'; @Injectable() export class RolesService { @@ -9,7 +10,9 @@ export class RolesService { async create(dto: CreateRoleDto) { if (await this.roles.findByName(dto.name)) throw new Error('Role already exists.'); - return this.roles.create({ name: dto.name, permissions: dto.permissions || [] }); + const permIds = (dto.permissions || []).map((p) => new Types.ObjectId(p)); + return this.roles.create({ name: dto.name, permissions: permIds }); + } async list() { @@ -17,14 +20,31 @@ export class RolesService { } async update(id: string, dto: UpdateRoleDto) { - const role = await this.roles.updateById(id, dto); + const data: any = { ...dto }; + + if (dto.permissions) { + data.permissions = dto.permissions.map((p) => new Types.ObjectId(p)); + } + + const role = await this.roles.updateById(id, data); if (!role) throw new Error('Role not found.'); return role; } + async delete(id: string) { const role = await this.roles.deleteById(id); if (!role) throw new Error('Role not found.'); return { ok: true }; } + + async setPermissions(roleId: string, permissionIds: string[]) { + const permIds = permissionIds.map((p) => new Types.ObjectId(p)); + const role = await this.roles.updateById(roleId, { permissions: permIds }); + if (!role) throw new Error('Role not found.'); + return role; + + + } + } diff --git a/src/services/seed.service.ts b/src/services/seed.service.ts new file mode 100644 index 0000000..bf43053 --- /dev/null +++ b/src/services/seed.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { RoleRepository } from '@repos/role.repository'; +import { PermissionRepository } from '@repos/permission.repository'; +import { ObjectId, Types } from 'mongoose'; + +@Injectable() +export class SeedService { + constructor( + private readonly roles: RoleRepository, + private readonly perms: PermissionRepository + ) { } + + async seedDefaults() { + const permNames = ['users:manage', 'roles:manage', 'permissions:manage']; + + const permIds: string[] = []; + for (const name of permNames) { + let p = await this.perms.findByName(name); + if (!p) p = await this.perms.create({ name }); + permIds.push(p._id.toString()); + } + + let admin = await this.roles.findByName('admin'); + const permissions = permIds.map((p) => new Types.ObjectId(p)); + if (!admin) admin = await this.roles.create({ name: 'admin', permissions: permissions }); + + let user = await this.roles.findByName('user'); + if (!user) user = await this.roles.create({ name: 'user', permissions: [] }); + + return { + adminRoleId: admin._id.toString(), + userRoleId: user._id.toString() + }; + } +} diff --git a/src/services/users.service.ts b/src/services/users.service.ts index 923befa..96bb786 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -1,11 +1,16 @@ import { Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { UserRepository } from '@repos/user.repository'; +import { RoleRepository } from '@repos/role.repository'; import { RegisterDto } from '@dtos/auth/register.dto'; +import { Types } from 'mongoose'; @Injectable() export class UsersService { - constructor(private readonly users: UserRepository) { } + constructor( + private readonly users: UserRepository, + private readonly rolesRepo: RoleRepository + ) { } async create(dto: RegisterDto) { if (await this.users.findByEmail(dto.email)) throw new Error('Email already in use.'); @@ -24,7 +29,7 @@ export class UsersService { phoneNumber: dto.phoneNumber, avatar: dto.avatar, password: hashed, - roles: [], // default role can be assigned here later + roles: [], isVerified: true, isBanned: false, passwordChangedAt: new Date() @@ -49,6 +54,14 @@ export class UsersService { return { ok: true }; } + async updateRoles(id: string, roles: string[]) { + const existing = await this.rolesRepo.findByIds(roles); + if (existing.length !== roles.length) throw new Error('One or more roles not found.'); + const roleIds = roles.map((r) => new Types.ObjectId(r)); + const user = await this.users.updateById(id, { roles: roleIds }); + if (!user) throw new Error('User not found.'); + return { id: user._id, roles: user.roles }; + } } From 8d847fc8d7a190747308b1fbfac72fa303b9668e Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 13:37:06 +0100 Subject: [PATCH 27/62] refactor: delete password reset controller, create roles & permissions controllers and update users controller --- src/controllers/password-reset.controller.ts | 130 ------------------- src/controllers/permissions.controller.ts | 2 + src/controllers/roles.controller.ts | 11 +- src/controllers/users.controller.ts | 2 + 4 files changed, 14 insertions(+), 131 deletions(-) delete mode 100644 src/controllers/password-reset.controller.ts diff --git a/src/controllers/password-reset.controller.ts b/src/controllers/password-reset.controller.ts deleted file mode 100644 index cd69c9c..0000000 --- a/src/controllers/password-reset.controller.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Controller, Post, Req, Res } from '@nestjs/common'; -import type { Request, Response } from 'express'; -import { randomBytes } from 'node:crypto'; -import bcrypt from 'bcryptjs'; -import nodemailer from 'nodemailer'; -import User from '../models/user.model'; - -const ACCOUNT_TYPES = ['user'] as const; -type AccountType = (typeof ACCOUNT_TYPES)[number]; - -const isAccountType = (value: unknown): value is AccountType => ACCOUNT_TYPES.includes(value as AccountType); - -const getModel = (type: AccountType) => (type === 'user' ? User : User); - -@Controller('api/auth') -export class PasswordResetController { - private async requestPasswordReset(req: Request, res: Response) { - try { - const { email, type } = req.body; - - if (!email || !type) { - return res.status(400).json({ message: 'Email and type are required.' }); - } - - if (!isAccountType(type)) { - return res.status(400).json({ message: 'Invalid account type.' }); - } - - const Model = getModel(type); - const account = await Model.findOne({ email }); - - if (!account) { - return res.status(200).json({ - message: 'If that email address is in our system, a password reset link has been sent.' - }); - } - - const token = randomBytes(20).toString('hex'); - account.resetPasswordToken = token; - account.resetPasswordExpires = Date.now() + 3600000; - - await account.save(); - - const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: parseInt(process.env.SMTP_PORT as string, 10), - secure: process.env.SMTP_SECURE === 'true', - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS - } - }); - - const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${token}&type=${type}`; - - const mailOptions = { - from: process.env.FROM_EMAIL, - to: account.email, - subject: 'Password Reset', - text: `You are receiving this email because you (or someone else) requested a password reset. -Please click the link below, or paste it into your browser: -${resetUrl} - -If you did not request this, please ignore this email. -This link will expire in 1 hour.` - }; - - await transporter.sendMail(mailOptions); - - return res.status(200).json({ - message: 'If that email address is in our system, a password reset link has been sent.' - }); - } catch (error: any) { - console.error('Error in requestPasswordReset:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } - } - - private async resetPassword(req: Request, res: Response) { - try { - const { token, newPassword, type } = req.body; - - if (!token || !newPassword || !type) { - return res.status(400).json({ message: 'Token, new password, and type are required.' }); - } - - if (!isAccountType(type)) { - return res.status(400).json({ message: 'Invalid account type.' }); - } - - const Model = getModel(type); - - const account = await Model.findOne({ - resetPasswordToken: token, - resetPasswordExpires: { $gt: Date.now() } - }); - - if (!account) { - return res.status(400).json({ message: 'Invalid or expired token.' }); - } - - const salt = await bcrypt.genSalt(10); - account.password = await bcrypt.hash(newPassword, salt); - account.resetPasswordToken = undefined; - account.resetPasswordExpires = undefined; - - await account.save(); - - return res.status(200).json({ message: 'Password has been reset successfully.' }); - } catch (error: any) { - console.error('Error in resetPassword:', error); - return res.status(500).json({ message: 'Server error', error: error.message }); - } - } - - @Post('forgot-password') - forgotPassword(@Req() req: Request, @Res() res: Response) { - return this.requestPasswordReset(req, res); - } - - @Post('request-password-reset') - requestPasswordResetRoute(@Req() req: Request, @Res() res: Response) { - return this.requestPasswordReset(req, res); - } - - @Post('reset-password') - resetPasswordRoute(@Req() req: Request, @Res() res: Response) { - return this.resetPassword(req, res); - } -} diff --git a/src/controllers/permissions.controller.ts b/src/controllers/permissions.controller.ts index f65bd5a..2ee2bab 100644 --- a/src/controllers/permissions.controller.ts +++ b/src/controllers/permissions.controller.ts @@ -3,7 +3,9 @@ import type { Response } from 'express'; import { PermissionsService } from '@services/permissions.service'; import { CreatePermissionDto } from '@dtos/permission/create-permission.dto'; import { UpdatePermissionDto } from '@dtos/permission/update-permission.dto'; +import { Admin } from '@middleware/admin.decorator'; +@Admin() @Controller('api/admin/permissions') export class PermissionsController { constructor(private readonly perms: PermissionsService) { } diff --git a/src/controllers/roles.controller.ts b/src/controllers/roles.controller.ts index b3f5cee..c4f1130 100644 --- a/src/controllers/roles.controller.ts +++ b/src/controllers/roles.controller.ts @@ -2,8 +2,10 @@ import { Body, Controller, Delete, Get, Param, Post, Put, Res } from '@nestjs/co import type { Response } from 'express'; import { RolesService } from '@services/roles.service'; import { CreateRoleDto } from '@dtos/role/create-role.dto'; -import { UpdateRoleDto } from '@dtos/role/update-role.dto'; +import { UpdateRoleDto, UpdateRolePermissionsDto } from '@dtos/role/update-role.dto'; +import { Admin } from '@middleware/admin.decorator'; +@Admin() @Controller('api/admin/roles') export class RolesController { constructor(private readonly roles: RolesService) { } @@ -31,4 +33,11 @@ export class RolesController { const result = await this.roles.delete(id); return res.status(200).json(result); } + + @Put(':id/permissions') + async setPermissions(@Param('id') id: string, @Body() dto: UpdateRolePermissionsDto, @Res() res: Response) { + const result = await this.roles.setPermissions(id, dto.permissions); + return res.status(200).json(result); + } + } diff --git a/src/controllers/users.controller.ts b/src/controllers/users.controller.ts index ff433d7..687cb6f 100644 --- a/src/controllers/users.controller.ts +++ b/src/controllers/users.controller.ts @@ -2,7 +2,9 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Res } from '@ import type { Response } from 'express'; import { UsersService } from '@services/users.service'; import { RegisterDto } from '@dtos/auth/register.dto'; +import { Admin } from '@middleware/admin.decorator'; +@Admin() @Controller('api/admin/users') export class UsersController { constructor(private readonly users: UsersService) { } From 080db8795cecf6f5d62082867c419c16ba018950 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 13:37:21 +0100 Subject: [PATCH 28/62] refactor: update role dto ; --- src/dtos/role/update-role.dto.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/dtos/role/update-role.dto.ts b/src/dtos/role/update-role.dto.ts index 2d90627..4085c14 100644 --- a/src/dtos/role/update-role.dto.ts +++ b/src/dtos/role/update-role.dto.ts @@ -10,3 +10,11 @@ export class UpdateRoleDto { @IsString({ each: true }) permissions?: string[]; } + + +export class UpdateRolePermissionsDto { + @IsArray() + @IsString({ each: true }) + permissions!: string[]; // ObjectId strings +} + From 87e9c7e2cabb46f0bd9551bebe15d966bda9dddb Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 13:37:52 +0100 Subject: [PATCH 29/62] refactor: updated roles repository --- src/repositories/role.repository.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/repositories/role.repository.ts b/src/repositories/role.repository.ts index a022cd0..96563d7 100644 --- a/src/repositories/role.repository.ts +++ b/src/repositories/role.repository.ts @@ -30,4 +30,9 @@ export class RoleRepository { deleteById(id: string | Types.ObjectId) { return this.roleModel.findByIdAndDelete(id); } + + findByIds(ids: string[]) { + return this.roleModel.find({ _id: { $in: ids } }).lean(); + } + } From eecf66bd1a95bbc960ac37ede008f647794f93b2 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 13:38:13 +0100 Subject: [PATCH 30/62] refactor: wiring updates in authkitModule and exporting needed exports for host apps --- src/auth-kit.module.ts | 21 ++++++++++++++++++++- src/config/passport.config.ts | 2 +- src/index.ts | 5 +++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/auth-kit.module.ts b/src/auth-kit.module.ts index a386e16..0dbf283 100644 --- a/src/auth-kit.module.ts +++ b/src/auth-kit.module.ts @@ -13,8 +13,15 @@ import { Role, RoleSchema } from '@models/role.model'; import { Permission, PermissionSchema } from '@models/permission.model'; import { AuthService } from '@services/auth.service'; +import { UsersService } from '@services/users.service'; +import { RolesService } from '@services/roles.service'; +import { PermissionsService } from '@services/permissions.service'; import { MailService } from '@services/mail.service'; +import { SeedService } from '@services/seed.service'; + import { UserRepository } from '@repos/user.repository'; +import { RoleRepository } from '@repos/role.repository'; +import { PermissionRepository } from '@repos/permission.repository'; import { AuthenticateGuard } from '@middleware/authenticate.guard'; @@ -34,14 +41,26 @@ import { AuthenticateGuard } from '@middleware/authenticate.guard'; ], providers: [ AuthService, + UsersService, + RolesService, + PermissionsService, MailService, + SeedService, UserRepository, + RoleRepository, + PermissionRepository, AuthenticateGuard, ], exports: [ - AuthenticateGuard, AuthService, + UsersService, + RolesService, + PermissionsService, + SeedService, + AuthenticateGuard, UserRepository, + RoleRepository, + PermissionRepository, ], }) export class AuthKitModule implements NestModule { diff --git a/src/config/passport.config.ts b/src/config/passport.config.ts index 7863381..25e11c1 100644 --- a/src/config/passport.config.ts +++ b/src/config/passport.config.ts @@ -5,7 +5,7 @@ import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { Strategy as FacebookStrategy } from 'passport-facebook'; import bcrypt from 'bcryptjs'; import { decode as jwtDecode } from 'jsonwebtoken'; -import User from '../models/user.model'; +import { User } from '../models/user.model'; import 'dotenv/config'; const MAX_FAILED = parseInt(process.env.MAX_FAILED_LOGIN_ATTEMPTS || '', 10) || 3; diff --git a/src/index.ts b/src/index.ts index eb447d3..f84fb4e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,6 @@ import 'reflect-metadata'; export { AuthKitModule } from './auth-kit.module'; export { AuthenticateGuard } from './middleware/authenticate.guard'; -export { AuthGuard } from './middleware/auth.guard'; -export { hasPermission } from './middleware/permission.guard'; +export { hasRole } from './middleware/role.guard'; +export { Admin } from './middleware/admin.decorator'; +export { SeedService } from './services/seed.service'; From 1e8f2e6fa2fd9d1a1623e3af37a35f05d792bc1e Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 16:17:57 +0100 Subject: [PATCH 31/62] refactor: delete db config (unneded), and setting up default role assigning methods --- src/config/db.config.ts | 17 ----------------- src/controllers/users.controller.ts | 8 ++++++++ src/services/auth.service.ts | 10 ++++++++-- 3 files changed, 16 insertions(+), 19 deletions(-) delete mode 100644 src/config/db.config.ts diff --git a/src/config/db.config.ts b/src/config/db.config.ts deleted file mode 100644 index 14d95f1..0000000 --- a/src/config/db.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import mongoose from 'mongoose'; - -mongoose.set('strictQuery', false); - -export async function connectDB(): Promise { - try { - const mongoURI = process.env.MONGO_URI_T; - if (!mongoURI) { - throw new Error('MONGO_URI is not defined in the environment variables.'); - } - await mongoose.connect(mongoURI); - console.log('MongoDB Connected...'); - } catch (error) { - console.error('MongoDB Connection Error:', error); - process.exit(1); - } -} diff --git a/src/controllers/users.controller.ts b/src/controllers/users.controller.ts index 687cb6f..b6ab5d4 100644 --- a/src/controllers/users.controller.ts +++ b/src/controllers/users.controller.ts @@ -3,6 +3,7 @@ import type { Response } from 'express'; import { UsersService } from '@services/users.service'; import { RegisterDto } from '@dtos/auth/register.dto'; import { Admin } from '@middleware/admin.decorator'; +import { UpdateUserRolesDto } from '@dtos/auth/update-user-role.dto'; @Admin() @Controller('api/admin/users') @@ -38,4 +39,11 @@ export class UsersController { const result = await this.users.delete(id); return res.status(200).json(result); } + + @Patch(':id/roles') + async updateRoles(@Param('id') id: string, @Body() dto: UpdateUserRolesDto, @Res() res: Response) { + const result = await this.users.updateRoles(id, dto.roles); + return res.status(200).json(result); + } + } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index dd41651..22edd50 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -6,6 +6,7 @@ import { UserRepository } from '@repos/user.repository'; import { RegisterDto } from '@dtos/auth/register.dto'; import { LoginDto } from '@dtos/auth/login.dto'; import { MailService } from '@services/mail.service'; +import { RoleRepository } from '@repos/role.repository'; type JwtExpiry = SignOptions['expiresIn']; @@ -13,7 +14,8 @@ type JwtExpiry = SignOptions['expiresIn']; export class AuthService { constructor( private readonly users: UserRepository, - private readonly mail: MailService + private readonly mail: MailService, + private readonly roles: RoleRepository, ) { } private resolveExpiry(value: string | undefined, fallback: JwtExpiry): JwtExpiry { @@ -62,6 +64,10 @@ export class AuthService { const salt = await bcrypt.genSalt(10); const hashed = await bcrypt.hash(dto.password, salt); + const userRole = await this.roles.findByName('user'); + if (!userRole) throw new Error('Default role not seeded.'); + + const user = await this.users.create({ fullname: dto.fullname, username: dto.username, @@ -69,7 +75,7 @@ export class AuthService { phoneNumber: dto.phoneNumber, avatar: dto.avatar, password: hashed, - roles: [], // assign default role in admin setup + roles: [userRole._id], isVerified: false, isBanned: false, passwordChangedAt: new Date() From 91c58d0c59f4371b03a52d476776ff99c45497be Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 16:29:19 +0100 Subject: [PATCH 32/62] refactor: create admin guard and update the service and decorator --- src/middleware/admin.decorator.ts | 4 ++-- src/middleware/admin.guard.ts | 19 +++++++++++++++++++ src/services/admin-role.service.ts | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/middleware/admin.guard.ts create mode 100644 src/services/admin-role.service.ts diff --git a/src/middleware/admin.decorator.ts b/src/middleware/admin.decorator.ts index ff8ca03..d5ee467 100644 --- a/src/middleware/admin.decorator.ts +++ b/src/middleware/admin.decorator.ts @@ -1,8 +1,8 @@ import { applyDecorators, UseGuards } from '@nestjs/common'; import { AuthenticateGuard } from '@middleware/authenticate.guard'; -import { hasRole } from '@middleware/role.guard'; +import { AdminGuard } from '@middleware/admin.guard'; export const Admin = () => applyDecorators( - UseGuards(AuthenticateGuard, hasRole(process.env.ADMIN_ROLE_ID as string)) + UseGuards(AuthenticateGuard, AdminGuard) ); diff --git a/src/middleware/admin.guard.ts b/src/middleware/admin.guard.ts new file mode 100644 index 0000000..2b5b337 --- /dev/null +++ b/src/middleware/admin.guard.ts @@ -0,0 +1,19 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { AdminRoleService } from '@services/admin-role.service'; + +@Injectable() +export class AdminGuard implements CanActivate { + constructor(private readonly adminRole: AdminRoleService) { } + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + const res = context.switchToHttp().getResponse(); + const roles = Array.isArray(req.user?.roles) ? req.user.roles : []; + + const adminRoleId = await this.adminRole.loadAdminRoleId(); + if (roles.includes(adminRoleId)) return true; + + res.status(403).json({ message: 'Forbidden: admin required.' }); + return false; + } +} diff --git a/src/services/admin-role.service.ts b/src/services/admin-role.service.ts new file mode 100644 index 0000000..7f178ab --- /dev/null +++ b/src/services/admin-role.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { RoleRepository } from '@repos/role.repository'; + +@Injectable() +export class AdminRoleService { + private adminRoleId?: string; + + constructor(private readonly roles: RoleRepository) { } + + async loadAdminRoleId() { + if (this.adminRoleId) return this.adminRoleId; + const admin = await this.roles.findByName('admin'); + if (!admin) throw new Error('Admin role not seeded.'); + this.adminRoleId = admin._id.toString(); + return this.adminRoleId; + } +} From 09643dc3bda2010261d1b60133ce2214590ae93b Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 16:35:39 +0100 Subject: [PATCH 33/62] refactor: wiring and exporting new admin service & guard --- src/auth-kit.module.ts | 4 ++++ src/index.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/auth-kit.module.ts b/src/auth-kit.module.ts index 0dbf283..534f59b 100644 --- a/src/auth-kit.module.ts +++ b/src/auth-kit.module.ts @@ -24,6 +24,8 @@ import { RoleRepository } from '@repos/role.repository'; import { PermissionRepository } from '@repos/permission.repository'; import { AuthenticateGuard } from '@middleware/authenticate.guard'; +import { AdminGuard } from '@middleware/admin.guard'; +import { AdminRoleService } from '@services/admin-role.service'; @Module({ imports: [ @@ -50,6 +52,8 @@ import { AuthenticateGuard } from '@middleware/authenticate.guard'; RoleRepository, PermissionRepository, AuthenticateGuard, + AdminGuard, + AdminRoleService, ], exports: [ AuthService, diff --git a/src/index.ts b/src/index.ts index f84fb4e..051b4aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,5 @@ export { AuthenticateGuard } from './middleware/authenticate.guard'; export { hasRole } from './middleware/role.guard'; export { Admin } from './middleware/admin.decorator'; export { SeedService } from './services/seed.service'; +export { AdminGuard } from './middleware/admin.guard'; +export { AdminRoleService } from './services/admin-role.service'; From 91a465e94a859d2ca3020fe6dfbd427c2c53b1ea Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 17:14:42 +0100 Subject: [PATCH 34/62] refactor: exporting admin providers in auth kit module, updated env variables retrieval in auth service, added a simple log for admin Id after seeds run --- src/auth-kit.module.ts | 2 ++ src/services/auth.service.ts | 28 +++++++++++++++++----------- src/services/seed.service.ts | 5 ++++- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/auth-kit.module.ts b/src/auth-kit.module.ts index 534f59b..18b9ea2 100644 --- a/src/auth-kit.module.ts +++ b/src/auth-kit.module.ts @@ -65,6 +65,8 @@ import { AdminRoleService } from '@services/admin-role.service'; UserRepository, RoleRepository, PermissionRepository, + AdminGuard, + AdminRoleService, ], }) export class AuthKitModule implements NestModule { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 22edd50..8243713 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -23,23 +23,23 @@ export class AuthService { } private signAccessToken(payload: any) { - const expiresIn = this.resolveExpiry(process.env.JWT_ACCESS_TOKEN_EXPIRES_IN, '15m'); - return jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn }); + const expiresIn = this.resolveExpiry(this.getEnv('JWT_ACCESS_TOKEN_EXPIRES_IN'), '15m'); + return jwt.sign(payload, this.getEnv('JWT_SECRET') as string, { expiresIn }); } private signRefreshToken(payload: any) { - const expiresIn = this.resolveExpiry(process.env.JWT_REFRESH_TOKEN_EXPIRES_IN, '7d'); - return jwt.sign(payload, process.env.JWT_REFRESH_SECRET as string, { expiresIn }); + const expiresIn = this.resolveExpiry(this.getEnv('JWT_REFRESH_TOKEN_EXPIRES_IN'), '7d'); + return jwt.sign(payload, this.getEnv('JWT_REFRESH_SECRET') as string, { expiresIn }); } private signEmailToken(payload: any) { - const expiresIn = this.resolveExpiry(process.env.JWT_EMAIL_TOKEN_EXPIRES_IN, '1d'); - return jwt.sign(payload, process.env.JWT_EMAIL_SECRET as string, { expiresIn }); + const expiresIn = this.resolveExpiry(this.getEnv('JWT_EMAIL_TOKEN_EXPIRES_IN'), '1d'); + return jwt.sign(payload, this.getEnv('JWT_EMAIL_SECRET') as string, { expiresIn }); } private signResetToken(payload: any) { - const expiresIn = this.resolveExpiry(process.env.JWT_RESET_TOKEN_EXPIRES_IN, '1h'); - return jwt.sign(payload, process.env.JWT_RESET_SECRET as string, { expiresIn }); + const expiresIn = this.resolveExpiry(this.getEnv('JWT_RESET_TOKEN_EXPIRES_IN'), '1h'); + return jwt.sign(payload, this.getEnv('JWT_RESET_SECRET') as string, { expiresIn }); } private async buildTokenPayload(userId: string) { @@ -54,6 +54,12 @@ export class AuthService { return { sub: user._id.toString(), roles, permissions }; } + private getEnv(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`${name} is not set`); + return v; + } + async register(dto: RegisterDto) { if (await this.users.findByEmail(dto.email)) throw new Error('Email already in use.'); if (await this.users.findByUsername(dto.username)) throw new Error('Username already in use.'); @@ -88,7 +94,7 @@ export class AuthService { } async verifyEmail(token: string) { - const decoded: any = jwt.verify(token, process.env.JWT_EMAIL_SECRET as string); + const decoded: any = jwt.verify(token, this.getEnv('JWT_EMAIL_SECRET') as string); if (decoded.purpose !== 'verify') throw new Error('Invalid token purpose.'); const user = await this.users.findById(decoded.sub); @@ -126,7 +132,7 @@ export class AuthService { } async refresh(refreshToken: string) { - const decoded: any = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET as string); + const decoded: any = jwt.verify(refreshToken, this.getEnv('JWT_REFRESH_SECRET') as string); if (decoded.purpose !== 'refresh') throw new Error('Invalid token purpose.'); const user = await this.users.findById(decoded.sub); @@ -155,7 +161,7 @@ export class AuthService { } async resetPassword(token: string, newPassword: string) { - const decoded: any = jwt.verify(token, process.env.JWT_RESET_SECRET as string); + const decoded: any = jwt.verify(token, this.getEnv('JWT_RESET_SECRET') as string); if (decoded.purpose !== 'reset') throw new Error('Invalid token purpose.'); const user = await this.users.findById(decoded.sub); diff --git a/src/services/seed.service.ts b/src/services/seed.service.ts index bf43053..d38d959 100644 --- a/src/services/seed.service.ts +++ b/src/services/seed.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { RoleRepository } from '@repos/role.repository'; import { PermissionRepository } from '@repos/permission.repository'; -import { ObjectId, Types } from 'mongoose'; +import { Types } from 'mongoose'; @Injectable() export class SeedService { @@ -27,6 +27,9 @@ export class SeedService { let user = await this.roles.findByName('user'); if (!user) user = await this.roles.create({ name: 'user', permissions: [] }); + + console.log('[AuthKit] Seeded roles:', { adminRoleId: admin._id.toString(), userRoleId: user._id.toString() }); + return { adminRoleId: admin._id.toString(), userRoleId: user._id.toString() From 2a1fbd43f4d74174f99dceefb0d9a321e7377e1a Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 17:17:31 +0100 Subject: [PATCH 35/62] refactor: removing unnecessary types --- src/services/auth.service.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 8243713..b7b8c18 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -23,23 +23,24 @@ export class AuthService { } private signAccessToken(payload: any) { - const expiresIn = this.resolveExpiry(this.getEnv('JWT_ACCESS_TOKEN_EXPIRES_IN'), '15m'); - return jwt.sign(payload, this.getEnv('JWT_SECRET') as string, { expiresIn }); + const expiresIn = this.resolveExpiry(process.env.JWT_ACCESS_TOKEN_EXPIRES_IN, '15m'); + return jwt.sign(payload, this.getEnv('JWT_SECRET'), { expiresIn }); } private signRefreshToken(payload: any) { - const expiresIn = this.resolveExpiry(this.getEnv('JWT_REFRESH_TOKEN_EXPIRES_IN'), '7d'); - return jwt.sign(payload, this.getEnv('JWT_REFRESH_SECRET') as string, { expiresIn }); + const expiresIn = this.resolveExpiry(process.env.JWT_REFRESH_TOKEN_EXPIRES_IN, '15m'); + return jwt.sign(payload, this.getEnv('JWT_REFRESH_SECRET'), { expiresIn }); } private signEmailToken(payload: any) { - const expiresIn = this.resolveExpiry(this.getEnv('JWT_EMAIL_TOKEN_EXPIRES_IN'), '1d'); - return jwt.sign(payload, this.getEnv('JWT_EMAIL_SECRET') as string, { expiresIn }); + const expiresIn = this.resolveExpiry(process.env.JWT_EMAIL_TOKEN_EXPIRES_IN, '15m'); + + return jwt.sign(payload, this.getEnv('JWT_EMAIL_SECRET'), { expiresIn }); } private signResetToken(payload: any) { - const expiresIn = this.resolveExpiry(this.getEnv('JWT_RESET_TOKEN_EXPIRES_IN'), '1h'); - return jwt.sign(payload, this.getEnv('JWT_RESET_SECRET') as string, { expiresIn }); + const expiresIn = this.resolveExpiry(process.env.JWT_RESET_TOKEN_EXPIRES_IN, '15m'); + return jwt.sign(payload, this.getEnv('JWT_RESET_SECRET'), { expiresIn }); } private async buildTokenPayload(userId: string) { @@ -94,7 +95,7 @@ export class AuthService { } async verifyEmail(token: string) { - const decoded: any = jwt.verify(token, this.getEnv('JWT_EMAIL_SECRET') as string); + const decoded: any = jwt.verify(token, this.getEnv('JWT_EMAIL_SECRET')); if (decoded.purpose !== 'verify') throw new Error('Invalid token purpose.'); const user = await this.users.findById(decoded.sub); @@ -132,7 +133,7 @@ export class AuthService { } async refresh(refreshToken: string) { - const decoded: any = jwt.verify(refreshToken, this.getEnv('JWT_REFRESH_SECRET') as string); + const decoded: any = jwt.verify(refreshToken, this.getEnv('JWT_REFRESH_SECRET')); if (decoded.purpose !== 'refresh') throw new Error('Invalid token purpose.'); const user = await this.users.findById(decoded.sub); @@ -161,7 +162,7 @@ export class AuthService { } async resetPassword(token: string, newPassword: string) { - const decoded: any = jwt.verify(token, this.getEnv('JWT_RESET_SECRET') as string); + const decoded: any = jwt.verify(token, this.getEnv('JWT_RESET_SECRET')); if (decoded.purpose !== 'reset') throw new Error('Invalid token purpose.'); const user = await this.users.findById(decoded.sub); From a974a67d733c7bb7dd707ce47bd4d293484196d2 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 17:20:44 +0100 Subject: [PATCH 36/62] refactor: created oAuth Service --- src/services/oauth.service.ts | 138 ++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/services/oauth.service.ts diff --git a/src/services/oauth.service.ts b/src/services/oauth.service.ts new file mode 100644 index 0000000..e0f3e91 --- /dev/null +++ b/src/services/oauth.service.ts @@ -0,0 +1,138 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import jwt from 'jsonwebtoken'; +import jwksClient from 'jwks-rsa'; +import { UserRepository } from '@repos/user.repository'; +import { RoleRepository } from '@repos/role.repository'; +import { AuthService } from '@services/auth.service'; + +@Injectable() +export class OAuthService { + private msJwks = jwksClient({ + jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys', + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + }); + + constructor( + private readonly users: UserRepository, + private readonly roles: RoleRepository, + private readonly auth: AuthService + ) { } + + private async getDefaultRoleId() { + const role = await this.roles.findByName('user'); + if (!role) throw new Error('Default role not seeded.'); + return role._id; + } + + private verifyMicrosoftIdToken(idToken: string) { + return new Promise((resolve, reject) => { + const getKey = (header: any, cb: (err: any, key?: string) => void) => { + this.msJwks + .getSigningKey(header.kid) + .then((k) => cb(null, k.getPublicKey())) + .catch(cb); + }; + + jwt.verify( + idToken, + getKey as any, + { algorithms: ['RS256'], audience: process.env.MICROSOFT_CLIENT_ID }, + (err, payload) => (err ? reject(err) : resolve(payload)) + ); + }); + } + + async loginWithMicrosoft(idToken: string) { + const ms: any = await this.verifyMicrosoftIdToken(idToken); + const email = ms.preferred_username || ms.email; + if (!email) throw new Error('Email missing'); + + return this.findOrCreateOAuthUser(email, ms.name); + } + + async loginWithGoogleIdToken(idToken: string) { + const verifyResp = await axios.get('https://oauth2.googleapis.com/tokeninfo', { + params: { id_token: idToken }, + }); + const email = verifyResp.data?.email; + if (!email) throw new Error('Email missing'); + + return this.findOrCreateOAuthUser(email, verifyResp.data?.name); + } + + async loginWithGoogleCode(code: string) { + const tokenResp = await axios.post('https://oauth2.googleapis.com/token', { + code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: 'postmessage', + grant_type: 'authorization_code', + }); + + const { access_token } = tokenResp.data || {}; + if (!access_token) throw new Error('Failed to exchange code'); + + const profileResp = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${access_token}` }, + }); + + const email = profileResp.data?.email; + if (!email) throw new Error('Email missing'); + + return this.findOrCreateOAuthUser(email, profileResp.data?.name); + } + + async loginWithFacebook(accessToken: string) { + const appTokenResp = await axios.get('https://graph.facebook.com/oauth/access_token', { + params: { + client_id: process.env.FB_CLIENT_ID, + client_secret: process.env.FB_CLIENT_SECRET, + grant_type: 'client_credentials', + }, + }); + + const appAccessToken = appTokenResp.data?.access_token; + const debug = await axios.get('https://graph.facebook.com/debug_token', { + params: { input_token: accessToken, access_token: appAccessToken }, + }); + + if (!debug.data?.data?.is_valid) throw new Error('Invalid Facebook token'); + + const me = await axios.get('https://graph.facebook.com/me', { + params: { access_token: accessToken, fields: 'id,name,email' }, + }); + + const email = me.data?.email; + if (!email) throw new Error('Email missing'); + + return this.findOrCreateOAuthUser(email, me.data?.name); + } + + private async findOrCreateOAuthUser(email: string, name?: string) { + let user = await this.users.findByEmail(email); + if (!user) { + const [fname, ...rest] = (name || 'User OAuth').split(' '); + const lname = rest.join(' ') || 'OAuth'; + + const defaultRoleId = await this.getDefaultRoleId(); + user = await this.users.create({ + fullname: { fname, lname }, + username: email.split('@')[0], + email, + roles: [defaultRoleId], + isVerified: true, + isBanned: false, + passwordChangedAt: new Date() + }); + } + + const payload = await this.auth['buildTokenPayload'](user._id.toString()); + const accessToken = this.auth['signAccessToken'](payload); + const refreshToken = this.auth['signRefreshToken']({ sub: user._id.toString(), purpose: 'refresh' }); + + return { accessToken, refreshToken }; + } +} From e53a1ee350284b2e88345d38062f9431d3c7b064 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 17:21:03 +0100 Subject: [PATCH 37/62] refactor: added OAuth endpoints for all providers --- src/controllers/auth.controller.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 240cbef..99c6aad 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -9,10 +9,11 @@ import { ResendVerificationDto } from '@dtos/auth/resend-verification.dto'; import { ForgotPasswordDto } from '@dtos/auth/forgot-password.dto'; import { ResetPasswordDto } from '@dtos/auth/reset-password.dto'; import { getMillisecondsFromExpiry } from '@utils/helper'; +import { OAuthService } from '@services/oauth.service'; @Controller('api/auth') export class AuthController { - constructor(private readonly auth: AuthService) { } + constructor(private readonly auth: AuthService, private readonly oauth: OAuthService) { } @Post('register') async register(@Body() dto: RegisterDto, @Res() res: Response) { @@ -88,4 +89,25 @@ export class AuthController { const result = await this.auth.deleteAccount(userId); return res.status(200).json(result); } + + @Post('oauth/microsoft') + async microsoftExchange(@Body() body: { idToken: string }, @Res() res: Response) { + const { accessToken, refreshToken } = await this.oauth.loginWithMicrosoft(body.idToken); + return res.status(200).json({ accessToken, refreshToken }); + } + + @Post('oauth/google') + async googleExchange(@Body() body: { idToken?: string; code?: string }, @Res() res: Response) { + const result = body.idToken + ? await this.oauth.loginWithGoogleIdToken(body.idToken) + : await this.oauth.loginWithGoogleCode(body.code as string); + return res.status(200).json(result); + } + + @Post('oauth/facebook') + async facebookExchange(@Body() body: { accessToken: string }, @Res() res: Response) { + const result = await this.oauth.loginWithFacebook(body.accessToken); + return res.status(200).json(result); + } + } From 7261bbb1502c03e249e897403f57e06e6e1e92ed Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 17:40:13 +0100 Subject: [PATCH 38/62] refactor: updated passport strategy --- src/config/passport.config.ts | 319 +++++++--------------------------- 1 file changed, 65 insertions(+), 254 deletions(-) diff --git a/src/config/passport.config.ts b/src/config/passport.config.ts index 25e11c1..cc19382 100644 --- a/src/config/passport.config.ts +++ b/src/config/passport.config.ts @@ -1,269 +1,80 @@ import passport from 'passport'; -import { Strategy as LocalStrategy } from 'passport-local'; import { Strategy as AzureStrategy } from 'passport-azure-ad-oauth2'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { Strategy as FacebookStrategy } from 'passport-facebook'; -import bcrypt from 'bcryptjs'; -import { decode as jwtDecode } from 'jsonwebtoken'; -import { User } from '../models/user.model'; -import 'dotenv/config'; - -const MAX_FAILED = parseInt(process.env.MAX_FAILED_LOGIN_ATTEMPTS || '', 10) || 3; -const LOCK_TIME_MIN = parseInt(process.env.ACCOUNT_LOCK_TIME_MINUTES || '', 10) || 15; -const LOCK_TIME_MS = LOCK_TIME_MIN * 60 * 1000; - -passport.use( - new LocalStrategy( - { usernameField: 'email', passwordField: 'password', passReqToCallback: true }, - async (req: any, email: string, password: string, done: any) => { - try { - const user = await User.findOne({ email }); - if (!user) return done(null, false, { message: 'Incorrect email.' }); - - if (user.lockUntil && user.lockUntil > Date.now()) { - return done(null, false, { message: `Account locked until ${new Date(user.lockUntil).toLocaleString()}.` }); - } - - const ok = await bcrypt.compare(password, user.password); - if (!ok) { - user.failedLoginAttempts += 1; - if (user.failedLoginAttempts >= MAX_FAILED) user.lockUntil = Date.now() + LOCK_TIME_MS; - await user.save(); - return done(null, false, { message: 'Incorrect password.' }); - } - - user.failedLoginAttempts = 0; - user.lockUntil = undefined; - await user.save(); - - return done(null, user); - } catch (err) { - return done(err); - } - } - ) -); - -passport.use( - new AzureStrategy( - { - clientID: process.env.MICROSOFT_CLIENT_ID, - clientSecret: process.env.MICROSOFT_CLIENT_SECRET, - callbackURL: process.env.MICROSOFT_CALLBACK_URL, - }, - async (_at: any, _rt: any, params: any, _profile: any, done: any) => { - try { - const decoded: any = jwtDecode(params.id_token); - const microsoftId = decoded.oid; - const email = decoded.preferred_username; - const name = decoded.name; - let user = await User.findOne({ $or: [{ microsoftId }, { email }] }); - if (!user) { - user = new User({ email, name, microsoftId, roles: [], status: 'active' }); - await user.save(); - } else { - let changed = false; - if (!user.microsoftId) { user.microsoftId = microsoftId; changed = true; } - if (changed) await user.save(); - } - return done(null, user); - } catch (err) { - return done(err); - } - } - ) -); - -passport.use( - 'azure_ad_oauth2_client', - new AzureStrategy( - { - clientID: process.env.MICROSOFT_CLIENT_ID_CLIENT || process.env.MICROSOFT_CLIENT_ID, - clientSecret: process.env.MICROSOFT_CLIENT_SECRET_CLIENT || process.env.MICROSOFT_CLIENT_SECRET, - callbackURL: process.env.MICROSOFT_CALLBACK_URL_CLIENT, - }, - async (_at: any, _rt: any, params: any, _profile: any, done: any) => { - try { - const decoded: any = jwtDecode(params.id_token); - const microsoftId = decoded.oid; - const email = decoded.preferred_username; - const name = decoded.name; - - let client = await User.findOne({ $or: [{ microsoftId }, { email }] }); - if (!client) { - client = new User({ email, name, microsoftId, roles: [] }); - await client.save(); - } else if (!client.microsoftId) { - client.microsoftId = microsoftId; - await client.save(); - } - return done(null, client); - } catch (err) { - return done(err); - } - } - ) -); - -if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET && process.env.GOOGLE_CALLBACK_URL_USER) { - passport.use( - 'google-user', - new GoogleStrategy( - { - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: process.env.GOOGLE_CALLBACK_URL_USER - }, - async (_at: any, _rt: any, profile: any, done: any) => { - try { - const email = profile.emails && profile.emails[0]?.value; - if (!email) return done(null, false); - - let user = await User.findOne({ email }); - if (!user) { - user = new User({ - email, - name: profile.displayName, - googleId: profile.id, - roles: [], - status: 'active' - }); - await user.save(); - } else { - let changed = false; - if (!user.googleId) { user.googleId = profile.id; changed = true; } - if (changed) await user.save(); - } - return done(null, user); - } catch (err) { - return done(err); - } - } - ) - ); -} - -if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET && process.env.GOOGLE_CALLBACK_URL_CLIENT) { - passport.use( - 'google-client', - new GoogleStrategy( - { - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: process.env.GOOGLE_CALLBACK_URL_CLIENT - }, - async (_at: any, _rt: any, profile: any, done: any) => { - try { - const email = profile.emails && profile.emails[0]?.value; - if (!email) return done(null, false); - - let client = await User.findOne({ email }); - if (!client) { - client = new User({ - email, - name: profile.displayName, - googleId: profile.id, - roles: [] - }); - await client.save(); - } else if (!client.googleId) { - client.googleId = profile.id; - await client.save(); +import { OAuthService } from '@services/oauth.service'; + +export const registerOAuthStrategies = ( + oauth: OAuthService +) => { + // Microsoft + if (process.env.MICROSOFT_CLIENT_ID && process.env.MICROSOFT_CLIENT_SECRET && process.env.MICROSOFT_CALLBACK_URL) { + passport.use( + new AzureStrategy( + { + clientID: process.env.MICROSOFT_CLIENT_ID, + clientSecret: process.env.MICROSOFT_CLIENT_SECRET, + callbackURL: process.env.MICROSOFT_CALLBACK_URL, + }, + async (_at: any, _rt: any, params: any, _profile: any, done: any) => { + try { + const idToken = params.id_token; + const { accessToken, refreshToken } = await oauth.loginWithMicrosoft(idToken); + return done(null, { accessToken, refreshToken }); + } catch (err) { + return done(err); } - return done(null, client); - } catch (err) { - return done(err); } - } - ) - ); -} - -if (process.env.FB_CLIENT_ID && process.env.FB_CLIENT_SECRET && process.env.FB_CALLBACK_URL_USER) { - passport.use( - 'facebook-user', - new FacebookStrategy( - { - clientID: process.env.FB_CLIENT_ID, - clientSecret: process.env.FB_CLIENT_SECRET, - callbackURL: process.env.FB_CALLBACK_URL_USER, - profileFields: ['id', 'displayName', 'emails'] - }, - async (_at: any, _rt: any, profile: any, done: any) => { - try { - const email = profile.emails && profile.emails[0]?.value; - if (!email) return done(null, false); + ) + ); + } - let user = await User.findOne({ email }); - if (!user) { - user = new User({ - email, - name: profile.displayName, - facebookId: profile.id, - roles: [], - status: 'active' - }); - await user.save(); - } else { - let changed = false; - if (!user.facebookId) { user.facebookId = profile.id; changed = true; } - if (changed) await user.save(); + // Google + if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET && process.env.GOOGLE_CALLBACK_URL) { + passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL, + }, + async (_at: any, _rt: any, profile: any, done: any) => { + try { + const email = profile.emails?.[0]?.value; + if (!email) return done(null, false); + const { accessToken, refreshToken } = await oauth.findOrCreateOAuthUser(email, profile.displayName); + return done(null, { accessToken, refreshToken }); + } catch (err) { + return done(err); } - return done(null, user); - } catch (err) { - return done(err); } - } - ) - ); -} - -if (process.env.FB_CLIENT_ID && process.env.FB_CLIENT_SECRET && process.env.FB_CALLBACK_URL_CLIENT) { - passport.use( - 'facebook-client', - new FacebookStrategy( - { - clientID: process.env.FB_CLIENT_ID, - clientSecret: process.env.FB_CLIENT_SECRET, - callbackURL: process.env.FB_CALLBACK_URL_CLIENT, - profileFields: ['id', 'displayName', 'emails'] - }, - async (_at: any, _rt: any, profile: any, done: any) => { - try { - const email = profile.emails && profile.emails[0]?.value; - if (!email) return done(null, false); + ) + ); + } - let client = await User.findOne({ email }); - if (!client) { - client = new User({ - email, - name: profile.displayName, - facebookId: profile.id, - roles: [] - }); - await client.save(); - } else if (!client.facebookId) { - client.facebookId = profile.id; - await client.save(); + // Facebook + if (process.env.FB_CLIENT_ID && process.env.FB_CLIENT_SECRET && process.env.FB_CALLBACK_URL) { + passport.use( + new FacebookStrategy( + { + clientID: process.env.FB_CLIENT_ID, + clientSecret: process.env.FB_CLIENT_SECRET, + callbackURL: process.env.FB_CALLBACK_URL, + profileFields: ['id', 'displayName', 'emails'], + }, + async (_at: any, _rt: any, profile: any, done: any) => { + try { + const email = profile.emails?.[0]?.value; + if (!email) return done(null, false); + const { accessToken, refreshToken } = await oauth.findOrCreateOAuthUser(email, profile.displayName); + return done(null, { accessToken, refreshToken }); + } catch (err) { + return done(err); } - return done(null, client); - } catch (err) { - return done(err); } - } - ) - ); -} - -passport.serializeUser((principal: any, done: any) => done(null, principal.id)); -passport.deserializeUser(async (id: string, done: any) => { - try { - let principal = await User.findById(id); - if (!principal) principal = await User.findById(id); - done(null, principal); - } catch (err) { - done(err); + ) + ); } -}); +}; export default passport; From 6824de3eaa54b6719ed28ca15aabe7af10bf0658 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 17:40:59 +0100 Subject: [PATCH 39/62] updated authentication middleware and auth controller with OAUth endpoint --- src/controllers/auth.controller.ts | 45 ++++++++++++++++++++++++++-- src/middleware/authenticate.guard.ts | 9 +++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 99c6aad..5231259 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,5 +1,5 @@ -import { Body, Controller, Delete, Post, Req, Res } from '@nestjs/common'; -import type { Request, Response } from 'express'; +import { Body, Controller, Delete, Get, Next, Post, Req, Res } from '@nestjs/common'; +import type { NextFunction, Request, Response } from 'express'; import { AuthService } from '@services/auth.service'; import { LoginDto } from '@dtos/auth/login.dto'; import { RegisterDto } from '@dtos/auth/register.dto'; @@ -10,6 +10,7 @@ import { ForgotPasswordDto } from '@dtos/auth/forgot-password.dto'; import { ResetPasswordDto } from '@dtos/auth/reset-password.dto'; import { getMillisecondsFromExpiry } from '@utils/helper'; import { OAuthService } from '@services/oauth.service'; +import passport from 'passport'; @Controller('api/auth') export class AuthController { @@ -110,4 +111,44 @@ export class AuthController { return res.status(200).json(result); } + @Get('google') + googleLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + return passport.authenticate('google', { scope: ['profile', 'email'], session: false })(req, res, next); + } + + @Get('google/callback') + googleCallback(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + passport.authenticate('google', { session: false }, (err: any, data: any) => { + if (err || !data) return res.status(400).json({ message: 'Google auth failed.' }); + return res.status(200).json(data); + })(req, res, next); + } + + @Get('microsoft') + microsoftLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + return passport.authenticate('azure_ad_oauth2', { session: false })(req, res, next); + } + + @Get('microsoft/callback') + microsoftCallback(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + passport.authenticate('azure_ad_oauth2', { session: false }, (err: any, data: any) => { + if (err || !data) return res.status(400).json({ message: 'Microsoft auth failed.' }); + return res.status(200).json(data); + })(req, res, next); + } + + @Get('facebook') + facebookLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + return passport.authenticate('facebook', { scope: ['email'], session: false })(req, res, next); + } + + @Get('facebook/callback') + facebookCallback(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { + passport.authenticate('facebook', { session: false }, (err: any, data: any) => { + if (err || !data) return res.status(400).json({ message: 'Facebook auth failed.' }); + return res.status(200).json(data); + })(req, res, next); + } + + } diff --git a/src/middleware/authenticate.guard.ts b/src/middleware/authenticate.guard.ts index fb7b637..b9f3f8b 100644 --- a/src/middleware/authenticate.guard.ts +++ b/src/middleware/authenticate.guard.ts @@ -6,6 +6,13 @@ import { UserRepository } from '@repos/user.repository'; export class AuthenticateGuard implements CanActivate { constructor(private readonly users: UserRepository) { } + private getEnv(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`${name} is not set`); + return v; + } + + async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest(); const res = context.switchToHttp().getResponse(); @@ -18,7 +25,7 @@ export class AuthenticateGuard implements CanActivate { const token = authHeader.split(' ')[1]; try { - const decoded: any = jwt.verify(token, process.env.JWT_SECRET as string); + const decoded: any = jwt.verify(token, this.getEnv('JWT_SECRET')); const user = await this.users.findById(decoded.sub); if (!user) { From 2167a1c2e2e121494cbcd0552bf5f98083abf1b7 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 17:41:19 +0100 Subject: [PATCH 40/62] created oauth service and updated auth service --- src/services/auth.service.ts | 14 +++++++++++--- src/services/oauth.service.ts | 7 ++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index b7b8c18..595d994 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -28,18 +28,18 @@ export class AuthService { } private signRefreshToken(payload: any) { - const expiresIn = this.resolveExpiry(process.env.JWT_REFRESH_TOKEN_EXPIRES_IN, '15m'); + const expiresIn = this.resolveExpiry(process.env.JWT_REFRESH_TOKEN_EXPIRES_IN, '7d'); return jwt.sign(payload, this.getEnv('JWT_REFRESH_SECRET'), { expiresIn }); } private signEmailToken(payload: any) { - const expiresIn = this.resolveExpiry(process.env.JWT_EMAIL_TOKEN_EXPIRES_IN, '15m'); + const expiresIn = this.resolveExpiry(process.env.JWT_EMAIL_TOKEN_EXPIRES_IN, '1d'); return jwt.sign(payload, this.getEnv('JWT_EMAIL_SECRET'), { expiresIn }); } private signResetToken(payload: any) { - const expiresIn = this.resolveExpiry(process.env.JWT_RESET_TOKEN_EXPIRES_IN, '15m'); + const expiresIn = this.resolveExpiry(process.env.JWT_RESET_TOKEN_EXPIRES_IN, '1h'); return jwt.sign(payload, this.getEnv('JWT_RESET_SECRET'), { expiresIn }); } @@ -61,6 +61,14 @@ export class AuthService { return v; } + public async issueTokensForUser(userId: string) { + const payload = await this.buildTokenPayload(userId); + const accessToken = this.signAccessToken(payload); + const refreshToken = this.signRefreshToken({ sub: userId, purpose: 'refresh' }); + return { accessToken, refreshToken }; + } + + async register(dto: RegisterDto) { if (await this.users.findByEmail(dto.email)) throw new Error('Email already in use.'); if (await this.users.findByUsername(dto.username)) throw new Error('Username already in use.'); diff --git a/src/services/oauth.service.ts b/src/services/oauth.service.ts index e0f3e91..e957e72 100644 --- a/src/services/oauth.service.ts +++ b/src/services/oauth.service.ts @@ -111,7 +111,7 @@ export class OAuthService { return this.findOrCreateOAuthUser(email, me.data?.name); } - private async findOrCreateOAuthUser(email: string, name?: string) { + async findOrCreateOAuthUser(email: string, name?: string) { let user = await this.users.findByEmail(email); if (!user) { const [fname, ...rest] = (name || 'User OAuth').split(' '); @@ -129,10 +129,7 @@ export class OAuthService { }); } - const payload = await this.auth['buildTokenPayload'](user._id.toString()); - const accessToken = this.auth['signAccessToken'](payload); - const refreshToken = this.auth['signRefreshToken']({ sub: user._id.toString(), purpose: 'refresh' }); - + const { accessToken, refreshToken } = await this.auth.issueTokensForUser(user._id.toString()); return { accessToken, refreshToken }; } } From d2880187c5f8cb28eadae6b16569928d47481a10 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 17:41:34 +0100 Subject: [PATCH 41/62] wiring all new implementations into authkit module --- src/auth-kit.module.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/auth-kit.module.ts b/src/auth-kit.module.ts index 18b9ea2..067cc92 100644 --- a/src/auth-kit.module.ts +++ b/src/auth-kit.module.ts @@ -26,6 +26,8 @@ import { PermissionRepository } from '@repos/permission.repository'; import { AuthenticateGuard } from '@middleware/authenticate.guard'; import { AdminGuard } from '@middleware/admin.guard'; import { AdminRoleService } from '@services/admin-role.service'; +import { OAuthService } from '@services/oauth.service'; +import passport from 'passport'; @Module({ imports: [ @@ -54,6 +56,7 @@ import { AdminRoleService } from '@services/admin-role.service'; AuthenticateGuard, AdminGuard, AdminRoleService, + OAuthService, ], exports: [ AuthService, @@ -72,7 +75,7 @@ import { AdminRoleService } from '@services/admin-role.service'; export class AuthKitModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer - .apply(cookieParser()) + .apply(cookieParser(), passport.initialize()) .forRoutes({ path: '*', method: RequestMethod.ALL }); } } From c680adb57897eeff2a0fce9bdc3238a1d7cfe25c Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Thu, 22 Jan 2026 17:48:23 +0100 Subject: [PATCH 42/62] doc: update readme file --- README.md | 246 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 139 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 38f2262..d933e7e 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,169 @@ -Auth Service (NestJS, JWT, RBAC) -Internal package - private to the company. -This package is not published on npmjs. Install it only from the company Azure Artifacts feed using a project or user-level .npmrc. +# AuthKit (NestJS Auth Package) -Authentication and authorization module for NestJS apps. -Provides local email/password auth with lockout, JWT access tokens and refresh, RBAC, and optional OAuth (Microsoft Entra, Google, Facebook). +A clean, production-ready authentication/authorization kit for NestJS. +Includes local auth, OAuth (Google/Microsoft/Facebook), JWT tokens, RBAC, admin user management, email verification, and password reset. -Features -Local auth (email/password) with account lockout policy. -JWT access tokens (Bearer) and refresh endpoint (cookie or body). -RBAC (roles -> permission strings). -Microsoft Entra (Azure AD), Google, Facebook OAuth (optional). -MongoDB/Mongoose models. +## Features -Routes are mounted under: +- Local auth (email + password) +- OAuth (Google / Microsoft Entra / Facebook) + - Web redirect (Passport) + - Mobile exchange (token/code) +- JWT access + refresh (stateless) +- Email verification (required before login) +- Password reset via JWT link +- Admin user management (create/list/ban/delete/role switch) +- RBAC (roles ↔ permissions) +- Host app owns DB (package uses host Mongoose connection) -/api/auth (auth, password reset) -/api/users (user admin) -/api/auth/roles and /api/auth/permissions (RBAC) -/api/admin (admin actions) +## Install -Installation - -1) Install the package +```bash npm i @ciscode/authentication-kit +``` -2) Required environment variables (host app) -Create a .env in the host project: +## Host App Setup -# Server -PORT=3000 -NODE_ENV=development -BASE_URL=http://localhost:3000 +1. Env Vars -# Database (the service connects to this on startup) -MONGO_URI_T=mongodb://127.0.0.1:27017/auth_service +```env + MONGO_URI=mongodb://127.0.0.1:27017/app_db -# JWT JWT_SECRET=change_me JWT_ACCESS_TOKEN_EXPIRES_IN=15m JWT_REFRESH_SECRET=change_me_too JWT_REFRESH_TOKEN_EXPIRES_IN=7d +JWT_EMAIL_SECRET=change_me_email +JWT_EMAIL_TOKEN_EXPIRES_IN=1d +JWT_RESET_SECRET=change_me_reset +JWT_RESET_TOKEN_EXPIRES_IN=1h + +SMTP_HOST=... +SMTP_PORT=587 +SMTP_USER=... +SMTP_PASS=... +SMTP_SECURE=false +FROM_EMAIL=no-reply@yourapp.com +FRONTEND_URL=http://localhost:3000 + +GOOGLE_CLIENT_ID=... +GOOGLE_CLIENT_SECRET=... +GOOGLE_CALLBACK_URL=http://localhost:3000/api/auth/google/callback + +MICROSOFT_CLIENT_ID=... +MICROSOFT_CLIENT_SECRET=... +MICROSOFT_CALLBACK_URL=http://localhost:3000/api/auth/microsoft/callback + +FB_CLIENT_ID=... +FB_CLIENT_SECRET=... +FB_CALLBACK_URL=http://localhost:3000/api/auth/facebook/callback +``` + +2. App Module + +```js + import { Module, OnModuleInit } from '@nestjs/common'; + import { MongooseModule } from '@nestjs/mongoose'; + import { AuthKitModule, SeedService } from '@ciscode/authentication-kit'; -# Lockout policy -MAX_FAILED_LOGIN_ATTEMPTS=5 -ACCOUNT_LOCK_TIME_MINUTES=15 +@Module({ +imports: [ +MongooseModule.forRoot(process.env.MONGO_URI), +AuthKitModule, +], +}) +export class AppModule implements OnModuleInit { +constructor(private readonly seed: SeedService) {} -# (Optional) Microsoft Entra ID (Azure AD) -MICROSOFT_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -MICROSOFT_CLIENT_SECRET=your-secret -MICROSOFT_CALLBACK_URL=${BASE_URL}/api/auth/microsoft/callback + async onModuleInit() { + await this.seed.seedDefaults(); + } -Use inside an existing Nest app -The module connects to Mongo on init and mounts its controllers. +} +``` -// app.module.ts (host app) -import { Module } from '@nestjs/common'; -import { AuthKitModule } from '@ciscode/authentication-kit'; +## Routes -@Module({ - imports: [AuthKitModule] -}) -export class AppModule {} - -If you need to run it standalone, build and start the package: - -npm run build -npm start - -What is included (routes and behavior) -Auth -POST /api/auth/login - Local login. On success, returns accessToken and may set a refreshToken httpOnly cookie. -POST /api/auth/refresh-token - New access token from a valid refresh token (cookie or body). -POST /api/auth/request-password-reset - Sends a reset token (e.g., by email). -POST /api/auth/reset-password - Consumes the reset token and sets a new password. -GET /api/auth/microsoft - GET /api/auth/microsoft/callback - Optional Microsoft Entra OAuth; issues first-party tokens. -Users -GET /api/users - List users (paginated). -POST /api/users - Create a user. -Additional CRUD endpoints as exposed by controllers. -Roles and Permissions -GET/POST /api/auth/roles - Manage roles (name, permissions: string[]). -GET /api/auth/permissions - List permission strings and metadata. - -Protecting your own routes (host app) -import { UseGuards } from '@nestjs/common'; -import { AuthenticateGuard, hasPermission } from '@ciscode/authentication-kit'; - -@UseGuards(AuthenticateGuard, hasPermission('reports:read')) -@Get('reports') -getReports() { - return { ok: true }; -} +```txt +Auth (public) + +- POST /api/auth/register +- POST /api/auth/verify-email +- POST /api/auth/resend-verification +- POST /api/auth/login +- POST /api/auth/refresh-token +- POST /api/auth/forgot-password +- POST /api/auth/reset-password +- DELETE /api/auth/account + +OAuth (mobile exchange) + +- POST /api/auth/oauth/google { idToken | code } +- POST /api/auth/oauth/microsoft { idToken } +- POST /api/auth/oauth/facebook { accessToken } + +OAuth (web redirect) + +- GET /api/auth/google +- GET /api/auth/google/callback +- GET /api/auth/microsoft +- GET /api/auth/microsoft/callback +- GET /api/auth/facebook +- GET /api/auth/facebook/callback + +Admin (protected) + +- POST /api/admin/users +- GET /api/admin/users +- PATCH /api/admin/users/:id/ban +- PATCH /api/admin/users/:id/unban +- PATCH /api/admin/users/:id/roles +- DELETE /api/admin/users/:id + +- POST /api/admin/roles +- GET /api/admin/roles +- PUT /api/admin/roles/:id +- PUT /api/admin/roles/:id/permissions +- DELETE /api/admin/roles/:id + +- POST /api/admin/permissions +- GET /api/admin/permissions +- PUT /api/admin/permissions/:id +- DELETE /api/admin/permissions/:id +``` + +## Guards -Quick start (smoke tests) -Start your host app, then create a user and log in: +```js +import { AuthenticateGuard } from '@ciscode/authentication-kit'; -curl -X POST http://localhost:3000/api/users \ - -H 'Content-Type: application/json' \ - -d '{"email":"a@b.com","password":"Secret123!","name":"Alice"}' +@UseGuards(AuthenticateGuard) +@Get('me') +getProfile() { ... } -curl -X POST http://localhost:3000/api/auth/login \ - -H 'Content-Type: application/json' \ - -d '{"email":"a@b.com","password":"Secret123!"}' -# => { "accessToken": "...", "refreshToken": "..." } +Admin Guard +import { Admin } from '@ciscode/authentication-kit'; -Call a protected route +@Admin() +@Get('admin-only') +adminRoute() { ... } +``` -ACCESS= -curl http://localhost:3000/api/users -H "Authorization: Bearer $ACCESS" +## Seeding -Refresh token +On startup, call: -curl -X POST http://localhost:3000/api/auth/refresh-token \ - -H 'Content-Type: application/json' \ - -d '{"refreshToken":""}' -# => { "accessToken": "..." } +```bash +await seed.seedDefaults(); +``` -Microsoft OAuth (optional) - Visit: http://localhost:3000/api/auth/microsoft to complete sign-in. -- Callback: ${BASE_URL}/api/auth/microsoft/callback returns tokens (and may set the refresh cookie). +It creates: -CI/CD (Azure Pipelines) -# azure-pipelines.yml (snippet) -- task: npmAuthenticate@0 - inputs: - workingFile: .npmrc # optional; the task wires npm auth for subsequent steps +- Roles: admin, user +- Permissions: users:manage, roles:manage, permissions:manage -- script: npm ci - displayName: Install deps -(For GitHub Actions, write a ~/.npmrc with the token from secrets.AZURE_ARTIFACTS_PAT before npm ci.) +## Notes -Security notes -Never commit real PATs. Use env vars or CI secrets. -Run behind HTTPS. Rotate JWT and refresh secrets periodically. -Limit login attempts; log auth events for auditing. -License -Internal - Company proprietary. +- AuthKit does not manage DB connection. Host app must connect to Mongo. +- JWTs are stateless; refresh tokens are signed JWTs. +- Email verification is required before login. From e938792b9536999ec023fbb998e32638d5a78445 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sun, 25 Jan 2026 08:55:40 +0100 Subject: [PATCH 43/62] refactor: Secure auth routes --- src/controllers/auth.controller.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 5231259..58571e5 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Next, Post, Req, Res } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Next, Post, Req, Res, UseGuards } from '@nestjs/common'; import type { NextFunction, Request, Response } from 'express'; import { AuthService } from '@services/auth.service'; import { LoginDto } from '@dtos/auth/login.dto'; @@ -11,6 +11,7 @@ import { ResetPasswordDto } from '@dtos/auth/reset-password.dto'; import { getMillisecondsFromExpiry } from '@utils/helper'; import { OAuthService } from '@services/oauth.service'; import passport from 'passport'; +import { AuthenticateGuard } from '@middleware/authenticate.guard'; @Controller('api/auth') export class AuthController { @@ -84,6 +85,7 @@ export class AuthController { } @Delete('account') + @UseGuards(AuthenticateGuard) async deleteAccount(@Req() req: Request, @Res() res: Response) { const userId = (req as any).user?.sub; if (!userId) return res.status(401).json({ message: 'Unauthorized.' }); From d68c293ff969f81b7247530ca730d7fc8bde7212 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sun, 25 Jan 2026 08:56:15 +0100 Subject: [PATCH 44/62] refactor: register oAuth Strategy once the module in init --- src/auth-kit.module.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/auth-kit.module.ts b/src/auth-kit.module.ts index 067cc92..3e5e4ba 100644 --- a/src/auth-kit.module.ts +++ b/src/auth-kit.module.ts @@ -1,5 +1,5 @@ import 'dotenv/config'; -import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule, OnModuleInit, RequestMethod } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import cookieParser from 'cookie-parser'; @@ -28,6 +28,7 @@ import { AdminGuard } from '@middleware/admin.guard'; import { AdminRoleService } from '@services/admin-role.service'; import { OAuthService } from '@services/oauth.service'; import passport from 'passport'; +import { registerOAuthStrategies } from '@config/passport.config'; @Module({ imports: [ @@ -72,7 +73,13 @@ import passport from 'passport'; AdminRoleService, ], }) -export class AuthKitModule implements NestModule { +export class AuthKitModule implements NestModule, OnModuleInit { + constructor(private readonly oauth: OAuthService) { } + + onModuleInit() { + registerOAuthStrategies(this.oauth); + } + configure(consumer: MiddlewareConsumer) { consumer .apply(cookieParser(), passport.initialize()) From 73cc584442f4b6eb964262609045046aa93e6158 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sun, 25 Jan 2026 08:56:29 +0100 Subject: [PATCH 45/62] refactor: create a new .envexample --- .env.example | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..56ebaa3 --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +MONGO_URI=mongodb://127.0.0.1:27017/app_db + +JWT_SECRET=change_me +JWT_ACCESS_TOKEN_EXPIRES_IN=15m +JWT_REFRESH_SECRET=change_me_too +JWT_REFRESH_TOKEN_EXPIRES_IN=7d +JWT_EMAIL_SECRET=change_me_email +JWT_EMAIL_TOKEN_EXPIRES_IN=1d +JWT_RESET_SECRET=change_me_reset +JWT_RESET_TOKEN_EXPIRES_IN=1h + +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USER=example_user +SMTP_PASS=example_pass +SMTP_SECURE=false +FROM_EMAIL=no-reply@yourapp.com +FRONTEND_URL=http://localhost:3000 + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_CALLBACK_URL=http://localhost:3000/api/auth/google/callback + +MICROSOFT_CLIENT_ID= +MICROSOFT_CLIENT_SECRET= +MICROSOFT_CALLBACK_URL=http://localhost:3000/api/auth/microsoft/callback + +FB_CLIENT_ID= +FB_CLIENT_SECRET= +FB_CALLBACK_URL=http://localhost:3000/api/auth/facebook/callback From b9bc5331e27d1b2ad5708dce36826a7b9d996f62 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sun, 25 Jan 2026 09:36:26 +0100 Subject: [PATCH 46/62] refactor: fix build errors withing typescript stricts --- package-lock.json | 414 +++++++++++++++++++++++++++++++++++++++ package.json | 5 +- src/models/user.model.ts | 4 +- 3 files changed, 420 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 946fe73..951ba35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@types/passport-local": "^1.0.38", "semantic-release": "^25.0.2", "ts-node": "^10.9.2", + "tsc-alias": "^1.8.10", "typescript": "^5.6.2" } }, @@ -313,6 +314,44 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@nuxtjs/opencollective": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", @@ -1333,6 +1372,20 @@ "dev": true, "license": "MIT" }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -1373,6 +1426,16 @@ "dev": true, "license": "MIT" }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1412,6 +1475,19 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -1562,6 +1638,31 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", @@ -1778,6 +1879,16 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -2574,12 +2685,39 @@ ], "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "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/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -2807,6 +2945,21 @@ "node": ">=14.14" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2902,6 +3055,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/git-log-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.1.tgz", @@ -2917,6 +3083,40 @@ "traverse": "0.6.8" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3192,6 +3392,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3350,6 +3560,29 @@ "dev": true, "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3360,6 +3593,19 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3922,6 +4168,16 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -4163,6 +4419,20 @@ "node": ">= 10.16.0" } }, + "node_modules/mylas": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -4280,6 +4550,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", @@ -6934,6 +7214,19 @@ "node": ">=4" } }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", @@ -7007,6 +7300,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7130,6 +7454,19 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -7169,6 +7506,51 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -7611,6 +7993,16 @@ "node": ">=8" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -8220,6 +8612,28 @@ } } }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index cb79740..0a3f7b9 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "LICENSE" ], "scripts": { - "build": "tsc -p tsconfig.json", + "build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", "start": "node dist/standalone.js", "test": "echo \"No tests defined\" && exit 0", "prepack": "npm run build", @@ -65,7 +65,8 @@ "@types/passport-google-oauth20": "^2.0.15", "@types/passport-local": "^1.0.38", "semantic-release": "^25.0.2", + "tsc-alias": "^1.8.10", "ts-node": "^10.9.2", "typescript": "^5.6.2" } -} \ No newline at end of file +} diff --git a/src/models/user.model.ts b/src/models/user.model.ts index a259300..f9cdaac 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -3,6 +3,7 @@ import { Document, Types } from 'mongoose'; export type UserDocument = User & Document; +@Schema({ _id: false }) class FullName { @Prop({ required: true, trim: true }) fname!: string; @@ -11,10 +12,11 @@ class FullName { lname!: string; } +const FullNameSchema = SchemaFactory.createForClass(FullName); @Schema({ timestamps: true }) export class User { - @Prop({ type: FullName, required: true }) + @Prop({ type: FullNameSchema, required: true }) fullname!: FullName; @Prop({ required: true, unique: true, trim: true, minlength: 3, maxlength: 30 }) From 5f15b1003ba0ea40ee34fbc1127e7cf415dc3bbf Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sun, 25 Jan 2026 10:28:22 +0100 Subject: [PATCH 47/62] refactor: fix dependencies misInstallation --- package-lock.json | 148 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 23 ++++--- 2 files changed, 156 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 951ba35..41411a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,6 @@ "version": "1.2.0", "license": "MIT", "dependencies": { - "@nestjs/common": "^10.4.0", - "@nestjs/core": "^10.4.0", - "@nestjs/mongoose": "^10.0.2", - "@nestjs/platform-express": "^10.4.0", "axios": "^1.7.7", "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", @@ -21,17 +17,18 @@ "dotenv": "^16.4.5", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "mongoose": "^7.6.4", "nodemailer": "^6.9.15", "passport": "^0.7.0", "passport-azure-ad-oauth2": "^0.0.4", "passport-facebook": "^3.0.0", "passport-google-oauth20": "^2.0.0", - "passport-local": "^1.0.0", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "passport-local": "^1.0.0" }, "devDependencies": { + "@nestjs/common": "^10.4.0", + "@nestjs/core": "^10.4.0", + "@nestjs/mongoose": "^10.0.2", + "@nestjs/platform-express": "^10.4.0", "@types/cookie-parser": "^1.4.6", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.6", @@ -39,10 +36,22 @@ "@types/passport-facebook": "^3.0.4", "@types/passport-google-oauth20": "^2.0.15", "@types/passport-local": "^1.0.38", + "mongoose": "^7.6.4", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", "semantic-release": "^25.0.2", "ts-node": "^10.9.2", "tsc-alias": "^1.8.10", "typescript": "^5.6.2" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "@nestjs/mongoose": "^10.0.0 || ^11.0.0", + "@nestjs/platform-express": "^10.0.0 || ^11.0.0", + "mongoose": "^7.0.0 || ^8.0.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.0.0" } }, "node_modules/@actions/core": { @@ -126,6 +135,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -198,6 +208,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -207,6 +218,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz", "integrity": "sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -217,6 +229,7 @@ "version": "10.4.22", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", + "dev": true, "license": "MIT", "dependencies": { "file-type": "20.4.1", @@ -247,6 +260,7 @@ "version": "10.4.22", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -285,6 +299,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-10.1.0.tgz", "integrity": "sha512-1ExAnZUfh2QffEaGjqYGgVPy/sYBQCVLCLqVgkcClKx/BCd0QNgND8MB70lwyobp3nm/+nbGQqBpu9F3/hgOCw==", + "dev": true, "license": "MIT", "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -297,6 +312,7 @@ "version": "10.4.22", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "dev": true, "license": "MIT", "dependencies": { "body-parser": "1.20.4", @@ -356,6 +372,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -933,6 +950,7 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -951,6 +969,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -968,12 +987,14 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "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==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node10": { @@ -1243,12 +1264,14 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "dev": true, "license": "MIT" }, "node_modules/@types/whatwg-url": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -1259,6 +1282,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -1354,6 +1378,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1390,6 +1415,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "dev": true, "license": "MIT" }, "node_modules/arg": { @@ -1417,6 +1443,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, "license": "MIT" }, "node_modules/array-ify": { @@ -1492,6 +1519,7 @@ "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -1536,6 +1564,7 @@ "version": "5.5.1", "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=14.20.1" @@ -1551,12 +1580,14 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, "dependencies": { "streamsearch": "^1.1.0" }, @@ -1568,6 +1599,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -1590,6 +1622,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1616,6 +1649,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -1853,6 +1887,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1865,6 +1900,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -1904,6 +1940,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, "engines": [ "node >= 6.0" ], @@ -1930,12 +1967,14 @@ "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "dev": true, "license": "MIT" }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -1948,6 +1987,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2063,6 +2103,7 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -2154,6 +2195,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -2182,6 +2224,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -2191,6 +2234,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8", @@ -2315,6 +2359,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, "license": "MIT" }, "node_modules/emoji-regex": { @@ -2335,6 +2380,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -2548,6 +2594,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -2567,6 +2614,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2620,6 +2668,7 @@ "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -2666,6 +2715,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, "license": "MIT" }, "node_modules/fast-content-type-parse": { @@ -2706,6 +2756,7 @@ "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==", + "dev": true, "license": "MIT" }, "node_modules/fastq": { @@ -2722,6 +2773,7 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, "license": "MIT" }, "node_modules/figures": { @@ -2744,6 +2796,7 @@ "version": "20.4.1", "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "dev": true, "license": "MIT", "dependencies": { "@tokenizer/inflate": "^0.2.6", @@ -2775,6 +2828,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -2872,6 +2926,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2881,6 +2936,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -3162,6 +3218,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3256,6 +3313,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -3364,6 +3422,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -3376,6 +3435,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", @@ -3509,6 +3569,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -3539,6 +3600,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -3548,6 +3610,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -3700,6 +3763,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "dev": true, "license": "ISC", "engines": { "node": ">=6" @@ -3863,6 +3927,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" @@ -4127,6 +4192,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4136,6 +4202,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "dev": true, "license": "MIT", "optional": true }, @@ -4156,6 +4223,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4182,6 +4250,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4255,6 +4324,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4264,6 +4334,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -4276,6 +4347,7 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "bson": "^5.5.0", @@ -4317,6 +4389,7 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@types/whatwg-url": "^8.2.1", @@ -4327,6 +4400,7 @@ "version": "7.8.8", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.8.tgz", "integrity": "sha512-0ntQOglVjlx3d+1sLK45oO5f6GuTgV/zbao0zkpE5S5W40qefpyYQ3Mq9e9nRzR58pp57WkVU+PgM64sVVcxNg==", + "dev": true, "license": "MIT", "dependencies": { "bson": "^5.5.0", @@ -4349,12 +4423,14 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "dev": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -4364,6 +4440,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dev": true, "license": "MIT", "dependencies": { "debug": "4.x" @@ -4376,6 +4453,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4393,18 +4471,21 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, "license": "MIT" }, "node_modules/multer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "dev": true, "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -4449,6 +4530,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4488,6 +4570,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" @@ -4508,18 +4591,21 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, "license": "MIT" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -6751,6 +6837,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6760,6 +6847,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6772,6 +6860,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -6999,6 +7088,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -7153,6 +7243,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, "license": "MIT" }, "node_modules/path-type": { @@ -7261,6 +7352,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -7280,6 +7372,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7289,6 +7382,7 @@ "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -7335,6 +7429,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7344,6 +7439,7 @@ "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -7444,6 +7540,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -7471,6 +7568,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, "license": "Apache-2.0" }, "node_modules/registry-auth-token": { @@ -7555,6 +7653,7 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -7584,6 +7683,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, "node_modules/semantic-release": { @@ -7701,6 +7801,7 @@ "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -7725,6 +7826,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -7737,12 +7839,14 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/serve-static": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -7758,6 +7862,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, "license": "ISC" }, "node_modules/shebang-command": { @@ -7787,6 +7892,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7806,6 +7912,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7822,6 +7929,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7840,6 +7948,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7859,6 +7968,7 @@ "version": "16.0.1", "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==", + "dev": true, "license": "MIT" }, "node_modules/signal-exit": { @@ -8007,6 +8117,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -8017,6 +8128,7 @@ "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, "license": "MIT", "dependencies": { "ip-address": "^10.0.1", @@ -8041,6 +8153,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -8104,6 +8217,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8157,6 +8271,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, "engines": { "node": ">=10.0.0" } @@ -8165,6 +8280,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -8245,6 +8361,7 @@ "version": "10.3.4", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "dev": true, "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0" @@ -8279,6 +8396,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -8520,6 +8638,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -8529,6 +8648,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, "license": "MIT", "dependencies": { "@borewit/text-codec": "^0.2.1", @@ -8547,6 +8667,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.1.1" @@ -8638,6 +8759,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD" }, "node_modules/tunnel": { @@ -8670,6 +8792,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -8683,6 +8806,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, "license": "MIT" }, "node_modules/typescript": { @@ -8717,6 +8841,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "dev": true, "license": "MIT", "dependencies": { "@lukeed/csprng": "^1.0.0" @@ -8735,6 +8860,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -8819,6 +8945,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8838,6 +8965,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -8880,6 +9008,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8896,6 +9025,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -8905,6 +9035,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, "license": "MIT", "dependencies": { "tr46": "^3.0.0", @@ -9013,6 +9144,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4" diff --git a/package.json b/package.json index 0a3f7b9..6525dce 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,6 @@ "author": "Ciscode", "license": "MIT", "dependencies": { - "@nestjs/common": "^10.4.0", - "@nestjs/core": "^10.4.0", - "@nestjs/platform-express": "^10.4.0", - "@nestjs/mongoose": "^10.0.2", "axios": "^1.7.7", "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", @@ -46,17 +42,30 @@ "dotenv": "^16.4.5", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "mongoose": "^7.6.4", "nodemailer": "^6.9.15", "passport": "^0.7.0", "passport-azure-ad-oauth2": "^0.0.4", "passport-facebook": "^3.0.0", "passport-google-oauth20": "^2.0.0", - "passport-local": "^1.0.0", + "passport-local": "^1.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "@nestjs/platform-express": "^10.0.0 || ^11.0.0", + "@nestjs/mongoose": "^10.0.0 || ^11.0.0", + "mongoose": "^7.0.0 || ^8.0.0", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.0.0" }, "devDependencies": { + "@nestjs/common": "^10.4.0", + "@nestjs/core": "^10.4.0", + "@nestjs/platform-express": "^10.4.0", + "@nestjs/mongoose": "^10.0.2", + "mongoose": "^7.6.4", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", "@types/cookie-parser": "^1.4.6", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.6", From feaf38882a52c41596c9ffe7a6371cc8aceaab5e Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sun, 25 Jan 2026 10:28:52 +0100 Subject: [PATCH 48/62] refactor: update userModel to pass null PhoneNumberValues --- src/models/user.model.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/user.model.ts b/src/models/user.model.ts index f9cdaac..25f2a5e 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -37,6 +37,7 @@ export class User { @Prop({ unique: true, trim: true, + sparse: true, match: /^[0-9]{10,14}$/, }) phoneNumber?: string; From 5f82cd9233722af86f1f6697d9dfc933378a66a6 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sun, 25 Jan 2026 10:29:15 +0100 Subject: [PATCH 49/62] refactor: update user repository to have a proper password finding method --- src/repositories/user.repository.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index b6920cf..226e8b1 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -19,6 +19,10 @@ export class UserRepository { return this.userModel.findOne({ email }); } + findByEmailWithPassword(email: string) { + return this.userModel.findOne({ email }).select('+password'); + } + findByUsername(username: string) { return this.userModel.findOne({ username }); } From 8b486bbcab1d4ac21287dd9fb0e6d7a822bce899 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sun, 25 Jan 2026 10:29:37 +0100 Subject: [PATCH 50/62] refactor: enhance auth service for login paths --- src/services/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 595d994..ae033be 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -125,7 +125,7 @@ export class AuthService { } async login(dto: LoginDto) { - const user = await this.users.findByEmail(dto.email); + const user = await this.users.findByEmailWithPassword(dto.email); if (!user) throw new Error('Invalid credentials.'); if (user.isBanned) throw new Error('Account banned.'); if (!user.isVerified) throw new Error('Email not verified.'); From 67cb44488d6e07c74cab1aff373df585d533d070 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Mon, 26 Jan 2026 12:27:37 +0100 Subject: [PATCH 51/62] refactor: fix peerDependencies issues --- package-lock.json | 6 +++--- package.json | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41411a4..0c512dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,15 +41,15 @@ "rxjs": "^7.8.1", "semantic-release": "^25.0.2", "ts-node": "^10.9.2", - "tsc-alias": "^1.8.10", + "tsc-alias": "^1.8.16", "typescript": "^5.6.2" }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", - "@nestjs/mongoose": "^10.0.0 || ^11.0.0", + "@nestjs/mongoose": "^11", "@nestjs/platform-express": "^10.0.0 || ^11.0.0", - "mongoose": "^7.0.0 || ^8.0.0", + "mongoose": "^9", "reflect-metadata": "^0.2.2", "rxjs": "^7.0.0" } diff --git a/package.json b/package.json index 6525dce..cdd2352 100644 --- a/package.json +++ b/package.json @@ -52,20 +52,17 @@ "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", + "@nestjs/mongoose": "^11", "@nestjs/platform-express": "^10.0.0 || ^11.0.0", - "@nestjs/mongoose": "^10.0.0 || ^11.0.0", - "mongoose": "^7.0.0 || ^8.0.0", + "mongoose": "^9", "reflect-metadata": "^0.2.2", "rxjs": "^7.0.0" }, "devDependencies": { "@nestjs/common": "^10.4.0", "@nestjs/core": "^10.4.0", - "@nestjs/platform-express": "^10.4.0", "@nestjs/mongoose": "^10.0.2", - "mongoose": "^7.6.4", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", + "@nestjs/platform-express": "^10.4.0", "@types/cookie-parser": "^1.4.6", "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.6", @@ -73,9 +70,12 @@ "@types/passport-facebook": "^3.0.4", "@types/passport-google-oauth20": "^2.0.15", "@types/passport-local": "^1.0.38", + "mongoose": "^7.6.4", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", "semantic-release": "^25.0.2", - "tsc-alias": "^1.8.10", "ts-node": "^10.9.2", + "tsc-alias": "^1.8.16", "typescript": "^5.6.2" } } From 30098006fb96a3ba5c8b632a64888e46dc9d9ca8 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Mon, 26 Jan 2026 12:27:57 +0100 Subject: [PATCH 52/62] refactor: Update OAuth strategies --- src/config/passport.config.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/config/passport.config.ts b/src/config/passport.config.ts index cc19382..a536b0e 100644 --- a/src/config/passport.config.ts +++ b/src/config/passport.config.ts @@ -3,6 +3,7 @@ import { Strategy as AzureStrategy } from 'passport-azure-ad-oauth2'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { Strategy as FacebookStrategy } from 'passport-facebook'; import { OAuthService } from '@services/oauth.service'; +import axios from 'axios'; export const registerOAuthStrategies = ( oauth: OAuthService @@ -10,17 +11,30 @@ export const registerOAuthStrategies = ( // Microsoft if (process.env.MICROSOFT_CLIENT_ID && process.env.MICROSOFT_CLIENT_SECRET && process.env.MICROSOFT_CALLBACK_URL) { passport.use( + 'azure_ad_oauth2', new AzureStrategy( { clientID: process.env.MICROSOFT_CLIENT_ID, clientSecret: process.env.MICROSOFT_CLIENT_SECRET, callbackURL: process.env.MICROSOFT_CALLBACK_URL, + resource: 'https://graph.microsoft.com', + tenant: process.env.MICROSOFT_TENANT_ID || 'common' }, - async (_at: any, _rt: any, params: any, _profile: any, done: any) => { + async (accessToken: any, _rt: any, _params: any, _profile: any, done: any) => { try { - const idToken = params.id_token; - const { accessToken, refreshToken } = await oauth.loginWithMicrosoft(idToken); - return done(null, { accessToken, refreshToken }); + const me = await axios.get('https://graph.microsoft.com/v1.0/me', { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + const email = me.data?.mail || me.data?.userPrincipalName; + const name = me.data?.displayName; + + if (!email) return done(null, false); + + const { accessToken: appToken, refreshToken } = + await oauth.findOrCreateOAuthUser(email, name); + + return done(null, { accessToken: appToken, refreshToken }); } catch (err) { return done(err); } @@ -32,6 +46,7 @@ export const registerOAuthStrategies = ( // Google if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET && process.env.GOOGLE_CALLBACK_URL) { passport.use( + 'google', new GoogleStrategy( { clientID: process.env.GOOGLE_CLIENT_ID, @@ -55,6 +70,7 @@ export const registerOAuthStrategies = ( // Facebook if (process.env.FB_CLIENT_ID && process.env.FB_CLIENT_SECRET && process.env.FB_CALLBACK_URL) { passport.use( + 'facebook', new FacebookStrategy( { clientID: process.env.FB_CLIENT_ID, From df70473f09bd27b3550dfc3b57fb8c7782f395f1 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Mon, 26 Jan 2026 12:29:33 +0100 Subject: [PATCH 53/62] refactor: adjust the auth controller and models for OAuth fix --- src/controllers/auth.controller.ts | 15 ++++++++++----- src/models/user.model.ts | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 58571e5..4844ab6 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -10,7 +10,7 @@ import { ForgotPasswordDto } from '@dtos/auth/forgot-password.dto'; import { ResetPasswordDto } from '@dtos/auth/reset-password.dto'; import { getMillisecondsFromExpiry } from '@utils/helper'; import { OAuthService } from '@services/oauth.service'; -import passport from 'passport'; +import passport from '@config/passport.config'; import { AuthenticateGuard } from '@middleware/authenticate.guard'; @Controller('api/auth') @@ -93,6 +93,7 @@ export class AuthController { return res.status(200).json(result); } + // Mobile exchange @Post('oauth/microsoft') async microsoftExchange(@Body() body: { idToken: string }, @Res() res: Response) { const { accessToken, refreshToken } = await this.oauth.loginWithMicrosoft(body.idToken); @@ -113,6 +114,7 @@ export class AuthController { return res.status(200).json(result); } + // Web redirect @Get('google') googleLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { return passport.authenticate('google', { scope: ['profile', 'email'], session: false })(req, res, next); @@ -128,15 +130,20 @@ export class AuthController { @Get('microsoft') microsoftLogin(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { - return passport.authenticate('azure_ad_oauth2', { session: false })(req, res, next); + return passport.authenticate('azure_ad_oauth2', { + session: false, + scope: ['openid', 'profile', 'email', 'User.Read'], + })(req, res, next); } @Get('microsoft/callback') microsoftCallback(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) { passport.authenticate('azure_ad_oauth2', { session: false }, (err: any, data: any) => { - if (err || !data) return res.status(400).json({ message: 'Microsoft auth failed.' }); + if (err) return res.status(400).json({ message: 'Microsoft auth failed', error: err?.message || err }); + if (!data) return res.status(400).json({ message: 'Microsoft auth failed', error: 'No data returned' }); return res.status(200).json(data); })(req, res, next); + } @Get('facebook') @@ -151,6 +158,4 @@ export class AuthController { return res.status(200).json(data); })(req, res, next); } - - } diff --git a/src/models/user.model.ts b/src/models/user.model.ts index 25f2a5e..4fbe44b 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -42,8 +42,8 @@ export class User { }) phoneNumber?: string; - @Prop({ required: true, minlength: 6, select: false }) - password!: string; + @Prop({ minlength: 8, select: false }) + password?: string; @Prop({ default: Date.now }) passwordChangedAt!: Date; From 2e74eeed3b12fc42fd09b23fac3ba491e3aa5b5f Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Mon, 26 Jan 2026 12:29:42 +0100 Subject: [PATCH 54/62] DOC: Update Readme documentation# --- README.md | 503 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 404 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index d933e7e..d8d3f06 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,30 @@ # AuthKit (NestJS Auth Package) -A clean, production-ready authentication/authorization kit for NestJS. -Includes local auth, OAuth (Google/Microsoft/Facebook), JWT tokens, RBAC, admin user management, email verification, and password reset. +A production-ready, comprehensive authentication/authorization kit for NestJS with local auth, OAuth (Google/Microsoft/Facebook), JWT tokens, RBAC, admin user management, email verification, and password reset. ## Features -- Local auth (email + password) -- OAuth (Google / Microsoft Entra / Facebook) - - Web redirect (Passport) - - Mobile exchange (token/code) -- JWT access + refresh (stateless) -- Email verification (required before login) -- Password reset via JWT link -- Admin user management (create/list/ban/delete/role switch) -- RBAC (roles ↔ permissions) -- Host app owns DB (package uses host Mongoose connection) +- **Local Authentication:** Email + password registration & login +- **OAuth Providers:** + - Google (ID Token validation + Authorization Code exchange) + - Microsoft (Entra ID with JWKS verification) + - Facebook (App token validation) + - Web redirect flow (Passport) + - Mobile token/code exchange +- **JWT Management:** + - Access tokens (stateless, short-lived) + - Refresh tokens (long-lived JWTs with automatic invalidation on password change) + - Email verification tokens (JWT-based links) + - Password reset tokens (JWT-based links) +- **Email Verification:** Required before login +- **Password Reset:** JWT-secured reset link +- **Admin User Management:** Create, list, ban/unban, delete, assign roles +- **RBAC (Role-Based Access Control):** + - Roles linked to users + - Permissions linked to roles + - Roles automatically included in JWT payload (Ids) + - Fine-grained access control +- **Host App Control:** Package uses host app's Mongoose connection (no DB lock-in) ## Install @@ -24,146 +34,441 @@ npm i @ciscode/authentication-kit ## Host App Setup -1. Env Vars +### 1. Environment Variables ```env - MONGO_URI=mongodb://127.0.0.1:27017/app_db +# Database +MONGO_URI=mongodb://127.0.0.1:27017/app_db -JWT_SECRET=change_me +# JWT Configuration +JWT_SECRET=your_super_secret_key_change_this JWT_ACCESS_TOKEN_EXPIRES_IN=15m -JWT_REFRESH_SECRET=change_me_too +JWT_REFRESH_SECRET=your_refresh_secret_change_this JWT_REFRESH_TOKEN_EXPIRES_IN=7d -JWT_EMAIL_SECRET=change_me_email +JWT_EMAIL_SECRET=your_email_secret_change_this JWT_EMAIL_TOKEN_EXPIRES_IN=1d -JWT_RESET_SECRET=change_me_reset +JWT_RESET_SECRET=your_reset_secret_change_this JWT_RESET_TOKEN_EXPIRES_IN=1h -SMTP_HOST=... +# Email (SMTP) +SMTP_HOST=smtp.gmail.com SMTP_PORT=587 -SMTP_USER=... -SMTP_PASS=... +SMTP_USER=your-email@gmail.com +SMTP_PASS=your-app-password SMTP_SECURE=false -FROM_EMAIL=no-reply@yourapp.com +FROM_EMAIL=noreply@yourapp.com + +# Frontend URL (for email links) FRONTEND_URL=http://localhost:3000 -GOOGLE_CLIENT_ID=... -GOOGLE_CLIENT_SECRET=... +# Google OAuth +GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=your-google-client-secret GOOGLE_CALLBACK_URL=http://localhost:3000/api/auth/google/callback -MICROSOFT_CLIENT_ID=... -MICROSOFT_CLIENT_SECRET=... +# Microsoft/Entra ID OAuth +MICROSOFT_CLIENT_ID=your-microsoft-client-id +MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret MICROSOFT_CALLBACK_URL=http://localhost:3000/api/auth/microsoft/callback +MICROSOFT_TENANT_ID=common # Optional, defaults to 'common' -FB_CLIENT_ID=... -FB_CLIENT_SECRET=... +# Facebook OAuth +FB_CLIENT_ID=your-facebook-app-id +FB_CLIENT_SECRET=your-facebook-app-secret FB_CALLBACK_URL=http://localhost:3000/api/auth/facebook/callback + +# Environment +NODE_ENV=development ``` -2. App Module +### 2. Host app example -```js - import { Module, OnModuleInit } from '@nestjs/common'; - import { MongooseModule } from '@nestjs/mongoose'; - import { AuthKitModule, SeedService } from '@ciscode/authentication-kit'; +```typescript +import { Module, OnModuleInit } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { AuthKitModule, SeedService } from '@ciscode/authentication-kit'; @Module({ -imports: [ -MongooseModule.forRoot(process.env.MONGO_URI), -AuthKitModule, -], + imports: [MongooseModule.forRoot(process.env.MONGO_URI), AuthKitModule], }) export class AppModule implements OnModuleInit { -constructor(private readonly seed: SeedService) {} - - async onModuleInit() { - await this.seed.seedDefaults(); - } + constructor(private readonly seed: SeedService) {} + async onModuleInit() { + await this.seed.seedDefaults(); + } } ``` -## Routes +> NOTES: +> +> The AuthKit, by default seeds database with default roles and permissions once the host app is bootstraped (logs are generated for info) +> Default user role, on first register, is 'user' ... Mongoose needs Id to do this relation (the app MUST seed db from the package before anything) + +## API Routes + +### Local Auth Routes (Public) + +``` +POST /api/auth/register +POST /api/auth/verify-email +POST /api/auth/resend-verification +POST /api/auth/login +POST /api/auth/refresh-token +POST /api/auth/forgot-password +POST /api/auth/reset-password +DELETE /api/auth/account (protected) +``` + +### OAuth Routes - Mobile Exchange (Public) + +Exchange OAuth provider tokens for app tokens: + +```txt +POST /api/auth/oauth/google { idToken?: string, code?: string } +POST /api/auth/oauth/microsoft { idToken: string } +POST /api/auth/oauth/facebook { accessToken: string } +``` + +### OAuth Routes - Web Redirect (Public) + +Passport-based OAuth flow for web browsers: + +```txt +GET /api/auth/google | Google OAuth +GET /api/auth/google/callback | Google Redirect (After login) +GET /api/auth/microsoft | Microsoft OAuth +GET /api/auth/microsoft/callback | Microsoft Redirect (After login) +GET /api/auth/facebook | Facebook OAuth +GET /api/auth/facebook/callback | Facebook Redirect (After login) +``` + +### Admin Routes - Users (Protected with @Admin()) + +```txt +POST /api/admin/users |Create user +GET /api/admin/users?email=...&username=... |List users (with filters) +PATCH /api/admin/users/:id/ban |Ban user +PATCH /api/admin/users/:id/unban |Unban user +PATCH /api/admin/users/:id/roles |Update user roles +DELETE /api/admin/users/:id |Delete user +``` + +### Admin Routes - Roles (Protected with @Admin()) + +```txt +POST /api/admin/roles Create role +GET /api/admin/roles List all roles +PUT /api/admin/roles/:id Update role name +PUT /api/admin/roles/:id/permissions Set role permissions +DELETE /api/admin/roles/:id Delete role +``` + +### Admin Routes - Permissions (Protected with @Admin()) ```txt -Auth (public) +POST /api/admin/permissions Create permission +GET /api/admin/permissions List all permissions +PUT /api/admin/permissions/:id Update permission +DELETE /api/admin/permissions/:id Delete permission +``` + +## Usage Examples + +### Register + +**Request:** + +```json +POST /api/auth/register +Content-Type: application/json + +{ + "fullname": { + "fname": "Test", + "lname": "User" + }, + "username": "Userrr", + "email": "user@example.com", + "password": "Pa$$word!", + "phoneNumber": "+1234567890", + "avatar": "https://example.com/avatar.jpg" +} +``` + +**Response:** + +```json +{ + "id": "507f1f77bcf86cd799439011", + "email": "user@example.com" +} +``` + +### Login + +**Request:** + +```json +POST /api/auth/login +Content-Type: application/json + +{ + "email": "user@example.com", + "password": "Pa$$word!" +} +``` + +**Response:** + +```json +{ + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +_Note: `refreshToken` is also set in httpOnly cookie_ + +### Verify Email + +**Request:** + +```json +POST /api/auth/verify-email +Content-Type: application/json + +{ + "token": "email-verification-token-from-email" +} +``` + +**Response:** + +```json +{ + "ok": true +} +``` + +### Refresh Token -- POST /api/auth/register -- POST /api/auth/verify-email -- POST /api/auth/resend-verification -- POST /api/auth/login -- POST /api/auth/refresh-token -- POST /api/auth/forgot-password -- POST /api/auth/reset-password -- DELETE /api/auth/account +**Request (from body):** + +```json +POST /api/auth/refresh-token +Content-Type: application/json + +{ + "refreshToken": "refresh-token-value" +} +``` + +**OR (from cookie - automatic):** + +```json +POST /api/auth/refresh-token +Cookie: refreshToken=refresh-token-value +``` -OAuth (mobile exchange) +**Response:** + +```json +{ + "accessToken": "new-access-token", + "refreshToken": "new-refresh-token" +} +``` + +### OAuth Google (Mobile Exchange) + +**Request (with ID Token):** + +```json +POST /api/auth/oauth/google +Content-Type: application/json + +{ + "idToken": "google-id-token-from-client" +} +``` + +**OR (with Authorization Code):** + +```json +POST /api/auth/oauth/google +Content-Type: application/json + +{ + "code": "authorization-code-from-google" +} +``` -- POST /api/auth/oauth/google { idToken | code } -- POST /api/auth/oauth/microsoft { idToken } -- POST /api/auth/oauth/facebook { accessToken } +**Response:** -OAuth (web redirect) +```json +{ + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` -- GET /api/auth/google -- GET /api/auth/google/callback -- GET /api/auth/microsoft -- GET /api/auth/microsoft/callback -- GET /api/auth/facebook -- GET /api/auth/facebook/callback +### Delete Account -Admin (protected) +**Request:** -- POST /api/admin/users -- GET /api/admin/users -- PATCH /api/admin/users/:id/ban -- PATCH /api/admin/users/:id/unban -- PATCH /api/admin/users/:id/roles -- DELETE /api/admin/users/:id +```json +DELETE /api/auth/account +Authorization: Bearer access-token +``` -- POST /api/admin/roles -- GET /api/admin/roles -- PUT /api/admin/roles/:id -- PUT /api/admin/roles/:id/permissions -- DELETE /api/admin/roles/:id +**Response:** -- POST /api/admin/permissions -- GET /api/admin/permissions -- PUT /api/admin/permissions/:id -- DELETE /api/admin/permissions/:id +```json +{ + "ok": true +} ``` -## Guards +## Guards & Decorators + +**AuthenticateGuard:** Protects routes that require authentication. (No Access if not authenticated) +**Admin Decorator:** Restricts routes to admin users only. (No Access if not an admin) + +## JWT Token Structure -```js -import { AuthenticateGuard } from '@ciscode/authentication-kit'; +### Access Token Payload -@UseGuards(AuthenticateGuard) -@Get('me') -getProfile() { ... } +```json +{ + "sub": "user-id", + "roles": ["ids"], + "iat": 1672531200, + "exp": 1672531900 +} +``` -Admin Guard -import { Admin } from '@ciscode/authentication-kit'; +### Refresh Token Payload -@Admin() -@Get('admin-only') -adminRoute() { ... } +```json +{ + "sub": "user-id", + "purpose": "refresh", + "iat": 1672531200, + "exp": 1672617600 +} ``` +**Security Note:** Refresh tokens are automatically invalidated if user changes password. The `passwordChangedAt` timestamp is checked during token refresh. + ## Seeding -On startup, call: +On app startup via `onModuleInit()`, the following are created: + +**Roles:** + +- `admin` - Full permissions +- `user` - No default permissions + +**Permissions:** + +- `users:manage` - Create, list, ban, delete users +- `roles:manage` - Create, list, update, delete roles +- `permissions:manage` - Create, list, update, delete permissions + +All permissions are assigned to the `admin` role. + +## User Model + +```typescript +{ + _id: ObjectId, + fullname: { + fname: string, + lname: string + }, + username: string (unique, 3-30 chars), + email: string (unique, validated), + phoneNumber?: string (unique, 10-14 digits), + avatar?: string (default: 'default.jpg'), + password: string (hashed, min 6 chars), + roles: ObjectId[] (references Role), + isVerified: boolean (default: false), + isBanned: boolean (default: false), + passwordChangedAt: Date, + createdAt: Date, + updatedAt: Date +} +``` + +## Role Model + +```typescript +{ + _id: ObjectId, + name: string (unique), + permissions: ObjectId[] (references Permission), + createdAt: Date, + updatedAt: Date +} +``` + +## Permission Model + +```typescript +{ + _id: ObjectId, + name: string (unique), + description?: string, + createdAt: Date, + updatedAt: Date +} +``` + +## Important Notes + +- **Database:** AuthKit does NOT manage MongoDB connection. Your host app must provide the connection via `MongooseModule.forRoot()`. +- **Stateless:** JWTs are stateless; refresh tokens are signed JWTs (not stored in DB). +- **Email Verification Required:** Users cannot login until they verify their email. +- **Password Changes Invalidate Tokens:** All refresh tokens become invalid immediately after password change. +- **OAuth Auto-Registration:** Users logging in via OAuth are automatically created with verified status. +- **Cookie + Body Support:** Refresh tokens can be passed via httpOnly cookies OR request body. +- **Admin Access:** Routes under `/api/admin/*` require the `admin` role (enforced by `@Admin()` decorator). + +## Error Handling + +The package throws errors with descriptive messages. Your host app should catch and format them appropriately: + +```typescript +try { + await authService.login(dto); +} catch (error) { + // Possible errors: + // "Invalid credentials." + // "Account banned." + // "Email not verified." + // "User not found." + // "JWT_SECRET is not set" + // etc. +} +``` + +## Development ```bash -await seed.seedDefaults(); +npm run build # Compile TypeScript + alias paths +npm run start # Run standalone (if applicable) +npm run test # Run tests (currently no tests defined) ``` -It creates: +## License + +MIT + +## Author -- Roles: admin, user -- Permissions: users:manage, roles:manage, permissions:manage +Ciscode -## Notes +--- -- AuthKit does not manage DB connection. Host app must connect to Mongo. -- JWTs are stateless; refresh tokens are signed JWTs. -- Email verification is required before login. +**Version:** 1.2.0 From 7cd8cbb49b193c9f999bcee75c1b95cf72948307 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Mon, 26 Jan 2026 13:42:49 +0100 Subject: [PATCH 55/62] refactor: update user model to contain new fields, and omitting username for unnecessary use --- README.md | 16 +++++++++++++--- src/dtos/auth/register.dto.ts | 11 ++++++++++- src/models/user.model.ts | 8 +++++++- src/services/auth.service.ts | 8 ++++++++ src/services/users.service.ts | 8 ++++++++ src/utils/helper.ts | 4 ++++ 6 files changed, 50 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d8d3f06..00105dd 100644 --- a/README.md +++ b/README.md @@ -188,14 +188,22 @@ Content-Type: application/json "fname": "Test", "lname": "User" }, - "username": "Userrr", + "username": "custom-username", "email": "user@example.com", "password": "Pa$$word!", "phoneNumber": "+1234567890", - "avatar": "https://example.com/avatar.jpg" + "avatar": "https://example.com/avatar.jpg", + "jobTitle": "Software Engineer", + "company": "Ciscode" } ``` +**Notes:** + +- `username` is now **optional**. If not provided, it will be auto-generated as `fname-lname` (e.g., `test-user`) +- `jobTitle` and `company` are **optional** profile fields +- All other fields work as before + **Response:** ```json @@ -387,10 +395,12 @@ All permissions are assigned to the `admin` role. fname: string, lname: string }, - username: string (unique, 3-30 chars), + username: string (unique, 3-30 chars, auto-generated as fname-lname if not provided), email: string (unique, validated), phoneNumber?: string (unique, 10-14 digits), avatar?: string (default: 'default.jpg'), + jobTitle?: string, + company?: string, password: string (hashed, min 6 chars), roles: ObjectId[] (references Role), isVerified: boolean (default: false), diff --git a/src/dtos/auth/register.dto.ts b/src/dtos/auth/register.dto.ts index 62ff572..dca0385 100644 --- a/src/dtos/auth/register.dto.ts +++ b/src/dtos/auth/register.dto.ts @@ -11,9 +11,10 @@ export class RegisterDto { @Type(() => FullNameDto) fullname!: FullNameDto; + @IsOptional() @IsString() @MinLength(3) - username!: string; + username?: string; @IsEmail() email!: string; @@ -29,4 +30,12 @@ export class RegisterDto { @IsOptional() @IsString() avatar?: string; + + @IsOptional() + @IsString() + jobTitle?: string; + + @IsOptional() + @IsString() + company?: string; } diff --git a/src/models/user.model.ts b/src/models/user.model.ts index 4fbe44b..956fda8 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -47,7 +47,7 @@ export class User { @Prop({ default: Date.now }) passwordChangedAt!: Date; - + @Prop({ type: [{ type: Types.ObjectId, ref: 'Role' }], required: true }) roles!: Types.ObjectId[]; @@ -57,6 +57,12 @@ export class User { @Prop({ default: false }) isBanned!: boolean; + @Prop({ trim: true, sparse: true }) + jobTitle?: string; + + @Prop({ trim: true, sparse: true }) + company?: string; + } export const UserSchema = SchemaFactory.createForClass(User); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index ae033be..f86988c 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -7,6 +7,7 @@ import { RegisterDto } from '@dtos/auth/register.dto'; import { LoginDto } from '@dtos/auth/login.dto'; import { MailService } from '@services/mail.service'; import { RoleRepository } from '@repos/role.repository'; +import { generateUsernameFromName } from '@utils/helper'; type JwtExpiry = SignOptions['expiresIn']; @@ -70,6 +71,11 @@ export class AuthService { async register(dto: RegisterDto) { + // Generate username from fname-lname if not provided + if (!dto.username || dto.username.trim() === '') { + dto.username = generateUsernameFromName(dto.fullname.fname, dto.fullname.lname); + } + if (await this.users.findByEmail(dto.email)) throw new Error('Email already in use.'); if (await this.users.findByUsername(dto.username)) throw new Error('Username already in use.'); if (dto.phoneNumber && (await this.users.findByPhone(dto.phoneNumber))) { @@ -89,6 +95,8 @@ export class AuthService { email: dto.email, phoneNumber: dto.phoneNumber, avatar: dto.avatar, + jobTitle: dto.jobTitle, + company: dto.company, password: hashed, roles: [userRole._id], isVerified: false, diff --git a/src/services/users.service.ts b/src/services/users.service.ts index 96bb786..f6f658a 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -4,6 +4,7 @@ import { UserRepository } from '@repos/user.repository'; import { RoleRepository } from '@repos/role.repository'; import { RegisterDto } from '@dtos/auth/register.dto'; import { Types } from 'mongoose'; +import { generateUsernameFromName } from '@utils/helper'; @Injectable() export class UsersService { @@ -13,6 +14,11 @@ export class UsersService { ) { } async create(dto: RegisterDto) { + // Generate username from fname-lname if not provided + if (!dto.username || dto.username.trim() === '') { + dto.username = generateUsernameFromName(dto.fullname.fname, dto.fullname.lname); + } + if (await this.users.findByEmail(dto.email)) throw new Error('Email already in use.'); if (await this.users.findByUsername(dto.username)) throw new Error('Username already in use.'); if (dto.phoneNumber && (await this.users.findByPhone(dto.phoneNumber))) { @@ -28,6 +34,8 @@ export class UsersService { email: dto.email, phoneNumber: dto.phoneNumber, avatar: dto.avatar, + jobTitle: dto.jobTitle, + company: dto.company, password: hashed, roles: [], isVerified: true, diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 23b4cd7..a025a98 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -19,3 +19,7 @@ export function getMillisecondsFromExpiry(expiry: string | number): number { return 0; } } + +export function generateUsernameFromName(fname: string, lname: string): string { + return `${fname.toLowerCase()}-${lname.toLowerCase()}`; +} From 3bea46f93f0294877fd66cb197c74b827732efb0 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Tue, 27 Jan 2026 10:42:52 +0100 Subject: [PATCH 56/62] feat: implement comprehensive error handling system Added GlobalExceptionFilter and LoggerService for centralized error handling. Replaced all generic Error throws with proper NestJS HTTP exceptions. --- src/auth-kit.module.ts | 9 + src/filters/http-exception.filter.ts | 88 ++++++ src/middleware/authenticate.guard.ts | 56 ++-- src/services/admin-role.service.ts | 31 ++- src/services/auth.service.ts | 402 ++++++++++++++++++++------- src/services/logger.service.ts | 30 ++ src/services/mail.service.ts | 47 +++- src/services/oauth.service.ts | 258 ++++++++++++----- src/services/permissions.service.ts | 64 ++++- src/services/roles.service.ts | 95 +++++-- src/services/users.service.ts | 146 +++++++--- tsconfig.json | 3 + 12 files changed, 939 insertions(+), 290 deletions(-) create mode 100644 src/filters/http-exception.filter.ts create mode 100644 src/services/logger.service.ts diff --git a/src/auth-kit.module.ts b/src/auth-kit.module.ts index 3e5e4ba..5ac0caa 100644 --- a/src/auth-kit.module.ts +++ b/src/auth-kit.module.ts @@ -1,6 +1,7 @@ import 'dotenv/config'; import { MiddlewareConsumer, Module, NestModule, OnModuleInit, RequestMethod } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; +import { APP_FILTER } from '@nestjs/core'; import cookieParser from 'cookie-parser'; import { AuthController } from '@controllers/auth.controller'; @@ -18,6 +19,7 @@ import { RolesService } from '@services/roles.service'; import { PermissionsService } from '@services/permissions.service'; import { MailService } from '@services/mail.service'; import { SeedService } from '@services/seed.service'; +import { LoggerService } from '@services/logger.service'; import { UserRepository } from '@repos/user.repository'; import { RoleRepository } from '@repos/role.repository'; @@ -27,6 +29,7 @@ import { AuthenticateGuard } from '@middleware/authenticate.guard'; import { AdminGuard } from '@middleware/admin.guard'; import { AdminRoleService } from '@services/admin-role.service'; import { OAuthService } from '@services/oauth.service'; +import { GlobalExceptionFilter } from '@filters/http-exception.filter'; import passport from 'passport'; import { registerOAuthStrategies } from '@config/passport.config'; @@ -45,12 +48,17 @@ import { registerOAuthStrategies } from '@config/passport.config'; PermissionsController, ], providers: [ + { + provide: APP_FILTER, + useClass: GlobalExceptionFilter, + }, AuthService, UsersService, RolesService, PermissionsService, MailService, SeedService, + LoggerService, UserRepository, RoleRepository, PermissionRepository, @@ -65,6 +73,7 @@ import { registerOAuthStrategies } from '@config/passport.config'; RolesService, PermissionsService, SeedService, + LoggerService, AuthenticateGuard, UserRepository, RoleRepository, diff --git a/src/filters/http-exception.filter.ts b/src/filters/http-exception.filter.ts new file mode 100644 index 0000000..77b1d92 --- /dev/null +++ b/src/filters/http-exception.filter.ts @@ -0,0 +1,88 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { Request, Response } from 'express'; + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger('ExceptionFilter'); + + catch(exception: any, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + let status = HttpStatus.INTERNAL_SERVER_ERROR; + let message = 'Internal server error'; + let errors: any = null; + + if (exception instanceof HttpException) { + status = exception.getStatus(); + const exceptionResponse = exception.getResponse(); + + if (typeof exceptionResponse === 'string') { + message = exceptionResponse; + } else if (typeof exceptionResponse === 'object') { + message = (exceptionResponse as any).message || exception.message; + errors = (exceptionResponse as any).errors || null; + } + } else if (exception?.code === 11000) { + // MongoDB duplicate key error + status = HttpStatus.CONFLICT; + message = 'Resource already exists'; + } else if (exception?.name === 'ValidationError') { + // Mongoose validation error + status = HttpStatus.BAD_REQUEST; + message = 'Validation failed'; + errors = exception.errors; + } else if (exception?.name === 'CastError') { + // Mongoose cast error (invalid ObjectId) + status = HttpStatus.BAD_REQUEST; + message = 'Invalid resource identifier'; + } else { + message = 'An unexpected error occurred'; + } + + // Log the error (but not in test environment) + if (process.env.NODE_ENV !== 'test') { + const errorLog = { + timestamp: new Date().toISOString(), + path: request.url, + method: request.method, + statusCode: status, + message: exception?.message || message, + stack: exception?.stack, + }; + + if (status >= 500) { + this.logger.error('Server error', JSON.stringify(errorLog)); + } else if (status >= 400) { + this.logger.warn('Client error', JSON.stringify(errorLog)); + } + } + + // Send response + const errorResponse: any = { + statusCode: status, + message, + timestamp: new Date().toISOString(), + path: request.url, + }; + + if (errors) { + errorResponse.errors = errors; + } + + // Don't send stack trace in production + if (process.env.NODE_ENV === 'development' && exception?.stack) { + errorResponse.stack = exception.stack; + } + + response.status(status).json(errorResponse); + } +} diff --git a/src/middleware/authenticate.guard.ts b/src/middleware/authenticate.guard.ts index b9f3f8b..1a7b96b 100644 --- a/src/middleware/authenticate.guard.ts +++ b/src/middleware/authenticate.guard.ts @@ -1,55 +1,77 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, ForbiddenException, InternalServerErrorException } from '@nestjs/common'; import jwt from 'jsonwebtoken'; import { UserRepository } from '@repos/user.repository'; +import { LoggerService } from '@services/logger.service'; @Injectable() export class AuthenticateGuard implements CanActivate { - constructor(private readonly users: UserRepository) { } + constructor( + private readonly users: UserRepository, + private readonly logger: LoggerService, + ) { } private getEnv(name: string): string { const v = process.env[name]; - if (!v) throw new Error(`${name} is not set`); + if (!v) { + this.logger.error(`Environment variable ${name} is not set`, 'AuthenticateGuard'); + throw new InternalServerErrorException('Server configuration error'); + } return v; } async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest(); - const res = context.switchToHttp().getResponse(); const authHeader = req.headers?.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { - res.status(401).json({ message: 'Missing or invalid Authorization header.' }); - return false; + throw new UnauthorizedException('Missing or invalid Authorization header'); } const token = authHeader.split(' ')[1]; + try { const decoded: any = jwt.verify(token, this.getEnv('JWT_SECRET')); const user = await this.users.findById(decoded.sub); if (!user) { - res.status(401).json({ message: 'User not found.' }); - return false; + throw new UnauthorizedException('User not found'); } + if (!user.isVerified) { - res.status(403).json({ message: 'Email not verified.' }); - return false; + throw new ForbiddenException('Email not verified. Please check your inbox'); } + if (user.isBanned) { - res.status(403).json({ message: 'Account banned.' }); - return false; + throw new ForbiddenException('Account has been banned. Please contact support'); } + + // Check if token was issued before password change if (user.passwordChangedAt && decoded.iat * 1000 < user.passwordChangedAt.getTime()) { - res.status(401).json({ message: 'Token expired.' }); - return false; + throw new UnauthorizedException('Token expired due to password change. Please login again'); } req.user = decoded; return true; - } catch { - res.status(401).json({ message: 'Invalid access token.' }); - return false; + } catch (error) { + if (error instanceof UnauthorizedException || error instanceof ForbiddenException) { + throw error; + } + + if (error.name === 'TokenExpiredError') { + throw new UnauthorizedException('Access token has expired'); + } + + if (error.name === 'JsonWebTokenError') { + throw new UnauthorizedException('Invalid access token'); + } + + if (error.name === 'NotBeforeError') { + throw new UnauthorizedException('Token not yet valid'); + } + + this.logger.error(`Authentication failed: ${error.message}`, error.stack, 'AuthenticateGuard'); + throw new UnauthorizedException('Authentication failed'); } } } diff --git a/src/services/admin-role.service.ts b/src/services/admin-role.service.ts index 7f178ab..856ee8c 100644 --- a/src/services/admin-role.service.ts +++ b/src/services/admin-role.service.ts @@ -1,17 +1,34 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { RoleRepository } from '@repos/role.repository'; +import { LoggerService } from '@services/logger.service'; @Injectable() export class AdminRoleService { private adminRoleId?: string; - constructor(private readonly roles: RoleRepository) { } + constructor( + private readonly roles: RoleRepository, + private readonly logger: LoggerService, + ) { } async loadAdminRoleId() { - if (this.adminRoleId) return this.adminRoleId; - const admin = await this.roles.findByName('admin'); - if (!admin) throw new Error('Admin role not seeded.'); - this.adminRoleId = admin._id.toString(); - return this.adminRoleId; + try { + if (this.adminRoleId) return this.adminRoleId; + + const admin = await this.roles.findByName('admin'); + if (!admin) { + this.logger.error('Admin role not found - seed data may be missing', 'AdminRoleService'); + throw new InternalServerErrorException('System configuration error'); + } + + this.adminRoleId = admin._id.toString(); + return this.adminRoleId; + } catch (error) { + if (error instanceof InternalServerErrorException) { + throw error; + } + this.logger.error(`Failed to load admin role: ${error.message}`, error.stack, 'AdminRoleService'); + throw new InternalServerErrorException('Failed to verify admin permissions'); + } } } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index f86988c..9cee40c 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, ConflictException, UnauthorizedException, NotFoundException, InternalServerErrorException, ForbiddenException, BadRequestException } from '@nestjs/common'; import type { SignOptions } from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import * as jwt from 'jsonwebtoken'; @@ -8,6 +8,7 @@ import { LoginDto } from '@dtos/auth/login.dto'; import { MailService } from '@services/mail.service'; import { RoleRepository } from '@repos/role.repository'; import { generateUsernameFromName } from '@utils/helper'; +import { LoggerService } from '@services/logger.service'; type JwtExpiry = SignOptions['expiresIn']; @@ -17,6 +18,7 @@ export class AuthService { private readonly users: UserRepository, private readonly mail: MailService, private readonly roles: RoleRepository, + private readonly logger: LoggerService, ) { } private resolveExpiry(value: string | undefined, fallback: JwtExpiry): JwtExpiry { @@ -45,20 +47,31 @@ export class AuthService { } private async buildTokenPayload(userId: string) { - const user = await this.users.findByIdWithRolesAndPermissions(userId); - if (!user) throw new Error('User not found.'); - - const roles = (user.roles || []).map((r: any) => r._id.toString()); - const permissions = (user.roles || []) - .flatMap((r: any) => (r.permissions || []).map((p: any) => p.name)) - .filter(Boolean); - - return { sub: user._id.toString(), roles, permissions }; + try { + const user = await this.users.findByIdWithRolesAndPermissions(userId); + if (!user) { + throw new NotFoundException('User not found'); + } + + const roles = (user.roles || []).map((r: any) => r._id.toString()); + const permissions = (user.roles || []) + .flatMap((r: any) => (r.permissions || []).map((p: any) => p.name)) + .filter(Boolean); + + return { sub: user._id.toString(), roles, permissions }; + } catch (error) { + if (error instanceof NotFoundException) throw error; + this.logger.error(`Failed to build token payload: ${error.message}`, error.stack, 'AuthService'); + throw new InternalServerErrorException('Failed to generate authentication token'); + } } private getEnv(name: string): string { const v = process.env[name]; - if (!v) throw new Error(`${name} is not set`); + if (!v) { + this.logger.error(`Environment variable ${name} is not set`, 'AuthService'); + throw new InternalServerErrorException('Server configuration error'); + } return v; } @@ -71,129 +84,306 @@ export class AuthService { async register(dto: RegisterDto) { - // Generate username from fname-lname if not provided - if (!dto.username || dto.username.trim() === '') { - dto.username = generateUsernameFromName(dto.fullname.fname, dto.fullname.lname); - } - - if (await this.users.findByEmail(dto.email)) throw new Error('Email already in use.'); - if (await this.users.findByUsername(dto.username)) throw new Error('Username already in use.'); - if (dto.phoneNumber && (await this.users.findByPhone(dto.phoneNumber))) { - throw new Error('Phone already in use.'); + try { + // Generate username from fname-lname if not provided + if (!dto.username || dto.username.trim() === '') { + dto.username = generateUsernameFromName(dto.fullname.fname, dto.fullname.lname); + } + + // Check for existing user (use generic message to prevent enumeration) + const [existingEmail, existingUsername, existingPhone] = await Promise.all([ + this.users.findByEmail(dto.email), + this.users.findByUsername(dto.username), + dto.phoneNumber ? this.users.findByPhone(dto.phoneNumber) : null, + ]); + + if (existingEmail || existingUsername || existingPhone) { + throw new ConflictException('An account with these credentials already exists'); + } + + // Hash password + let hashed: string; + try { + const salt = await bcrypt.genSalt(10); + hashed = await bcrypt.hash(dto.password, salt); + } catch (error) { + this.logger.error(`Password hashing failed: ${error.message}`, error.stack, 'AuthService'); + throw new InternalServerErrorException('Registration failed'); + } + + // Get default role + const userRole = await this.roles.findByName('user'); + if (!userRole) { + this.logger.error('Default user role not found - seed data may be missing', 'AuthService'); + throw new InternalServerErrorException('System configuration error'); + } + + // Create user + const user = await this.users.create({ + fullname: dto.fullname, + username: dto.username, + email: dto.email, + phoneNumber: dto.phoneNumber, + avatar: dto.avatar, + jobTitle: dto.jobTitle, + company: dto.company, + password: hashed, + roles: [userRole._id], + isVerified: false, + isBanned: false, + passwordChangedAt: new Date() + }); + + // Send verification email (don't let email failures crash registration) + try { + const emailToken = this.signEmailToken({ sub: user._id.toString(), purpose: 'verify' }); + await this.mail.sendVerificationEmail(user.email, emailToken); + } catch (error) { + this.logger.error(`Failed to send verification email: ${error.message}`, error.stack, 'AuthService'); + // Continue - user is created, they can resend verification + } + + return { id: user._id, email: user.email }; + } catch (error) { + // Re-throw HTTP exceptions + if (error instanceof ConflictException || error instanceof InternalServerErrorException) { + throw error; + } + + // Handle MongoDB duplicate key error (race condition) + if (error?.code === 11000) { + throw new ConflictException('An account with these credentials already exists'); + } + + this.logger.error(`Registration failed: ${error.message}`, error.stack, 'AuthService'); + throw new InternalServerErrorException('Registration failed. Please try again'); } + } - const salt = await bcrypt.genSalt(10); - const hashed = await bcrypt.hash(dto.password, salt); + async verifyEmail(token: string) { + try { + const decoded: any = jwt.verify(token, this.getEnv('JWT_EMAIL_SECRET')); - const userRole = await this.roles.findByName('user'); - if (!userRole) throw new Error('Default role not seeded.'); + if (decoded.purpose !== 'verify') { + throw new BadRequestException('Invalid verification token'); + } + const user = await this.users.findById(decoded.sub); + if (!user) { + throw new NotFoundException('User not found'); + } - const user = await this.users.create({ - fullname: dto.fullname, - username: dto.username, - email: dto.email, - phoneNumber: dto.phoneNumber, - avatar: dto.avatar, - jobTitle: dto.jobTitle, - company: dto.company, - password: hashed, - roles: [userRole._id], - isVerified: false, - isBanned: false, - passwordChangedAt: new Date() - }); + if (user.isVerified) { + return { ok: true, message: 'Email already verified' }; + } - const emailToken = this.signEmailToken({ sub: user._id.toString(), purpose: 'verify' }); - await this.mail.sendVerificationEmail(user.email, emailToken); + user.isVerified = true; + await user.save(); - return { id: user._id, email: user.email }; - } + return { ok: true, message: 'Email verified successfully' }; + } catch (error) { + if (error instanceof BadRequestException || error instanceof NotFoundException) { + throw error; + } - async verifyEmail(token: string) { - const decoded: any = jwt.verify(token, this.getEnv('JWT_EMAIL_SECRET')); - if (decoded.purpose !== 'verify') throw new Error('Invalid token purpose.'); + if (error.name === 'TokenExpiredError') { + throw new UnauthorizedException('Verification token has expired'); + } - const user = await this.users.findById(decoded.sub); - if (!user) throw new Error('User not found.'); - if (user.isVerified) return { ok: true }; + if (error.name === 'JsonWebTokenError') { + throw new UnauthorizedException('Invalid verification token'); + } - user.isVerified = true; - await user.save(); - return { ok: true }; + this.logger.error(`Email verification failed: ${error.message}`, error.stack, 'AuthService'); + throw new InternalServerErrorException('Email verification failed'); + } } async resendVerification(email: string) { - const user = await this.users.findByEmail(email); - if (!user || user.isVerified) return { ok: true }; - - const emailToken = this.signEmailToken({ sub: user._id.toString(), purpose: 'verify' }); - await this.mail.sendVerificationEmail(user.email, emailToken); - return { ok: true }; + try { + const user = await this.users.findByEmail(email); + + // Return success even if user not found (prevent email enumeration) + if (!user || user.isVerified) { + return { ok: true, message: 'If the email exists and is unverified, a verification email has been sent' }; + } + + const emailToken = this.signEmailToken({ sub: user._id.toString(), purpose: 'verify' }); + await this.mail.sendVerificationEmail(user.email, emailToken); + + return { ok: true, message: 'Verification email sent successfully' }; + } catch (error) { + this.logger.error(`Resend verification failed: ${error.message}`, error.stack, 'AuthService'); + // Return success to prevent email enumeration + return { ok: true, message: 'If the email exists and is unverified, a verification email has been sent' }; + } } async login(dto: LoginDto) { - const user = await this.users.findByEmailWithPassword(dto.email); - if (!user) throw new Error('Invalid credentials.'); - if (user.isBanned) throw new Error('Account banned.'); - if (!user.isVerified) throw new Error('Email not verified.'); - - const ok = await bcrypt.compare(dto.password, user.password as string); - if (!ok) throw new Error('Invalid credentials.'); - - const payload = await this.buildTokenPayload(user._id.toString()); - const accessToken = this.signAccessToken(payload); - const refreshToken = this.signRefreshToken({ sub: user._id.toString(), purpose: 'refresh' }); - - return { accessToken, refreshToken }; + try { + const user = await this.users.findByEmailWithPassword(dto.email); + + // Use generic message to prevent user enumeration + if (!user) { + throw new UnauthorizedException('Invalid email or password'); + } + + if (user.isBanned) { + throw new ForbiddenException('Account has been banned. Please contact support'); + } + + if (!user.isVerified) { + throw new ForbiddenException('Email not verified. Please check your inbox'); + } + + const passwordMatch = await bcrypt.compare(dto.password, user.password as string); + if (!passwordMatch) { + throw new UnauthorizedException('Invalid email or password'); + } + + const payload = await this.buildTokenPayload(user._id.toString()); + const accessToken = this.signAccessToken(payload); + const refreshToken = this.signRefreshToken({ sub: user._id.toString(), purpose: 'refresh' }); + + return { accessToken, refreshToken }; + } catch (error) { + if (error instanceof UnauthorizedException || error instanceof ForbiddenException) { + throw error; + } + + this.logger.error(`Login failed: ${error.message}`, error.stack, 'AuthService'); + throw new InternalServerErrorException('Login failed. Please try again'); + } } async refresh(refreshToken: string) { - const decoded: any = jwt.verify(refreshToken, this.getEnv('JWT_REFRESH_SECRET')); - if (decoded.purpose !== 'refresh') throw new Error('Invalid token purpose.'); - - const user = await this.users.findById(decoded.sub); - if (!user) throw new Error('User not found.'); - if (user.isBanned) throw new Error('Account banned.'); - if (!user.isVerified) throw new Error('Email not verified.'); - - if (user.passwordChangedAt && decoded.iat * 1000 < user.passwordChangedAt.getTime()) { - throw new Error('Token expired.'); + try { + const decoded: any = jwt.verify(refreshToken, this.getEnv('JWT_REFRESH_SECRET')); + + if (decoded.purpose !== 'refresh') { + throw new UnauthorizedException('Invalid token type'); + } + + const user = await this.users.findById(decoded.sub); + if (!user) { + throw new UnauthorizedException('Invalid refresh token'); + } + + if (user.isBanned) { + throw new ForbiddenException('Account has been banned'); + } + + if (!user.isVerified) { + throw new ForbiddenException('Email not verified'); + } + + // Check if token was issued before password change + if (user.passwordChangedAt && decoded.iat * 1000 < user.passwordChangedAt.getTime()) { + throw new UnauthorizedException('Token expired due to password change'); + } + + const payload = await this.buildTokenPayload(user._id.toString()); + const accessToken = this.signAccessToken(payload); + const newRefreshToken = this.signRefreshToken({ sub: user._id.toString(), purpose: 'refresh' }); + + return { accessToken, refreshToken: newRefreshToken }; + } catch (error) { + if (error instanceof UnauthorizedException || error instanceof ForbiddenException) { + throw error; + } + + if (error.name === 'TokenExpiredError') { + throw new UnauthorizedException('Refresh token has expired'); + } + + if (error.name === 'JsonWebTokenError') { + throw new UnauthorizedException('Invalid refresh token'); + } + + this.logger.error(`Token refresh failed: ${error.message}`, error.stack, 'AuthService'); + throw new InternalServerErrorException('Token refresh failed'); } - - const payload = await this.buildTokenPayload(user._id.toString()); - const accessToken = this.signAccessToken(payload); - const newRefreshToken = this.signRefreshToken({ sub: user._id.toString(), purpose: 'refresh' }); - - return { accessToken, refreshToken: newRefreshToken }; } async forgotPassword(email: string) { - const user = await this.users.findByEmail(email); - if (!user) return { ok: true }; - - const resetToken = this.signResetToken({ sub: user._id.toString(), purpose: 'reset' }); - await this.mail.sendPasswordResetEmail(user.email, resetToken); - return { ok: true }; + try { + const user = await this.users.findByEmail(email); + + // Always return success to prevent email enumeration + if (!user) { + return { ok: true, message: 'If the email exists, a password reset link has been sent' }; + } + + const resetToken = this.signResetToken({ sub: user._id.toString(), purpose: 'reset' }); + await this.mail.sendPasswordResetEmail(user.email, resetToken); + + return { ok: true, message: 'Password reset link sent successfully' }; + } catch (error) { + this.logger.error(`Forgot password failed: ${error.message}`, error.stack, 'AuthService'); + // Return success to prevent email enumeration + return { ok: true, message: 'If the email exists, a password reset link has been sent' }; + } } async resetPassword(token: string, newPassword: string) { - const decoded: any = jwt.verify(token, this.getEnv('JWT_RESET_SECRET')); - if (decoded.purpose !== 'reset') throw new Error('Invalid token purpose.'); - - const user = await this.users.findById(decoded.sub); - if (!user) throw new Error('User not found.'); - - const salt = await bcrypt.genSalt(10); - user.password = await bcrypt.hash(newPassword, salt); - user.passwordChangedAt = new Date(); - await user.save(); - - return { ok: true }; + try { + const decoded: any = jwt.verify(token, this.getEnv('JWT_RESET_SECRET')); + + if (decoded.purpose !== 'reset') { + throw new BadRequestException('Invalid reset token'); + } + + const user = await this.users.findById(decoded.sub); + if (!user) { + throw new NotFoundException('User not found'); + } + + // Hash new password + let hashedPassword: string; + try { + const salt = await bcrypt.genSalt(10); + hashedPassword = await bcrypt.hash(newPassword, salt); + } catch (error) { + this.logger.error(`Password hashing failed: ${error.message}`, error.stack, 'AuthService'); + throw new InternalServerErrorException('Password reset failed'); + } + + user.password = hashedPassword; + user.passwordChangedAt = new Date(); + await user.save(); + + return { ok: true, message: 'Password reset successfully' }; + } catch (error) { + if (error instanceof BadRequestException || error instanceof NotFoundException || error instanceof InternalServerErrorException) { + throw error; + } + + if (error.name === 'TokenExpiredError') { + throw new UnauthorizedException('Reset token has expired'); + } + + if (error.name === 'JsonWebTokenError') { + throw new UnauthorizedException('Invalid reset token'); + } + + this.logger.error(`Password reset failed: ${error.message}`, error.stack, 'AuthService'); + throw new InternalServerErrorException('Password reset failed'); + } } async deleteAccount(userId: string) { - await this.users.deleteById(userId); - return { ok: true }; + try { + const user = await this.users.deleteById(userId); + if (!user) { + throw new NotFoundException('User not found'); + } + return { ok: true, message: 'Account deleted successfully' }; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Account deletion failed: ${error.message}`, error.stack, 'AuthService'); + throw new InternalServerErrorException('Account deletion failed'); + } } } diff --git a/src/services/logger.service.ts b/src/services/logger.service.ts new file mode 100644 index 0000000..ff2e737 --- /dev/null +++ b/src/services/logger.service.ts @@ -0,0 +1,30 @@ +import { Injectable, Logger as NestLogger } from '@nestjs/common'; + +@Injectable() +export class LoggerService { + private logger = new NestLogger('AuthKit'); + + log(message: string, context?: string) { + this.logger.log(message, context); + } + + error(message: string, trace?: string, context?: string) { + this.logger.error(message, trace, context); + } + + warn(message: string, context?: string) { + this.logger.warn(message, context); + } + + debug(message: string, context?: string) { + if (process.env.NODE_ENV === 'development') { + this.logger.debug(message, context); + } + } + + verbose(message: string, context?: string) { + if (process.env.NODE_ENV === 'development') { + this.logger.verbose(message, context); + } + } +} diff --git a/src/services/mail.service.ts b/src/services/mail.service.ts index b771a90..81a50c6 100644 --- a/src/services/mail.service.ts +++ b/src/services/mail.service.ts @@ -1,5 +1,8 @@ import nodemailer from 'nodemailer'; +import { Injectable } from '@nestjs/common'; +import { LoggerService } from '@services/logger.service'; +@Injectable() export class MailService { private transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, @@ -11,23 +14,39 @@ export class MailService { } }); + constructor(private readonly logger: LoggerService) { } + async sendVerificationEmail(email: string, token: string) { - const url = `${process.env.FRONTEND_URL}/confirm-email?token=${token}`; - await this.transporter.sendMail({ - from: process.env.FROM_EMAIL, - to: email, - subject: 'Verify your email', - text: `Click to verify your email: ${url}` - }); + try { + const url = `${process.env.FRONTEND_URL}/confirm-email?token=${token}`; + await this.transporter.sendMail({ + from: process.env.FROM_EMAIL, + to: email, + subject: 'Verify your email', + text: `Click to verify your email: ${url}`, + html: `

Click here to verify your email

` + }); + this.logger.log(`Verification email sent to ${email}`, 'MailService'); + } catch (error) { + this.logger.error(`Failed to send verification email to ${email}: ${error.message}`, error.stack, 'MailService'); + throw error; // Re-throw so caller can handle + } } async sendPasswordResetEmail(email: string, token: string) { - const url = `${process.env.FRONTEND_URL}/reset-password?token=${token}`; - await this.transporter.sendMail({ - from: process.env.FROM_EMAIL, - to: email, - subject: 'Reset your password', - text: `Reset your password: ${url}` - }); + try { + const url = `${process.env.FRONTEND_URL}/reset-password?token=${token}`; + await this.transporter.sendMail({ + from: process.env.FROM_EMAIL, + to: email, + subject: 'Reset your password', + text: `Reset your password: ${url}`, + html: `

Click here to reset your password

` + }); + this.logger.log(`Password reset email sent to ${email}`, 'MailService'); + } catch (error) { + this.logger.error(`Failed to send password reset email to ${email}: ${error.message}`, error.stack, 'MailService'); + throw error; // Re-throw so caller can handle + } } } diff --git a/src/services/oauth.service.ts b/src/services/oauth.service.ts index e957e72..bb0c26f 100644 --- a/src/services/oauth.service.ts +++ b/src/services/oauth.service.ts @@ -1,10 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import axios from 'axios'; +import { Injectable, UnauthorizedException, InternalServerErrorException, BadRequestException } from '@nestjs/common'; +import axios, { AxiosError } from 'axios'; import jwt from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; import { UserRepository } from '@repos/user.repository'; import { RoleRepository } from '@repos/role.repository'; import { AuthService } from '@services/auth.service'; +import { LoggerService } from '@services/logger.service'; @Injectable() export class OAuthService { @@ -15,15 +16,24 @@ export class OAuthService { jwksRequestsPerMinute: 5, }); + // Configure axios with timeout + private axiosConfig = { + timeout: 10000, // 10 seconds + }; + constructor( private readonly users: UserRepository, private readonly roles: RoleRepository, - private readonly auth: AuthService + private readonly auth: AuthService, + private readonly logger: LoggerService, ) { } private async getDefaultRoleId() { const role = await this.roles.findByName('user'); - if (!role) throw new Error('Default role not seeded.'); + if (!role) { + this.logger.error('Default user role not found - seed data missing', 'OAuthService'); + throw new InternalServerErrorException('System configuration error'); + } return role._id; } @@ -33,103 +43,209 @@ export class OAuthService { this.msJwks .getSigningKey(header.kid) .then((k) => cb(null, k.getPublicKey())) - .catch(cb); + .catch((err) => { + this.logger.error(`Failed to get Microsoft signing key: ${err.message}`, err.stack, 'OAuthService'); + cb(err); + }); }; jwt.verify( idToken, getKey as any, { algorithms: ['RS256'], audience: process.env.MICROSOFT_CLIENT_ID }, - (err, payload) => (err ? reject(err) : resolve(payload)) + (err, payload) => { + if (err) { + this.logger.error(`Microsoft token verification failed: ${err.message}`, err.stack, 'OAuthService'); + reject(new UnauthorizedException('Invalid Microsoft token')); + } else { + resolve(payload); + } + } ); }); } async loginWithMicrosoft(idToken: string) { - const ms: any = await this.verifyMicrosoftIdToken(idToken); - const email = ms.preferred_username || ms.email; - if (!email) throw new Error('Email missing'); - - return this.findOrCreateOAuthUser(email, ms.name); + try { + const ms: any = await this.verifyMicrosoftIdToken(idToken); + const email = ms.preferred_username || ms.email; + + if (!email) { + throw new BadRequestException('Email not provided by Microsoft'); + } + + return this.findOrCreateOAuthUser(email, ms.name); + } catch (error) { + if (error instanceof UnauthorizedException || error instanceof BadRequestException) { + throw error; + } + this.logger.error(`Microsoft login failed: ${error.message}`, error.stack, 'OAuthService'); + throw new UnauthorizedException('Microsoft authentication failed'); + } } async loginWithGoogleIdToken(idToken: string) { - const verifyResp = await axios.get('https://oauth2.googleapis.com/tokeninfo', { - params: { id_token: idToken }, - }); - const email = verifyResp.data?.email; - if (!email) throw new Error('Email missing'); + try { + const verifyResp = await axios.get('https://oauth2.googleapis.com/tokeninfo', { + params: { id_token: idToken }, + ...this.axiosConfig, + }); - return this.findOrCreateOAuthUser(email, verifyResp.data?.name); + const email = verifyResp.data?.email; + if (!email) { + throw new BadRequestException('Email not provided by Google'); + } + + return this.findOrCreateOAuthUser(email, verifyResp.data?.name); + } catch (error) { + if (error instanceof BadRequestException) { + throw error; + } + + const axiosError = error as AxiosError; + if (axiosError.code === 'ECONNABORTED') { + this.logger.error('Google API timeout', axiosError.stack, 'OAuthService'); + throw new InternalServerErrorException('Authentication service timeout'); + } + + this.logger.error(`Google ID token login failed: ${error.message}`, error.stack, 'OAuthService'); + throw new UnauthorizedException('Google authentication failed'); + } } async loginWithGoogleCode(code: string) { - const tokenResp = await axios.post('https://oauth2.googleapis.com/token', { - code, - client_id: process.env.GOOGLE_CLIENT_ID, - client_secret: process.env.GOOGLE_CLIENT_SECRET, - redirect_uri: 'postmessage', - grant_type: 'authorization_code', - }); - - const { access_token } = tokenResp.data || {}; - if (!access_token) throw new Error('Failed to exchange code'); - - const profileResp = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { - headers: { Authorization: `Bearer ${access_token}` }, - }); - - const email = profileResp.data?.email; - if (!email) throw new Error('Email missing'); + try { + const tokenResp = await axios.post('https://oauth2.googleapis.com/token', { + code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: 'postmessage', + grant_type: 'authorization_code', + }, this.axiosConfig); + + const { access_token } = tokenResp.data || {}; + if (!access_token) { + throw new BadRequestException('Failed to exchange authorization code'); + } + + const profileResp = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${access_token}` }, + ...this.axiosConfig, + }); - return this.findOrCreateOAuthUser(email, profileResp.data?.name); + const email = profileResp.data?.email; + if (!email) { + throw new BadRequestException('Email not provided by Google'); + } + + return this.findOrCreateOAuthUser(email, profileResp.data?.name); + } catch (error) { + if (error instanceof BadRequestException) { + throw error; + } + + const axiosError = error as AxiosError; + if (axiosError.code === 'ECONNABORTED') { + this.logger.error('Google API timeout', axiosError.stack, 'OAuthService'); + throw new InternalServerErrorException('Authentication service timeout'); + } + + this.logger.error(`Google code exchange failed: ${error.message}`, error.stack, 'OAuthService'); + throw new UnauthorizedException('Google authentication failed'); + } } async loginWithFacebook(accessToken: string) { - const appTokenResp = await axios.get('https://graph.facebook.com/oauth/access_token', { - params: { - client_id: process.env.FB_CLIENT_ID, - client_secret: process.env.FB_CLIENT_SECRET, - grant_type: 'client_credentials', - }, - }); + try { + const appTokenResp = await axios.get('https://graph.facebook.com/oauth/access_token', { + params: { + client_id: process.env.FB_CLIENT_ID, + client_secret: process.env.FB_CLIENT_SECRET, + grant_type: 'client_credentials', + }, + ...this.axiosConfig, + }); - const appAccessToken = appTokenResp.data?.access_token; - const debug = await axios.get('https://graph.facebook.com/debug_token', { - params: { input_token: accessToken, access_token: appAccessToken }, - }); + const appAccessToken = appTokenResp.data?.access_token; + if (!appAccessToken) { + throw new InternalServerErrorException('Failed to get Facebook app token'); + } - if (!debug.data?.data?.is_valid) throw new Error('Invalid Facebook token'); + const debug = await axios.get('https://graph.facebook.com/debug_token', { + params: { input_token: accessToken, access_token: appAccessToken }, + ...this.axiosConfig, + }); - const me = await axios.get('https://graph.facebook.com/me', { - params: { access_token: accessToken, fields: 'id,name,email' }, - }); + if (!debug.data?.data?.is_valid) { + throw new UnauthorizedException('Invalid Facebook access token'); + } - const email = me.data?.email; - if (!email) throw new Error('Email missing'); + const me = await axios.get('https://graph.facebook.com/me', { + params: { access_token: accessToken, fields: 'id,name,email' }, + ...this.axiosConfig, + }); - return this.findOrCreateOAuthUser(email, me.data?.name); + const email = me.data?.email; + if (!email) { + throw new BadRequestException('Email not provided by Facebook'); + } + + return this.findOrCreateOAuthUser(email, me.data?.name); + } catch (error) { + if (error instanceof UnauthorizedException || error instanceof BadRequestException || error instanceof InternalServerErrorException) { + throw error; + } + + const axiosError = error as AxiosError; + if (axiosError.code === 'ECONNABORTED') { + this.logger.error('Facebook API timeout', axiosError.stack, 'OAuthService'); + throw new InternalServerErrorException('Authentication service timeout'); + } + + this.logger.error(`Facebook login failed: ${error.message}`, error.stack, 'OAuthService'); + throw new UnauthorizedException('Facebook authentication failed'); + } } async findOrCreateOAuthUser(email: string, name?: string) { - let user = await this.users.findByEmail(email); - if (!user) { - const [fname, ...rest] = (name || 'User OAuth').split(' '); - const lname = rest.join(' ') || 'OAuth'; - - const defaultRoleId = await this.getDefaultRoleId(); - user = await this.users.create({ - fullname: { fname, lname }, - username: email.split('@')[0], - email, - roles: [defaultRoleId], - isVerified: true, - isBanned: false, - passwordChangedAt: new Date() - }); + try { + let user = await this.users.findByEmail(email); + + if (!user) { + const [fname, ...rest] = (name || 'User OAuth').split(' '); + const lname = rest.join(' ') || 'OAuth'; + + const defaultRoleId = await this.getDefaultRoleId(); + + user = await this.users.create({ + fullname: { fname, lname }, + username: email.split('@')[0], + email, + roles: [defaultRoleId], + isVerified: true, + isBanned: false, + passwordChangedAt: new Date() + }); + } + + const { accessToken, refreshToken } = await this.auth.issueTokensForUser(user._id.toString()); + return { accessToken, refreshToken }; + } catch (error) { + if (error?.code === 11000) { + // Race condition - user was created between check and insert, retry once + try { + const user = await this.users.findByEmail(email); + if (user) { + const { accessToken, refreshToken } = await this.auth.issueTokensForUser(user._id.toString()); + return { accessToken, refreshToken }; + } + } catch (retryError) { + this.logger.error(`OAuth user retry failed: ${retryError.message}`, retryError.stack, 'OAuthService'); + } + } + + this.logger.error(`OAuth user creation/login failed: ${error.message}`, error.stack, 'OAuthService'); + throw new InternalServerErrorException('Authentication failed'); } - - const { accessToken, refreshToken } = await this.auth.issueTokensForUser(user._id.toString()); - return { accessToken, refreshToken }; } } diff --git a/src/services/permissions.service.ts b/src/services/permissions.service.ts index 41893e3..2b4f645 100644 --- a/src/services/permissions.service.ts +++ b/src/services/permissions.service.ts @@ -1,30 +1,72 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, ConflictException, NotFoundException, InternalServerErrorException } from '@nestjs/common'; import { PermissionRepository } from '@repos/permission.repository'; import { CreatePermissionDto } from '@dtos/permission/create-permission.dto'; import { UpdatePermissionDto } from '@dtos/permission/update-permission.dto'; +import { LoggerService } from '@services/logger.service'; @Injectable() export class PermissionsService { - constructor(private readonly perms: PermissionRepository) { } + constructor( + private readonly perms: PermissionRepository, + private readonly logger: LoggerService, + ) { } async create(dto: CreatePermissionDto) { - if (await this.perms.findByName(dto.name)) throw new Error('Permission already exists.'); - return this.perms.create(dto); + try { + if (await this.perms.findByName(dto.name)) { + throw new ConflictException('Permission already exists'); + } + return this.perms.create(dto); + } catch (error) { + if (error instanceof ConflictException) { + throw error; + } + if (error?.code === 11000) { + throw new ConflictException('Permission already exists'); + } + this.logger.error(`Permission creation failed: ${error.message}`, error.stack, 'PermissionsService'); + throw new InternalServerErrorException('Failed to create permission'); + } } async list() { - return this.perms.list(); + try { + return this.perms.list(); + } catch (error) { + this.logger.error(`Permission list failed: ${error.message}`, error.stack, 'PermissionsService'); + throw new InternalServerErrorException('Failed to retrieve permissions'); + } } async update(id: string, dto: UpdatePermissionDto) { - const perm = await this.perms.updateById(id, dto); - if (!perm) throw new Error('Permission not found.'); - return perm; + try { + const perm = await this.perms.updateById(id, dto); + if (!perm) { + throw new NotFoundException('Permission not found'); + } + return perm; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Permission update failed: ${error.message}`, error.stack, 'PermissionsService'); + throw new InternalServerErrorException('Failed to update permission'); + } } async delete(id: string) { - const perm = await this.perms.deleteById(id); - if (!perm) throw new Error('Permission not found.'); - return { ok: true }; + try { + const perm = await this.perms.deleteById(id); + if (!perm) { + throw new NotFoundException('Permission not found'); + } + return { ok: true }; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Permission deletion failed: ${error.message}`, error.stack, 'PermissionsService'); + throw new InternalServerErrorException('Failed to delete permission'); + } } } diff --git a/src/services/roles.service.ts b/src/services/roles.service.ts index 74eec05..cabf16f 100644 --- a/src/services/roles.service.ts +++ b/src/services/roles.service.ts @@ -1,50 +1,99 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, ConflictException, NotFoundException, InternalServerErrorException } from '@nestjs/common'; import { RoleRepository } from '@repos/role.repository'; import { CreateRoleDto } from '@dtos/role/create-role.dto'; import { UpdateRoleDto } from '@dtos/role/update-role.dto'; import { Types } from 'mongoose'; +import { LoggerService } from '@services/logger.service'; @Injectable() export class RolesService { - constructor(private readonly roles: RoleRepository) { } + constructor( + private readonly roles: RoleRepository, + private readonly logger: LoggerService, + ) { } async create(dto: CreateRoleDto) { - if (await this.roles.findByName(dto.name)) throw new Error('Role already exists.'); - const permIds = (dto.permissions || []).map((p) => new Types.ObjectId(p)); - return this.roles.create({ name: dto.name, permissions: permIds }); - + try { + if (await this.roles.findByName(dto.name)) { + throw new ConflictException('Role already exists'); + } + const permIds = (dto.permissions || []).map((p) => new Types.ObjectId(p)); + return this.roles.create({ name: dto.name, permissions: permIds }); + } catch (error) { + if (error instanceof ConflictException) { + throw error; + } + if (error?.code === 11000) { + throw new ConflictException('Role already exists'); + } + this.logger.error(`Role creation failed: ${error.message}`, error.stack, 'RolesService'); + throw new InternalServerErrorException('Failed to create role'); + } } async list() { - return this.roles.list(); + try { + return this.roles.list(); + } catch (error) { + this.logger.error(`Role list failed: ${error.message}`, error.stack, 'RolesService'); + throw new InternalServerErrorException('Failed to retrieve roles'); + } } async update(id: string, dto: UpdateRoleDto) { - const data: any = { ...dto }; + try { + const data: any = { ...dto }; - if (dto.permissions) { - data.permissions = dto.permissions.map((p) => new Types.ObjectId(p)); - } + if (dto.permissions) { + data.permissions = dto.permissions.map((p) => new Types.ObjectId(p)); + } - const role = await this.roles.updateById(id, data); - if (!role) throw new Error('Role not found.'); - return role; + const role = await this.roles.updateById(id, data); + if (!role) { + throw new NotFoundException('Role not found'); + } + return role; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Role update failed: ${error.message}`, error.stack, 'RolesService'); + throw new InternalServerErrorException('Failed to update role'); + } } async delete(id: string) { - const role = await this.roles.deleteById(id); - if (!role) throw new Error('Role not found.'); - return { ok: true }; + try { + const role = await this.roles.deleteById(id); + if (!role) { + throw new NotFoundException('Role not found'); + } + return { ok: true }; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Role deletion failed: ${error.message}`, error.stack, 'RolesService'); + throw new InternalServerErrorException('Failed to delete role'); + } } async setPermissions(roleId: string, permissionIds: string[]) { - const permIds = permissionIds.map((p) => new Types.ObjectId(p)); - const role = await this.roles.updateById(roleId, { permissions: permIds }); - if (!role) throw new Error('Role not found.'); - return role; - - + try { + const permIds = permissionIds.map((p) => new Types.ObjectId(p)); + const role = await this.roles.updateById(roleId, { permissions: permIds }); + if (!role) { + throw new NotFoundException('Role not found'); + } + return role; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Set permissions failed: ${error.message}`, error.stack, 'RolesService'); + throw new InternalServerErrorException('Failed to set permissions'); + } } } diff --git a/src/services/users.service.ts b/src/services/users.service.ts index f6f658a..be563b2 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -1,75 +1,139 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, ConflictException, NotFoundException, InternalServerErrorException } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { UserRepository } from '@repos/user.repository'; import { RoleRepository } from '@repos/role.repository'; import { RegisterDto } from '@dtos/auth/register.dto'; import { Types } from 'mongoose'; import { generateUsernameFromName } from '@utils/helper'; +import { LoggerService } from '@services/logger.service'; @Injectable() export class UsersService { constructor( private readonly users: UserRepository, - private readonly rolesRepo: RoleRepository + private readonly rolesRepo: RoleRepository, + private readonly logger: LoggerService, ) { } async create(dto: RegisterDto) { - // Generate username from fname-lname if not provided - if (!dto.username || dto.username.trim() === '') { - dto.username = generateUsernameFromName(dto.fullname.fname, dto.fullname.lname); - } + try { + // Generate username from fname-lname if not provided + if (!dto.username || dto.username.trim() === '') { + dto.username = generateUsernameFromName(dto.fullname.fname, dto.fullname.lname); + } - if (await this.users.findByEmail(dto.email)) throw new Error('Email already in use.'); - if (await this.users.findByUsername(dto.username)) throw new Error('Username already in use.'); - if (dto.phoneNumber && (await this.users.findByPhone(dto.phoneNumber))) { - throw new Error('Phone already in use.'); - } + // Check for existing user + const [existingEmail, existingUsername, existingPhone] = await Promise.all([ + this.users.findByEmail(dto.email), + this.users.findByUsername(dto.username), + dto.phoneNumber ? this.users.findByPhone(dto.phoneNumber) : null, + ]); + + if (existingEmail || existingUsername || existingPhone) { + throw new ConflictException('An account with these credentials already exists'); + } + + // Hash password + let hashed: string; + try { + const salt = await bcrypt.genSalt(10); + hashed = await bcrypt.hash(dto.password, salt); + } catch (error) { + this.logger.error(`Password hashing failed: ${error.message}`, error.stack, 'UsersService'); + throw new InternalServerErrorException('User creation failed'); + } + + const user = await this.users.create({ + fullname: dto.fullname, + username: dto.username, + email: dto.email, + phoneNumber: dto.phoneNumber, + avatar: dto.avatar, + jobTitle: dto.jobTitle, + company: dto.company, + password: hashed, + roles: [], + isVerified: true, + isBanned: false, + passwordChangedAt: new Date() + }); - const salt = await bcrypt.genSalt(10); - const hashed = await bcrypt.hash(dto.password, salt); + return { id: user._id, email: user.email }; + } catch (error) { + if (error instanceof ConflictException || error instanceof InternalServerErrorException) { + throw error; + } - const user = await this.users.create({ - fullname: dto.fullname, - username: dto.username, - email: dto.email, - phoneNumber: dto.phoneNumber, - avatar: dto.avatar, - jobTitle: dto.jobTitle, - company: dto.company, - password: hashed, - roles: [], - isVerified: true, - isBanned: false, - passwordChangedAt: new Date() - }); + if (error?.code === 11000) { + throw new ConflictException('An account with these credentials already exists'); + } - return { id: user._id, email: user.email }; + this.logger.error(`User creation failed: ${error.message}`, error.stack, 'UsersService'); + throw new InternalServerErrorException('User creation failed'); + } } async list(filter: { email?: string; username?: string }) { - return this.users.list(filter); + try { + return this.users.list(filter); + } catch (error) { + this.logger.error(`User list failed: ${error.message}`, error.stack, 'UsersService'); + throw new InternalServerErrorException('Failed to retrieve users'); + } } async setBan(id: string, banned: boolean) { - const user = await this.users.updateById(id, { isBanned: banned }); - if (!user) throw new Error('User not found.'); - return { id: user._id, isBanned: user.isBanned }; + try { + const user = await this.users.updateById(id, { isBanned: banned }); + if (!user) { + throw new NotFoundException('User not found'); + } + return { id: user._id, isBanned: user.isBanned }; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Set ban status failed: ${error.message}`, error.stack, 'UsersService'); + throw new InternalServerErrorException('Failed to update user ban status'); + } } async delete(id: string) { - const user = await this.users.deleteById(id); - if (!user) throw new Error('User not found.'); - return { ok: true }; + try { + const user = await this.users.deleteById(id); + if (!user) { + throw new NotFoundException('User not found'); + } + return { ok: true }; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`User deletion failed: ${error.message}`, error.stack, 'UsersService'); + throw new InternalServerErrorException('Failed to delete user'); + } } async updateRoles(id: string, roles: string[]) { - const existing = await this.rolesRepo.findByIds(roles); - if (existing.length !== roles.length) throw new Error('One or more roles not found.'); + try { + const existing = await this.rolesRepo.findByIds(roles); + if (existing.length !== roles.length) { + throw new NotFoundException('One or more roles not found'); + } - const roleIds = roles.map((r) => new Types.ObjectId(r)); - const user = await this.users.updateById(id, { roles: roleIds }); - if (!user) throw new Error('User not found.'); - return { id: user._id, roles: user.roles }; + const roleIds = roles.map((r) => new Types.ObjectId(r)); + const user = await this.users.updateById(id, { roles: roleIds }); + if (!user) { + throw new NotFoundException('User not found'); + } + return { id: user._id, roles: user.roles }; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Update user roles failed: ${error.message}`, error.stack, 'UsersService'); + throw new InternalServerErrorException('Failed to update user roles'); + } } } diff --git a/tsconfig.json b/tsconfig.json index 7495a09..7024411 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,9 @@ "@middleware/*": [ "src/middleware/*" ], + "@filters/*": [ + "src/filters/*" + ], "@utils/*": [ "src/utils/*" ] From 3bcb6cce6984dc29a604e34eb25e4ae6206fd48e Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 28 Jan 2026 09:53:12 +0100 Subject: [PATCH 57/62] chore create new user service funcion to retrieve user data --- src/services/auth.service.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 9cee40c..4bed51f 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -82,6 +82,35 @@ export class AuthService { return { accessToken, refreshToken }; } + async getMe(userId: string) { + try { + const user = await this.users.findByIdWithRolesAndPermissions(userId); + + if (!user) { + throw new NotFoundException('User not found'); + } + + if (user.isBanned) { + throw new ForbiddenException('Account has been banned. Please contact support'); + } + + // Return user data without sensitive information + const userObject = user.toObject ? user.toObject() : user; + const { password, passwordChangedAt, ...safeUser } = userObject as any; + + return { + ok: true, + data: safeUser + }; + } catch (error) { + if (error instanceof NotFoundException || error instanceof ForbiddenException) { + throw error; + } + + this.logger.error(`Get profile failed: ${error.message}`, error.stack, 'AuthService'); + throw new InternalServerErrorException('Failed to retrieve profile'); + } + } async register(dto: RegisterDto) { try { From c4ab64e5298f4d3032f4570fef761fd8b058fafe Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 28 Jan 2026 09:53:25 +0100 Subject: [PATCH 58/62] chore: added users `me` end point --- src/controllers/auth.controller.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 4844ab6..1933dff 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -84,6 +84,15 @@ export class AuthController { return res.status(200).json(result); } + @Get('me') + @UseGuards(AuthenticateGuard) + async getMe(@Req() req: Request, @Res() res: Response) { + const userId = (req as any).user?.sub; + if (!userId) return res.status(401).json({ message: 'Unauthorized.' }); + const result = await this.auth.getMe(userId); + return res.status(200).json(result); + } + @Delete('account') @UseGuards(AuthenticateGuard) async deleteAccount(@Req() req: Request, @Res() res: Response) { From a05eed2e0aa0d4db6ba49f0ad1a2df888234677e Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Wed, 28 Jan 2026 09:53:41 +0100 Subject: [PATCH 59/62] docs: updated README doc for new endpoint implementation --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 00105dd..6364d45 100644 --- a/README.md +++ b/README.md @@ -107,17 +107,18 @@ export class AppModule implements OnModuleInit { ## API Routes -### Local Auth Routes (Public) +### Local Auth Routes ``` -POST /api/auth/register -POST /api/auth/verify-email -POST /api/auth/resend-verification -POST /api/auth/login -POST /api/auth/refresh-token -POST /api/auth/forgot-password -POST /api/auth/reset-password -DELETE /api/auth/account (protected) +POST /api/auth/register | Register new user (public) +POST /api/auth/verify-email | Verify email with token (public) +POST /api/auth/resend-verification | Resend verification email (public) +POST /api/auth/login | Login with credentials (public) +POST /api/auth/refresh-token | Refresh access token (public) +POST /api/auth/forgot-password | Request password reset (public) +POST /api/auth/reset-password | Reset password with token (public) +GET /api/auth/me | Get current user profile (protected) +DELETE /api/auth/account | Delete own account (protected) ``` ### OAuth Routes - Mobile Exchange (Public) @@ -321,6 +322,54 @@ Content-Type: application/json } ``` +### Get Current User Profile + +**Request:** + +```json +GET /api/auth/me +Authorization: Bearer access-token +``` + +**Response:** + +```json +{ + "ok": true, + "data": { + "_id": "507f1f77bcf86cd799439011", + "fullname": { + "fname": "Test", + "lname": "User" + }, + "username": "test-user", + "email": "user@example.com", + "avatar": "https://example.com/avatar.jpg", + "phoneNumber": "+1234567890", + "jobTitle": "Software Engineer", + "company": "Ciscode", + "isVerified": true, + "isBanned": false, + "roles": [ + { + "_id": "507f1f77bcf86cd799439012", + "name": "user", + "permissions": [ + { + "_id": "507f1f77bcf86cd799439013", + "name": "read:profile" + } + ] + } + ], + "createdAt": "2026-01-28T10:00:00.000Z", + "updatedAt": "2026-01-28T10:00:00.000Z" + } +} +``` + +**Note:** Sensitive fields like `password` and `passwordChangedAt` are automatically excluded from the response. + ### Delete Account **Request:** From a419adb75ecb023326e388ab495319e94e4c115b Mon Sep 17 00:00:00 2001 From: Zaiid Moumni <141942826+Zaiidmo@users.noreply.github.com> Date: Sat, 31 Jan 2026 19:36:44 +0100 Subject: [PATCH 60/62] docs(workflow): add Git Flow and npm version requirements (#6) - Add Git Flow branching strategy (develop/master) - Document npm version command before push - Add prepublishOnly hook recommendation - Update workflow with proper branch management - Clear warnings about PR targeting Co-authored-by: Reda Channa --- .github/copilot-instructions.md | 73 +++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 069204e..f8a6d8d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -316,6 +316,44 @@ Move to archive: docs/tasks/archive/by-release/v2.0.0/MODULE-123-add-refresh-token.md ``` +### Git Flow - Module Specific + +**Branch Structure:** +- `master` - Production releases only +- `develop` - Active development +- `feature/MODULE-*` - New features +- `bugfix/MODULE-*` - Bug fixes + +**Workflow:** +```bash +# 1. Stacca da develop +git checkout develop +git pull origin develop +git checkout -b feature/MODULE-123-add-refresh-token + +# 2. Sviluppo +# ... implementa, testa, documenta ... + +# 3. Bump version e push +npm version minor +git push origin feature/MODULE-123-add-refresh-token --tags + +# 4. PR verso develop +gh pr create --base develop + +# 5. Dopo merge in develop, per release: +git checkout master +git merge develop +git push origin master --tags +npm publish +``` + +**⚠️ IMPORTANTE:** +- ✅ Feature branch da `develop` +- ✅ PR verso `develop` +- ✅ `master` solo per release +- ❌ MAI PR dirette a `master` + ### Development Workflow **Simple changes** (bug fix, small improvements): @@ -379,6 +417,22 @@ docs/tasks/archive/by-release/v2.0.0/MODULE-123-add-refresh-token.md - Token expiration validation ``` +### Version Bump Command +**ALWAYS run before pushing:** +```bash +npm version patch # Bug fixes (0.0.x) +npm version minor # New features (0.x.0) +npm version major # Breaking changes (x.0.0) + +# This automatically: +# - Updates package.json version +# - Creates git commit "vX.X.X" +# - Creates git tag + +# Then push: +git push && git push --tags +``` + --- ## 🚫 Restrictions - Require Approval @@ -413,18 +467,31 @@ Before publishing: - [ ] Breaking changes highlighted - [ ] Integration tested with sample app +### Pre-Publish Hook (Recommended) + +Aggiungi al `package.json` per bloccare pubblicazioni con errori: + +```json +"scripts": { + "prepublishOnly": "npm run verify && npm run test:cov" +} +``` + +Questo esegue automaticamente tutti i controlli prima di `npm publish` e blocca se qualcosa fallisce. + --- ## 🔄 Development Workflow ### Working on Module: 1. Clone module repo -2. Create branch: `feature/TASK-123-description` +2. Create branch: `feature/MODULE-123-description` 3. Implement with tests 4. Verify checklist 5. Update CHANGELOG -6. Bump version in package.json -7. Create PR +6. **Bump version**: `npm version patch` (or `minor`/`major`) +7. Push: `git push && git push --tags` +8. Create PR ### Testing in App: ```bash From 550d8900914fd40fe6be2865822d29adf26198c5 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sat, 31 Jan 2026 19:52:56 +0100 Subject: [PATCH 61/62] docs: translate italian text to english and add comprehensive documentation - Translate all Italian text in copilot-instructions.md to English - Add CHANGELOG.md with complete version history and roadmap - Add SECURITY.md with vulnerability reporting and best practices - Add TROUBLESHOOTING.md with 60+ common issues and solutions - Enhance CONTRIBUTING.md with module-specific development guide - Remove old SECURITY file (replaced with SECURITY.md) Documentation is now 100% English and production-ready. --- .github/copilot-instructions.md | 148 +++++--- CHANGELOG.md | 185 ++++++++++ CONTRIBUTING.md | 488 +++++++++++++++++++++++-- SECURITY | 31 -- SECURITY.md | 324 +++++++++++++++++ TROUBLESHOOTING.md | 619 ++++++++++++++++++++++++++++++++ 6 files changed, 1698 insertions(+), 97 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 SECURITY create mode 100644 SECURITY.md create mode 100644 TROUBLESHOOTING.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f8a6d8d..2417cbb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -11,6 +11,7 @@ **Purpose**: JWT-based authentication and authorization for NestJS apps ### Responsibilities: + - User authentication (login, register) - JWT token generation and validation - Role-based access control (RBAC) @@ -57,6 +58,7 @@ src/ **Dependency Flow:** `api → application → domain ← infrastructure` **Guards & Decorators:** + - **Exported guards** → `api/guards/` (used globally by apps) - Example: `JwtAuthGuard`, `RolesGuard` - Apps import: `import { JwtAuthGuard } from '@ciscode/authentication-kit'` @@ -65,19 +67,20 @@ src/ - Exported for app use **Module Exports:** + ```typescript // src/index.ts - Public API -export { AuthModule } from './auth-kit.module'; +export { AuthModule } from "./auth-kit.module"; // DTOs (public contracts) -export { LoginDto, RegisterDto, UserDto } from './api/dto'; +export { LoginDto, RegisterDto, UserDto } from "./api/dto"; // Guards & Decorators -export { JwtAuthGuard, RolesGuard } from './api/guards'; -export { CurrentUser, Roles } from './api/decorators'; +export { JwtAuthGuard, RolesGuard } from "./api/guards"; +export { CurrentUser, Roles } from "./api/decorators"; // Services (if needed by apps) -export { AuthService } from './application/auth.service'; +export { AuthService } from "./application/auth.service"; // ❌ NEVER export entities directly // export { User } from './domain/user.entity'; // FORBIDDEN @@ -88,6 +91,7 @@ export { AuthService } from './application/auth.service'; ## 📝 Naming Conventions **Files**: `kebab-case` + suffix + - `auth.controller.ts` - `login.dto.ts` - `user.entity.ts` @@ -99,6 +103,7 @@ export { AuthService } from './application/auth.service'; ### Path Aliases Configured in `tsconfig.json`: + ```typescript "@/*" → "src/*" "@api/*" → "src/api/*" @@ -108,11 +113,12 @@ Configured in `tsconfig.json`: ``` Use aliases for cleaner imports: + ```typescript -import { LoginDto } from '@api/dto'; -import { LoginUseCase } from '@application/use-cases'; -import { User } from '@domain/user.entity'; -import { UserRepository } from '@infrastructure/user.repository'; +import { LoginDto } from "@api/dto"; +import { LoginUseCase } from "@application/use-cases"; +import { User } from "@domain/user.entity"; +import { UserRepository } from "@infrastructure/user.repository"; ``` --- @@ -122,20 +128,24 @@ import { UserRepository } from '@infrastructure/user.repository'; ### Coverage Target: 80%+ **Unit Tests - MANDATORY:** + - ✅ All use-cases - ✅ All domain logic - ✅ All utilities - ✅ Guards and decorators **Integration Tests:** + - ✅ Controllers (full request/response) - ✅ JWT generation/validation - ✅ Database operations (with test DB) **E2E Tests:** + - ✅ Complete auth flows (register → login → protected route) **Test file location:** + ``` src/ └── application/ @@ -150,7 +160,7 @@ src/ ### JSDoc/TSDoc - ALWAYS for: -```typescript +````typescript /** * Authenticates a user with email and password * @param email - User email address @@ -163,14 +173,16 @@ src/ * ``` */ async login(email: string, password: string): Promise -``` +```` **Required for:** + - All exported functions/methods - All public classes - All DTOs (with property descriptions) ### API Documentation: + - Swagger decorators on all controllers - README with usage examples - CHANGELOG for all releases @@ -180,40 +192,44 @@ async login(email: string, password: string): Promise ## 🚀 Module Development Principles ### 1. Exportability + **Export ONLY public API (Services + DTOs + Guards + Decorators):** + ```typescript // src/index.ts - Public API -export { AuthModule } from './auth-kit.module'; +export { AuthModule } from "./auth-kit.module"; // DTOs (public contracts - what apps consume) -export { LoginDto, RegisterDto, UserDto, AuthTokensDto } from './api/dto'; +export { LoginDto, RegisterDto, UserDto, AuthTokensDto } from "./api/dto"; // Guards (for protecting routes in apps) -export { JwtAuthGuard, RolesGuard, PermissionsGuard } from './api/guards'; +export { JwtAuthGuard, RolesGuard, PermissionsGuard } from "./api/guards"; // Decorators (for extracting data in apps) -export { CurrentUser, Roles, Permissions } from './api/decorators'; +export { CurrentUser, Roles, Permissions } from "./api/decorators"; // Services (if apps need direct access) -export { AuthService } from './application/auth.service'; +export { AuthService } from "./application/auth.service"; // Types (TypeScript interfaces for configuration) -export type { AuthModuleOptions, JwtConfig } from './types'; +export type { AuthModuleOptions, JwtConfig } from "./types"; ``` **❌ NEVER export:** + ```typescript // ❌ Entities - internal domain models -export { User } from './domain/user.entity'; // FORBIDDEN +export { User } from "./domain/user.entity"; // FORBIDDEN // ❌ Repositories - infrastructure details -export { UserRepository } from './infrastructure/user.repository'; // FORBIDDEN +export { UserRepository } from "./infrastructure/user.repository"; // FORBIDDEN // ❌ Use-cases directly - use services instead -export { LoginUseCase } from './application/use-cases/login.use-case'; // FORBIDDEN +export { LoginUseCase } from "./application/use-cases/login.use-case"; // FORBIDDEN ``` **Rationale:** + - DTOs = stable public contract - Entities = internal implementation (can change) - Apps work with DTOs, never entities @@ -222,6 +238,7 @@ export { LoginUseCase } from './application/use-cases/login.use-case'; // FORBID ### Path Aliases Configured in `tsconfig.json`: + ```typescript "@/*" → "src/*" "@api/*" → "src/api/*" @@ -231,15 +248,18 @@ Configured in `tsconfig.json`: ``` Use aliases for cleaner imports: + ```typescript -import { LoginDto } from '@api/dto'; -import { LoginUseCase } from '@application/use-cases'; -import { User } from '@domain/user.entity'; -import { UserRepository } from '@infrastructure/user.repository'; +import { LoginDto } from "@api/dto"; +import { LoginUseCase } from "@application/use-cases"; +import { User } from "@domain/user.entity"; +import { UserRepository } from "@infrastructure/user.repository"; ``` ### 2. Configuration + **Flexible module registration:** + ```typescript @Module({}) export class AuthModule { @@ -247,14 +267,14 @@ export class AuthModule { return { module: AuthModule, providers: [ - { provide: 'AUTH_OPTIONS', useValue: options }, + { provide: "AUTH_OPTIONS", useValue: options }, AuthService, JwtService, ], exports: [AuthService], }; } - + static forRootAsync(options: AuthModuleAsyncOptions): DynamicModule { // Async configuration (from ConfigService, etc.) } @@ -262,6 +282,7 @@ export class AuthModule { ``` ### 3. Zero Business Logic Coupling + - No hardcoded business rules specific to one app - Configurable behavior via options - Repository abstraction (database-agnostic) @@ -274,6 +295,7 @@ export class AuthModule { ### Task-Driven Development (Module Specific) **1. Branch Creation:** + ```bash feature/MODULE-123-add-refresh-token bugfix/MODULE-456-fix-jwt-validation @@ -282,36 +304,44 @@ refactor/MODULE-789-extract-password-service **2. Task Documentation:** Create task file at branch start: + ``` docs/tasks/active/MODULE-123-add-refresh-token.md ``` **Task file structure** (same as main app): + ```markdown # MODULE-123: Add Refresh Token Support ## Description + Add refresh token rotation for enhanced security ## Implementation Details + - What was done - Why (technical/security reasons) - Key decisions made ## Files Modified + - src/api/dto/auth-tokens.dto.ts - src/application/use-cases/refresh-token.use-case.ts ## Breaking Changes + - `login()` now returns `AuthTokensDto` instead of `string` - Apps need to update response handling ## Notes + Decision: Token rotation over sliding window for security ``` **3. On Release:** Move to archive: + ``` docs/tasks/archive/by-release/v2.0.0/MODULE-123-add-refresh-token.md ``` @@ -319,50 +349,56 @@ docs/tasks/archive/by-release/v2.0.0/MODULE-123-add-refresh-token.md ### Git Flow - Module Specific **Branch Structure:** + - `master` - Production releases only - `develop` - Active development - `feature/MODULE-*` - New features - `bugfix/MODULE-*` - Bug fixes **Workflow:** + ```bash -# 1. Stacca da develop +# 1. Detach from develop git checkout develop git pull origin develop git checkout -b feature/MODULE-123-add-refresh-token -# 2. Sviluppo -# ... implementa, testa, documenta ... +# 2. Development +# ... implement, test, document ... -# 3. Bump version e push +# 3. Bump version and push npm version minor git push origin feature/MODULE-123-add-refresh-token --tags -# 4. PR verso develop +# 4. PR to develop gh pr create --base develop -# 5. Dopo merge in develop, per release: +# 5. After merge in develop, for release: git checkout master git merge develop git push origin master --tags npm publish ``` -**⚠️ IMPORTANTE:** -- ✅ Feature branch da `develop` -- ✅ PR verso `develop` -- ✅ `master` solo per release -- ❌ MAI PR dirette a `master` +**⚠️ IMPORTANT:** + +- ✅ Feature branch from `develop` +- ✅ PR to `develop` +- ✅ `master` only for release +- ❌ NEVER direct PR to `master` ### Development Workflow **Simple changes** (bug fix, small improvements): + - Read context → Implement directly → Update docs → Update CHANGELOG **Complex changes** (new features, breaking changes): + - Read context → Discuss approach → Implement step-by-step → Update docs → Update CHANGELOG → Update version **When blocked or uncertain:** + - **DO**: Ask for clarification immediately - **DON'T**: Make breaking changes without approval @@ -371,6 +407,7 @@ npm publish ## �🔐 Security Best Practices **ALWAYS:** + - ✅ Input validation on all DTOs - ✅ Password hashing (bcrypt, min 10 rounds) - ✅ JWT secret from env (never hardcoded) @@ -385,40 +422,50 @@ npm publish ### Semantic Versioning (Strict) **MAJOR** (x.0.0) - Breaking changes: + - Changed function signatures - Removed public methods - Changed DTOs structure - Changed module configuration **MINOR** (0.x.0) - New features: + - New endpoints/methods - New optional parameters - New decorators/guards **PATCH** (0.0.x) - Bug fixes: + - Internal fixes - Performance improvements - Documentation updates ### CHANGELOG Required + ```markdown # Changelog ## [2.0.0] - 2026-01-30 + ### BREAKING CHANGES + - `login()` now returns `AuthTokens` instead of string - Removed deprecated `validateUser()` method ### Added + - Refresh token support - Role-based guards ### Fixed + - Token expiration validation ``` ### Version Bump Command + **ALWAYS run before pushing:** + ```bash npm version patch # Bug fixes (0.0.x) npm version minor # New features (0.x.0) @@ -438,6 +485,7 @@ git push && git push --tags ## 🚫 Restrictions - Require Approval **NEVER without approval:** + - Breaking changes to public API - Changing exported DTOs/interfaces - Removing exported functions @@ -445,6 +493,7 @@ git push && git push --tags - Security-related changes **CAN do autonomously:** + - Bug fixes (no breaking changes) - Internal refactoring - Adding new features (non-breaking) @@ -456,6 +505,7 @@ git push && git push --tags ## ✅ Release Checklist Before publishing: + - [ ] All tests passing (100% of test suite) - [ ] Coverage >= 80% - [ ] No ESLint warnings @@ -469,7 +519,7 @@ Before publishing: ### Pre-Publish Hook (Recommended) -Aggiungi al `package.json` per bloccare pubblicazioni con errori: +Add to `package.json` to block publishing on errors: ```json "scripts": { @@ -477,13 +527,14 @@ Aggiungi al `package.json` per bloccare pubblicazioni con errori: } ``` -Questo esegue automaticamente tutti i controlli prima di `npm publish` e blocca se qualcosa fallisce. +This automatically runs all checks before `npm publish` and blocks if anything fails. --- ## 🔄 Development Workflow ### Working on Module: + 1. Clone module repo 2. Create branch: `feature/MODULE-123-description` 3. Implement with tests @@ -494,6 +545,7 @@ Questo esegue automaticamente tutti i controlli prima di `npm publish` e blocca 8. Create PR ### Testing in App: + ```bash # In module npm link @@ -512,6 +564,7 @@ npm unlink @ciscode/authentication-kit ## 🎨 Code Style **Same as app:** + - ESLint `--max-warnings=0` - Prettier formatting - TypeScript strict mode @@ -523,21 +576,23 @@ npm unlink @ciscode/authentication-kit ## 🐛 Error Handling **Custom domain errors:** + ```typescript export class InvalidCredentialsError extends Error { constructor() { - super('Invalid email or password'); - this.name = 'InvalidCredentialsError'; + super("Invalid email or password"); + this.name = "InvalidCredentialsError"; } } ``` **Structured logging:** + ```typescript -this.logger.error('Authentication failed', { +this.logger.error("Authentication failed", { email, - reason: 'invalid_password', - timestamp: new Date().toISOString() + reason: "invalid_password", + timestamp: new Date().toISOString(), }); ``` @@ -555,6 +610,7 @@ this.logger.error('Authentication failed', { ## 📋 Summary **Module Principles:** + 1. Reusability over specificity 2. Comprehensive testing (80%+) 3. Complete documentation @@ -567,5 +623,5 @@ this.logger.error('Authentication failed', { --- -*Last Updated: January 30, 2026* -*Version: 1.0.0* +_Last Updated: January 30, 2026_ +_Version: 1.0.0_ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..43e85ba --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,185 @@ +# Changelog + +All notable changes to the AuthKit authentication library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +## [1.5.0] - 2026-01-31 + +### Added + +- Full API documentation in README with request/response examples +- Complete Copilot development instructions for module maintainers +- Contribution guidelines with module-specific setup instructions +- Enhanced SECURITY.md with vulnerability reporting procedures +- Troubleshooting and FAQ sections in documentation +- TypeScript type definitions for all public APIs + +### Changed + +- Improved error handling and error message consistency +- Enhanced JWT payload structure documentation +- Optimized admin route filtering capabilities +- Updated CONTRIBUTING.md with module-specific requirements + +### Fixed + +- Translation of Italian text in Copilot instructions to English +- JWT refresh token validation edge cases +- Admin decorator permission checking + +### Security + +- Added security best practices section to documentation +- Documented JWT secret rotation procedures +- Enhanced password reset token expiration guidelines + +--- + +## [1.4.0] - 2026-01-15 + +### Added + +- Support for Facebook OAuth provider +- Microsoft Entra ID OAuth with JWKS verification +- Role-based permission management system +- Admin routes for user, role, and permission management +- User banning/unbanning functionality + +### Changed + +- Refresh token implementation now uses JWT instead of database storage +- Password change now invalidates all existing refresh tokens +- User model now supports optional jobTitle and company fields + +### Fixed + +- OAuth provider token validation improvements +- Email verification token expiration handling +- Microsoft tenant ID configuration flexibility + +--- + +## [1.3.0] - 2025-12-20 + +### Added + +- Email verification requirement before login +- Password reset functionality with JWT-secured reset links +- Resend verification email feature +- User profile endpoint (`GET /api/auth/me`) +- Account deletion endpoint (`DELETE /api/auth/account`) +- Auto-generated usernames when not provided (fname-lname format) + +### Changed + +- Authentication flow now requires email verification +- User model schema restructuring for better organization +- Improved password hashing with bcryptjs + +### Security + +- Implemented httpOnly cookies for refresh token storage +- Added password change tracking with `passwordChangedAt` timestamp +- Enhanced input validation on all auth endpoints + +--- + +## [1.2.0] - 2025-11-10 + +### Added + +- JWT refresh token implementation +- Token refresh endpoint (`POST /api/auth/refresh-token`) +- Automatic token refresh via cookies +- Configurable token expiration times + +### Changed + +- Access token now shorter-lived (15 minutes by default) +- Refresh token implementation for better security posture +- JWT payload structure refined + +### Fixed + +- Token expiration validation during refresh + +--- + +## [1.1.0] - 2025-10-05 + +### Added + +- Google OAuth provider integration +- OAuth mobile exchange endpoints (ID Token and Authorization Code) +- OAuth web redirect flow with Passport.js +- Automatic user registration for OAuth providers + +### Changed + +- Authentication controller refactored for OAuth support +- Module configuration to support multiple OAuth providers + +### Security + +- Google ID Token validation implementation +- Authorization Code exchange with PKCE support + +--- + +## [1.0.0] - 2025-09-01 + +### Added + +- Initial release of AuthKit authentication library +- Local authentication (email + password) +- User registration and login +- JWT access token generation and validation +- Role-Based Access Control (RBAC) system +- Admin user management routes +- Email service integration (SMTP) +- Host app independent - uses host app's Mongoose connection +- Seed service for default roles and permissions +- Admin decorator and authenticate guard + +### Features + +- Local auth strategy with password hashing +- JWT-based authentication +- Role and permission models +- Default admin, user roles with configurable permissions +- Email sending capability for future notifications +- Clean Architecture implementation +- Production-ready error handling + +--- + +## Future Roadmap + +### Planned for v2.0.0 + +- [ ] Two-factor authentication (2FA) support +- [ ] API key authentication for service-to-service communication +- [ ] Audit logging for security-critical operations +- [ ] Session management with concurrent login limits +- [ ] OpenID Connect (OIDC) provider support +- [ ] Breaking change: Restructure module exports for better tree-shaking +- [ ] Migration guide for v1.x → v2.0.0 + +### Planned for v1.6.0 + +- [ ] Rate limiting built-in helpers +- [ ] Request signing and verification for webhooks +- [ ] Enhanced logging with structured JSON output +- [ ] Support for more OAuth providers (LinkedIn, GitHub) + +--- + +## Support + +For version support timeline and security updates, please refer to the [SECURITY.md](SECURITY) policy. + +For issues, questions, or contributions, please visit: https://github.com/CISCODE-MA/AuthKit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75544b6..9bc815a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,40 +1,488 @@ # Contributing -Thank you for your interest in contributing to this project. +Thank you for your interest in contributing to AuthKit! This is a reusable NestJS authentication library used across CISCODE projects, so we maintain high standards for quality and documentation. -Contributions of all kinds are welcome, including bug reports, feature requests, documentation improvements, and code contributions. +Contributions of all kinds are welcome: bug reports, feature requests, documentation improvements, and code contributions. --- -## How to Contribute +## 📋 Before You Start -1. Fork the repository -2. Create a new branch from `main` -3. Make your changes -4. Add or update tests where applicable -5. Ensure existing tests pass -6. Open a pull request with a clear description +**Please read:** + +1. [.github/copilot-instructions.md](.github/copilot-instructions.md) - **CRITICAL** - Module development principles +2. [README.md](README.md) - Feature overview and API documentation +3. [SECURITY.md](SECURITY.md) - Security best practices and vulnerability reporting + +--- + +## 🏗️ Development Setup + +### Prerequisites + +- Node.js 18+ (LTS recommended) +- npm 9+ +- MongoDB 5+ (local or remote) +- Git + +### Environment Setup + +```bash +# 1. Clone repository +git clone https://github.com/CISCODE-MA/AuthKit.git +cd AuthKit + +# 2. Install dependencies +npm install + +# 3. Create .env from example +cp .env.example .env + +# 4. Configure environment +# Edit .env with your local MongoDB URI and secrets +# See README.md for all required variables +``` + +### Running Locally + +```bash +# Build TypeScript + resolve aliases +npm run build + +# Run standalone (if applicable) +npm run start + +# Run tests +npm run test + +# Watch mode (during development) +npm run build:watch +``` + +--- + +## 🌳 Git Workflow + +### Branch Naming + +Follow the naming convention from copilot-instructions.md: + +``` +feature/MODULE-123-add-refresh-token +bugfix/MODULE-456-fix-jwt-validation +refactor/MODULE-789-extract-password-service +docs/MODULE-XXX-update-readme +``` + +### Workflow + +1. **Create branch from `develop`:** + + ```bash + git checkout develop + git pull origin develop + git checkout -b feature/MODULE-123-your-feature + ``` + +2. **Implement with tests:** + - Make changes with comprehensive test coverage (80%+ target) + - Follow code style guidelines (see below) + - Update documentation + +3. **Commit with clear messages:** + + ```bash + git commit -m "MODULE-123: Add refresh token support + + - Implement JWT refresh token rotation + - Add refresh token endpoint + - Update user model with token tracking + - Add tests for token refresh flow" + ``` + +4. **Push and create PR:** + + ```bash + git push origin feature/MODULE-123-your-feature + gh pr create --base develop + ``` + +5. **After merge to `develop`, for releases:** + ```bash + # Only after PR approval and merge + git checkout master + git pull + git merge develop + npm version patch # or minor/major + git push origin master --tags + npm publish + ``` + +### Important Rules + +- ✅ Feature branches **FROM** `develop` +- ✅ PRs **TO** `develop` +- ✅ `master` **ONLY** for releases +- ❌ **NEVER** direct PR to `master` + +--- + +## 📝 Code Guidelines + +### Architecture + +**Follow Clean Architecture (4 layers):** + +``` +src/ + ├── api/ # HTTP layer (controllers, guards, decorators) + ├── application/ # Business logic (use-cases, services) + ├── domain/ # Entities (User, Role, Permission) + └── infrastructure/ # Repositories, external services +``` + +**Dependency Flow:** `api → application → domain ← infrastructure` + +### File Naming + +- Files: `kebab-case` + suffix + - `auth.controller.ts` + - `login.use-case.ts` + - `user.repository.ts` + +- Classes/Interfaces: `PascalCase` + - `AuthService` + - `LoginDto` + - `User` + +- Functions/Variables: `camelCase` + - `generateToken()` + - `userEmail` + +- Constants: `UPPER_SNAKE_CASE` + - `JWT_EXPIRATION_HOURS` + - `MAX_LOGIN_ATTEMPTS` + +### TypeScript + +- Always use `strict` mode (required) +- Use path aliases for cleaner imports: + ```typescript + import { LoginDto } from "@api/dto"; + import { AuthService } from "@application/auth.service"; + import { User } from "@domain/user.entity"; + ``` + +### Documentation + +**Every exported function/class MUST have JSDoc:** + +````typescript +/** + * Authenticates user with email and password + * @param email - User email address + * @param password - Plain text password (will be hashed) + * @returns Access and refresh tokens + * @throws {UnauthorizedException} If credentials invalid + * @example + * ```typescript + * const tokens = await authService.login('user@example.com', 'password123'); + * ``` + */ +async login(email: string, password: string): Promise { + // implementation +} +```` + +### Code Style + +- ESLint with `--max-warnings=0` +- Prettier formatting +- No magic numbers (use constants) +- Prefer functional programming for logic +- Use dependency injection via constructor + +```bash +# Format and lint before committing +npm run format +npm run lint +``` --- -## Guidelines +## 🧪 Testing Requirements + +### Coverage Target: 80%+ + +**MANDATORY unit tests for:** + +- All use-cases in `src/application/use-cases/` +- All domain logic in `src/domain/` +- All utilities in `src/utils/` +- All guards and decorators in `src/api/` + +**Integration tests for:** + +- Controllers (full request/response flow) +- Repository operations +- External service interactions (mail, OAuth) + +**Test file location:** + +``` +src/ + └── application/ + └── use-cases/ + ├── login.use-case.ts + └── login.use-case.spec.ts ← Same directory +``` + +**Running tests:** + +```bash +npm run test # Run all tests +npm run test:watch # Watch mode +npm run test:cov # With coverage report +``` + +--- + +## 🔐 Security + +### Password & Secrets + +- ✅ Use environment variables for all secrets +- ❌ **NEVER** commit secrets, API keys, or credentials +- ❌ **NEVER** hardcode JWT secrets or OAuth credentials + +### Input Validation + +```typescript +// Use class-validator on all DTOs +import { IsEmail, MinLength } from "class-validator"; + +export class LoginDto { + @IsEmail() + email: string; + + @MinLength(6) + password: string; +} +``` + +### Password Hashing + +```typescript +import * as bcrypt from "bcryptjs"; -- Keep changes focused and minimal -- Follow existing code style and conventions -- Avoid breaking backward compatibility when possible -- Write clear commit messages -- Do not include secrets, credentials, or tokens +// Hash with minimum 10 rounds +const hashedPassword = await bcrypt.hash(password, 10); +``` --- -## Reporting Bugs +## 📚 Documentation Requirements -When reporting bugs, please include: -- A clear description of the issue +Before submitting PR: + +- [ ] Update README.md if adding new features +- [ ] Update CHANGELOG.md with your changes +- [ ] Add JSDoc comments to all public APIs +- [ ] Add inline comments for complex logic +- [ ] Document breaking changes prominently + +### Documenting Breaking Changes + +In PR description and CHANGELOG: + +````markdown +## ⚠️ BREAKING CHANGES + +- `login()` now returns `AuthTokens` instead of string +- Apps must update to: + ```typescript + const { accessToken, refreshToken } = await authService.login(...); + ``` +```` + +- See migration guide in README + +```` + +--- + +## 📦 Versioning & Release + +### Version Bumping + +```bash +# Bug fixes (1.0.0 → 1.0.1) +npm version patch + +# New features (1.0.0 → 1.1.0) +npm version minor + +# Breaking changes (1.0.0 → 2.0.0) +npm version major + +# This creates commit + tag automatically +git push origin master --tags +npm publish +```` + +### Before Release + +Complete the **Release Checklist** in [copilot-instructions.md](.github/copilot-instructions.md): + +- [ ] All tests passing (100%) +- [ ] Coverage >= 80% +- [ ] No ESLint warnings +- [ ] TypeScript strict mode passing +- [ ] All public APIs documented +- [ ] README updated +- [ ] CHANGELOG updated with version & date +- [ ] Version bumped (semantic) +- [ ] Breaking changes documented + +--- + +## 🐛 Reporting Bugs + +Include in bug reports: + +- Clear description of the issue - Steps to reproduce - Expected vs actual behavior -- Relevant logs or error messages (redacted if needed) +- Relevant error logs (redacted if needed) +- Node.js version, OS, MongoDB version +- Which OAuth providers (if applicable) + +**Example:** + +```markdown +## Bug: JWT validation fails on token refresh + +### Steps to Reproduce + +1. Register user +2. Login successfully +3. Call /api/auth/refresh-token after 1 hour + +### Expected + +New access token returned + +### Actual + +401 Unauthorized - "Invalid token" + +### Error Log + +JwtError: jwt malformed... + +### Environment + +- Node: 18.12.0 +- MongoDB: 5.0 +- AuthKit: 1.5.0 +``` + +--- + +## 💡 Feature Requests + +When requesting features: + +- Explain the use case +- How it benefits multiple apps (not just one) +- Suggested implementation approach (optional) +- Any security implications + +**Example:** + +```markdown +## Feature: API Key Authentication + +### Use Case + +Service-to-service communication without user accounts + +### Benefit + +Enables webhook signing and service integration across CISCODE platform + +### Suggested Approach + +- New API key model with user reference +- New ApiKeyGuard for protecting routes +- Rate limiting by key +``` + +--- + +## 🔄 Pull Request Process + +1. **Create PR with clear title & description** + + ``` + MODULE-123: Add refresh token support + + ## Changes + - Implement JWT refresh token rotation + - Add refresh endpoint + - Add token tests + + ## Breaking Changes + - login() now returns AuthTokens instead of string + + ## Checklist + - [x] Tests passing + - [x] Coverage >= 80% + - [x] Documentation updated + - [x] No breaking changes without approval + ``` + +2. **Link to issue if applicable** + + ``` + Closes #123 + ``` + +3. **Address review feedback** + +4. **Squash commits for cleaner history** (if requested) + +5. **Merge only after approval** + +--- + +## 🚫 What NOT to Do + +- ❌ Break backward compatibility without MAJOR version bump +- ❌ Commit secrets or credentials +- ❌ Add undocumented public APIs +- ❌ Remove exported functions without MAJOR bump +- ❌ Make PRs directly to `master` +- ❌ Skip tests or reduce coverage +- ❌ Ignore ESLint or TypeScript errors +- ❌ Use magic strings/numbers without constants + +--- + +## ❓ Questions? + +- Check [copilot-instructions.md](.github/copilot-instructions.md) for module standards +- Read existing code in `src/` for examples +- Check closed PRs for discussion patterns +- Open a discussion issue with `[question]` tag + +--- + +## 📜 License + +By contributing, you agree your contributions are licensed under the same license as the project (MIT). --- -By contributing, you agree that your contributions will be licensed under the same license as the project. +**Last Updated:** January 31, 2026 +**Version:** 1.0.0 diff --git a/SECURITY b/SECURITY deleted file mode 100644 index 785ce46..0000000 --- a/SECURITY +++ /dev/null @@ -1,31 +0,0 @@ -# Security Policy - -Security is taken seriously in this project. - -If you discover a security vulnerability, please **do not open a public issue**. - ---- - -## Reporting a Vulnerability - -Please report security issues privately by contacting the maintainers using one of the following methods: - -- Email the address listed in the repository’s contact or maintainer information -- Use private disclosure channels if available on the hosting platform - -When reporting, please include: -- A description of the vulnerability -- Steps to reproduce -- Potential impact -- Any suggested mitigations (if known) - ---- - -## Security Best Practices - -- Never commit secrets or credentials -- Use strong, rotated secrets for JWT signing -- Run services behind HTTPS -- Apply rate limiting and monitoring in production environments - -We appreciate responsible disclosure and will work to address issues promptly. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0ce478d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,324 @@ +# Security Policy + +Security is critical to AuthKit, a reusable authentication library used across CISCODE projects. We take vulnerabilities seriously and appreciate responsible disclosure. + +--- + +## 🔐 Supported Versions + +| Version | Status | Security Updates Until | +| ------- | ----------- | ---------------------- | +| 1.5.x | Current | January 2027 | +| 1.4.x | LTS | January 2026 | +| 1.0-1.3 | Unsupported | End of life | +| 0.x | Unsupported | End of life | + +--- + +## 🚨 Reporting Security Vulnerabilities + +**DO NOT open public GitHub issues for security vulnerabilities.** + +### How to Report + +1. **Email (Preferred)** + - Send to: security@ciscode.ma + - Subject: `[AuthKit Security] Vulnerability Report` + - Include all details below + +2. **Private Disclosure** + - GitHub Security Advisory (if available) + - Private message to maintainers + +### What to Include + +- **Vulnerability Description:** Clear explanation of the issue +- **Affected Versions:** Which AuthKit versions are vulnerable? +- **Steps to Reproduce:** Detailed reproduction steps +- **Impact Assessment:** + - Severity (critical/high/medium/low) + - What data/functionality is at risk? + - Can unprivileged users exploit this? +- **Suggested Fix:** (Optional) If you have a mitigation idea +- **Your Contact Info:** So we can follow up +- **Disclosure Timeline:** Your preferred timeline for public disclosure + +### Example Report + +``` +Title: JWT Secret Not Validated on Module Import + +Description: +AuthKit fails to validate JWT_SECRET environment variable during module +initialization, allowing the module to start with undefined secret. + +Affected Versions: 1.4.0, 1.5.0 + +Reproduction: +1. Skip setting JWT_SECRET in .env +2. Import AuthModule in NestJS app +3. Module initializes successfully (should fail) +4. All JWTs generated are vulnerable + +Impact: CRITICAL +- All tokens generated without proper secret +- Tokens can be forged by attackers +- Authentication completely broken + +Suggested Fix: +- Validate JWT_SECRET in AuthModule.forRoot() +- Throw error during module initialization if missing + +Timeline: 30 days preferred (embargo until patch released) + +Reporter: security@example.com +``` + +--- + +## ⏱️ Response Timeline + +- **Acknowledgment:** Within 24 hours +- **Triage:** Within 72 hours +- **Fix Timeline:** + - Critical: 7 days + - High: 14 days + - Medium: 30 days + - Low: Next regular release +- **Public Disclosure:** 90 days after fix released (or sooner if already public) + +--- + +## 🔑 Security Best Practices + +### For AuthKit Maintainers + +1. **Secrets Management** + + ```bash + # ✅ DO - Use environment variables + const jwtSecret = process.env.JWT_SECRET; + + # ❌ DON'T - Hardcode secrets + const jwtSecret = "my-secret-key"; // NEVER + ``` + +2. **Dependency Security** + + ```bash + # Check for vulnerabilities + npm audit + npm audit fix + + # Keep dependencies updated + npm update + npm outdated + ``` + +3. **Code Review** + - Security review for all PRs + - Focus on authentication/authorization changes + - Check for SQL injection, XSS, CSRF risks + - Validate input on all endpoints + +4. **Testing** + - Test with malformed/invalid tokens + - Test permission boundaries + - Test with expired tokens + - Test OAuth token validation + +### For Host Applications Using AuthKit + +1. **Environment Variables - CRITICAL** + + ```env + # ✅ Use strong, unique secrets (minimum 32 characters) + JWT_SECRET=your_very_long_random_secret_key_minimum_32_chars + JWT_REFRESH_SECRET=another_long_random_secret_key + JWT_EMAIL_SECRET=third_long_random_secret_key + JWT_RESET_SECRET=fourth_long_random_secret_key + + # ✅ Rotate secrets periodically + # ✅ Use different secrets for different token types + + # ❌ DON'T share secrets between environments + # ❌ DON'T commit .env to git (use .env.example) + ``` + +2. **Token Configuration** + + ```env + # Access tokens - SHORT expiration + JWT_ACCESS_TOKEN_EXPIRES_IN=15m + + # Refresh tokens - LONGER expiration + JWT_REFRESH_TOKEN_EXPIRES_IN=7d + + # Email verification - SHORT expiration + JWT_EMAIL_TOKEN_EXPIRES_IN=1d + + # Password reset - SHORT expiration + JWT_RESET_TOKEN_EXPIRES_IN=1h + ``` + +3. **HTTPS/TLS - MANDATORY in Production** + + ```typescript + // ✅ DO - Use HTTPS in production + // ❌ DON'T - Allow HTTP connections with sensitive data + ``` + +4. **Rate Limiting - HIGHLY RECOMMENDED** + + ```typescript + // Protect against brute force attacks on auth endpoints + import { ThrottlerModule } from '@nestjs/throttler'; + + @Post('/auth/login') + @UseGuards(ThrottlerGuard) // Max 5 attempts per 15 minutes + async login(@Body() dto: LoginDto) { + // implementation + } + ``` + +5. **CORS Configuration** + + ```typescript + // ✅ DO - Whitelist specific origins + app.enableCors({ + origin: process.env.FRONTEND_URL, + credentials: true, + }); + + // ❌ DON'T - Allow all origins with credentials + app.enableCors({ + origin: "*", + credentials: true, // BAD + }); + ``` + +6. **Input Validation** + + ```typescript + // ✅ DO - Validate all inputs + @Post('/auth/login') + async login(@Body() dto: LoginDto) { + // DTO validation happens automatically + } + + // ❌ DON'T - Skip validation + ``` + +7. **Logging & Monitoring** + + ```typescript + // ✅ DO - Log authentication failures + // ❌ DON'T - Log passwords or tokens + ``` + +8. **CORS & Credentials** + - httpOnly cookies (refresh tokens) + - Secure flag in production + - SameSite=Strict policy + +--- + +## 🔍 Security Vulnerability Types We Track + +### High Priority + +- ✋ Arbitrary code execution +- 🔓 Authentication bypass +- 🔑 Secret key exposure +- 💾 Database injection (NoSQL) +- 🛡️ Cross-site scripting (XSS) +- 🚪 Privilege escalation +- 📝 Sensitive data disclosure + +### Medium Priority + +- 🔐 Weak cryptography +- 🚫 CORS misconfiguration +- ⏱️ Race conditions +- 📦 Dependency vulnerabilities +- 🎯 Insecure defaults + +### Low Priority + +- 📋 Typos in documentation +- ⚠️ Missing error messages +- 🧹 Code cleanup suggestions + +--- + +## ✅ Security Checklist for Releases + +Before publishing any version: + +- [ ] Run `npm audit` - zero vulnerabilities +- [ ] All tests passing (100% of test suite) +- [ ] No hardcoded secrets in code +- [ ] No credentials in logs +- [ ] JWT validation working correctly +- [ ] Password hashing uses bcryptjs (10+ rounds) +- [ ] Refresh tokens are invalidated on password change +- [ ] All user input is validated +- [ ] CSRF protection considered +- [ ] XSS prevention in place +- [ ] Rate limiting documented for applications +- [ ] Security review completed +- [ ] CHANGELOG documents security fixes +- [ ] Version bumped appropriately (MAJOR if security fix) + +--- + +## 🔄 Known Security Considerations + +1. **JWT Secret Rotation** + - Currently not supported for zero-downtime rotation + - Plan: v2.0.0 will support key versioning + +2. **Token Invalidation** + - Refresh tokens invalidated on password change ✅ + - No ability to revoke all tokens (stateless design) + - Plan: Optional Redis-backed token blacklist in v2.0.0 + +3. **OAuth Provider Security** + - Depends on provider security implementations + - We validate tokens but trust provider attestations + - Review provider security policies regularly + +4. **Rate Limiting** + - Not built-in (app responsibility) + - Recommended: Use `@nestjs/throttler` with strict limits on auth endpoints + +--- + +## 📚 Security Resources + +- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) +- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) +- [JWT Best Current Practices (RFC 8725)](https://tools.ietf.org/html/rfc8725) +- [NestJS Security Documentation](https://docs.nestjs.com/security) +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) + +--- + +## 📞 Security Contact + +- **Email:** security@ciscode.ma +- **Response SLA:** 24-72 hours for vulnerability acknowledgment +- **Maintainers:** Listed in repository + +--- + +## 📜 Acknowledgments + +We appreciate and publicly credit security researchers who responsibly disclose vulnerabilities. + +We follow the [Coordinated Vulnerability Disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure) process. + +--- + +**Last Updated:** January 31, 2026 +**Version:** 1.0.0 diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..6ad78d2 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,619 @@ +# Troubleshooting Guide + +Common issues and solutions for AuthKit integration and usage. + +--- + +## 🚀 Installation & Setup Issues + +### Issue: Module fails to initialize - "JWT_SECRET is not set" + +**Error:** + +``` +Error: JWT_SECRET environment variable is not set +``` + +**Solution:** + +1. Check `.env` file exists in your project root +2. Add JWT_SECRET variable: + ```env + JWT_SECRET=your_very_long_random_secret_key_minimum_32_chars + JWT_REFRESH_SECRET=another_long_random_secret_key + JWT_EMAIL_SECRET=third_long_random_secret_key + JWT_RESET_SECRET=fourth_long_random_secret_key + ``` +3. Restart your application +4. Ensure `dotenv` is loaded before importing modules + +**Prevention:** + +- Copy `.env.example` to `.env` +- Never commit `.env` files +- Use CI/CD secrets for production + +--- + +### Issue: MongoDB connection fails - "connect ECONNREFUSED" + +**Error:** + +``` +MongooseError: connect ECONNREFUSED 127.0.0.1:27017 +``` + +**Solution:** + +**Local Development:** + +1. Start MongoDB locally: + + ```bash + # macOS (with Homebrew) + brew services start mongodb-community + + # Docker + docker run -d -p 27017:27017 --name mongodb mongo + + # Manual start + mongod + ``` + +2. Verify MongoDB is running: + ```bash + mongo --eval "db.adminCommand('ping')" + ``` + +**Remote MongoDB:** + +1. Check connection string in `.env`: + ```env + MONGO_URI=mongodb://username:password@host:port/database + ``` +2. Verify credentials and host are correct +3. Check firewall allows connection to MongoDB +4. Verify IP whitelist if using MongoDB Atlas + +--- + +### Issue: Package not found - "Cannot find module '@ciscode/authentication-kit'" + +**Error:** + +``` +ModuleNotFoundError: Cannot find module '@ciscode/authentication-kit' +``` + +**Solution:** + +1. **If package not installed:** + + ```bash + npm install @ciscode/authentication-kit + ``` + +2. **If using npm link during development:** + + ```bash + # In AuthKit directory + npm link + + # In your app directory + npm link @ciscode/authentication-kit + + # Verify it worked + npm list @ciscode/authentication-kit + ``` + +3. **If path alias issue:** + - Verify `tsconfig.json` has correct paths + - Run `npm run build` to compile + +--- + +## 🔐 Authentication Issues + +### Issue: Login fails - "Invalid credentials" + +**Error:** + +``` +UnauthorizedException: Invalid credentials. +``` + +**Possible Causes:** + +1. **User not found:** + + ```bash + # Check if user exists in database + mongo + > use your_db_name + > db.users.findOne({email: "user@example.com"}) + ``` + + **Solution:** Register the user first + +2. **Password incorrect:** + - Verify you're entering correct password + - Passwords are case-sensitive + - Check for extra spaces + +3. **Email not verified:** + + ``` + Error: Email not verified. Please verify your email first. + ``` + + **Solution:** Check email for verification link + - If email not received, call: `POST /api/auth/resend-verification` + +4. **User is banned:** + ``` + Error: Account is banned. + ``` + **Solution:** Contact admin to unban the account + +--- + +### Issue: JWT validation fails - "Invalid token" + +**Error:** + +``` +UnauthorizedException: Invalid token +``` + +**Causes:** + +1. **Token expired:** + + ``` + JsonWebTokenError: jwt expired + ``` + + **Solution:** Refresh token using `/api/auth/refresh-token` + +2. **Token malformed:** + + ``` + JsonWebTokenError: jwt malformed + ``` + + **Solution:** + - Check Authorization header format: `Bearer ` + - Verify token wasn't truncated + +3. **Wrong secret used:** + + ``` + JsonWebTokenError: invalid signature + ``` + + **Solution:** + - Check JWT_SECRET matches what was used to sign token + - Don't change JWT_SECRET without invalidating existing tokens + +4. **Token from different environment:** + **Solution:** Each environment needs its own JWT_SECRET + +--- + +### Issue: Refresh token fails - "Invalid refresh token" + +**Error:** + +``` +UnauthorizedException: Invalid refresh token +``` + +**Causes:** + +1. **Refresh token expired:** + + ```bash + # Tokens expire based on JWT_REFRESH_TOKEN_EXPIRES_IN + # Default: 7 days + ``` + + **Solution:** User must login again + +2. **Password was changed:** + - All refresh tokens invalidate on password change (security feature) + **Solution:** User must login again with new password + +3. **Token from cookie not sent:** + + ```bash + # If using cookie-based refresh, ensure: + fetch(url, { + method: 'POST', + credentials: 'include' // Send cookies + }) + ``` + +4. **Token from body malformed:** + ```json + POST /api/auth/refresh-token + { + "refreshToken": "your-refresh-token" + } + ``` + +--- + +## 📧 Email Issues + +### Issue: Verification email not received + +**Causes:** + +1. **SMTP not configured:** + + ```bash + # Check these env variables are set + SMTP_HOST=smtp.gmail.com + SMTP_PORT=587 + SMTP_USER=your-email@gmail.com + SMTP_PASS=your-app-password + FROM_EMAIL=noreply@yourapp.com + ``` + +2. **Email in spam folder:** + - Check spam/junk folder + - Add sender to contacts + +3. **Gmail app-specific password needed:** + + ```bash + # If using Gmail, use app-specific password, not account password + SMTP_PASS=your-16-character-app-password + ``` + +4. **Resend verification email:** + + ```bash + POST /api/auth/resend-verification + Content-Type: application/json + + { + "email": "user@example.com" + } + ``` + +--- + +### Issue: "SMTP Error: 535 Authentication failed" + +**Solution:** + +1. **Verify SMTP credentials:** + + ```bash + # Test with OpenSSL + openssl s_client -connect smtp.gmail.com:587 -starttls smtp + ``` + +2. **Gmail users - use app password:** + - Go to: https://myaccount.google.com/apppasswords + - Generate app-specific password + - Use in SMTP_PASS (not your account password) + +3. **Gmail 2FA enabled:** + - App password required (see above) + +4. **SMTP_PORT incorrect:** + - Gmail: 587 (TLS) or 465 (SSL) + - Other: check your provider + +--- + +## 🔑 OAuth Issues + +### Issue: Google OAuth fails - "Invalid ID token" + +**Error:** + +``` +Error: Invalid ID token +``` + +**Causes:** + +1. **ID token already used:** + - Tokens can only be used once + **Solution:** Get new token from Google + +2. **Token expired:** + - Google ID tokens expire quickly (~1 hour) + **Solution:** Request new token before calling endpoint + +3. **GOOGLE_CLIENT_ID mismatch:** + + ```bash + # Check env variable matches Google Console + GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com + ``` + + **Solution:** Verify in Google Console + +4. **Frontend sending code instead of idToken:** + + ```typescript + // ✅ Correct - send ID token + fetch("/api/auth/oauth/google", { + method: "POST", + body: JSON.stringify({ + idToken: googleResponse.tokenId, + }), + }); + + // ❌ Wrong - using code + fetch("/api/auth/oauth/google", { + method: "POST", + body: JSON.stringify({ + code: googleResponse.code, // Wrong format + }), + }); + ``` + +--- + +### Issue: Microsoft OAuth fails - "Invalid token" + +**Error:** + +``` +Error: Invalid Microsoft token +``` + +**Causes:** + +1. **MICROSOFT_CLIENT_ID mismatch:** + + ```env + MICROSOFT_CLIENT_ID=your-azure-app-id + ``` + +2. **Token from wrong authority:** + - Ensure token is from same tenant/app + - Check MICROSOFT_TENANT_ID if using specific tenant + +3. **JWKS endpoint unreachable:** + - Microsoft provides public key endpoint + - Verify internet connectivity + +--- + +### Issue: Facebook OAuth fails - "Invalid access token" + +**Error:** + +``` +Error: Invalid Facebook token +``` + +**Causes:** + +1. **FB_CLIENT_ID or FB_CLIENT_SECRET incorrect:** + + ```bash + # Verify in Facebook Developer Console + FB_CLIENT_ID=your-app-id + FB_CLIENT_SECRET=your-app-secret + ``` + +2. **User access token:** + - Must be server access token, not user token + **Solution:** Get token from Facebook SDK correctly + +--- + +## 🛡️ Permission & Authorization Issues + +### Issue: Admin endpoint returns "Forbidden" + +**Error:** + +``` +ForbiddenException: Access denied +``` + +**Causes:** + +1. **User doesn't have admin role:** + + ```bash + # Check user roles in database + mongo + > db.users.findOne({email: "user@example.com"}, {roles: 1}) + ``` + + **Solution:** Assign admin role to user + +2. **Token doesn't include roles:** + - Verify JWT includes role IDs in payload + **Solution:** Token might be from before role assignment + +3. **@Admin() decorator not used:** + + ```typescript + // ✅ Correct + @Post('/admin/users') + @UseGuards(AuthenticateGuard) + @UseGuards(AdminGuard) + async createUser(@Body() dto: CreateUserDto) {} + + // ❌ Missing guard + @Post('/admin/users') + async createUser(@Body() dto: CreateUserDto) {} + ``` + +--- + +### Issue: Protected route returns "Unauthorized" + +**Error:** + +``` +UnauthorizedException: Unauthorized +``` + +**Causes:** + +1. **No Authorization header:** + + ```typescript + // ✅ Correct + fetch("/api/auth/me", { + headers: { + Authorization: "Bearer " + accessToken, + }, + }); + + // ❌ Wrong + fetch("/api/auth/me"); + ``` + +2. **Invalid Authorization format:** + - Must be: `Bearer ` + - Not: `JWT ` or just `` + +3. **AuthenticateGuard not applied:** + + ```typescript + // ✅ Correct + @Get('/protected-route') + @UseGuards(AuthenticateGuard) + async protectedRoute() {} + + // ❌ Missing guard + @Get('/protected-route') + async protectedRoute() {} + ``` + +--- + +## 🚫 Permission Model Issues + +### Issue: User has role but still can't access permission-based endpoint + +**Error:** + +``` +ForbiddenException: Permission denied +``` + +**Causes:** + +1. **Role doesn't have permission:** + + ```bash + # Check role permissions + mongo + > db.roles.findOne({name: "user"}, {permissions: 1}) + ``` + + **Solution:** Add permission to role + +2. **Permission doesn't exist:** + - Check permission in database + - Verify spelling matches exactly + +3. **@Permissions() not used:** + ```typescript + // ✅ Correct + @Patch('/admin/users') + @Permissions('users:manage') + async updateUser() {} + ``` + +--- + +## 🐛 Debugging + +### Enable Verbose Logging + +```typescript +// In your main.ts or app.module.ts +import { Logger } from "@nestjs/common"; + +const logger = new Logger(); +logger.debug("AuthKit initialized"); + +// For development, log JWT payload +import * as jwt from "jsonwebtoken"; +const decoded = jwt.decode(token); +logger.debug("Token payload:", decoded); +``` + +### Check JWT Payload + +```bash +# Use jwt.io website to decode token (verify only!) +# Or use CLI: +npx jwt-decode +``` + +### Database Inspection + +```bash +# MongoDB shell +mongo + +# List all users +> db.users.find() + +# Find specific user +> db.users.findOne({email: "user@example.com"}) + +# Check roles +> db.roles.find() + +# Check permissions +> db.permissions.find() +``` + +### Network Debugging + +```bash +# Check what's being sent +curl -v -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"user@example.com","password":"password"}' + +# Check response headers +curl -i http://localhost:3000/api/auth/me \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +--- + +## 📞 Getting Help + +If your issue isn't listed: + +1. **Check existing issues:** https://github.com/CISCODE-MA/AuthKit/issues +2. **Read documentation:** [README.md](README.md) +3. **Check copilot instructions:** [.github/copilot-instructions.md](.github/copilot-instructions.md) +4. **Open new issue** with: + - Error message (full stack trace) + - Steps to reproduce + - Your Node/npm/MongoDB versions + - What you've already tried + +--- + +## 🔗 Useful Links + +- [NestJS Documentation](https://docs.nestjs.com/) +- [MongoDB Documentation](https://docs.mongodb.com/) +- [JWT.io (Token Decoder)](https://jwt.io) +- [OWASP Security](https://owasp.org/) +- [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices) + +--- + +**Last Updated:** January 31, 2026 +**Version:** 1.0.0 From 79e2cdfd74f8126f62d068c6d9b011e182582f65 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sat, 31 Jan 2026 19:53:04 +0100 Subject: [PATCH 62/62] 1.5.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index de28c19..ae6d160 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ciscode/authentication-kit", - "version": "1.5.0", + "version": "1.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ciscode/authentication-kit", - "version": "1.5.0", + "version": "1.5.1", "license": "MIT", "dependencies": { "axios": "^1.7.7", diff --git a/package.json b/package.json index 969b7d4..77be1d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ciscode/authentication-kit", - "version": "1.5.0", + "version": "1.5.1", "description": "NestJS auth kit with local + OAuth, JWT, RBAC, password reset.", "publishConfig": { "access": "public"