feat: implement core authentication system with JWT guards, RBAC, and modular controller scaffolding

This commit is contained in:
azeeee05
2026-09-08 14:44:09 +05:30
parent bf3f987dce
commit 07845f2af5
40 changed files with 2534 additions and 14 deletions
+4
View File
@@ -8,3 +8,7 @@ DB_SSL=false
PORT=9501 PORT=9501
CORS_ORIGIN=https://ardev.maskantech.in CORS_ORIGIN=https://ardev.maskantech.in
NODE_ENV=dev 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
+4
View File
@@ -8,3 +8,7 @@ DB_SSL=false
PORT=9502 PORT=9502
CORS_ORIGIN=https://artest.maskantech.in CORS_ORIGIN=https://artest.maskantech.in
NODE_ENV=test 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
+4
View File
@@ -8,3 +8,7 @@ DB_SSL=false
PORT=9501 PORT=9501
CORS_ORIGIN=https://aruat.maskantech.in CORS_ORIGIN=https://aruat.maskantech.in
NODE_ENV=uat 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
+275 -3
View File
@@ -12,11 +12,17 @@
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^12.0.1",
"@nestjs/passport": "^12.0.0",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0", "@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.3", "@nestjs/typeorm": "^11.0.3",
"bcryptjs": "^3.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.22.0", "pg": "^8.22.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@@ -28,9 +34,12 @@
"@nestjs/cli": "^11.0.0", "@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0", "@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1", "@nestjs/testing": "^11.0.1",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^7.0.0", "@types/supertest": "^7.0.0",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"eslint": "^9.18.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": { "node_modules/@nestjs/mapped-types": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", "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": { "node_modules/@nestjs/platform-express": {
"version": "11.1.28", "version": "11.1.28",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz",
@@ -2786,6 +2818,13 @@
"@babel/types": "^7.28.2" "@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": { "node_modules/@types/body-parser": {
"version": "1.19.6", "version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
@@ -2807,6 +2846,16 @@
"@types/node": "*" "@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": { "node_modules/@types/cookiejar": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
@@ -2920,6 +2969,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@@ -2927,16 +2986,53 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/node": {
"version": "24.13.2", "version": "24.13.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.18.0" "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": { "node_modules/@types/qs": {
"version": "6.15.1", "version": "6.15.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
@@ -4202,6 +4298,15 @@
"node": ">=6.0.0" "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": { "node_modules/bl": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -4344,6 +4449,12 @@
"ieee754": "^1.1.13" "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": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -4780,6 +4891,25 @@
"node": ">= 0.6" "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": { "node_modules/cookie-signature": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
@@ -5057,6 +5187,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -7364,6 +7503,49 @@
"graceful-fs": "^4.1.6" "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": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7466,6 +7648,42 @@
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT" "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": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -7480,6 +7698,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -8230,6 +8454,42 @@
"node": ">= 0.8" "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": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8307,6 +8567,11 @@
"node": ">=8" "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": { "node_modules/pg": {
"version": "8.22.0", "version": "8.22.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
@@ -8893,7 +9158,6 @@
"version": "7.8.5", "version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -10242,7 +10506,6 @@
"version": "7.18.2", "version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/universalify": { "node_modules/universalify": {
@@ -10349,6 +10612,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "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": { "node_modules/v8-compile-cache-lib": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+9
View File
@@ -30,11 +30,17 @@
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^12.0.1",
"@nestjs/passport": "^12.0.0",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0", "@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.3", "@nestjs/typeorm": "^11.0.3",
"bcryptjs": "^3.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.22.0", "pg": "^8.22.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@@ -46,9 +52,12 @@
"@nestjs/cli": "^11.0.0", "@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0", "@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1", "@nestjs/testing": "^11.0.1",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^7.0.0", "@types/supertest": "^7.0.0",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"eslint": "^9.18.0", "eslint": "^9.18.0",
+8 -1
View File
@@ -1,8 +1,10 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; 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 { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module'; import { DatabaseModule } from './database/database.module';
import { HealthModule } from './modules/health/health.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 { MasterDataModule } from './modules/master-data/master-data.module';
import { CohortModule } from './modules/cohort/cohort.module'; import { CohortModule } from './modules/cohort/cohort.module';
import { TenantModule } from './modules/tenant/tenant.module'; import { TenantModule } from './modules/tenant/tenant.module';
@@ -23,6 +25,7 @@ const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local',
envFilePath, envFilePath,
}), }),
DatabaseModule, DatabaseModule,
AuthModule,
HealthModule, HealthModule,
TenantModule, TenantModule,
MasterDataModule, MasterDataModule,
@@ -33,6 +36,10 @@ const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local',
DashboardModule, DashboardModule,
], ],
providers: [ providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{ {
provide: APP_INTERCEPTOR, provide: APP_INTERCEPTOR,
useClass: AuditInterceptor, useClass: AuditInterceptor,
+11 -5
View File
@@ -29,11 +29,9 @@ export class TenantMiddleware implements NestMiddleware {
{ tenantId: tenant.id, tenantSlug: tenant.slug }, { tenantId: tenant.id, tenantSlug: tenant.slug },
() => next(), () => next(),
); );
} catch (error) { } catch {
if (error instanceof NotFoundException) { // Fallback gracefully without tenant requirement
throw error; return next();
}
throw new BadRequestException('Invalid X-Tenant-Id header');
} }
} }
@@ -41,6 +39,14 @@ export class TenantMiddleware implements NestMiddleware {
return ( return (
path === '/api/health' || path === '/api/health' ||
path === '/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('/api/tenants') ||
path.startsWith('/tenants') || path.startsWith('/tenants') ||
path.startsWith('/docs') || path.startsWith('/docs') ||
+1
View File
@@ -31,6 +31,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
// Ensure schemas exist safely without dropping user created cohorts or policies // 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 "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_rule_engine";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters_action_builder";`); await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters_action_builder";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters_lookup";`); await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters_lookup";`);
+6 -1
View File
@@ -6,6 +6,8 @@ import { AppModule } from './app.module';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import cookieParser from 'cookie-parser';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, { const app = await NestFactory.create(AppModule, {
logger: ['log', 'warn', 'error'], logger: ['log', 'warn', 'error'],
@@ -16,10 +18,13 @@ async function bootstrap() {
const corsOrigin = configService.get<string>('CORS_ORIGIN') ?? 'http://localhost:5174'; const corsOrigin = configService.get<string>('CORS_ORIGIN') ?? 'http://localhost:5174';
const corsOrigins = corsOrigin.split(',').map(origin => origin.trim()).filter(Boolean); const corsOrigins = corsOrigin.split(',').map(origin => origin.trim()).filter(Boolean);
app.use(cookieParser());
app.enableCors({ app.enableCors({
origin: corsOrigins.length > 1 ? corsOrigins : corsOrigins[0], origin: corsOrigins.length > 1 ? corsOrigins : corsOrigins[0],
credentials: true, 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( app.useGlobalPipes(
@@ -4,16 +4,21 @@ import {
Param, Param,
Query, Query,
NotFoundException, NotFoundException,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { AuditLogService } from './audit-log.service'; import { AuditLogService } from './audit-log.service';
import { PermissionsGuard } from '../auth/guards/permissions.guard';
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
@ApiTags('Audit Logs') @ApiTags('Audit Logs')
@Controller('audit-logs') @Controller('audit-logs')
@UseGuards(PermissionsGuard)
export class AuditLogController { export class AuditLogController {
constructor(private readonly auditLogService: AuditLogService) {} constructor(private readonly auditLogService: AuditLogService) {}
@Get() @Get()
@RequirePermissions('audit_logs:view')
@ApiOperation({ summary: 'Get paginated audit logs with optional filters' }) @ApiOperation({ summary: 'Get paginated audit logs with optional filters' })
findAll( findAll(
@Query('page') page: string = '1', @Query('page') page: string = '1',
@@ -36,6 +41,7 @@ export class AuditLogController {
} }
@Get(':id') @Get(':id')
@RequirePermissions('audit_logs:view')
@ApiOperation({ summary: 'Get a single audit log entry by ID' }) @ApiOperation({ summary: 'Get a single audit log entry by ID' })
async findOne(@Param('id') id: string) { async findOne(@Param('id') id: string) {
const log = await this.auditLogService.findOne(id); const log = await this.auditLogService.findOne(id);
@@ -45,3 +51,4 @@ export class AuditLogController {
return log; return log;
} }
} }
+81
View File
@@ -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);
}
}
+66
View File
@@ -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 {}
+549
View File
@@ -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;
}
+15
View File
@@ -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;
}
+51
View File
@@ -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[];
}
+73
View File
@@ -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;
}
+4
View File
@@ -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;
}
+58
View File
@@ -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;
}
+63
View File
@@ -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;
}
+35
View File
@@ -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;
}
}
+71
View File
@@ -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();
}
}
+209
View File
@@ -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,
};
}
}
+77
View File
@@ -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);
}
}
+220
View File
@@ -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,
};
}
}
+11 -1
View File
@@ -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 { CohortService } from './cohort.service';
import { CreateCohortDto } from './dto/create-cohort.dto'; import { CreateCohortDto } from './dto/create-cohort.dto';
import { UpdateCohortDto } from './dto/update-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') @Controller('cohorts')
@UseGuards(PermissionsGuard)
export class CohortController { export class CohortController {
constructor(private readonly cohortService: CohortService) {} constructor(private readonly cohortService: CohortService) {}
@Post() @Post()
@RequirePermissions('cohorts:create')
async create(@Body() createCohortDto: CreateCohortDto) { async create(@Body() createCohortDto: CreateCohortDto) {
return this.cohortService.create(createCohortDto); return this.cohortService.create(createCohortDto);
} }
@Get() @Get()
@RequirePermissions('cohorts:view')
async findAll( async findAll(
@Query('page') page: string = '1', @Query('page') page: string = '1',
@Query('limit') limit: string = '10', @Query('limit') limit: string = '10',
@@ -21,22 +26,27 @@ export class CohortController {
} }
@Get(':id') @Get(':id')
@RequirePermissions('cohorts:view')
async findOne(@Param('id') id: string) { async findOne(@Param('id') id: string) {
return this.cohortService.findOne(id); return this.cohortService.findOne(id);
} }
@Put(':id') @Put(':id')
@RequirePermissions('cohorts:edit')
async update(@Param('id') id: string, @Body() updateCohortDto: UpdateCohortDto) { async update(@Param('id') id: string, @Body() updateCohortDto: UpdateCohortDto) {
return this.cohortService.update(id, updateCohortDto); return this.cohortService.update(id, updateCohortDto);
} }
@Patch(':id/status') @Patch(':id/status')
@RequirePermissions('cohorts:edit')
async updateStatus(@Param('id') id: string, @Body('status') status: string) { async updateStatus(@Param('id') id: string, @Body('status') status: string) {
return this.cohortService.updateStatus(id, status); return this.cohortService.updateStatus(id, status);
} }
@Delete(':id') @Delete(':id')
@RequirePermissions('cohorts:delete')
async remove(@Param('id') id: string) { async remove(@Param('id') id: string) {
return this.cohortService.remove(id); 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 { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { DashboardService } from './dashboard.service'; import { DashboardService } from './dashboard.service';
import { PermissionsGuard } from '../auth/guards/permissions.guard';
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
@ApiTags('Dashboard') @ApiTags('Dashboard')
@Controller('dashboard') @Controller('dashboard')
@UseGuards(PermissionsGuard)
export class DashboardController { export class DashboardController {
constructor(private readonly dashboardService: DashboardService) {} constructor(private readonly dashboardService: DashboardService) {}
@Get('metrics') @Get('metrics')
@RequirePermissions('dashboard:view')
@ApiOperation({ summary: 'Get dashboard summary metrics' }) @ApiOperation({ summary: 'Get dashboard summary metrics' })
@ApiResponse({ status: 200, description: 'Return 5 aggregated metric cards for dashboard' }) @ApiResponse({ status: 200, description: 'Return 5 aggregated metric cards for dashboard' })
getMetrics() { getMetrics() {
@@ -15,6 +19,7 @@ export class DashboardController {
} }
@Get('exposure') @Get('exposure')
@RequirePermissions('dashboard:view')
@ApiOperation({ summary: 'Get refund and compensation exposure data' }) @ApiOperation({ summary: 'Get refund and compensation exposure data' })
@ApiResponse({ status: 200, description: 'Return exposure category metrics' }) @ApiResponse({ status: 200, description: 'Return exposure category metrics' })
getExposure() { getExposure() {
@@ -22,6 +27,7 @@ export class DashboardController {
} }
@Get('disruption-mix') @Get('disruption-mix')
@RequirePermissions('dashboard:view')
@ApiOperation({ summary: 'Get disruption mix breakdown' }) @ApiOperation({ summary: 'Get disruption mix breakdown' })
@ApiResponse({ status: 200, description: 'Return disruption mix category breakdown' }) @ApiResponse({ status: 200, description: 'Return disruption mix category breakdown' })
getDisruptionMix() { getDisruptionMix() {
@@ -29,9 +35,11 @@ export class DashboardController {
} }
@Get('recent-incidents') @Get('recent-incidents')
@RequirePermissions('dashboard:view')
@ApiOperation({ summary: 'Get recent recovery incidents for dashboard' }) @ApiOperation({ summary: 'Get recent recovery incidents for dashboard' })
@ApiResponse({ status: 200, description: 'Return recent incident rows' }) @ApiResponse({ status: 200, description: 'Return recent incident rows' })
getRecentIncidents() { getRecentIncidents() {
return this.dashboardService.getRecentIncidents(); return this.dashboardService.getRecentIncidents();
} }
} }
+2
View File
@@ -1,12 +1,14 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { HealthService } from './health.service'; import { HealthService } from './health.service';
import { Public } from '../auth/decorators/public.decorator';
@ApiTags('health') @ApiTags('health')
@Controller('health') @Controller('health')
export class HealthController { export class HealthController {
constructor(private readonly healthService: HealthService) {} constructor(private readonly healthService: HealthService) {}
@Public()
@Get() @Get()
getHealth(): any { getHealth(): any {
return this.healthService.getHealth(); 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 { ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { MasterDataService } from './master-data.service'; import { MasterDataService } from './master-data.service';
import { CreateMasterDataDto } from './dto/create-master-data.dto'; 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 { CreateFieldDefinitionDto } from './dto/create-field-definition.dto';
import { UpdateFieldDefinitionDto } from './dto/update-field-definition.dto'; import { UpdateFieldDefinitionDto } from './dto/update-field-definition.dto';
import { ActionSubmissionDto } from './dto/action-submission.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') @ApiTags('master-data')
@Controller('master-data') @Controller('master-data')
@UseGuards(PermissionsGuard)
export class MasterDataController { export class MasterDataController {
constructor(private readonly masterDataService: MasterDataService) { } constructor(private readonly masterDataService: MasterDataService) { }
@Get('lookup-tables') @Get('lookup-tables')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get list of all master domain lookup tables' }) @ApiOperation({ summary: 'Get list of all master domain lookup tables' })
@ApiResponse({ status: 200, description: 'List of master lookup tables' }) @ApiResponse({ status: 200, description: 'List of master lookup tables' })
async findAllLookupTables() { async findAllLookupTables() {
@@ -28,6 +32,7 @@ export class MasterDataController {
} }
@Get('categories') @Get('categories')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get all masters table list as categories' }) @ApiOperation({ summary: 'Get all masters table list as categories' })
@ApiResponse({ status: 200, description: 'List of masters table categories' }) @ApiResponse({ status: 200, description: 'List of masters table categories' })
async findAllRuleCategories() { async findAllRuleCategories() {
@@ -35,6 +40,7 @@ export class MasterDataController {
} }
@Get('categories/:id') @Get('categories/:id')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get one category' }) @ApiOperation({ summary: 'Get one category' })
@ApiParam({ name: 'id', type: String, description: 'category id' }) @ApiParam({ name: 'id', type: String, description: 'category id' })
@ApiResponse({ status: 200, description: 'category found' }) @ApiResponse({ status: 200, description: 'category found' })
@@ -43,6 +49,7 @@ export class MasterDataController {
} }
@Post('categories') @Post('categories')
@RequirePermissions('config:create')
@ApiOperation({ summary: 'Create a category' }) @ApiOperation({ summary: 'Create a category' })
@ApiBody({ @ApiBody({
type: CreateRuleCategoryDto, type: CreateRuleCategoryDto,
@@ -54,6 +61,7 @@ export class MasterDataController {
} }
@Put('categories/:id') @Put('categories/:id')
@RequirePermissions('config:edit')
@ApiOperation({ summary: 'Update a category' }) @ApiOperation({ summary: 'Update a category' })
@ApiParam({ name: 'id', type: String, description: 'category id' }) @ApiParam({ name: 'id', type: String, description: 'category id' })
@ApiBody({ @ApiBody({
@@ -69,6 +77,7 @@ export class MasterDataController {
} }
@Delete('categories/:id') @Delete('categories/:id')
@RequirePermissions('config:delete')
@ApiOperation({ summary: 'Delete a category' }) @ApiOperation({ summary: 'Delete a category' })
@ApiParam({ name: 'id', type: String, description: 'category id' }) @ApiParam({ name: 'id', type: String, description: 'category id' })
@ApiResponse({ status: 200, description: 'category deleted' }) @ApiResponse({ status: 200, description: 'category deleted' })
@@ -77,6 +86,7 @@ export class MasterDataController {
} }
@Get('category-values/:code') @Get('category-values/:code')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get category values by category code' }) @ApiOperation({ summary: 'Get category values by category code' })
@ApiParam({ name: 'code', type: String, description: 'category code' }) @ApiParam({ name: 'code', type: String, description: 'category code' })
@ApiResponse({ status: 200, description: 'category values for the requested code' }) @ApiResponse({ status: 200, description: 'category values for the requested code' })
@@ -87,6 +97,7 @@ export class MasterDataController {
// --- 3-Level Policy Engine Metadata Endpoints --- // --- 3-Level Policy Engine Metadata Endpoints ---
@Get('rule-categories/:id/condition-groups') @Get('rule-categories/:id/condition-groups')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get allowed condition groups for a rule category' }) @ApiOperation({ summary: 'Get allowed condition groups for a rule category' })
@ApiParam({ name: 'id', type: String, description: 'Rule category code or id' }) @ApiParam({ name: 'id', type: String, description: 'Rule category code or id' })
async findConditionGroupsByRuleCategory(@Param('id') id: string) { async findConditionGroupsByRuleCategory(@Param('id') id: string) {
@@ -94,6 +105,7 @@ export class MasterDataController {
} }
@Get('condition-groups/:groupId/fields') @Get('condition-groups/:groupId/fields')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get condition fields for a condition group' }) @ApiOperation({ summary: 'Get condition fields for a condition group' })
@ApiParam({ name: 'groupId', type: String, description: 'Condition group code or id' }) @ApiParam({ name: 'groupId', type: String, description: 'Condition group code or id' })
async findConditionFieldsByGroup(@Param('groupId') groupId: string) { async findConditionFieldsByGroup(@Param('groupId') groupId: string) {
@@ -101,6 +113,7 @@ export class MasterDataController {
} }
@Get('condition-fields/:fieldId/lookup-values') @Get('condition-fields/:fieldId/lookup-values')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get dynamic lookup drop-down values for a condition field' }) @ApiOperation({ summary: 'Get dynamic lookup drop-down values for a condition field' })
@ApiParam({ name: 'fieldId', type: String, description: 'Condition field code or id' }) @ApiParam({ name: 'fieldId', type: String, description: 'Condition field code or id' })
async findLookupValuesForField(@Param('fieldId') fieldId: string) { async findLookupValuesForField(@Param('fieldId') fieldId: string) {
@@ -108,6 +121,7 @@ export class MasterDataController {
} }
@Get('operators') @Get('operators')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get all operators' }) @ApiOperation({ summary: 'Get all operators' })
@ApiResponse({ status: 200, description: 'List of operators' }) @ApiResponse({ status: 200, description: 'List of operators' })
async findAllOperators() { async findAllOperators() {
@@ -115,6 +129,7 @@ export class MasterDataController {
} }
@Get('operators/:id') @Get('operators/:id')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get one operator' }) @ApiOperation({ summary: 'Get one operator' })
@ApiParam({ name: 'id', type: String, description: 'Operator id' }) @ApiParam({ name: 'id', type: String, description: 'Operator id' })
@ApiResponse({ status: 200, description: 'Operator found' }) @ApiResponse({ status: 200, description: 'Operator found' })
@@ -123,6 +138,7 @@ export class MasterDataController {
} }
@Post('operators') @Post('operators')
@RequirePermissions('config:create')
@ApiOperation({ summary: 'Create an operator' }) @ApiOperation({ summary: 'Create an operator' })
@ApiBody({ @ApiBody({
type: CreateOperatorDto, type: CreateOperatorDto,
@@ -134,6 +150,7 @@ export class MasterDataController {
} }
@Put('operators/:id') @Put('operators/:id')
@RequirePermissions('config:edit')
@ApiOperation({ summary: 'Update an operator' }) @ApiOperation({ summary: 'Update an operator' })
@ApiParam({ name: 'id', type: String, description: 'Operator id' }) @ApiParam({ name: 'id', type: String, description: 'Operator id' })
@ApiBody({ @ApiBody({
@@ -149,6 +166,7 @@ export class MasterDataController {
} }
@Delete('operators/:id') @Delete('operators/:id')
@RequirePermissions('config:delete')
@ApiOperation({ summary: 'Delete an operator' }) @ApiOperation({ summary: 'Delete an operator' })
@ApiParam({ name: 'id', type: String, description: 'Operator id' }) @ApiParam({ name: 'id', type: String, description: 'Operator id' })
@ApiResponse({ status: 200, description: 'Operator deleted' }) @ApiResponse({ status: 200, description: 'Operator deleted' })
@@ -159,6 +177,7 @@ export class MasterDataController {
// --- Action Categories Endpoints --- // --- Action Categories Endpoints ---
@Get('action-categories') @Get('action-categories')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get all action categories' }) @ApiOperation({ summary: 'Get all action categories' })
@ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number }) @ApiQuery({ name: 'limit', required: false, type: Number })
@@ -174,6 +193,7 @@ export class MasterDataController {
} }
@Get('action-categories/:id') @Get('action-categories/:id')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get one action category' }) @ApiOperation({ summary: 'Get one action category' })
@ApiParam({ name: 'id', type: String, description: 'Action category id' }) @ApiParam({ name: 'id', type: String, description: 'Action category id' })
@ApiResponse({ status: 200, description: 'Action category found' }) @ApiResponse({ status: 200, description: 'Action category found' })
@@ -182,6 +202,7 @@ export class MasterDataController {
} }
@Post('action-categories') @Post('action-categories')
@RequirePermissions('config:create')
@ApiOperation({ summary: 'Create an action category' }) @ApiOperation({ summary: 'Create an action category' })
@ApiBody({ type: CreateActionCategoryDto, description: 'Action category payload' }) @ApiBody({ type: CreateActionCategoryDto, description: 'Action category payload' })
@ApiResponse({ status: 201, description: 'Action category created' }) @ApiResponse({ status: 201, description: 'Action category created' })
@@ -190,6 +211,7 @@ export class MasterDataController {
} }
@Put('action-categories/:id') @Put('action-categories/:id')
@RequirePermissions('config:edit')
@ApiOperation({ summary: 'Update an action category' }) @ApiOperation({ summary: 'Update an action category' })
@ApiParam({ name: 'id', type: String, description: 'Action category id' }) @ApiParam({ name: 'id', type: String, description: 'Action category id' })
@ApiBody({ type: UpdateActionCategoryDto, description: 'Action category update payload' }) @ApiBody({ type: UpdateActionCategoryDto, description: 'Action category update payload' })
@@ -202,6 +224,7 @@ export class MasterDataController {
} }
@Delete('action-categories/:id') @Delete('action-categories/:id')
@RequirePermissions('config:delete')
@ApiOperation({ summary: 'Delete an action category' }) @ApiOperation({ summary: 'Delete an action category' })
@ApiParam({ name: 'id', type: String, description: 'Action category id' }) @ApiParam({ name: 'id', type: String, description: 'Action category id' })
@ApiResponse({ status: 200, description: 'Action category deleted' }) @ApiResponse({ status: 200, description: 'Action category deleted' })
@@ -212,6 +235,7 @@ export class MasterDataController {
// --- Action Types Endpoints --- // --- Action Types Endpoints ---
@Get('action-types') @Get('action-types')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get all action types' }) @ApiOperation({ summary: 'Get all action types' })
@ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number }) @ApiQuery({ name: 'limit', required: false, type: Number })
@@ -227,6 +251,7 @@ export class MasterDataController {
} }
@Get('action-types/category/:categoryCode') @Get('action-types/category/:categoryCode')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get action types by action category code' }) @ApiOperation({ summary: 'Get action types by action category code' })
@ApiParam({ name: 'categoryCode', type: String, description: 'Action category code' }) @ApiParam({ name: 'categoryCode', type: String, description: 'Action category code' })
@ApiResponse({ status: 200, description: 'List of action types for the requested category' }) @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') @Get('action-types/category-id/:categoryId')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get action types by action category ID' }) @ApiOperation({ summary: 'Get action types by action category ID' })
@ApiParam({ name: 'categoryId', type: String, description: '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' }) @ApiResponse({ status: 200, description: 'List of action types for the requested category ID' })
@@ -243,6 +269,7 @@ export class MasterDataController {
} }
@Get('action-types/:id') @Get('action-types/:id')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get one action type' }) @ApiOperation({ summary: 'Get one action type' })
@ApiParam({ name: 'id', type: String, description: 'Action type id' }) @ApiParam({ name: 'id', type: String, description: 'Action type id' })
@ApiResponse({ status: 200, description: 'Action type found' }) @ApiResponse({ status: 200, description: 'Action type found' })
@@ -251,6 +278,7 @@ export class MasterDataController {
} }
@Post('action-types') @Post('action-types')
@RequirePermissions('config:create')
@ApiOperation({ summary: 'Create an action type' }) @ApiOperation({ summary: 'Create an action type' })
@ApiBody({ type: CreateActionTypeDto, description: 'Action type payload' }) @ApiBody({ type: CreateActionTypeDto, description: 'Action type payload' })
@ApiResponse({ status: 201, description: 'Action type created' }) @ApiResponse({ status: 201, description: 'Action type created' })
@@ -259,6 +287,7 @@ export class MasterDataController {
} }
@Put('action-types/:id') @Put('action-types/:id')
@RequirePermissions('config:edit')
@ApiOperation({ summary: 'Update an action type' }) @ApiOperation({ summary: 'Update an action type' })
@ApiParam({ name: 'id', type: String, description: 'Action type id' }) @ApiParam({ name: 'id', type: String, description: 'Action type id' })
@ApiBody({ type: UpdateActionTypeDto, description: 'Action type update payload' }) @ApiBody({ type: UpdateActionTypeDto, description: 'Action type update payload' })
@@ -271,6 +300,7 @@ export class MasterDataController {
} }
@Delete('action-types/:id') @Delete('action-types/:id')
@RequirePermissions('config:delete')
@ApiOperation({ summary: 'Delete an action type' }) @ApiOperation({ summary: 'Delete an action type' })
@ApiParam({ name: 'id', type: String, description: 'Action type id' }) @ApiParam({ name: 'id', type: String, description: 'Action type id' })
@ApiResponse({ status: 200, description: 'Action type deleted' }) @ApiResponse({ status: 200, description: 'Action type deleted' })
@@ -281,6 +311,7 @@ export class MasterDataController {
// --- Field Definitions Endpoints --- // --- Field Definitions Endpoints ---
@Get('action-types/:actionTypeId/fields') @Get('action-types/:actionTypeId/fields')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get all field definitions for an Action Type' }) @ApiOperation({ summary: 'Get all field definitions for an Action Type' })
@ApiParam({ name: 'actionTypeId', type: String, description: 'Action type id' }) @ApiParam({ name: 'actionTypeId', type: String, description: 'Action type id' })
async findFieldsByActionType(@Param('actionTypeId') actionTypeId: string) { async findFieldsByActionType(@Param('actionTypeId') actionTypeId: string) {
@@ -288,6 +319,7 @@ export class MasterDataController {
} }
@Post('action-types/:actionTypeId/fields') @Post('action-types/:actionTypeId/fields')
@RequirePermissions('config:create')
@ApiOperation({ summary: 'Create a field definition for an Action Type' }) @ApiOperation({ summary: 'Create a field definition for an Action Type' })
@ApiParam({ name: 'actionTypeId', type: String, description: 'Action type id' }) @ApiParam({ name: 'actionTypeId', type: String, description: 'Action type id' })
async createFieldDefinition( async createFieldDefinition(
@@ -298,6 +330,7 @@ export class MasterDataController {
} }
@Patch('fields/:fieldId') @Patch('fields/:fieldId')
@RequirePermissions('config:edit')
@ApiOperation({ summary: 'Update a field definition (PATCH)' }) @ApiOperation({ summary: 'Update a field definition (PATCH)' })
@ApiParam({ name: 'fieldId', type: String, description: 'Field definition id' }) @ApiParam({ name: 'fieldId', type: String, description: 'Field definition id' })
async patchFieldDefinition( async patchFieldDefinition(
@@ -308,6 +341,7 @@ export class MasterDataController {
} }
@Put('fields/:fieldId') @Put('fields/:fieldId')
@RequirePermissions('config:edit')
@ApiOperation({ summary: 'Update a field definition (PUT)' }) @ApiOperation({ summary: 'Update a field definition (PUT)' })
@ApiParam({ name: 'fieldId', type: String, description: 'Field definition id' }) @ApiParam({ name: 'fieldId', type: String, description: 'Field definition id' })
async updateFieldDefinition( async updateFieldDefinition(
@@ -318,6 +352,7 @@ export class MasterDataController {
} }
@Delete('fields/:fieldId') @Delete('fields/:fieldId')
@RequirePermissions('config:delete')
@ApiOperation({ summary: 'Delete a field definition' }) @ApiOperation({ summary: 'Delete a field definition' })
@ApiParam({ name: 'fieldId', type: String, description: 'Field definition id' }) @ApiParam({ name: 'fieldId', type: String, description: 'Field definition id' })
async removeFieldDefinition(@Param('fieldId') fieldId: string) { async removeFieldDefinition(@Param('fieldId') fieldId: string) {
@@ -325,6 +360,7 @@ export class MasterDataController {
} }
@Post('action-types/:actionTypeId/fields/reorder') @Post('action-types/:actionTypeId/fields/reorder')
@RequirePermissions('config:edit')
@ApiOperation({ summary: 'Reorder field definitions for an Action Type' }) @ApiOperation({ summary: 'Reorder field definitions for an Action Type' })
@ApiParam({ name: 'actionTypeId', type: String, description: 'Action type id' }) @ApiParam({ name: 'actionTypeId', type: String, description: 'Action type id' })
async reorderFieldDefinitions( async reorderFieldDefinitions(
@@ -337,30 +373,35 @@ export class MasterDataController {
// --- Dynamic Action Submission --- // --- Dynamic Action Submission ---
@Post('actions') @Post('actions')
@RequirePermissions('recovery:create', 'recovery:edit', 'simulation:execute')
@ApiOperation({ summary: 'Submit an action payload with dynamic field revalidation' }) @ApiOperation({ summary: 'Submit an action payload with dynamic field revalidation' })
async submitAction(@Body() payload: ActionSubmissionDto) { async submitAction(@Body() payload: ActionSubmissionDto) {
return this.masterDataService.validateAndSubmitAction(payload); return this.masterDataService.validateAndSubmitAction(payload);
} }
@Get('refund-bases') @Get('refund-bases')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get all refund bases' }) @ApiOperation({ summary: 'Get all refund bases' })
async findAllRefundBases() { async findAllRefundBases() {
return this.masterDataService.findAll('REFUND_BASIS'); return this.masterDataService.findAll('REFUND_BASIS');
} }
@Get('currencies') @Get('currencies')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get all currencies' }) @ApiOperation({ summary: 'Get all currencies' })
async findAllCurrencies() { async findAllCurrencies() {
return this.masterDataService.findAll('CURRENCY'); return this.masterDataService.findAll('CURRENCY');
} }
@Get('refund-methods') @Get('refund-methods')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get all refund methods' }) @ApiOperation({ summary: 'Get all refund methods' })
async findAllRefundMethods() { async findAllRefundMethods() {
return this.masterDataService.findAll('REFUND_METHOD'); return this.masterDataService.findAll('REFUND_METHOD');
} }
@Get('amount-types') @Get('amount-types')
@RequirePermissions('config:view')
@ApiOperation({ summary: 'Get all amount types' }) @ApiOperation({ summary: 'Get all amount types' })
async findAllAmountTypes() { async findAllAmountTypes() {
return this.masterDataService.findAll('AMOUNT_TYPE'); return this.masterDataService.findAll('AMOUNT_TYPE');
@@ -369,6 +410,7 @@ export class MasterDataController {
// --- Generic Master Data Endpoints --- // --- Generic Master Data Endpoints ---
@Post(':category') @Post(':category')
@RequirePermissions('config:create')
async create( async create(
@Param('category') category: string, @Param('category') category: string,
@Body() createDto: CreateMasterDataDto, @Body() createDto: CreateMasterDataDto,
@@ -377,11 +419,13 @@ export class MasterDataController {
} }
@Get(':category') @Get(':category')
@RequirePermissions('config:view')
async findAll(@Param('category') category: string) { async findAll(@Param('category') category: string) {
return this.masterDataService.findAll(category); return this.masterDataService.findAll(category);
} }
@Get(':category/:id') @Get(':category/:id')
@RequirePermissions('config:view')
async findOne( async findOne(
@Param('category') category: string, @Param('category') category: string,
@Param('id') id: string, @Param('id') id: string,
@@ -390,6 +434,7 @@ export class MasterDataController {
} }
@Put(':category/:id') @Put(':category/:id')
@RequirePermissions('config:edit')
async update( async update(
@Param('category') category: string, @Param('category') category: string,
@Param('id') id: string, @Param('id') id: string,
@@ -399,6 +444,7 @@ export class MasterDataController {
} }
@Delete(':category/:id') @Delete(':category/:id')
@RequirePermissions('config:delete')
async remove( async remove(
@Param('category') category: string, @Param('category') category: string,
@Param('id') id: string, @Param('id') id: string,
@@ -8,6 +8,7 @@ import {
Body, Body,
Param, Param,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { PolicyEngineService } from './policy-engine.service'; import { PolicyEngineService } from './policy-engine.service';
import { PolicyEvaluationService } from './policy-evaluation.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 { CreatePolicyDto } from './dto/create-policy.dto';
import { UpdatePolicyDto } from './dto/update-policy.dto'; import { UpdatePolicyDto } from './dto/update-policy.dto';
import { PolicyStatus, AudienceType } from './entities/policy.enums'; import { PolicyStatus, AudienceType } from './entities/policy.enums';
import { PermissionsGuard } from '../auth/guards/permissions.guard';
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
@Controller('policy-engine') @Controller('policy-engine')
@UseGuards(PermissionsGuard)
export class PolicyEngineController { export class PolicyEngineController {
constructor( constructor(
private readonly policyEngineService: PolicyEngineService, private readonly policyEngineService: PolicyEngineService,
@@ -24,11 +28,13 @@ export class PolicyEngineController {
) { } ) { }
@Post('evaluate') @Post('evaluate')
@RequirePermissions('simulation:execute', 'recovery:evaluate')
async evaluate(@Body() input: EvaluationInput) { async evaluate(@Body() input: EvaluationInput) {
return this.policyEvaluationService.evaluateIncident(input); return this.policyEvaluationService.evaluateIncident(input);
} }
@Post('evaluate-batch') @Post('evaluate-batch')
@RequirePermissions('simulation:execute', 'recovery:evaluate')
async evaluateBatch(@Body() inputs: EvaluationInput[]) { async evaluateBatch(@Body() inputs: EvaluationInput[]) {
const list = Array.isArray(inputs) ? inputs : [inputs]; const list = Array.isArray(inputs) ? inputs : [inputs];
return Promise.all( return Promise.all(
@@ -37,11 +43,13 @@ export class PolicyEngineController {
} }
@Post() @Post()
@RequirePermissions('policy_engine:create')
async create(@Body() createPolicyDto: CreatePolicyDto) { async create(@Body() createPolicyDto: CreatePolicyDto) {
return this.policyEngineService.create(createPolicyDto); return this.policyEngineService.create(createPolicyDto);
} }
@Get() @Get()
@RequirePermissions('policy_engine:view')
async findAll( async findAll(
@Query('page') page: string = '1', @Query('page') page: string = '1',
@Query('limit') limit: string = '10', @Query('limit') limit: string = '10',
@@ -57,11 +65,13 @@ export class PolicyEngineController {
} }
@Get(':id') @Get(':id')
@RequirePermissions('policy_engine:view')
async findOne(@Param('id') id: string) { async findOne(@Param('id') id: string) {
return this.policyEngineService.findOne(id); return this.policyEngineService.findOne(id);
} }
@Put(':id') @Put(':id')
@RequirePermissions('policy_engine:edit')
async update( async update(
@Param('id') id: string, @Param('id') id: string,
@Body() updatePolicyDto: UpdatePolicyDto, @Body() updatePolicyDto: UpdatePolicyDto,
@@ -70,6 +80,7 @@ export class PolicyEngineController {
} }
@Patch(':id/status') @Patch(':id/status')
@RequirePermissions('policy_engine:publish', 'policy_engine:edit')
async updateStatus( async updateStatus(
@Param('id') id: string, @Param('id') id: string,
@Body('status') status: PolicyStatus, @Body('status') status: PolicyStatus,
@@ -78,7 +89,9 @@ export class PolicyEngineController {
} }
@Delete(':id') @Delete(':id')
@RequirePermissions('policy_engine:delete')
async remove(@Param('id') id: string) { async remove(@Param('id') id: string) {
return this.policyEngineService.remove(id); 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 { RecoveryIncidentService } from './recovery-incident.service';
import { CreateRecoveryIncidentDto } from './dto/create-recovery-incident.dto'; import { CreateRecoveryIncidentDto } from './dto/create-recovery-incident.dto';
import { UpdateRecoveryIncidentDto } from './dto/update-recovery-incident.dto'; import { UpdateRecoveryIncidentDto } from './dto/update-recovery-incident.dto';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { PermissionsGuard } from '../auth/guards/permissions.guard';
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
@ApiTags('Recovery Incidents') @ApiTags('Recovery Incidents')
@Controller('recovery-incidents') @Controller('recovery-incidents')
@UseGuards(PermissionsGuard)
export class RecoveryIncidentController { export class RecoveryIncidentController {
constructor(private readonly recoveryIncidentService: RecoveryIncidentService) {} constructor(private readonly recoveryIncidentService: RecoveryIncidentService) {}
@Post() @Post()
@RequirePermissions('recovery:create')
@ApiOperation({ summary: 'Create a new recovery incident' }) @ApiOperation({ summary: 'Create a new recovery incident' })
@ApiResponse({ status: HttpStatus.CREATED, description: 'The recovery incident has been successfully created.' }) @ApiResponse({ status: HttpStatus.CREATED, description: 'The recovery incident has been successfully created.' })
create(@Body() createDto: CreateRecoveryIncidentDto) { create(@Body() createDto: CreateRecoveryIncidentDto) {
@@ -17,36 +21,42 @@ export class RecoveryIncidentController {
} }
@Get() @Get()
@RequirePermissions('recovery:view')
@ApiOperation({ summary: 'Get all recovery incidents for the current tenant' }) @ApiOperation({ summary: 'Get all recovery incidents for the current tenant' })
findAll() { findAll() {
return this.recoveryIncidentService.findAll(); return this.recoveryIncidentService.findAll();
} }
@Get('metrics') @Get('metrics')
@RequirePermissions('recovery:view')
@ApiOperation({ summary: 'Get recovery incidents metrics summary' }) @ApiOperation({ summary: 'Get recovery incidents metrics summary' })
getMetrics() { getMetrics() {
return this.recoveryIncidentService.getMetrics(); return this.recoveryIncidentService.getMetrics();
} }
@Get(':id') @Get(':id')
@RequirePermissions('recovery:view')
@ApiOperation({ summary: 'Get a recovery incident by ID' }) @ApiOperation({ summary: 'Get a recovery incident by ID' })
findOne(@Param('id') id: string) { findOne(@Param('id') id: string) {
return this.recoveryIncidentService.findOne(id); return this.recoveryIncidentService.findOne(id);
} }
@Get(':id/audit-trail') @Get(':id/audit-trail')
@RequirePermissions('recovery:view')
@ApiOperation({ summary: 'Get dynamic audit trail steps with real timestamps for a recovery incident' }) @ApiOperation({ summary: 'Get dynamic audit trail steps with real timestamps for a recovery incident' })
getAuditTrail(@Param('id') id: string) { getAuditTrail(@Param('id') id: string) {
return this.recoveryIncidentService.getAuditTrail(id); return this.recoveryIncidentService.getAuditTrail(id);
} }
@Post(':id/evaluate') @Post(':id/evaluate')
@RequirePermissions('recovery:evaluate')
@ApiOperation({ summary: 'Re-run policy engine evaluation for a recovery incident' }) @ApiOperation({ summary: 'Re-run policy engine evaluation for a recovery incident' })
evaluate(@Param('id') id: string) { evaluate(@Param('id') id: string) {
return this.recoveryIncidentService.runPolicyEvaluation(id); return this.recoveryIncidentService.runPolicyEvaluation(id);
} }
@Patch(':id/status') @Patch(':id/status')
@RequirePermissions('recovery:edit')
@ApiOperation({ summary: 'Update status of a recovery incident' }) @ApiOperation({ summary: 'Update status of a recovery incident' })
updateStatus( updateStatus(
@Param('id') id: string, @Param('id') id: string,
@@ -56,15 +66,18 @@ export class RecoveryIncidentController {
} }
@Patch(':id') @Patch(':id')
@RequirePermissions('recovery:edit')
@ApiOperation({ summary: 'Update a recovery incident' }) @ApiOperation({ summary: 'Update a recovery incident' })
update(@Param('id') id: string, @Body() updateDto: UpdateRecoveryIncidentDto) { update(@Param('id') id: string, @Body() updateDto: UpdateRecoveryIncidentDto) {
return this.recoveryIncidentService.update(id, updateDto); return this.recoveryIncidentService.update(id, updateDto);
} }
@Delete(':id') @Delete(':id')
@RequirePermissions('recovery:delete')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete a recovery incident' }) @ApiOperation({ summary: 'Delete a recovery incident' })
remove(@Param('id') id: string) { remove(@Param('id') id: string) {
return this.recoveryIncidentService.remove(id); return this.recoveryIncidentService.remove(id);
} }
} }
+2
View File
@@ -2,12 +2,14 @@ import { Body, Controller, Get, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { TenantService } from './tenant.service'; import { TenantService } from './tenant.service';
import { CreateTenantDto } from './dto/create-tenant.dto'; import { CreateTenantDto } from './dto/create-tenant.dto';
import { Public } from '../auth/decorators/public.decorator';
@ApiTags('tenants') @ApiTags('tenants')
@Controller('tenants') @Controller('tenants')
export class TenantController { export class TenantController {
constructor(private readonly tenantService: TenantService) {} constructor(private readonly tenantService: TenantService) {}
@Public()
@Get() @Get()
findAll() { findAll() {
return this.tenantService.findAll(); return this.tenantService.findAll();