Compare commits
4
Commits
test
...
development
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca446e42b8 | ||
|
|
4fcbc4b925 | ||
|
|
7cb8a7ea84 | ||
|
|
07845f2af5 |
@@ -8,3 +8,7 @@ DB_SSL=false
|
||||
PORT=9501
|
||||
CORS_ORIGIN=https://ardev.maskantech.in
|
||||
NODE_ENV=dev
|
||||
JWT_SECRET=aeroresolve_jwt_access_secret_key_dev_2026_@#!
|
||||
JWT_REFRESH_SECRET=aeroresolve_jwt_refresh_secret_key_dev_2026_@#!
|
||||
JWT_EXPIRES_IN=15m
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
|
||||
@@ -8,3 +8,7 @@ DB_SSL=false
|
||||
PORT=9502
|
||||
CORS_ORIGIN=https://artest.maskantech.in
|
||||
NODE_ENV=test
|
||||
JWT_SECRET=aeroresolve_jwt_access_secret_key_test_2026_@#!
|
||||
JWT_REFRESH_SECRET=aeroresolve_jwt_refresh_secret_key_test_2026_@#!
|
||||
JWT_EXPIRES_IN=15m
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
|
||||
@@ -8,3 +8,7 @@ DB_SSL=false
|
||||
PORT=9501
|
||||
CORS_ORIGIN=https://aruat.maskantech.in
|
||||
NODE_ENV=uat
|
||||
JWT_SECRET=aeroresolve_jwt_access_secret_key_uat_2026_@#!
|
||||
JWT_REFRESH_SECRET=aeroresolve_jwt_refresh_secret_key_uat_2026_@#!
|
||||
JWT_EXPIRES_IN=15m
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
|
||||
Generated
+275
-3
@@ -12,11 +12,17 @@
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^12.0.1",
|
||||
"@nestjs/passport": "^12.0.0",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@nestjs/typeorm": "^11.0.3",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.22.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
@@ -28,9 +34,12 @@
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^7.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.18.0",
|
||||
@@ -2373,6 +2382,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/jwt": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-12.0.1.tgz",
|
||||
"integrity": "sha512-AOvjo0JIuVMNr+6JgbEz4fP+4afclGR5ffZnHtjOFs4+HhzerUYKcIVUL06u5uQYQi/ra8WHO+NZR7OFdQeGvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"jsonwebtoken": "9.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/mapped-types": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz",
|
||||
@@ -2393,6 +2415,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/passport": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-12.0.0.tgz",
|
||||
"integrity": "sha512-FM9ioo2Dyj+cC7aeO4g19ijAXoRaHlW/GQZFbgXsM4Z9pyQedA8yOseb7ivfqlPwUKJlH9PFgeWniu6c0vb6dw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^11.0.0 || ^12.0.0",
|
||||
"passport": "^0.5.0 || ^0.6.0 || ^0.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-express": {
|
||||
"version": "11.1.28",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz",
|
||||
@@ -2786,6 +2818,13 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcryptjs": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
@@ -2807,6 +2846,16 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie-parser": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
|
||||
"integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookiejar": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
||||
@@ -2920,6 +2969,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsonwebtoken": {
|
||||
"version": "9.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ms": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
@@ -2927,16 +2986,53 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
||||
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport": {
|
||||
"version": "1.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
|
||||
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport-jwt": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||
"integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/jsonwebtoken": "*",
|
||||
"@types/passport-strategy": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport-strategy": {
|
||||
"version": "0.2.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
|
||||
"integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*",
|
||||
"@types/passport": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
|
||||
@@ -4202,6 +4298,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
@@ -4344,6 +4449,12 @@
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -4780,6 +4891,25 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser/node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
@@ -5057,6 +5187,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -7364,6 +7503,49 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^4.0.1",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -7466,6 +7648,42 @@
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -7480,6 +7698,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
@@ -8230,6 +8454,42 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/passport": {
|
||||
"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",
|
||||
"pause": "0.0.1",
|
||||
"utils-merge": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-jwt": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"passport-strategy": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-strategy": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -8307,6 +8567,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pause": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
|
||||
@@ -8893,7 +9158,6 @@
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -10242,7 +10506,6 @@
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
@@ -10349,6 +10612,15 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
|
||||
@@ -30,11 +30,17 @@
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^12.0.1",
|
||||
"@nestjs/passport": "^12.0.0",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@nestjs/typeorm": "^11.0.3",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.22.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
@@ -46,9 +52,12 @@
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^7.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.18.0",
|
||||
|
||||
+10
-1
@@ -1,12 +1,15 @@
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
|
||||
import { MasterDataModule } from './modules/master-data/master-data.module';
|
||||
import { CohortModule } from './modules/cohort/cohort.module';
|
||||
import { TenantModule } from './modules/tenant/tenant.module';
|
||||
import { TenantMiddleware } from './common/tenant/tenant.middleware';
|
||||
import { HttpLoggerMiddleware } from './common/middleware/http-logger.middleware';
|
||||
import { PolicyEngineModule } from './modules/policy-engine/policy-engine.module';
|
||||
import { RecoveryIncidentModule } from './modules/recovery-incident/recovery-incident.module';
|
||||
import { AuditLogModule } from './modules/audit-log/audit-log.module';
|
||||
@@ -23,6 +26,7 @@ const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local',
|
||||
envFilePath,
|
||||
}),
|
||||
DatabaseModule,
|
||||
AuthModule,
|
||||
HealthModule,
|
||||
TenantModule,
|
||||
MasterDataModule,
|
||||
@@ -33,6 +37,10 @@ const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local',
|
||||
DashboardModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: AuditInterceptor,
|
||||
@@ -41,6 +49,7 @@ const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local',
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(HttpLoggerMiddleware).forRoutes('*');
|
||||
consumer.apply(TenantMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,14 @@ const MODULE_MAP: Array<{ pattern: RegExp; module: string }> = [
|
||||
{ pattern: /master-data/, module: 'master-data' },
|
||||
];
|
||||
|
||||
function resolveModule(url: string): string {
|
||||
// Routes that should never trigger an entity creation audit log
|
||||
const EXCLUDED_PATTERNS: RegExp[] = [/\/auth\b/, /\/evaluate\b/];
|
||||
|
||||
function resolveModule(url: string): string | null {
|
||||
for (const entry of MODULE_MAP) {
|
||||
if (entry.pattern.test(url)) return entry.module;
|
||||
}
|
||||
return 'unknown';
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveAction(method: string, url: string): string {
|
||||
@@ -54,33 +57,62 @@ export class AuditInterceptor implements NestInterceptor {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
// Never audit authentication endpoints or non-persisting evaluation endpoints
|
||||
if (EXCLUDED_PATTERNS.some((pattern) => pattern.test(url))) {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const module = resolveModule(url);
|
||||
if (!module) {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const action = resolveAction(method, url);
|
||||
const userAgent = headers['user-agent'] ?? '';
|
||||
const tenantId = (headers['x-tenant-id'] as string) ?? 'unknown';
|
||||
|
||||
this.logger.debug(`Intercepted ${method} ${url} -> Module: ${module}, Action: ${action}`);
|
||||
this.logger.debug(
|
||||
`Intercepted ${method} ${url} -> Module: ${module}, Action: ${action}`,
|
||||
);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: async (responseData: any) => {
|
||||
next: (responseData: unknown) => {
|
||||
this.logger.debug(`Writing audit log for ${module} ${action}...`);
|
||||
try {
|
||||
await this.auditLogService.log({
|
||||
const resObj =
|
||||
typeof responseData === 'object' && responseData !== null
|
||||
? (responseData as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
||||
const entityId =
|
||||
typeof resObj?.id === 'string' ? resObj.id : undefined;
|
||||
const entityLabel =
|
||||
typeof resObj?.name === 'string'
|
||||
? resObj.name
|
||||
: typeof resObj?.recoveryCode === 'string'
|
||||
? resObj.recoveryCode
|
||||
: typeof resObj?.flightNumber === 'string'
|
||||
? resObj.flightNumber
|
||||
: undefined;
|
||||
|
||||
void this.auditLogService
|
||||
.log({
|
||||
module,
|
||||
action,
|
||||
entityId: responseData?.id ?? undefined,
|
||||
entityLabel: responseData?.name ?? responseData?.recoveryCode ?? responseData?.flightNumber ?? undefined,
|
||||
entityId,
|
||||
entityLabel,
|
||||
before: undefined, // no before on CREATE
|
||||
after: responseData ?? undefined,
|
||||
after: resObj as Record<string, any> | undefined,
|
||||
performedBy: tenantId,
|
||||
ipAddress: ip,
|
||||
userAgent: String(userAgent),
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
// Never let audit failure break the main request
|
||||
this.logger.warn(
|
||||
`Audit log failed for ${method} ${url}: ${String(err)}`,
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
// Never let audit failure break the main request
|
||||
this.logger.warn(`Audit log failed for ${method} ${url}: ${err}`);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class HttpLoggerMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger('HTTP');
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction): void {
|
||||
const { method, originalUrl, ip } = req;
|
||||
const startTime = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const { statusCode } = res;
|
||||
const duration = Date.now() - startTime;
|
||||
const rawTenant = req.headers['x-tenant-id'];
|
||||
const tenantHeader = Array.isArray(rawTenant) ? rawTenant[0] : rawTenant;
|
||||
const tenantInfo = tenantHeader ? ` [tenant: ${tenantHeader}]` : '';
|
||||
const clientIp = typeof ip === 'string' ? ip : 'unknown';
|
||||
const message = `${method} ${originalUrl} ${statusCode} +${duration}ms - ${clientIp}${tenantInfo}`;
|
||||
|
||||
if (statusCode >= 500) {
|
||||
this.logger.error(message);
|
||||
} else if (statusCode >= 400) {
|
||||
this.logger.warn(message);
|
||||
} else {
|
||||
this.logger.log(message);
|
||||
}
|
||||
});
|
||||
|
||||
res.on('close', () => {
|
||||
if (!res.writableEnded) {
|
||||
const duration = Date.now() - startTime;
|
||||
const clientIp = typeof ip === 'string' ? ip : 'unknown';
|
||||
this.logger.warn(
|
||||
`${method} ${originalUrl} CLOSED_PREMATURELY +${duration}ms - ${clientIp}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -29,11 +29,9 @@ export class TenantMiddleware implements NestMiddleware {
|
||||
{ tenantId: tenant.id, tenantSlug: tenant.slug },
|
||||
() => next(),
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Invalid X-Tenant-Id header');
|
||||
} catch {
|
||||
// Fallback gracefully without tenant requirement
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +39,14 @@ export class TenantMiddleware implements NestMiddleware {
|
||||
return (
|
||||
path === '/api/health' ||
|
||||
path === '/health' ||
|
||||
path.startsWith('/api/auth') ||
|
||||
path.startsWith('/auth') ||
|
||||
path.startsWith('/api/users') ||
|
||||
path.startsWith('/users') ||
|
||||
path.startsWith('/api/roles') ||
|
||||
path.startsWith('/roles') ||
|
||||
path.startsWith('/api/permissions') ||
|
||||
path.startsWith('/permissions') ||
|
||||
path.startsWith('/api/tenants') ||
|
||||
path.startsWith('/tenants') ||
|
||||
path.startsWith('/docs') ||
|
||||
|
||||
@@ -31,6 +31,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
// Ensure schemas exist safely without dropping user created cohorts or policies
|
||||
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "tenant";`);
|
||||
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "auth";`);
|
||||
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters_rule_engine";`);
|
||||
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters_action_builder";`);
|
||||
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters_lookup";`);
|
||||
|
||||
+6
-1
@@ -6,6 +6,8 @@ import { AppModule } from './app.module';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
|
||||
import cookieParser from 'cookie-parser';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: ['log', 'warn', 'error'],
|
||||
@@ -16,10 +18,13 @@ async function bootstrap() {
|
||||
const corsOrigin = configService.get<string>('CORS_ORIGIN') ?? 'http://localhost:5174';
|
||||
const corsOrigins = corsOrigin.split(',').map(origin => origin.trim()).filter(Boolean);
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
app.enableCors({
|
||||
origin: corsOrigins.length > 1 ? corsOrigins : corsOrigins[0],
|
||||
credentials: true,
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Tenant-Id'],
|
||||
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Tenant-Id', 'X-Refresh-Token'],
|
||||
});
|
||||
|
||||
app.useGlobalPipes(
|
||||
|
||||
@@ -4,16 +4,21 @@ import {
|
||||
Param,
|
||||
Query,
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
import { PermissionsGuard } from '../auth/guards/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
|
||||
|
||||
@ApiTags('Audit Logs')
|
||||
@Controller('audit-logs')
|
||||
@UseGuards(PermissionsGuard)
|
||||
export class AuditLogController {
|
||||
constructor(private readonly auditLogService: AuditLogService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('audit_logs:view')
|
||||
@ApiOperation({ summary: 'Get paginated audit logs with optional filters' })
|
||||
findAll(
|
||||
@Query('page') page: string = '1',
|
||||
@@ -36,6 +41,7 @@ export class AuditLogController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions('audit_logs:view')
|
||||
@ApiOperation({ summary: 'Get a single audit log entry by ID' })
|
||||
async findOne(@Param('id') id: string) {
|
||||
const log = await this.auditLogService.findOne(id);
|
||||
@@ -45,3 +51,4 @@ export class AuditLogController {
|
||||
return log;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import type { Request, Response } from 'express';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { ChangePasswordDto } from './dto/change-password.dto';
|
||||
import { Public } from './decorators/public.decorator';
|
||||
import { CurrentUser } from './decorators/current-user.decorator';
|
||||
|
||||
@ApiTags('Authentication')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Public()
|
||||
@Post('login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Login user with email & password, sets secure HTTP-only cookies' })
|
||||
@ApiResponse({ status: 200, description: 'Authentication successful' })
|
||||
@ApiResponse({ status: 401, description: 'Invalid credentials or inactive account' })
|
||||
async login(
|
||||
@Body() loginDto: LoginDto,
|
||||
@Req() req: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
return this.authService.login(loginDto, req, res);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('refresh')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Rotate refresh token and issue new access token via cookies' })
|
||||
@ApiResponse({ status: 200, description: 'Session refreshed successfully' })
|
||||
@ApiResponse({ status: 401, description: 'Invalid or revoked refresh token' })
|
||||
async refresh(
|
||||
@Req() req: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
return this.authService.refreshSession(req, res);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('logout')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Revoke active refresh token and clear authentication cookies' })
|
||||
async logout(
|
||||
@Req() req: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
return this.authService.logout(req, res);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@ApiOperation({ summary: 'Get current authenticated user profile and permissions' })
|
||||
@ApiResponse({ status: 200, description: 'Current profile retrieved' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async getMe(@CurrentUser('id') userId: string) {
|
||||
return this.authService.getMe(userId);
|
||||
}
|
||||
|
||||
@Post('change-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Change password for currently authenticated user' })
|
||||
@ApiResponse({ status: 200, description: 'Password changed successfully' })
|
||||
@ApiResponse({ status: 400, description: 'Current password invalid' })
|
||||
async changePassword(
|
||||
@CurrentUser('id') userId: string,
|
||||
@Body() dto: ChangePasswordDto,
|
||||
) {
|
||||
return this.authService.changePassword(userId, dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
import { User } from './entities/user.entity';
|
||||
import { Role } from './entities/role.entity';
|
||||
import { Permission } from './entities/permission.entity';
|
||||
import { RefreshToken } from './entities/refresh-token.entity';
|
||||
import { Tenant } from '../tenant/tenant.entity';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
import { UsersService } from './users.service';
|
||||
import { RolesService } from './roles.service';
|
||||
|
||||
import { AuthController } from './auth.controller';
|
||||
import { UsersController } from './users.controller';
|
||||
import { PermissionsController, RolesController } from './roles.controller';
|
||||
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { PermissionsGuard } from './guards/permissions.guard';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([User, Role, Permission, RefreshToken, Tenant]),
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret:
|
||||
configService.get<string>('JWT_SECRET') ||
|
||||
'aeroresolve_ultra_secure_jwt_secret_key_2026_@#!',
|
||||
signOptions: {
|
||||
expiresIn: (configService.get<string>('JWT_EXPIRES_IN') || '15m') as any,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [
|
||||
AuthController,
|
||||
UsersController,
|
||||
RolesController,
|
||||
PermissionsController,
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
UsersService,
|
||||
RolesService,
|
||||
JwtStrategy,
|
||||
JwtAuthGuard,
|
||||
PermissionsGuard,
|
||||
],
|
||||
exports: [
|
||||
AuthService,
|
||||
UsersService,
|
||||
RolesService,
|
||||
JwtAuthGuard,
|
||||
PermissionsGuard,
|
||||
PassportModule,
|
||||
JwtModule,
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -0,0 +1,549 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
OnModuleInit,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { Request, Response } from 'express';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import * as crypto from 'crypto';
|
||||
import { User } from './entities/user.entity';
|
||||
import { Role } from './entities/role.entity';
|
||||
import { Permission } from './entities/permission.entity';
|
||||
import { RefreshToken } from './entities/refresh-token.entity';
|
||||
import { Tenant } from '../tenant/tenant.entity';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { ChangePasswordDto } from './dto/change-password.dto';
|
||||
import { SYSTEM_PERMISSIONS } from './constants/permissions.constant';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService implements OnModuleInit {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Role)
|
||||
private readonly roleRepository: Repository<Role>,
|
||||
@InjectRepository(Permission)
|
||||
private readonly permissionRepository: Repository<Permission>,
|
||||
@InjectRepository(RefreshToken)
|
||||
private readonly refreshTokenRepository: Repository<RefreshToken>,
|
||||
@InjectRepository(Tenant)
|
||||
private readonly tenantRepository: Repository<Tenant>,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.seedPermissionsAndSuperAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically initializes system permissions, default roles, and super admin user
|
||||
*/
|
||||
async seedPermissionsAndSuperAdmin() {
|
||||
try {
|
||||
this.logger.log('Verifying & seeding authentication permissions and default roles...');
|
||||
|
||||
// 0. Ensure default demo tenant exists
|
||||
let defaultTenant = await this.tenantRepository.findOne({
|
||||
where: { slug: 'demo-airline' },
|
||||
});
|
||||
if (!defaultTenant) {
|
||||
defaultTenant = this.tenantRepository.create({
|
||||
slug: 'demo-airline',
|
||||
name: 'Demo Airline',
|
||||
tier: 'enterprise',
|
||||
isActive: true,
|
||||
});
|
||||
defaultTenant = await this.tenantRepository.save(defaultTenant);
|
||||
this.logger.log('Created default tenant: demo-airline');
|
||||
}
|
||||
|
||||
// 1. Seed / update system permissions
|
||||
const existingPermissions = await this.permissionRepository.find();
|
||||
const existingPermCodeMap = new Map(existingPermissions.map((p) => [p.code, p]));
|
||||
|
||||
const permissionsToSave: Permission[] = [];
|
||||
for (const def of SYSTEM_PERMISSIONS) {
|
||||
if (!existingPermCodeMap.has(def.code)) {
|
||||
const perm = this.permissionRepository.create({
|
||||
code: def.code,
|
||||
name: def.name,
|
||||
module: def.module,
|
||||
action: def.action,
|
||||
groupName: def.groupName,
|
||||
description: def.description,
|
||||
});
|
||||
permissionsToSave.push(perm);
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionsToSave.length > 0) {
|
||||
await this.permissionRepository.save(permissionsToSave);
|
||||
this.logger.log(`Seeded ${permissionsToSave.length} new system permissions.`);
|
||||
}
|
||||
|
||||
const allPermissions = await this.permissionRepository.find();
|
||||
|
||||
// 2. Seed Super Admin Role (isSystem: true with ALL permissions)
|
||||
let superAdminRole = await this.roleRepository.findOne({
|
||||
where: { slug: 'super_admin' },
|
||||
relations: { permissions: true },
|
||||
});
|
||||
|
||||
if (!superAdminRole) {
|
||||
superAdminRole = this.roleRepository.create({
|
||||
name: 'Super Admin',
|
||||
slug: 'super_admin',
|
||||
description: 'Full system privileges across all modules. Immutable system role.',
|
||||
isSystem: true,
|
||||
tenantId: defaultTenant.id,
|
||||
permissions: allPermissions,
|
||||
});
|
||||
await this.roleRepository.save(superAdminRole);
|
||||
this.logger.log('Created default Super Admin role with all permissions.');
|
||||
} else {
|
||||
// Ensure Super Admin always has all permissions
|
||||
superAdminRole.permissions = allPermissions;
|
||||
superAdminRole.isSystem = true;
|
||||
if (!superAdminRole.tenantId) {
|
||||
superAdminRole.tenantId = defaultTenant.id;
|
||||
}
|
||||
await this.roleRepository.save(superAdminRole);
|
||||
}
|
||||
|
||||
// 3. Seed Starter Role: Operations Manager
|
||||
let opsManagerRole = await this.roleRepository.findOne({
|
||||
where: { slug: 'operations_manager' },
|
||||
});
|
||||
if (!opsManagerRole) {
|
||||
const opsPermissions = allPermissions.filter(
|
||||
(p) =>
|
||||
p.module === 'dashboard' ||
|
||||
p.module === 'simulation' ||
|
||||
p.module === 'recovery' ||
|
||||
p.module === 'cohorts' ||
|
||||
p.module === 'policy_engine' ||
|
||||
p.module === 'config' ||
|
||||
p.module === 'audit_logs',
|
||||
);
|
||||
opsManagerRole = this.roleRepository.create({
|
||||
name: 'Operations Manager',
|
||||
slug: 'operations_manager',
|
||||
description: 'Manages flight operations, policies, cohorts, and recovery workflows',
|
||||
isSystem: false,
|
||||
tenantId: defaultTenant.id,
|
||||
permissions: opsPermissions,
|
||||
});
|
||||
await this.roleRepository.save(opsManagerRole);
|
||||
}
|
||||
|
||||
// 4. Seed Starter Role: Disruption Agent
|
||||
let agentRole = await this.roleRepository.findOne({
|
||||
where: { slug: 'disruption_agent' },
|
||||
});
|
||||
if (!agentRole) {
|
||||
const agentPermissions = allPermissions.filter(
|
||||
(p) =>
|
||||
p.code === 'dashboard:view' ||
|
||||
p.code === 'simulation:view' ||
|
||||
p.code === 'simulation:execute' ||
|
||||
p.code === 'recovery:view' ||
|
||||
p.code === 'recovery:create' ||
|
||||
p.code === 'recovery:edit' ||
|
||||
p.code === 'recovery:evaluate',
|
||||
);
|
||||
agentRole = this.roleRepository.create({
|
||||
name: 'Disruption Agent',
|
||||
slug: 'disruption_agent',
|
||||
description: 'Operational agent handling passenger incidents and simulations',
|
||||
isSystem: false,
|
||||
tenantId: defaultTenant.id,
|
||||
permissions: agentPermissions,
|
||||
});
|
||||
await this.roleRepository.save(agentRole);
|
||||
}
|
||||
|
||||
// 5. Seed Starter Role: Read-Only Auditor
|
||||
let auditorRole = await this.roleRepository.findOne({
|
||||
where: { slug: 'auditor' },
|
||||
});
|
||||
if (!auditorRole) {
|
||||
const viewPermissions = allPermissions.filter((p) => p.action === 'view' || p.action === 'export');
|
||||
auditorRole = this.roleRepository.create({
|
||||
name: 'Auditor',
|
||||
slug: 'auditor',
|
||||
description: 'Read-only access to audit logs and system analytics',
|
||||
isSystem: false,
|
||||
tenantId: defaultTenant.id,
|
||||
permissions: viewPermissions,
|
||||
});
|
||||
await this.roleRepository.save(auditorRole);
|
||||
}
|
||||
|
||||
// 6. Seed Default Super Admin User
|
||||
const defaultEmail = 'admin@aeroresolve.com';
|
||||
let adminUser = await this.userRepository.findOne({
|
||||
where: { email: defaultEmail },
|
||||
});
|
||||
|
||||
if (!adminUser) {
|
||||
const salt = await bcrypt.genSalt(12);
|
||||
const hashedPassword = await bcrypt.hash('Password@123', salt);
|
||||
|
||||
adminUser = this.userRepository.create({
|
||||
email: defaultEmail,
|
||||
passwordHash: hashedPassword,
|
||||
firstName: 'System',
|
||||
lastName: 'Administrator',
|
||||
roleId: superAdminRole.id,
|
||||
role: superAdminRole,
|
||||
tenantId: defaultTenant.id,
|
||||
tenant: defaultTenant,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
});
|
||||
|
||||
await this.userRepository.save(adminUser);
|
||||
this.logger.log(`Seeded default Super Admin user (${defaultEmail} / Password@123) for tenant "${defaultTenant.name}"`);
|
||||
} else {
|
||||
// Ensure admin user points to superAdminRole and defaultTenant
|
||||
let needsSave = false;
|
||||
if (!adminUser.isSystem || adminUser.roleId !== superAdminRole.id) {
|
||||
adminUser.isSystem = true;
|
||||
adminUser.roleId = superAdminRole.id;
|
||||
needsSave = true;
|
||||
}
|
||||
if (!adminUser.tenantId) {
|
||||
adminUser.tenantId = defaultTenant.id;
|
||||
needsSave = true;
|
||||
}
|
||||
if (needsSave) {
|
||||
await this.userRepository.save(adminUser);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to seed authentication data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User login with credential validation, token generation, and secure cookie setting
|
||||
*/
|
||||
async login(loginDto: LoginDto, req: Request, res: Response) {
|
||||
const { email, password } = loginDto;
|
||||
const normalizedEmail = email.toLowerCase().trim();
|
||||
|
||||
const user = await this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.passwordHash')
|
||||
.leftJoinAndSelect('user.role', 'role')
|
||||
.leftJoinAndSelect('role.permissions', 'permissions')
|
||||
.where('LOWER(user.email) = :email', { email: normalizedEmail })
|
||||
.getOne();
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid email or password');
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
throw new UnauthorizedException('Account is deactivated. Please contact an administrator.');
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!isMatch) {
|
||||
throw new UnauthorizedException('Invalid email or password');
|
||||
}
|
||||
|
||||
// Generate Tokens
|
||||
const { accessToken, refreshToken, refreshTokenExpiresAt } = await this.generateTokens(user);
|
||||
|
||||
// Store hashed refresh token in DB
|
||||
const tokenHash = this.hashToken(refreshToken);
|
||||
const ipAddress = req.ip || req.socket.remoteAddress || 'unknown';
|
||||
const deviceInfo = req.headers['user-agent'] || 'unknown';
|
||||
|
||||
const tokenEntity = this.refreshTokenRepository.create({
|
||||
userId: user.id,
|
||||
tokenHash,
|
||||
ipAddress: String(ipAddress).slice(0, 45),
|
||||
deviceInfo: String(deviceInfo).slice(0, 500),
|
||||
expiresAt: refreshTokenExpiresAt,
|
||||
isRevoked: false,
|
||||
});
|
||||
await this.refreshTokenRepository.save(tokenEntity);
|
||||
|
||||
// Update last login
|
||||
user.lastLoginAt = new Date();
|
||||
await this.userRepository.save(user);
|
||||
|
||||
// Set secure HTTP-only cookies
|
||||
this.setAuthCookies(res, accessToken, refreshToken);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
user: this.sanitizeUser(user),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Token rotation flow
|
||||
*/
|
||||
async refreshSession(req: Request, res: Response) {
|
||||
let rawRefreshToken = req.cookies?.['aero_refresh_token'];
|
||||
if (!rawRefreshToken && req.headers['x-refresh-token']) {
|
||||
rawRefreshToken = req.headers['x-refresh-token'] as string;
|
||||
}
|
||||
|
||||
if (!rawRefreshToken) {
|
||||
throw new UnauthorizedException('Refresh token missing');
|
||||
}
|
||||
|
||||
let payload: any;
|
||||
try {
|
||||
const refreshSecret =
|
||||
this.configService.get<string>('JWT_REFRESH_SECRET') ||
|
||||
'aeroresolve_ultra_refresh_secret_key_2026_@#!';
|
||||
payload = this.jwtService.verify(rawRefreshToken, { secret: refreshSecret });
|
||||
} catch {
|
||||
this.clearAuthCookies(res);
|
||||
throw new UnauthorizedException('Invalid or expired refresh token');
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: payload.sub },
|
||||
relations: { role: { permissions: true } },
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
this.clearAuthCookies(res);
|
||||
throw new UnauthorizedException('User account invalid or deactivated');
|
||||
}
|
||||
|
||||
const tokenHash = this.hashToken(rawRefreshToken);
|
||||
const existingTokenRecord = await this.refreshTokenRepository.findOne({
|
||||
where: {
|
||||
userId: user.id,
|
||||
tokenHash,
|
||||
isRevoked: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingTokenRecord || existingTokenRecord.expiresAt < new Date()) {
|
||||
// Possible reuse detection / attack -> revoke all user sessions
|
||||
if (existingTokenRecord?.isRevoked) {
|
||||
await this.refreshTokenRepository.update({ userId: user.id }, { isRevoked: true });
|
||||
}
|
||||
this.clearAuthCookies(res);
|
||||
throw new UnauthorizedException('Refresh token revoked or expired');
|
||||
}
|
||||
|
||||
// Revoke old refresh token (rotation)
|
||||
existingTokenRecord.isRevoked = true;
|
||||
await this.refreshTokenRepository.save(existingTokenRecord);
|
||||
|
||||
// Issue new pair
|
||||
const { accessToken, refreshToken: newRefreshToken, refreshTokenExpiresAt } =
|
||||
await this.generateTokens(user);
|
||||
|
||||
const newTokenHash = this.hashToken(newRefreshToken);
|
||||
const ipAddress = req.ip || req.socket.remoteAddress || 'unknown';
|
||||
const deviceInfo = req.headers['user-agent'] || 'unknown';
|
||||
|
||||
const newTokenEntity = this.refreshTokenRepository.create({
|
||||
userId: user.id,
|
||||
tokenHash: newTokenHash,
|
||||
ipAddress: String(ipAddress).slice(0, 45),
|
||||
deviceInfo: String(deviceInfo).slice(0, 500),
|
||||
expiresAt: refreshTokenExpiresAt,
|
||||
isRevoked: false,
|
||||
});
|
||||
await this.refreshTokenRepository.save(newTokenEntity);
|
||||
|
||||
// Set updated cookies
|
||||
this.setAuthCookies(res, accessToken, newRefreshToken);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
user: this.sanitizeUser(user),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* User logout
|
||||
*/
|
||||
async logout(req: Request, res: Response) {
|
||||
const rawRefreshToken = req.cookies?.['aero_refresh_token'];
|
||||
if (rawRefreshToken) {
|
||||
const tokenHash = this.hashToken(rawRefreshToken);
|
||||
await this.refreshTokenRepository.update({ tokenHash }, { isRevoked: true });
|
||||
}
|
||||
|
||||
this.clearAuthCookies(res);
|
||||
return { success: true, message: 'Logged out successfully' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authenticated user profile with permissions
|
||||
*/
|
||||
async getMe(userId: string) {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
relations: { role: { permissions: true } },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User profile not found');
|
||||
}
|
||||
|
||||
return this.sanitizeUser(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change user password
|
||||
*/
|
||||
async changePassword(userId: string, dto: ChangePasswordDto) {
|
||||
const user = await this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.passwordHash')
|
||||
.where('user.id = :userId', { userId })
|
||||
.getOne();
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(dto.currentPassword, user.passwordHash);
|
||||
if (!isMatch) {
|
||||
throw new BadRequestException('Current password does not match');
|
||||
}
|
||||
|
||||
const salt = await bcrypt.genSalt(12);
|
||||
user.passwordHash = await bcrypt.hash(dto.newPassword, salt);
|
||||
await this.userRepository.save(user);
|
||||
|
||||
// Invalidate all active refresh tokens on password change
|
||||
await this.refreshTokenRepository.update({ userId: user.id }, { isRevoked: true });
|
||||
|
||||
return { success: true, message: 'Password updated successfully' };
|
||||
}
|
||||
|
||||
// Helper: Generate JWT access and refresh tokens
|
||||
private async generateTokens(user: User) {
|
||||
const accessSecret =
|
||||
this.configService.get<string>('JWT_SECRET') ||
|
||||
'aeroresolve_ultra_secure_jwt_secret_key_2026_@#!';
|
||||
const refreshSecret =
|
||||
this.configService.get<string>('JWT_REFRESH_SECRET') ||
|
||||
'aeroresolve_ultra_refresh_secret_key_2026_@#!';
|
||||
|
||||
const accessExpiresIn =
|
||||
(this.configService.get<string>('JWT_EXPIRES_IN') || '15m') as any;
|
||||
const refreshExpiresIn =
|
||||
(this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') || '7d') as any;
|
||||
|
||||
const payload = {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
roleId: user.roleId,
|
||||
roleSlug: user.role?.slug || 'unknown',
|
||||
};
|
||||
|
||||
const accessToken = this.jwtService.sign(payload, {
|
||||
secret: accessSecret,
|
||||
expiresIn: accessExpiresIn,
|
||||
});
|
||||
|
||||
const refreshToken = this.jwtService.sign(payload, {
|
||||
secret: refreshSecret,
|
||||
expiresIn: refreshExpiresIn,
|
||||
});
|
||||
|
||||
const refreshTokenExpiresAt = new Date();
|
||||
refreshTokenExpiresAt.setDate(refreshTokenExpiresAt.getDate() + 7);
|
||||
|
||||
return { accessToken, refreshToken, refreshTokenExpiresAt };
|
||||
}
|
||||
|
||||
// Helper: SHA256 hash for stored refresh tokens
|
||||
private hashToken(token: string): string {
|
||||
return crypto.createHash('sha256').update(token).digest('hex');
|
||||
}
|
||||
|
||||
// Helper: Set secure HTTP-only cookies
|
||||
private setAuthCookies(res: Response, accessToken: string, refreshToken: string) {
|
||||
const isProd = this.configService.get<string>('NODE_ENV') === 'production';
|
||||
|
||||
res.cookie('aero_access_token', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: isProd,
|
||||
sameSite: isProd ? 'strict' : 'lax',
|
||||
path: '/',
|
||||
maxAge: 15 * 60 * 1000, // 15 minutes
|
||||
});
|
||||
|
||||
res.cookie('aero_refresh_token', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: isProd,
|
||||
sameSite: isProd ? 'strict' : 'lax',
|
||||
path: '/',
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: Clear cookies
|
||||
private clearAuthCookies(res: Response) {
|
||||
const isProd = this.configService.get<string>('NODE_ENV') === 'production';
|
||||
res.cookie('aero_access_token', '', {
|
||||
httpOnly: true,
|
||||
secure: isProd,
|
||||
sameSite: isProd ? 'strict' : 'lax',
|
||||
path: '/',
|
||||
maxAge: 0,
|
||||
});
|
||||
res.cookie('aero_refresh_token', '', {
|
||||
httpOnly: true,
|
||||
secure: isProd,
|
||||
sameSite: isProd ? 'strict' : 'lax',
|
||||
path: '/',
|
||||
maxAge: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: Format user object for client consumption
|
||||
private sanitizeUser(user: User) {
|
||||
const permissions = user.role?.permissions?.map((p) => p.code) || [];
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
fullName: `${user.firstName} ${user.lastName || ''}`.trim(),
|
||||
roleId: user.roleId,
|
||||
tenantId: user.tenantId,
|
||||
role: {
|
||||
id: user.role?.id,
|
||||
name: user.role?.name,
|
||||
slug: user.role?.slug,
|
||||
description: user.role?.description,
|
||||
isSystem: user.role?.isSystem,
|
||||
},
|
||||
isActive: user.isActive,
|
||||
isSystem: user.isSystem,
|
||||
permissions,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
createdAt: user.createdAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
export interface SystemPermissionDef {
|
||||
code: string;
|
||||
name: string;
|
||||
module: string;
|
||||
action: string;
|
||||
groupName: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const SYSTEM_PERMISSIONS: SystemPermissionDef[] = [
|
||||
// Dashboard
|
||||
{
|
||||
code: 'dashboard:view',
|
||||
name: 'View Dashboard',
|
||||
module: 'dashboard',
|
||||
action: 'view',
|
||||
groupName: 'Dashboard',
|
||||
description: 'Allows viewing dashboard metrics, recovery status overview, and KPIs',
|
||||
},
|
||||
{
|
||||
code: 'dashboard:export',
|
||||
name: 'Export Dashboard Metrics',
|
||||
module: 'dashboard',
|
||||
action: 'export',
|
||||
groupName: 'Dashboard',
|
||||
description: 'Allows exporting aggregated dashboard statistics and operational summaries',
|
||||
},
|
||||
|
||||
// Simulation Engine
|
||||
{
|
||||
code: 'simulation:view',
|
||||
name: 'Access Simulation Terminal',
|
||||
module: 'simulation',
|
||||
action: 'view',
|
||||
groupName: 'Simulation Engine',
|
||||
description: 'Allows accessing manifest simulation workspace and previewing policy outcomes',
|
||||
},
|
||||
{
|
||||
code: 'simulation:execute',
|
||||
name: 'Execute Simulation Run',
|
||||
module: 'simulation',
|
||||
action: 'execute',
|
||||
groupName: 'Simulation Engine',
|
||||
description: 'Allows running real-time impact simulations against live or test passenger manifests',
|
||||
},
|
||||
|
||||
// Recovery Incidents
|
||||
{
|
||||
code: 'recovery:view',
|
||||
name: 'View Recovery Incidents',
|
||||
module: 'recovery',
|
||||
action: 'view',
|
||||
groupName: 'Recovery Incidents',
|
||||
description: 'Allows viewing flight disruption incidents and affected passenger lists',
|
||||
},
|
||||
{
|
||||
code: 'recovery:create',
|
||||
name: 'Create Recovery Incident',
|
||||
module: 'recovery',
|
||||
action: 'create',
|
||||
groupName: 'Recovery Incidents',
|
||||
description: 'Allows registering and initiating a new flight recovery incident',
|
||||
},
|
||||
{
|
||||
code: 'recovery:edit',
|
||||
name: 'Update Recovery Incident',
|
||||
module: 'recovery',
|
||||
action: 'edit',
|
||||
groupName: 'Recovery Incidents',
|
||||
description: 'Allows modifying incident parameters, status, and notes',
|
||||
},
|
||||
{
|
||||
code: 'recovery:evaluate',
|
||||
name: 'Evaluate Incident Policies',
|
||||
module: 'recovery',
|
||||
action: 'execute',
|
||||
groupName: 'Recovery Incidents',
|
||||
description: 'Allows running policy engine evaluations and generating compensation offers',
|
||||
},
|
||||
{
|
||||
code: 'recovery:override',
|
||||
name: 'Manual Action Override',
|
||||
module: 'recovery',
|
||||
action: 'approve',
|
||||
groupName: 'Recovery Incidents',
|
||||
description: 'Allows overriding automated decisions and granting exceptional goodwill gestures',
|
||||
},
|
||||
{
|
||||
code: 'recovery:delete',
|
||||
name: 'Delete Recovery Incident',
|
||||
module: 'recovery',
|
||||
action: 'delete',
|
||||
groupName: 'Recovery Incidents',
|
||||
description: 'Allows removing incident records from the system',
|
||||
},
|
||||
|
||||
// Cohort Management
|
||||
{
|
||||
code: 'cohorts:view',
|
||||
name: 'View Cohorts',
|
||||
module: 'cohorts',
|
||||
action: 'view',
|
||||
groupName: 'Cohort Management',
|
||||
description: 'Allows viewing passenger segmentation cohorts and classification rules',
|
||||
},
|
||||
{
|
||||
code: 'cohorts:create',
|
||||
name: 'Create Cohort',
|
||||
module: 'cohorts',
|
||||
action: 'create',
|
||||
groupName: 'Cohort Management',
|
||||
description: 'Allows designing and creating new passenger segmentation cohorts',
|
||||
},
|
||||
{
|
||||
code: 'cohorts:edit',
|
||||
name: 'Edit Cohort',
|
||||
module: 'cohorts',
|
||||
action: 'edit',
|
||||
groupName: 'Cohort Management',
|
||||
description: 'Allows updating cohort criteria, priority, and definitions',
|
||||
},
|
||||
{
|
||||
code: 'cohorts:delete',
|
||||
name: 'Delete Cohort',
|
||||
module: 'cohorts',
|
||||
action: 'delete',
|
||||
groupName: 'Cohort Management',
|
||||
description: 'Allows deleting passenger cohorts',
|
||||
},
|
||||
|
||||
// Policy Engine
|
||||
{
|
||||
code: 'policy_engine:view',
|
||||
name: 'View Policy Frameworks',
|
||||
module: 'policy_engine',
|
||||
action: 'view',
|
||||
groupName: 'Policy Engine',
|
||||
description: 'Allows viewing policy frameworks, rules, and condition sets',
|
||||
},
|
||||
{
|
||||
code: 'policy_engine:create',
|
||||
name: 'Create Policy Framework',
|
||||
module: 'policy_engine',
|
||||
action: 'create',
|
||||
groupName: 'Policy Engine',
|
||||
description: 'Allows authoring new policy frameworks and rules',
|
||||
},
|
||||
{
|
||||
code: 'policy_engine:edit',
|
||||
name: 'Edit Policy Framework',
|
||||
module: 'policy_engine',
|
||||
action: 'edit',
|
||||
groupName: 'Policy Engine',
|
||||
description: 'Allows modifying existing policy conditions, rules, and actions',
|
||||
},
|
||||
{
|
||||
code: 'policy_engine:publish',
|
||||
name: 'Publish/Activate Policy',
|
||||
module: 'policy_engine',
|
||||
action: 'publish',
|
||||
groupName: 'Policy Engine',
|
||||
description: 'Allows promoting draft policies to active production status',
|
||||
},
|
||||
{
|
||||
code: 'policy_engine:delete',
|
||||
name: 'Delete Policy Framework',
|
||||
module: 'policy_engine',
|
||||
action: 'delete',
|
||||
groupName: 'Policy Engine',
|
||||
description: 'Allows removing policy frameworks from the registry',
|
||||
},
|
||||
|
||||
// Configuration & Action Builder & Master Data
|
||||
{
|
||||
code: 'config:view',
|
||||
name: 'View Master Configurations',
|
||||
module: 'config',
|
||||
action: 'view',
|
||||
groupName: 'Configuration & Masters',
|
||||
description: 'Allows viewing system master data lookups and action builder templates',
|
||||
},
|
||||
{
|
||||
code: 'config:create',
|
||||
name: 'Create Master Data Item',
|
||||
module: 'config',
|
||||
action: 'create',
|
||||
groupName: 'Configuration & Masters',
|
||||
description: 'Allows creating new master lookup entries, categories, and field definitions',
|
||||
},
|
||||
{
|
||||
code: 'config:edit',
|
||||
name: 'Edit Master Data Item',
|
||||
module: 'config',
|
||||
action: 'edit',
|
||||
groupName: 'Configuration & Masters',
|
||||
description: 'Allows editing master lookup values, action types, and metadata schemas',
|
||||
},
|
||||
{
|
||||
code: 'config:delete',
|
||||
name: 'Delete Master Data Item',
|
||||
module: 'config',
|
||||
action: 'delete',
|
||||
groupName: 'Configuration & Masters',
|
||||
description: 'Allows deleting master lookup records',
|
||||
},
|
||||
|
||||
// Audit Logs
|
||||
{
|
||||
code: 'audit_logs:view',
|
||||
name: 'View Audit Logs',
|
||||
module: 'audit_logs',
|
||||
action: 'view',
|
||||
groupName: 'Audit Logs',
|
||||
description: 'Allows inspecting immutable compliance and event logs across the system',
|
||||
},
|
||||
{
|
||||
code: 'audit_logs:export',
|
||||
name: 'Export Audit Logs',
|
||||
module: 'audit_logs',
|
||||
action: 'export',
|
||||
groupName: 'Audit Logs',
|
||||
description: 'Allows exporting regulatory audit trails and security event logs',
|
||||
},
|
||||
|
||||
// User Management
|
||||
{
|
||||
code: 'users:view',
|
||||
name: 'View Users',
|
||||
module: 'users',
|
||||
action: 'view',
|
||||
groupName: 'Access Control',
|
||||
description: 'Allows viewing system users, profile details, and account statuses',
|
||||
},
|
||||
{
|
||||
code: 'users:create',
|
||||
name: 'Create User',
|
||||
module: 'users',
|
||||
action: 'create',
|
||||
groupName: 'Access Control',
|
||||
description: 'Allows provisioning new user accounts with role assignment',
|
||||
},
|
||||
{
|
||||
code: 'users:edit',
|
||||
name: 'Edit User',
|
||||
module: 'users',
|
||||
action: 'edit',
|
||||
groupName: 'Access Control',
|
||||
description: 'Allows updating user profiles, roles, and status (active/inactive)',
|
||||
},
|
||||
{
|
||||
code: 'users:reset_password',
|
||||
name: 'Reset User Password',
|
||||
module: 'users',
|
||||
action: 'edit',
|
||||
groupName: 'Access Control',
|
||||
description: 'Allows resetting credentials for user accounts',
|
||||
},
|
||||
{
|
||||
code: 'users:delete',
|
||||
name: 'Delete User',
|
||||
module: 'users',
|
||||
action: 'delete',
|
||||
groupName: 'Access Control',
|
||||
description: 'Allows removing non-system user accounts',
|
||||
},
|
||||
|
||||
// Role Management
|
||||
{
|
||||
code: 'roles:view',
|
||||
name: 'View Roles & Permissions',
|
||||
module: 'roles',
|
||||
action: 'view',
|
||||
groupName: 'Access Control',
|
||||
description: 'Allows viewing defined roles and assigned permission matrices',
|
||||
},
|
||||
{
|
||||
code: 'roles:create',
|
||||
name: 'Create Custom Role',
|
||||
module: 'roles',
|
||||
action: 'create',
|
||||
groupName: 'Access Control',
|
||||
description: 'Allows defining custom roles with granular permission mappings',
|
||||
},
|
||||
{
|
||||
code: 'roles:edit',
|
||||
name: 'Edit Role Permissions',
|
||||
module: 'roles',
|
||||
action: 'edit',
|
||||
groupName: 'Access Control',
|
||||
description: 'Allows updating name, description, and permissions of custom roles',
|
||||
},
|
||||
{
|
||||
code: 'roles:delete',
|
||||
name: 'Delete Role',
|
||||
module: 'roles',
|
||||
action: 'delete',
|
||||
groupName: 'Access Control',
|
||||
description: 'Allows deleting non-system roles that have no active users',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: string | undefined, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const PERMISSIONS_KEY = 'permissions';
|
||||
export const RequirePermissions = (...permissions: string[]) =>
|
||||
SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class ChangePasswordDto {
|
||||
@ApiProperty({ example: 'OldPassword@123' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Current password is required' })
|
||||
currentPassword!: string;
|
||||
|
||||
@ApiProperty({ example: 'NewSecretPass@2026' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'New password is required' })
|
||||
@MinLength(8, { message: 'New password must be at least 8 characters long' })
|
||||
newPassword!: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({ example: 'admin@aeroresolve.com' })
|
||||
@IsEmail({}, { message: 'Please provide a valid email address' })
|
||||
@IsNotEmpty({ message: 'Email is required' })
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({ example: 'Password@123' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Password is required' })
|
||||
@MinLength(6, { message: 'Password must be at least 6 characters long' })
|
||||
password!: string;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateRoleDto {
|
||||
@ApiProperty({ example: 'Flight Disruption Lead' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Role name is required' })
|
||||
name!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Manages flight recovery incident workflows and exceptions' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: ['11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222'],
|
||||
description: 'Array of Permission UUIDs to assign to this role',
|
||||
})
|
||||
@IsArray()
|
||||
@ArrayNotEmpty({ message: 'At least one permission must be assigned to the role' })
|
||||
@IsUUID('all', { each: true, message: 'Each permission ID must be a valid UUID' })
|
||||
permissionIds!: string[];
|
||||
}
|
||||
|
||||
export class UpdateRoleDto {
|
||||
@ApiPropertyOptional({ example: 'Flight Disruption Lead' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Updated description of role responsibilities' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: ['11111111-1111-1111-1111-111111111111'],
|
||||
description: 'Updated array of Permission UUIDs for this role',
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsUUID('all', { each: true, message: 'Each permission ID must be a valid UUID' })
|
||||
permissionIds?: string[];
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@ApiProperty({ example: 'john.doe@aeroresolve.com' })
|
||||
@IsEmail({}, { message: 'Please provide a valid email address' })
|
||||
@IsNotEmpty({ message: 'Email is required' })
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({ example: 'Password@123' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Password is required' })
|
||||
@MinLength(8, { message: 'Password must be at least 8 characters long' })
|
||||
password!: string;
|
||||
|
||||
@ApiProperty({ example: 'John' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'First name is required' })
|
||||
firstName!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Doe' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
lastName?: string;
|
||||
|
||||
@ApiProperty({ example: '11111111-1111-1111-1111-111111111111' })
|
||||
@IsUUID('all', { message: 'Valid Role ID (UUID) is required' })
|
||||
@IsNotEmpty({ message: 'Role ID is required' })
|
||||
roleId!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: true, default: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class UpdateUserDto {
|
||||
@ApiPropertyOptional({ example: 'John' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
firstName?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Doe' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
lastName?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '11111111-1111-1111-1111-111111111111' })
|
||||
@IsUUID('all', { message: 'Valid Role ID (UUID) is required' })
|
||||
@IsOptional()
|
||||
roleId?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class ResetUserPasswordDto {
|
||||
@ApiProperty({ example: 'NewSecretPass@2026' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'New password is required' })
|
||||
@MinLength(8, { message: 'Password must be at least 8 characters long' })
|
||||
newPassword!: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './permission.entity';
|
||||
export * from './role.entity';
|
||||
export * from './user.entity';
|
||||
export * from './refresh-token.entity';
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_permissions', schema: 'auth' })
|
||||
export class Permission {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
module!: string; // e.g., 'dashboard', 'simulation', 'recovery', 'cohorts', 'policy_engine', 'config', 'audit_logs', 'users', 'roles'
|
||||
|
||||
@Column({ type: 'varchar', length: 50 })
|
||||
action!: string; // e.g., 'view', 'create', 'edit', 'delete', 'export', 'execute', 'publish'
|
||||
|
||||
@Column({ type: 'varchar', length: 100, unique: true })
|
||||
code!: string; // e.g., 'policy_engine:create', 'users:delete'
|
||||
|
||||
@Column({ type: 'varchar', length: 150 })
|
||||
name!: string; // e.g., 'Create Policy'
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description!: string;
|
||||
|
||||
@Column({ name: 'group_name', type: 'varchar', length: 100, default: 'General' })
|
||||
groupName!: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
|
||||
@Entity({ name: 'tbl_refresh_tokens', schema: 'auth' })
|
||||
export class RefreshToken {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'user_id', type: 'uuid' })
|
||||
userId!: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.refreshTokens, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User;
|
||||
|
||||
@Column({ name: 'token_hash', type: 'varchar', length: 255 })
|
||||
tokenHash!: string;
|
||||
|
||||
@Column({ name: 'device_info', type: 'text', nullable: true })
|
||||
deviceInfo!: string;
|
||||
|
||||
@Column({ name: 'ip_address', type: 'varchar', length: 45, nullable: true })
|
||||
ipAddress!: string;
|
||||
|
||||
@Column({ name: 'is_revoked', type: 'boolean', default: false })
|
||||
isRevoked!: boolean;
|
||||
|
||||
@Column({ name: 'expires_at', type: 'timestamp with time zone' })
|
||||
expiresAt!: Date;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Permission } from './permission.entity';
|
||||
import { User } from './user.entity';
|
||||
import { Tenant } from '../../tenant/tenant.entity';
|
||||
|
||||
@Entity({ name: 'tbl_roles', schema: 'auth' })
|
||||
export class Role {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
|
||||
tenantId!: string | null;
|
||||
|
||||
@ManyToOne(() => Tenant, { eager: false, nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'tenant_id' })
|
||||
tenant!: Tenant | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, unique: true })
|
||||
slug!: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description!: string;
|
||||
|
||||
@Column({ name: 'is_system', type: 'boolean', default: false })
|
||||
isSystem!: boolean; // System roles (like Super Admin) cannot be edited or deleted
|
||||
|
||||
@ManyToMany(() => Permission, { eager: true, cascade: true })
|
||||
@JoinTable({
|
||||
name: 'tbl_role_permissions',
|
||||
schema: 'auth',
|
||||
joinColumn: { name: 'role_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'permission_id', referencedColumnName: 'id' },
|
||||
})
|
||||
permissions!: Permission[];
|
||||
|
||||
@OneToMany(() => User, (user) => user.role)
|
||||
users!: User[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Role } from './role.entity';
|
||||
import { RefreshToken } from './refresh-token.entity';
|
||||
import { Tenant } from '../../tenant/tenant.entity';
|
||||
|
||||
@Entity({ name: 'tbl_users', schema: 'auth' })
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
|
||||
tenantId!: string | null;
|
||||
|
||||
@ManyToOne(() => Tenant, { eager: false, nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'tenant_id' })
|
||||
tenant!: Tenant | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, unique: true })
|
||||
email!: string;
|
||||
|
||||
@Column({ name: 'password_hash', type: 'varchar', length: 255, select: false })
|
||||
passwordHash!: string;
|
||||
|
||||
@Column({ name: 'first_name', type: 'varchar', length: 100 })
|
||||
firstName!: string;
|
||||
|
||||
@Column({ name: 'last_name', type: 'varchar', length: 100, nullable: true })
|
||||
lastName!: string;
|
||||
|
||||
@Column({ name: 'role_id', type: 'uuid' })
|
||||
roleId!: string;
|
||||
|
||||
@ManyToOne(() => Role, (role) => role.users, { eager: true, onDelete: 'RESTRICT' })
|
||||
@JoinColumn({ name: 'role_id' })
|
||||
role!: Role;
|
||||
|
||||
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@Column({ name: 'is_system', type: 'boolean', default: false })
|
||||
isSystem!: boolean; // System default admin user cannot be deleted
|
||||
|
||||
@Column({ name: 'last_login_at', type: 'timestamp with time zone', nullable: true })
|
||||
lastLoginAt!: Date | null;
|
||||
|
||||
@OneToMany(() => RefreshToken, (token) => token.user, { cascade: true })
|
||||
refreshTokens!: RefreshToken[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
handleRequest(err: any, user: any, info: any) {
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException('Authentication required');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PERMISSIONS_KEY } from '../decorators/permissions.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
|
||||
PERMISSIONS_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
|
||||
if (!user) {
|
||||
throw new ForbiddenException('User is not authenticated');
|
||||
}
|
||||
|
||||
// Super Admin has full unrestricted access across all endpoints
|
||||
if (user.role?.slug === 'super_admin' || user.isSystem) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userPermissions: string[] = user.permissions || [];
|
||||
|
||||
const hasPermission = requiredPermissions.every((permission) =>
|
||||
userPermissions.includes(permission),
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new ForbiddenException(
|
||||
`Insufficient privileges. Required permission(s): ${requiredPermissions.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { RolesService } from './roles.service';
|
||||
import { CreateRoleDto, UpdateRoleDto } from './dto/role.dto';
|
||||
import { RequirePermissions } from './decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from './guards/permissions.guard';
|
||||
|
||||
@ApiTags('Roles & Permissions')
|
||||
@Controller('roles')
|
||||
@UseGuards(PermissionsGuard)
|
||||
export class RolesController {
|
||||
constructor(private readonly rolesService: RolesService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('roles:view')
|
||||
@ApiOperation({ summary: 'List all roles with assigned user and permission counts' })
|
||||
async findAll() {
|
||||
return this.rolesService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions('roles:view')
|
||||
@ApiOperation({ summary: 'Get role details with all assigned permissions' })
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.rolesService.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions('roles:create')
|
||||
@ApiOperation({ summary: 'Create a new custom role with assigned permissions' })
|
||||
async create(@Body() dto: CreateRoleDto) {
|
||||
return this.rolesService.create(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions('roles:edit')
|
||||
@ApiOperation({ summary: 'Update role details and assigned permissions (System roles protected)' })
|
||||
async update(@Param('id') id: string, @Body() dto: UpdateRoleDto) {
|
||||
return this.rolesService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions('roles:delete')
|
||||
@ApiOperation({ summary: 'Delete custom role (System roles & roles with active users protected)' })
|
||||
async remove(@Param('id') id: string) {
|
||||
return this.rolesService.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiTags('Permissions')
|
||||
@Controller('permissions')
|
||||
@UseGuards(PermissionsGuard)
|
||||
export class PermissionsController {
|
||||
constructor(private readonly rolesService: RolesService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('roles:view')
|
||||
@ApiOperation({ summary: 'List all available system permissions categorized by module and group' })
|
||||
async findAll() {
|
||||
return this.rolesService.findAllPermissions();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { Role } from './entities/role.entity';
|
||||
import { Permission } from './entities/permission.entity';
|
||||
import { User } from './entities/user.entity';
|
||||
import { CreateRoleDto, UpdateRoleDto } from './dto/role.dto';
|
||||
|
||||
@Injectable()
|
||||
export class RolesService {
|
||||
constructor(
|
||||
@InjectRepository(Role)
|
||||
private readonly roleRepository: Repository<Role>,
|
||||
@InjectRepository(Permission)
|
||||
private readonly permissionRepository: Repository<Permission>,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* List all roles with assigned user count
|
||||
*/
|
||||
async findAll() {
|
||||
const roles = await this.roleRepository.find({
|
||||
relations: { permissions: true },
|
||||
order: { isSystem: 'DESC', name: 'ASC' },
|
||||
});
|
||||
|
||||
const userCounts = await this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.select('user.roleId', 'roleId')
|
||||
.addSelect('COUNT(user.id)', 'count')
|
||||
.groupBy('user.roleId')
|
||||
.getRawMany();
|
||||
|
||||
const countMap = new Map<string, number>(
|
||||
userCounts.map((uc) => [uc.roleId, parseInt(uc.count, 10)]),
|
||||
);
|
||||
|
||||
return roles.map((role) => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
slug: role.slug,
|
||||
description: role.description,
|
||||
isSystem: role.isSystem,
|
||||
permissionCount: role.permissions?.length || 0,
|
||||
userCount: countMap.get(role.id) || 0,
|
||||
createdAt: role.createdAt,
|
||||
updatedAt: role.updatedAt,
|
||||
permissions: role.permissions,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find role by ID
|
||||
*/
|
||||
async findOne(id: string) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { id },
|
||||
relations: { permissions: true },
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new NotFoundException(`Role with ID "${id}" not found`);
|
||||
}
|
||||
|
||||
const userCount = await this.userRepository.count({ where: { roleId: id } });
|
||||
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
slug: role.slug,
|
||||
description: role.description,
|
||||
isSystem: role.isSystem,
|
||||
userCount,
|
||||
permissions: role.permissions,
|
||||
createdAt: role.createdAt,
|
||||
updatedAt: role.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom role
|
||||
*/
|
||||
async create(dto: CreateRoleDto) {
|
||||
const baseSlug = dto.name
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
|
||||
const existingSlug = await this.roleRepository.findOne({ where: { slug: baseSlug } });
|
||||
if (existingSlug) {
|
||||
throw new ConflictException(`A role with name "${dto.name}" already exists`);
|
||||
}
|
||||
|
||||
const permissions = await this.permissionRepository.findBy({
|
||||
id: In(dto.permissionIds),
|
||||
});
|
||||
|
||||
if (permissions.length === 0) {
|
||||
throw new BadRequestException('At least one valid permission must be selected');
|
||||
}
|
||||
|
||||
const role = this.roleRepository.create({
|
||||
name: dto.name.trim(),
|
||||
slug: baseSlug,
|
||||
description: dto.description?.trim(),
|
||||
isSystem: false,
|
||||
permissions,
|
||||
});
|
||||
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role details and permissions
|
||||
*/
|
||||
async update(id: string, dto: UpdateRoleDto) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { id },
|
||||
relations: { permissions: true },
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new NotFoundException(`Role with ID "${id}" not found`);
|
||||
}
|
||||
|
||||
// System roles (Super Admin) cannot be modified or edited
|
||||
if (role.isSystem) {
|
||||
throw new ForbiddenException('System default roles (Super Admin) are immutable and cannot be edited');
|
||||
}
|
||||
|
||||
if (dto.name) {
|
||||
role.name = dto.name.trim();
|
||||
}
|
||||
|
||||
if (dto.description !== undefined) {
|
||||
role.description = dto.description?.trim() || '';
|
||||
}
|
||||
|
||||
if (dto.permissionIds) {
|
||||
const permissions = await this.permissionRepository.findBy({
|
||||
id: In(dto.permissionIds),
|
||||
});
|
||||
if (permissions.length === 0) {
|
||||
throw new BadRequestException('A role must have at least one permission');
|
||||
}
|
||||
role.permissions = permissions;
|
||||
}
|
||||
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete role (guarded against system roles and roles in use)
|
||||
*/
|
||||
async remove(id: string) {
|
||||
const role = await this.roleRepository.findOne({ where: { id } });
|
||||
|
||||
if (!role) {
|
||||
throw new NotFoundException(`Role with ID "${id}" not found`);
|
||||
}
|
||||
|
||||
if (role.isSystem) {
|
||||
throw new ForbiddenException('System default roles (Super Admin) cannot be deleted');
|
||||
}
|
||||
|
||||
const userCount = await this.userRepository.count({ where: { roleId: id } });
|
||||
if (userCount > 0) {
|
||||
throw new ConflictException(
|
||||
`Cannot delete role "${role.name}" because it is currently assigned to ${userCount} user(s). Reassign them first.`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.roleRepository.remove(role);
|
||||
return { success: true, message: `Role "${role.name}" removed successfully` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all system permissions organized by module / group
|
||||
*/
|
||||
async findAllPermissions() {
|
||||
const permissions = await this.permissionRepository.find({
|
||||
order: { groupName: 'ASC', name: 'ASC' },
|
||||
});
|
||||
|
||||
// Group permissions for matrix UI
|
||||
const grouped: Record<string, Permission[]> = {};
|
||||
for (const perm of permissions) {
|
||||
const group = perm.groupName || perm.module;
|
||||
if (!grouped[group]) {
|
||||
grouped[group] = [];
|
||||
}
|
||||
grouped[group].push(perm);
|
||||
}
|
||||
|
||||
return {
|
||||
all: permissions,
|
||||
grouped,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { Request } from 'express';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from '../entities/user.entity';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
roleId: string;
|
||||
roleSlug: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||
(request: Request) => {
|
||||
let token = null;
|
||||
if (request && request.cookies) {
|
||||
token = request.cookies['aero_access_token'];
|
||||
}
|
||||
if (!token && request?.headers?.authorization) {
|
||||
const parts = request.headers.authorization.split(' ');
|
||||
if (parts.length === 2 && parts[0] === 'Bearer') {
|
||||
token = parts[1];
|
||||
}
|
||||
}
|
||||
return token;
|
||||
},
|
||||
]),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey:
|
||||
configService.get<string>('JWT_SECRET') ||
|
||||
'aeroresolve_ultra_secure_jwt_secret_key_2026_@#!',
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload) {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: payload.sub },
|
||||
relations: { role: { permissions: true } },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('User account no longer exists');
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
throw new UnauthorizedException('User account has been deactivated');
|
||||
}
|
||||
|
||||
const permissionCodes = user.role?.permissions?.map((p) => p.code) || [];
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
fullName: `${user.firstName} ${user.lastName || ''}`.trim(),
|
||||
roleId: user.roleId,
|
||||
role: {
|
||||
id: user.role?.id,
|
||||
name: user.role?.name,
|
||||
slug: user.role?.slug,
|
||||
isSystem: user.role?.isSystem,
|
||||
},
|
||||
isSystem: user.isSystem,
|
||||
permissions: permissionCodes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { UsersService } from './users.service';
|
||||
import { CreateUserDto, ResetUserPasswordDto, UpdateUserDto } from './dto/user.dto';
|
||||
import { RequirePermissions } from './decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from './guards/permissions.guard';
|
||||
import { CurrentUser } from './decorators/current-user.decorator';
|
||||
|
||||
@ApiTags('Users')
|
||||
@Controller('users')
|
||||
@UseGuards(PermissionsGuard)
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('users:view')
|
||||
@ApiOperation({ summary: 'List users with optional search and role filtering' })
|
||||
@ApiQuery({ name: 'search', required: false })
|
||||
@ApiQuery({ name: 'roleId', required: false })
|
||||
async findAll(
|
||||
@Query('search') search?: string,
|
||||
@Query('roleId') roleId?: string,
|
||||
) {
|
||||
return this.usersService.findAll(search, roleId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions('users:view')
|
||||
@ApiOperation({ summary: 'Get single user details by ID' })
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.usersService.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions('users:create')
|
||||
@ApiOperation({ summary: 'Create a new user with role assignment' })
|
||||
async create(@Body() dto: CreateUserDto) {
|
||||
return this.usersService.create(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions('users:edit')
|
||||
@ApiOperation({ summary: 'Update user profile, status, or role' })
|
||||
async update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
|
||||
return this.usersService.update(id, dto);
|
||||
}
|
||||
|
||||
@Post(':id/reset-password')
|
||||
@RequirePermissions('users:reset_password')
|
||||
@ApiOperation({ summary: 'Reset password for a user account' })
|
||||
async resetPassword(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: ResetUserPasswordDto,
|
||||
) {
|
||||
return this.usersService.resetPassword(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions('users:delete')
|
||||
@ApiOperation({ summary: 'Delete a non-system user' })
|
||||
async remove(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser('id') currentUserId: string,
|
||||
) {
|
||||
return this.usersService.remove(id, currentUserId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { User } from './entities/user.entity';
|
||||
import { Role } from './entities/role.entity';
|
||||
import { RefreshToken } from './entities/refresh-token.entity';
|
||||
import { CreateUserDto, ResetUserPasswordDto, UpdateUserDto } from './dto/user.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Role)
|
||||
private readonly roleRepository: Repository<Role>,
|
||||
@InjectRepository(RefreshToken)
|
||||
private readonly refreshTokenRepository: Repository<RefreshToken>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* List all users with optional search and role filtering
|
||||
*/
|
||||
async findAll(search?: string, roleId?: string) {
|
||||
const qb = this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.leftJoinAndSelect('user.role', 'role')
|
||||
.leftJoinAndSelect('role.permissions', 'permissions')
|
||||
.orderBy('user.isSystem', 'DESC')
|
||||
.addOrderBy('user.createdAt', 'DESC');
|
||||
|
||||
if (search) {
|
||||
const term = `%${search.toLowerCase().trim()}%`;
|
||||
qb.andWhere(
|
||||
'(LOWER(user.email) LIKE :term OR LOWER(user.firstName) LIKE :term OR LOWER(user.lastName) LIKE :term)',
|
||||
{ term },
|
||||
);
|
||||
}
|
||||
|
||||
if (roleId) {
|
||||
qb.andWhere('user.roleId = :roleId', { roleId });
|
||||
}
|
||||
|
||||
const users = await qb.getMany();
|
||||
return users.map((u) => this.sanitizeUser(u));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find single user by ID
|
||||
*/
|
||||
async findOne(id: string) {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id },
|
||||
relations: { role: { permissions: true } },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID "${id}" not found`);
|
||||
}
|
||||
|
||||
return this.sanitizeUser(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new user
|
||||
*/
|
||||
async create(dto: CreateUserDto) {
|
||||
const normalizedEmail = dto.email.toLowerCase().trim();
|
||||
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { email: normalizedEmail },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new ConflictException(`User with email "${normalizedEmail}" already exists`);
|
||||
}
|
||||
|
||||
const role = await this.roleRepository.findOne({ where: { id: dto.roleId } });
|
||||
if (!role) {
|
||||
throw new BadRequestException(`Role with ID "${dto.roleId}" not found`);
|
||||
}
|
||||
|
||||
const salt = await bcrypt.genSalt(12);
|
||||
const passwordHash = await bcrypt.hash(dto.password, salt);
|
||||
|
||||
const user = this.userRepository.create({
|
||||
email: normalizedEmail,
|
||||
passwordHash,
|
||||
firstName: dto.firstName.trim(),
|
||||
lastName: dto.lastName?.trim(),
|
||||
roleId: role.id,
|
||||
role,
|
||||
tenantId: role.tenantId,
|
||||
isActive: dto.isActive !== undefined ? dto.isActive : true,
|
||||
isSystem: false,
|
||||
});
|
||||
|
||||
const saved = await this.userRepository.save(user);
|
||||
return this.findOne(saved.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user details
|
||||
*/
|
||||
async update(id: string, dto: UpdateUserDto) {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id },
|
||||
relations: { role: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID "${id}" not found`);
|
||||
}
|
||||
|
||||
// System user safety: cannot deactivate default admin
|
||||
if (user.isSystem && dto.isActive === false) {
|
||||
throw new ForbiddenException('The default System Administrator account cannot be deactivated');
|
||||
}
|
||||
|
||||
if (dto.firstName) {
|
||||
user.firstName = dto.firstName.trim();
|
||||
}
|
||||
|
||||
if (dto.lastName !== undefined) {
|
||||
user.lastName = dto.lastName?.trim();
|
||||
}
|
||||
|
||||
if (dto.roleId && dto.roleId !== user.roleId) {
|
||||
const newRole = await this.roleRepository.findOne({ where: { id: dto.roleId } });
|
||||
if (!newRole) {
|
||||
throw new BadRequestException(`Role with ID "${dto.roleId}" not found`);
|
||||
}
|
||||
user.roleId = newRole.id;
|
||||
user.role = newRole;
|
||||
}
|
||||
|
||||
if (dto.isActive !== undefined) {
|
||||
user.isActive = dto.isActive;
|
||||
}
|
||||
|
||||
await this.userRepository.save(user);
|
||||
return this.findOne(user.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user password (by admin)
|
||||
*/
|
||||
async resetPassword(id: string, dto: ResetUserPasswordDto) {
|
||||
const user = await this.userRepository.findOne({ where: { id } });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID "${id}" not found`);
|
||||
}
|
||||
|
||||
const salt = await bcrypt.genSalt(12);
|
||||
user.passwordHash = await bcrypt.hash(dto.newPassword, salt);
|
||||
await this.userRepository.save(user);
|
||||
|
||||
// Invalidate active sessions
|
||||
await this.refreshTokenRepository.update({ userId: user.id }, { isRevoked: true });
|
||||
|
||||
return { success: true, message: `Password for "${user.email}" was reset successfully` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user
|
||||
*/
|
||||
async remove(id: string, currentUserId: string) {
|
||||
if (id === currentUserId) {
|
||||
throw new ForbiddenException('You cannot delete your own active account');
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({ where: { id } });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID "${id}" not found`);
|
||||
}
|
||||
|
||||
if (user.isSystem) {
|
||||
throw new ForbiddenException('The default System Administrator account cannot be deleted');
|
||||
}
|
||||
|
||||
// Revoke all refresh tokens
|
||||
await this.refreshTokenRepository.delete({ userId: user.id });
|
||||
await this.userRepository.remove(user);
|
||||
|
||||
return { success: true, message: `User "${user.email}" removed successfully` };
|
||||
}
|
||||
|
||||
private sanitizeUser(user: User) {
|
||||
const permissions = user.role?.permissions?.map((p) => p.code) || [];
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
fullName: `${user.firstName} ${user.lastName || ''}`.trim(),
|
||||
roleId: user.roleId,
|
||||
tenantId: user.tenantId,
|
||||
role: {
|
||||
id: user.role?.id,
|
||||
name: user.role?.name,
|
||||
slug: user.role?.slug,
|
||||
description: user.role?.description,
|
||||
isSystem: user.role?.isSystem,
|
||||
},
|
||||
isActive: user.isActive,
|
||||
isSystem: user.isSystem,
|
||||
permissions,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
import { Controller, Get, Post, Body, Param, Put, Delete, Patch, Query } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Param, Put, Delete, Patch, Query, UseGuards } from '@nestjs/common';
|
||||
import { CohortService } from './cohort.service';
|
||||
import { CreateCohortDto } from './dto/create-cohort.dto';
|
||||
import { UpdateCohortDto } from './dto/update-cohort.dto';
|
||||
import { PermissionsGuard } from '../auth/guards/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
|
||||
|
||||
@Controller('cohorts')
|
||||
@UseGuards(PermissionsGuard)
|
||||
export class CohortController {
|
||||
constructor(private readonly cohortService: CohortService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions('cohorts:create')
|
||||
async create(@Body() createCohortDto: CreateCohortDto) {
|
||||
return this.cohortService.create(createCohortDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('cohorts:view')
|
||||
async findAll(
|
||||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '10',
|
||||
@@ -21,22 +26,27 @@ export class CohortController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions('cohorts:view')
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.cohortService.findOne(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@RequirePermissions('cohorts:edit')
|
||||
async update(@Param('id') id: string, @Body() updateCohortDto: UpdateCohortDto) {
|
||||
return this.cohortService.update(id, updateCohortDto);
|
||||
}
|
||||
|
||||
@Patch(':id/status')
|
||||
@RequirePermissions('cohorts:edit')
|
||||
async updateStatus(@Param('id') id: string, @Body('status') status: string) {
|
||||
return this.cohortService.updateStatus(id, status);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions('cohorts:delete')
|
||||
async remove(@Param('id') id: string) {
|
||||
return this.cohortService.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
import { PermissionsGuard } from '../auth/guards/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
|
||||
|
||||
@ApiTags('Dashboard')
|
||||
@Controller('dashboard')
|
||||
@UseGuards(PermissionsGuard)
|
||||
export class DashboardController {
|
||||
constructor(private readonly dashboardService: DashboardService) {}
|
||||
|
||||
@Get('metrics')
|
||||
@RequirePermissions('dashboard:view')
|
||||
@ApiOperation({ summary: 'Get dashboard summary metrics' })
|
||||
@ApiResponse({ status: 200, description: 'Return 5 aggregated metric cards for dashboard' })
|
||||
getMetrics() {
|
||||
@@ -15,6 +19,7 @@ export class DashboardController {
|
||||
}
|
||||
|
||||
@Get('exposure')
|
||||
@RequirePermissions('dashboard:view')
|
||||
@ApiOperation({ summary: 'Get refund and compensation exposure data' })
|
||||
@ApiResponse({ status: 200, description: 'Return exposure category metrics' })
|
||||
getExposure() {
|
||||
@@ -22,6 +27,7 @@ export class DashboardController {
|
||||
}
|
||||
|
||||
@Get('disruption-mix')
|
||||
@RequirePermissions('dashboard:view')
|
||||
@ApiOperation({ summary: 'Get disruption mix breakdown' })
|
||||
@ApiResponse({ status: 200, description: 'Return disruption mix category breakdown' })
|
||||
getDisruptionMix() {
|
||||
@@ -29,9 +35,11 @@ export class DashboardController {
|
||||
}
|
||||
|
||||
@Get('recent-incidents')
|
||||
@RequirePermissions('dashboard:view')
|
||||
@ApiOperation({ summary: 'Get recent recovery incidents for dashboard' })
|
||||
@ApiResponse({ status: 200, description: 'Return recent incident rows' })
|
||||
getRecentIncidents() {
|
||||
return this.dashboardService.getRecentIncidents();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { HealthService } from './health.service';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
|
||||
@ApiTags('health')
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(private readonly healthService: HealthService) {}
|
||||
|
||||
@Public()
|
||||
@Get()
|
||||
getHealth(): any {
|
||||
return this.healthService.getHealth();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Put, Patch, Delete, Param, Body, Query } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Put, Patch, Delete, Param, Body, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { MasterDataService } from './master-data.service';
|
||||
import { CreateMasterDataDto } from './dto/create-master-data.dto';
|
||||
@@ -14,13 +14,17 @@ import { UpdateActionTypeDto } from './dto/update-action-type.dto';
|
||||
import { CreateFieldDefinitionDto } from './dto/create-field-definition.dto';
|
||||
import { UpdateFieldDefinitionDto } from './dto/update-field-definition.dto';
|
||||
import { ActionSubmissionDto } from './dto/action-submission.dto';
|
||||
import { PermissionsGuard } from '../auth/guards/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
|
||||
|
||||
@ApiTags('master-data')
|
||||
@Controller('master-data')
|
||||
@UseGuards(PermissionsGuard)
|
||||
export class MasterDataController {
|
||||
constructor(private readonly masterDataService: MasterDataService) { }
|
||||
|
||||
@Get('lookup-tables')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get list of all master domain lookup tables' })
|
||||
@ApiResponse({ status: 200, description: 'List of master lookup tables' })
|
||||
async findAllLookupTables() {
|
||||
@@ -28,6 +32,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('categories')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get all masters table list as categories' })
|
||||
@ApiResponse({ status: 200, description: 'List of masters table categories' })
|
||||
async findAllRuleCategories() {
|
||||
@@ -35,6 +40,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('categories/:id')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get one category' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'category id' })
|
||||
@ApiResponse({ status: 200, description: 'category found' })
|
||||
@@ -43,6 +49,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Post('categories')
|
||||
@RequirePermissions('config:create')
|
||||
@ApiOperation({ summary: 'Create a category' })
|
||||
@ApiBody({
|
||||
type: CreateRuleCategoryDto,
|
||||
@@ -54,6 +61,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Put('categories/:id')
|
||||
@RequirePermissions('config:edit')
|
||||
@ApiOperation({ summary: 'Update a category' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'category id' })
|
||||
@ApiBody({
|
||||
@@ -69,6 +77,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Delete('categories/:id')
|
||||
@RequirePermissions('config:delete')
|
||||
@ApiOperation({ summary: 'Delete a category' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'category id' })
|
||||
@ApiResponse({ status: 200, description: 'category deleted' })
|
||||
@@ -77,6 +86,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('category-values/:code')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get category values by category code' })
|
||||
@ApiParam({ name: 'code', type: String, description: 'category code' })
|
||||
@ApiResponse({ status: 200, description: 'category values for the requested code' })
|
||||
@@ -87,6 +97,7 @@ export class MasterDataController {
|
||||
// --- 3-Level Policy Engine Metadata Endpoints ---
|
||||
|
||||
@Get('rule-categories/:id/condition-groups')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get allowed condition groups for a rule category' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'Rule category code or id' })
|
||||
async findConditionGroupsByRuleCategory(@Param('id') id: string) {
|
||||
@@ -94,6 +105,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('condition-groups/:groupId/fields')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get condition fields for a condition group' })
|
||||
@ApiParam({ name: 'groupId', type: String, description: 'Condition group code or id' })
|
||||
async findConditionFieldsByGroup(@Param('groupId') groupId: string) {
|
||||
@@ -101,6 +113,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('condition-fields/:fieldId/lookup-values')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get dynamic lookup drop-down values for a condition field' })
|
||||
@ApiParam({ name: 'fieldId', type: String, description: 'Condition field code or id' })
|
||||
async findLookupValuesForField(@Param('fieldId') fieldId: string) {
|
||||
@@ -108,6 +121,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('operators')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get all operators' })
|
||||
@ApiResponse({ status: 200, description: 'List of operators' })
|
||||
async findAllOperators() {
|
||||
@@ -115,6 +129,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('operators/:id')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get one operator' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'Operator id' })
|
||||
@ApiResponse({ status: 200, description: 'Operator found' })
|
||||
@@ -123,6 +138,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Post('operators')
|
||||
@RequirePermissions('config:create')
|
||||
@ApiOperation({ summary: 'Create an operator' })
|
||||
@ApiBody({
|
||||
type: CreateOperatorDto,
|
||||
@@ -134,6 +150,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Put('operators/:id')
|
||||
@RequirePermissions('config:edit')
|
||||
@ApiOperation({ summary: 'Update an operator' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'Operator id' })
|
||||
@ApiBody({
|
||||
@@ -149,6 +166,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Delete('operators/:id')
|
||||
@RequirePermissions('config:delete')
|
||||
@ApiOperation({ summary: 'Delete an operator' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'Operator id' })
|
||||
@ApiResponse({ status: 200, description: 'Operator deleted' })
|
||||
@@ -159,6 +177,7 @@ export class MasterDataController {
|
||||
// --- Action Categories Endpoints ---
|
||||
|
||||
@Get('action-categories')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get all action categories' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@@ -174,6 +193,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('action-categories/:id')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get one action category' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'Action category id' })
|
||||
@ApiResponse({ status: 200, description: 'Action category found' })
|
||||
@@ -182,6 +202,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Post('action-categories')
|
||||
@RequirePermissions('config:create')
|
||||
@ApiOperation({ summary: 'Create an action category' })
|
||||
@ApiBody({ type: CreateActionCategoryDto, description: 'Action category payload' })
|
||||
@ApiResponse({ status: 201, description: 'Action category created' })
|
||||
@@ -190,6 +211,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Put('action-categories/:id')
|
||||
@RequirePermissions('config:edit')
|
||||
@ApiOperation({ summary: 'Update an action category' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'Action category id' })
|
||||
@ApiBody({ type: UpdateActionCategoryDto, description: 'Action category update payload' })
|
||||
@@ -202,6 +224,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Delete('action-categories/:id')
|
||||
@RequirePermissions('config:delete')
|
||||
@ApiOperation({ summary: 'Delete an action category' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'Action category id' })
|
||||
@ApiResponse({ status: 200, description: 'Action category deleted' })
|
||||
@@ -212,6 +235,7 @@ export class MasterDataController {
|
||||
// --- Action Types Endpoints ---
|
||||
|
||||
@Get('action-types')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get all action types' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@@ -227,6 +251,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('action-types/category/:categoryCode')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get action types by action category code' })
|
||||
@ApiParam({ name: 'categoryCode', type: String, description: 'Action category code' })
|
||||
@ApiResponse({ status: 200, description: 'List of action types for the requested category' })
|
||||
@@ -235,6 +260,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('action-types/category-id/:categoryId')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get action types by action category ID' })
|
||||
@ApiParam({ name: 'categoryId', type: String, description: 'Action category ID' })
|
||||
@ApiResponse({ status: 200, description: 'List of action types for the requested category ID' })
|
||||
@@ -243,6 +269,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get('action-types/:id')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get one action type' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'Action type id' })
|
||||
@ApiResponse({ status: 200, description: 'Action type found' })
|
||||
@@ -251,6 +278,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Post('action-types')
|
||||
@RequirePermissions('config:create')
|
||||
@ApiOperation({ summary: 'Create an action type' })
|
||||
@ApiBody({ type: CreateActionTypeDto, description: 'Action type payload' })
|
||||
@ApiResponse({ status: 201, description: 'Action type created' })
|
||||
@@ -259,6 +287,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Put('action-types/:id')
|
||||
@RequirePermissions('config:edit')
|
||||
@ApiOperation({ summary: 'Update an action type' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'Action type id' })
|
||||
@ApiBody({ type: UpdateActionTypeDto, description: 'Action type update payload' })
|
||||
@@ -271,6 +300,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Delete('action-types/:id')
|
||||
@RequirePermissions('config:delete')
|
||||
@ApiOperation({ summary: 'Delete an action type' })
|
||||
@ApiParam({ name: 'id', type: String, description: 'Action type id' })
|
||||
@ApiResponse({ status: 200, description: 'Action type deleted' })
|
||||
@@ -281,6 +311,7 @@ export class MasterDataController {
|
||||
// --- Field Definitions Endpoints ---
|
||||
|
||||
@Get('action-types/:actionTypeId/fields')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get all field definitions for an Action Type' })
|
||||
@ApiParam({ name: 'actionTypeId', type: String, description: 'Action type id' })
|
||||
async findFieldsByActionType(@Param('actionTypeId') actionTypeId: string) {
|
||||
@@ -288,6 +319,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Post('action-types/:actionTypeId/fields')
|
||||
@RequirePermissions('config:create')
|
||||
@ApiOperation({ summary: 'Create a field definition for an Action Type' })
|
||||
@ApiParam({ name: 'actionTypeId', type: String, description: 'Action type id' })
|
||||
async createFieldDefinition(
|
||||
@@ -298,6 +330,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Patch('fields/:fieldId')
|
||||
@RequirePermissions('config:edit')
|
||||
@ApiOperation({ summary: 'Update a field definition (PATCH)' })
|
||||
@ApiParam({ name: 'fieldId', type: String, description: 'Field definition id' })
|
||||
async patchFieldDefinition(
|
||||
@@ -308,6 +341,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Put('fields/:fieldId')
|
||||
@RequirePermissions('config:edit')
|
||||
@ApiOperation({ summary: 'Update a field definition (PUT)' })
|
||||
@ApiParam({ name: 'fieldId', type: String, description: 'Field definition id' })
|
||||
async updateFieldDefinition(
|
||||
@@ -318,6 +352,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Delete('fields/:fieldId')
|
||||
@RequirePermissions('config:delete')
|
||||
@ApiOperation({ summary: 'Delete a field definition' })
|
||||
@ApiParam({ name: 'fieldId', type: String, description: 'Field definition id' })
|
||||
async removeFieldDefinition(@Param('fieldId') fieldId: string) {
|
||||
@@ -325,6 +360,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Post('action-types/:actionTypeId/fields/reorder')
|
||||
@RequirePermissions('config:edit')
|
||||
@ApiOperation({ summary: 'Reorder field definitions for an Action Type' })
|
||||
@ApiParam({ name: 'actionTypeId', type: String, description: 'Action type id' })
|
||||
async reorderFieldDefinitions(
|
||||
@@ -337,30 +373,35 @@ export class MasterDataController {
|
||||
// --- Dynamic Action Submission ---
|
||||
|
||||
@Post('actions')
|
||||
@RequirePermissions('recovery:create', 'recovery:edit', 'simulation:execute')
|
||||
@ApiOperation({ summary: 'Submit an action payload with dynamic field revalidation' })
|
||||
async submitAction(@Body() payload: ActionSubmissionDto) {
|
||||
return this.masterDataService.validateAndSubmitAction(payload);
|
||||
}
|
||||
|
||||
@Get('refund-bases')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get all refund bases' })
|
||||
async findAllRefundBases() {
|
||||
return this.masterDataService.findAll('REFUND_BASIS');
|
||||
}
|
||||
|
||||
@Get('currencies')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get all currencies' })
|
||||
async findAllCurrencies() {
|
||||
return this.masterDataService.findAll('CURRENCY');
|
||||
}
|
||||
|
||||
@Get('refund-methods')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get all refund methods' })
|
||||
async findAllRefundMethods() {
|
||||
return this.masterDataService.findAll('REFUND_METHOD');
|
||||
}
|
||||
|
||||
@Get('amount-types')
|
||||
@RequirePermissions('config:view')
|
||||
@ApiOperation({ summary: 'Get all amount types' })
|
||||
async findAllAmountTypes() {
|
||||
return this.masterDataService.findAll('AMOUNT_TYPE');
|
||||
@@ -369,6 +410,7 @@ export class MasterDataController {
|
||||
// --- Generic Master Data Endpoints ---
|
||||
|
||||
@Post(':category')
|
||||
@RequirePermissions('config:create')
|
||||
async create(
|
||||
@Param('category') category: string,
|
||||
@Body() createDto: CreateMasterDataDto,
|
||||
@@ -377,11 +419,13 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Get(':category')
|
||||
@RequirePermissions('config:view')
|
||||
async findAll(@Param('category') category: string) {
|
||||
return this.masterDataService.findAll(category);
|
||||
}
|
||||
|
||||
@Get(':category/:id')
|
||||
@RequirePermissions('config:view')
|
||||
async findOne(
|
||||
@Param('category') category: string,
|
||||
@Param('id') id: string,
|
||||
@@ -390,6 +434,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Put(':category/:id')
|
||||
@RequirePermissions('config:edit')
|
||||
async update(
|
||||
@Param('category') category: string,
|
||||
@Param('id') id: string,
|
||||
@@ -399,6 +444,7 @@ export class MasterDataController {
|
||||
}
|
||||
|
||||
@Delete(':category/:id')
|
||||
@RequirePermissions('config:delete')
|
||||
async remove(
|
||||
@Param('category') category: string,
|
||||
@Param('id') id: string,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { PolicyEngineService } from './policy-engine.service';
|
||||
import { PolicyEvaluationService } from './policy-evaluation.service';
|
||||
@@ -15,8 +16,11 @@ import type { EvaluationInput } from './services/master-value-resolver.service';
|
||||
import { CreatePolicyDto } from './dto/create-policy.dto';
|
||||
import { UpdatePolicyDto } from './dto/update-policy.dto';
|
||||
import { PolicyStatus, AudienceType } from './entities/policy.enums';
|
||||
import { PermissionsGuard } from '../auth/guards/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
|
||||
|
||||
@Controller('policy-engine')
|
||||
@UseGuards(PermissionsGuard)
|
||||
export class PolicyEngineController {
|
||||
constructor(
|
||||
private readonly policyEngineService: PolicyEngineService,
|
||||
@@ -24,11 +28,13 @@ export class PolicyEngineController {
|
||||
) { }
|
||||
|
||||
@Post('evaluate')
|
||||
@RequirePermissions('simulation:execute', 'recovery:evaluate')
|
||||
async evaluate(@Body() input: EvaluationInput) {
|
||||
return this.policyEvaluationService.evaluateIncident(input);
|
||||
}
|
||||
|
||||
@Post('evaluate-batch')
|
||||
@RequirePermissions('simulation:execute', 'recovery:evaluate')
|
||||
async evaluateBatch(@Body() inputs: EvaluationInput[]) {
|
||||
const list = Array.isArray(inputs) ? inputs : [inputs];
|
||||
return Promise.all(
|
||||
@@ -37,11 +43,13 @@ export class PolicyEngineController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions('policy_engine:create')
|
||||
async create(@Body() createPolicyDto: CreatePolicyDto) {
|
||||
return this.policyEngineService.create(createPolicyDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('policy_engine:view')
|
||||
async findAll(
|
||||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '10',
|
||||
@@ -57,11 +65,13 @@ export class PolicyEngineController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions('policy_engine:view')
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.policyEngineService.findOne(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@RequirePermissions('policy_engine:edit')
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() updatePolicyDto: UpdatePolicyDto,
|
||||
@@ -70,6 +80,7 @@ export class PolicyEngineController {
|
||||
}
|
||||
|
||||
@Patch(':id/status')
|
||||
@RequirePermissions('policy_engine:publish', 'policy_engine:edit')
|
||||
async updateStatus(
|
||||
@Param('id') id: string,
|
||||
@Body('status') status: PolicyStatus,
|
||||
@@ -78,7 +89,9 @@ export class PolicyEngineController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions('policy_engine:delete')
|
||||
async remove(@Param('id') id: string) {
|
||||
return this.policyEngineService.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, HttpCode, HttpStatus, UseGuards } from '@nestjs/common';
|
||||
import { RecoveryIncidentService } from './recovery-incident.service';
|
||||
import { CreateRecoveryIncidentDto } from './dto/create-recovery-incident.dto';
|
||||
import { UpdateRecoveryIncidentDto } from './dto/update-recovery-incident.dto';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { PermissionsGuard } from '../auth/guards/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
|
||||
|
||||
@ApiTags('Recovery Incidents')
|
||||
@Controller('recovery-incidents')
|
||||
@UseGuards(PermissionsGuard)
|
||||
export class RecoveryIncidentController {
|
||||
constructor(private readonly recoveryIncidentService: RecoveryIncidentService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions('recovery:create')
|
||||
@ApiOperation({ summary: 'Create a new recovery incident' })
|
||||
@ApiResponse({ status: HttpStatus.CREATED, description: 'The recovery incident has been successfully created.' })
|
||||
create(@Body() createDto: CreateRecoveryIncidentDto) {
|
||||
@@ -17,36 +21,42 @@ export class RecoveryIncidentController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('recovery:view')
|
||||
@ApiOperation({ summary: 'Get all recovery incidents for the current tenant' })
|
||||
findAll() {
|
||||
return this.recoveryIncidentService.findAll();
|
||||
}
|
||||
|
||||
@Get('metrics')
|
||||
@RequirePermissions('recovery:view')
|
||||
@ApiOperation({ summary: 'Get recovery incidents metrics summary' })
|
||||
getMetrics() {
|
||||
return this.recoveryIncidentService.getMetrics();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions('recovery:view')
|
||||
@ApiOperation({ summary: 'Get a recovery incident by ID' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.recoveryIncidentService.findOne(id);
|
||||
}
|
||||
|
||||
@Get(':id/audit-trail')
|
||||
@RequirePermissions('recovery:view')
|
||||
@ApiOperation({ summary: 'Get dynamic audit trail steps with real timestamps for a recovery incident' })
|
||||
getAuditTrail(@Param('id') id: string) {
|
||||
return this.recoveryIncidentService.getAuditTrail(id);
|
||||
}
|
||||
|
||||
@Post(':id/evaluate')
|
||||
@RequirePermissions('recovery:evaluate')
|
||||
@ApiOperation({ summary: 'Re-run policy engine evaluation for a recovery incident' })
|
||||
evaluate(@Param('id') id: string) {
|
||||
return this.recoveryIncidentService.runPolicyEvaluation(id);
|
||||
}
|
||||
|
||||
@Patch(':id/status')
|
||||
@RequirePermissions('recovery:edit')
|
||||
@ApiOperation({ summary: 'Update status of a recovery incident' })
|
||||
updateStatus(
|
||||
@Param('id') id: string,
|
||||
@@ -56,15 +66,18 @@ export class RecoveryIncidentController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions('recovery:edit')
|
||||
@ApiOperation({ summary: 'Update a recovery incident' })
|
||||
update(@Param('id') id: string, @Body() updateDto: UpdateRecoveryIncidentDto) {
|
||||
return this.recoveryIncidentService.update(id, updateDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermissions('recovery:delete')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@ApiOperation({ summary: 'Delete a recovery incident' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.recoveryIncidentService.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@ import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { TenantService } from './tenant.service';
|
||||
import { CreateTenantDto } from './dto/create-tenant.dto';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
|
||||
@ApiTags('tenants')
|
||||
@Controller('tenants')
|
||||
export class TenantController {
|
||||
constructor(private readonly tenantService: TenantService) {}
|
||||
|
||||
@Public()
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.tenantService.findAll();
|
||||
|
||||
Reference in New Issue
Block a user