enhanced worknote service and create service for worknote live chat and inapp notificatio, also addd api for audit logs
This commit is contained in:
parent
113e87b66d
commit
6b68364785
353
package-lock.json
generated
353
package-lock.json
generated
@ -24,7 +24,9 @@
|
||||
"pg": "^8.18.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.37.7",
|
||||
"socket.io": "^4.8.3",
|
||||
"uuid": "^13.0.0",
|
||||
"web-push": "^3.6.7",
|
||||
"winston": "^3.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -41,6 +43,7 @@
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/validator": "^13.15.10",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"jest": "^30.2.0",
|
||||
"nodemon": "^3.0.2",
|
||||
"supertest": "^7.2.2",
|
||||
@ -3018,6 +3021,12 @@
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
@ -3152,7 +3161,6 @@
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@ -3388,6 +3396,16 @@
|
||||
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-push": {
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz",
|
||||
"integrity": "sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||
@ -3745,6 +3763,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
@ -3834,6 +3861,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1.js": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
@ -3953,6 +3992,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.16",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.16.tgz",
|
||||
@ -3985,6 +4033,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
@ -4838,6 +4892,80 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.5",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz",
|
||||
"integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.7.2",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.18.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/engine.io/node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||
@ -5666,6 +5794,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http_ece": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
|
||||
"integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@ -5686,6 +5823,42 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
@ -7015,6 +7188,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@ -8311,6 +8490,138 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
|
||||
"integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
|
||||
"integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "~4.4.1",
|
||||
"ws": "~8.18.3"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
|
||||
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io/node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io/node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@ -9078,6 +9389,25 @@
|
||||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/web-push": {
|
||||
"version": "3.6.7",
|
||||
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
|
||||
"integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"asn1.js": "^5.3.0",
|
||||
"http_ece": "1.2.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"bin": {
|
||||
"web-push": "src/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@ -9260,6 +9590,27 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@ -39,7 +39,9 @@
|
||||
"pg": "^8.18.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.37.7",
|
||||
"socket.io": "^4.8.3",
|
||||
"uuid": "^13.0.0",
|
||||
"web-push": "^3.6.7",
|
||||
"winston": "^3.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -56,6 +58,7 @@
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/validator": "^13.15.10",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"jest": "^30.2.0",
|
||||
"nodemon": "^3.0.2",
|
||||
"supertest": "^7.2.2",
|
||||
|
||||
26
scripts/cleanup-interview-orphans.ts
Normal file
26
scripts/cleanup-interview-orphans.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function cleanup() {
|
||||
try {
|
||||
await db.sequelize.authenticate();
|
||||
console.log('Database connected.');
|
||||
|
||||
const [results1]: any = await db.sequelize.query(`
|
||||
DELETE FROM interview_participants
|
||||
WHERE "interviewId" NOT IN (SELECT id FROM interviews)
|
||||
`);
|
||||
|
||||
const [results2]: any = await db.sequelize.query(`
|
||||
DELETE FROM interview_evaluations
|
||||
WHERE "interviewId" NOT IN (SELECT id FROM interviews)
|
||||
`);
|
||||
|
||||
console.log('Cleanup finished.');
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Cleanup failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup();
|
||||
@ -189,15 +189,83 @@ export const FNF_STATUS = {
|
||||
|
||||
// Audit Actions
|
||||
export const AUDIT_ACTIONS = {
|
||||
// General CRUD
|
||||
CREATED: 'CREATED',
|
||||
UPDATED: 'UPDATED',
|
||||
APPROVED: 'APPROVED',
|
||||
REJECTED: 'REJECTED',
|
||||
DELETED: 'DELETED',
|
||||
|
||||
// Auth & User Actions
|
||||
LOGIN: 'LOGIN',
|
||||
LOGOUT: 'LOGOUT',
|
||||
REGISTERED: 'REGISTERED',
|
||||
PASSWORD_CHANGED: 'PASSWORD_CHANGED',
|
||||
PROFILE_UPDATED: 'PROFILE_UPDATED',
|
||||
|
||||
// Application Lifecycle
|
||||
STAGE_CHANGED: 'STAGE_CHANGED',
|
||||
SHORTLISTED: 'SHORTLISTED',
|
||||
DISQUALIFIED: 'DISQUALIFIED',
|
||||
QUESTIONNAIRE_SUBMITTED: 'QUESTIONNAIRE_SUBMITTED',
|
||||
QUESTIONNAIRE_LINK_SENT: 'QUESTIONNAIRE_LINK_SENT',
|
||||
|
||||
// Documents & Collaboration
|
||||
DOCUMENT_UPLOADED: 'DOCUMENT_UPLOADED',
|
||||
WORKNOTE_ADDED: 'WORKNOTE_ADDED'
|
||||
DOCUMENT_VERIFIED: 'DOCUMENT_VERIFIED',
|
||||
WORKNOTE_ADDED: 'WORKNOTE_ADDED',
|
||||
ATTACHMENT_UPLOADED: 'ATTACHMENT_UPLOADED',
|
||||
PARTICIPANT_ADDED: 'PARTICIPANT_ADDED',
|
||||
PARTICIPANT_REMOVED: 'PARTICIPANT_REMOVED',
|
||||
|
||||
// Interviews & Assessment
|
||||
INTERVIEW_SCHEDULED: 'INTERVIEW_SCHEDULED',
|
||||
INTERVIEW_UPDATED: 'INTERVIEW_UPDATED',
|
||||
INTERVIEW_COMPLETED: 'INTERVIEW_COMPLETED',
|
||||
KT_MATRIX_SUBMITTED: 'KT_MATRIX_SUBMITTED',
|
||||
FEEDBACK_SUBMITTED: 'FEEDBACK_SUBMITTED',
|
||||
RECOMMENDATION_UPDATED: 'RECOMMENDATION_UPDATED',
|
||||
DECISION_MADE: 'DECISION_MADE',
|
||||
|
||||
// FDD
|
||||
FDD_ASSIGNED: 'FDD_ASSIGNED',
|
||||
FDD_REPORT_UPLOADED: 'FDD_REPORT_UPLOADED',
|
||||
|
||||
// LOI & LOA
|
||||
LOI_REQUESTED: 'LOI_REQUESTED',
|
||||
LOI_APPROVED: 'LOI_APPROVED',
|
||||
LOI_REJECTED: 'LOI_REJECTED',
|
||||
LOI_GENERATED: 'LOI_GENERATED',
|
||||
LOA_REQUESTED: 'LOA_REQUESTED',
|
||||
LOA_APPROVED: 'LOA_APPROVED',
|
||||
LOA_GENERATED: 'LOA_GENERATED',
|
||||
|
||||
// EOR
|
||||
EOR_CHECKLIST_CREATED: 'EOR_CHECKLIST_CREATED',
|
||||
EOR_ITEM_UPDATED: 'EOR_ITEM_UPDATED',
|
||||
EOR_AUDIT_SUBMITTED: 'EOR_AUDIT_SUBMITTED',
|
||||
|
||||
// Dealer & Finance
|
||||
DEALER_CREATED: 'DEALER_CREATED',
|
||||
DEALER_UPDATED: 'DEALER_UPDATED',
|
||||
DEALER_CODE_GENERATED: 'DEALER_CODE_GENERATED',
|
||||
PAYMENT_UPDATED: 'PAYMENT_UPDATED',
|
||||
SECURITY_DEPOSIT_UPDATED: 'SECURITY_DEPOSIT_UPDATED',
|
||||
FNF_UPDATED: 'FNF_UPDATED',
|
||||
|
||||
// Admin
|
||||
USER_CREATED: 'USER_CREATED',
|
||||
USER_UPDATED: 'USER_UPDATED',
|
||||
USER_STATUS_CHANGED: 'USER_STATUS_CHANGED',
|
||||
ROLE_CREATED: 'ROLE_CREATED',
|
||||
ROLE_UPDATED: 'ROLE_UPDATED',
|
||||
|
||||
// Resignation & Self-Service
|
||||
RESIGNATION_SUBMITTED: 'RESIGNATION_SUBMITTED',
|
||||
RESIGNATION_APPROVED: 'RESIGNATION_APPROVED',
|
||||
RESIGNATION_REJECTED: 'RESIGNATION_REJECTED',
|
||||
EMAIL_SENT: 'EMAIL_SENT',
|
||||
REMINDER_SENT: 'REMINDER_SENT'
|
||||
} as const;
|
||||
|
||||
// Document Types
|
||||
|
||||
108
src/common/utils/notification.service.ts
Normal file
108
src/common/utils/notification.service.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import webpush from 'web-push';
|
||||
import db from '../../database/models/index.js';
|
||||
import { getIO } from './socket.js';
|
||||
import logger from './logger.js';
|
||||
|
||||
const { Notification, PushSubscription } = db;
|
||||
|
||||
// Initialize VAPID keys
|
||||
if (process.env.VAPID_PUBLIC_KEY && process.env.VAPID_PRIVATE_KEY) {
|
||||
webpush.setVapidDetails(
|
||||
`mailto:${process.env.VAPID_EMAIL || 'admin@royalenfield.com'}`,
|
||||
process.env.VAPID_PUBLIC_KEY,
|
||||
process.env.VAPID_PRIVATE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to a specific user via multiple channels.
|
||||
*/
|
||||
export const sendNotification = async (params: {
|
||||
userId: string;
|
||||
title: string;
|
||||
message: string;
|
||||
type?: string;
|
||||
link?: string;
|
||||
data?: any;
|
||||
}) => {
|
||||
const { userId, title, message, type = 'info', link, data } = params;
|
||||
logger.info(`Starting sendNotification for user: ${userId}`);
|
||||
|
||||
try {
|
||||
// 1. Create In-App Notification (Database)
|
||||
const notification = await Notification.create({
|
||||
userId,
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
link,
|
||||
isRead: false
|
||||
});
|
||||
logger.info(`Notification created in DB with ID: ${notification.id}`);
|
||||
|
||||
// 2. Real-time update via Socket.io
|
||||
try {
|
||||
const io = getIO();
|
||||
const roomName = `user_${userId}`;
|
||||
logger.info(`Emitting real-time notification to room: ${roomName}`);
|
||||
|
||||
// Emit to a private room for the user
|
||||
io.to(roomName).emit('notification', {
|
||||
id: notification.id,
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
link,
|
||||
createdAt: notification.createdAt
|
||||
});
|
||||
} catch (socketError: any) {
|
||||
logger.warn(`Socket.io emit failed: ${socketError.message}`);
|
||||
}
|
||||
|
||||
// 3. Web Push Notification
|
||||
const subscriptions = await PushSubscription.findAll({ where: { userId } });
|
||||
|
||||
for (const sub of subscriptions) {
|
||||
try {
|
||||
const pushConfig = {
|
||||
endpoint: sub.endpoint,
|
||||
keys: {
|
||||
p256dh: sub.p256dh,
|
||||
auth: sub.auth
|
||||
}
|
||||
};
|
||||
|
||||
const payload = JSON.stringify({
|
||||
title,
|
||||
body: message,
|
||||
icon: '/re-logo.png', // Fallback icon path
|
||||
data: {
|
||||
url: link || '/',
|
||||
...data
|
||||
}
|
||||
});
|
||||
|
||||
await webpush.sendNotification(pushConfig, payload);
|
||||
} catch (error: any) {
|
||||
logger.error(`Error sending Web Push to subscription ${sub.id}:`, error.message);
|
||||
// If subscription is expired or invalid, remove it
|
||||
if (error.statusCode === 410 || error.statusCode === 404) {
|
||||
await sub.destroy();
|
||||
logger.info(`Removed invalid push subscription: ${sub.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notification;
|
||||
} catch (error) {
|
||||
logger.error('Error in sendNotification:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Notifies multiple users (e.g., participants in a request).
|
||||
*/
|
||||
export const notifyUsers = async (userIds: string[], params: any) => {
|
||||
return Promise.all(userIds.map(userId => sendNotification({ ...params, userId })));
|
||||
};
|
||||
51
src/common/utils/socket.ts
Normal file
51
src/common/utils/socket.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Server as SocketServer } from 'socket.io';
|
||||
import { Server as HTTPServer } from 'http';
|
||||
import logger from './logger.js';
|
||||
|
||||
let io: SocketServer | null = null;
|
||||
|
||||
/**
|
||||
* Initializes the Socket.io server and attaches it to the provided HTTP server.
|
||||
* @param httpServer The HTTP server to attach Socket.io to.
|
||||
*/
|
||||
export const initSocket = (httpServer: HTTPServer) => {
|
||||
io = new SocketServer(httpServer, {
|
||||
cors: {
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||
methods: ['GET', 'POST', 'PUT'],
|
||||
credentials: true
|
||||
}
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
logger.info(`Socket connected: ${socket.id}`);
|
||||
|
||||
// Join room based on requestId (applicationId, etc.)
|
||||
socket.on('join_room', (roomId: string) => {
|
||||
socket.join(roomId);
|
||||
logger.info(`Socket ${socket.id} joined room: ${roomId}`);
|
||||
});
|
||||
|
||||
socket.on('leave_room', (roomId: string) => {
|
||||
socket.leave(roomId);
|
||||
logger.info(`Socket ${socket.id} left room: ${roomId}`);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
logger.info(`Socket disconnected: ${socket.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
return io;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the initialized Socket.io instance.
|
||||
* @throws Error if Socket.io has not been initialized.
|
||||
*/
|
||||
export const getIO = (): SocketServer => {
|
||||
if (!io) {
|
||||
throw new Error('Socket.io has not been initialized. Please call initSocket first.');
|
||||
}
|
||||
return io;
|
||||
};
|
||||
@ -246,6 +246,12 @@ export default (sequelize: Sequelize) => {
|
||||
});
|
||||
|
||||
Application.hasMany(models.QuestionnaireResponse, { foreignKey: 'applicationId', as: 'questionnaireResponses' });
|
||||
Application.hasMany(models.Worknote, {
|
||||
foreignKey: 'requestId',
|
||||
as: 'worknotes',
|
||||
scope: { requestType: 'application' },
|
||||
constraints: false
|
||||
});
|
||||
Application.hasMany(models.RequestParticipant, {
|
||||
foreignKey: 'requestId',
|
||||
as: 'participants',
|
||||
|
||||
@ -96,7 +96,8 @@ export default (sequelize: Sequelize) => {
|
||||
ConstitutionalChange.hasMany(models.Worknote, {
|
||||
foreignKey: 'requestId',
|
||||
as: 'worknotes',
|
||||
scope: { requestType: 'constitutional' }
|
||||
scope: { requestType: 'constitutional' },
|
||||
constraints: false
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -102,7 +102,12 @@ export default (sequelize: Sequelize) => {
|
||||
Document.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealer' });
|
||||
|
||||
Document.hasMany(models.DocumentVersion, { foreignKey: 'documentId', as: 'versions' });
|
||||
Document.hasMany(models.WorkNoteAttachment, { foreignKey: 'documentId', as: 'workNoteAttachments' });
|
||||
Document.belongsToMany(models.Worknote, {
|
||||
through: models.WorkNoteAttachment,
|
||||
foreignKey: 'documentId',
|
||||
otherKey: 'noteId',
|
||||
as: 'workNotes'
|
||||
});
|
||||
};
|
||||
|
||||
return Document;
|
||||
|
||||
62
src/database/models/PushSubscription.ts
Normal file
62
src/database/models/PushSubscription.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||
|
||||
export interface PushSubscriptionAttributes {
|
||||
id: string;
|
||||
userId: string;
|
||||
endpoint: string;
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
export interface PushSubscriptionInstance extends Model<PushSubscriptionAttributes>, PushSubscriptionAttributes { }
|
||||
|
||||
export default (sequelize: Sequelize) => {
|
||||
const PushSubscription = sequelize.define<PushSubscriptionInstance>('PushSubscription', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
endpoint: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
p256dh: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
auth: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
userAgent: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'push_subscriptions',
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ fields: ['userId'] },
|
||||
{ fields: ['endpoint'], unique: true }
|
||||
]
|
||||
});
|
||||
|
||||
(PushSubscription as any).associate = (models: any) => {
|
||||
PushSubscription.belongsTo(models.User, {
|
||||
foreignKey: 'userId',
|
||||
as: 'user'
|
||||
});
|
||||
};
|
||||
|
||||
return PushSubscription;
|
||||
};
|
||||
@ -111,7 +111,8 @@ export default (sequelize: Sequelize) => {
|
||||
RelocationRequest.hasMany(models.Worknote, {
|
||||
foreignKey: 'requestId',
|
||||
as: 'worknotes',
|
||||
scope: { requestType: 'relocation' }
|
||||
scope: { requestType: 'relocation' },
|
||||
constraints: false
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -124,7 +124,8 @@ export default (sequelize: Sequelize) => {
|
||||
as: 'worknotes',
|
||||
scope: {
|
||||
requestType: 'resignation'
|
||||
}
|
||||
},
|
||||
constraints: false
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -71,7 +71,12 @@ export default (sequelize: Sequelize) => {
|
||||
Worknote.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
|
||||
|
||||
Worknote.hasMany(models.WorkNoteTag, { foreignKey: 'noteId', as: 'tags' });
|
||||
Worknote.hasMany(models.WorkNoteAttachment, { foreignKey: 'noteId', as: 'attachments' });
|
||||
Worknote.belongsToMany(models.Document, {
|
||||
through: models.WorkNoteAttachment,
|
||||
foreignKey: 'noteId',
|
||||
otherKey: 'documentId',
|
||||
as: 'attachments'
|
||||
});
|
||||
};
|
||||
|
||||
return Worknote;
|
||||
|
||||
@ -82,6 +82,7 @@ import createExitFeedback from './ExitFeedback.js';
|
||||
|
||||
// Batch 7: Notifications, Logs & Templates
|
||||
import createEmailTemplate from './EmailTemplate.js';
|
||||
import createPushSubscription from './PushSubscription.js';
|
||||
|
||||
// Batch 8: SLA & TAT Tracking
|
||||
import createSLATracking from './SLATracking.js';
|
||||
@ -188,6 +189,7 @@ db.ExitFeedback = createExitFeedback(sequelize);
|
||||
|
||||
// Batch 7: Notifications, Logs & Templates
|
||||
db.EmailTemplate = createEmailTemplate(sequelize);
|
||||
db.PushSubscription = createPushSubscription(sequelize);
|
||||
|
||||
// Batch 8: SLA & TAT Tracking
|
||||
db.SLATracking = createSLATracking(sequelize);
|
||||
|
||||
233
src/modules/audit/audit.controller.ts
Normal file
233
src/modules/audit/audit.controller.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { Request, Response } from 'express';
|
||||
import db from '../../database/models/index.js';
|
||||
const { AuditLog, User } = db;
|
||||
import { AuthRequest } from '../../types/express.types.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
// Human-readable descriptions for audit actions
|
||||
const ACTION_DESCRIPTIONS: Record<string, string> = {
|
||||
CREATED: 'Record created',
|
||||
UPDATED: 'Record updated',
|
||||
APPROVED: 'Approved',
|
||||
REJECTED: 'Rejected',
|
||||
DELETED: 'Record deleted',
|
||||
LOGIN: 'User logged in',
|
||||
LOGOUT: 'User logged out',
|
||||
REGISTERED: 'User registered',
|
||||
PASSWORD_CHANGED: 'Password changed',
|
||||
PROFILE_UPDATED: 'Profile updated',
|
||||
STAGE_CHANGED: 'Stage changed',
|
||||
SHORTLISTED: 'Application shortlisted',
|
||||
DISQUALIFIED: 'Application disqualified',
|
||||
QUESTIONNAIRE_SUBMITTED: 'Questionnaire response submitted',
|
||||
QUESTIONNAIRE_LINK_SENT: 'Questionnaire link sent to applicant',
|
||||
DOCUMENT_UPLOADED: 'Document uploaded',
|
||||
DOCUMENT_VERIFIED: 'Document verified',
|
||||
WORKNOTE_ADDED: 'Work note added',
|
||||
ATTACHMENT_UPLOADED: 'Attachment uploaded',
|
||||
PARTICIPANT_ADDED: 'Participant added',
|
||||
PARTICIPANT_REMOVED: 'Participant removed',
|
||||
INTERVIEW_SCHEDULED: 'Interview scheduled',
|
||||
INTERVIEW_UPDATED: 'Interview updated',
|
||||
INTERVIEW_COMPLETED: 'Interview completed',
|
||||
KT_MATRIX_SUBMITTED: 'KT Matrix score submitted',
|
||||
FEEDBACK_SUBMITTED: 'Interview feedback submitted',
|
||||
RECOMMENDATION_UPDATED: 'Recommendation updated',
|
||||
DECISION_MADE: 'Interview decision made',
|
||||
FDD_ASSIGNED: 'FDD agency assigned',
|
||||
FDD_REPORT_UPLOADED: 'FDD report uploaded',
|
||||
LOI_REQUESTED: 'LOI requested',
|
||||
LOI_APPROVED: 'LOI approved',
|
||||
LOI_REJECTED: 'LOI rejected',
|
||||
LOI_GENERATED: 'LOI document generated',
|
||||
LOA_REQUESTED: 'LOA requested',
|
||||
LOA_APPROVED: 'LOA approved',
|
||||
LOA_GENERATED: 'LOA document generated',
|
||||
EOR_CHECKLIST_CREATED: 'EOR checklist initiated',
|
||||
EOR_ITEM_UPDATED: 'EOR checklist item updated',
|
||||
EOR_AUDIT_SUBMITTED: 'EOR audit submitted',
|
||||
DEALER_CREATED: 'Dealer profile created',
|
||||
DEALER_UPDATED: 'Dealer profile updated',
|
||||
DEALER_CODE_GENERATED: 'Dealer code generated',
|
||||
PAYMENT_UPDATED: 'Payment record updated',
|
||||
SECURITY_DEPOSIT_UPDATED: 'Security deposit updated',
|
||||
FNF_UPDATED: 'F&F settlement updated',
|
||||
USER_CREATED: 'User account created',
|
||||
USER_UPDATED: 'User account updated',
|
||||
USER_STATUS_CHANGED: 'User status changed',
|
||||
ROLE_CREATED: 'Role created',
|
||||
ROLE_UPDATED: 'Role updated',
|
||||
RESIGNATION_SUBMITTED: 'Resignation submitted',
|
||||
RESIGNATION_APPROVED: 'Resignation approved',
|
||||
RESIGNATION_REJECTED: 'Resignation rejected',
|
||||
EMAIL_SENT: 'Email notification sent',
|
||||
REMINDER_SENT: 'Reminder sent'
|
||||
};
|
||||
|
||||
/**
|
||||
* Get audit logs for a specific entity (e.g., application)
|
||||
* Query params: entityType, entityId, page, limit
|
||||
*/
|
||||
export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { entityType, entityId, page = '1', limit = '50' } = req.query;
|
||||
|
||||
if (!entityType || !entityId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'entityType and entityId are required'
|
||||
});
|
||||
}
|
||||
|
||||
const pageNum = Math.max(1, parseInt(page as string));
|
||||
const limitNum = Math.min(100, Math.max(1, parseInt(limit as string)));
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
const { count, rows: logs } = await AuditLog.findAndCountAll({
|
||||
where: {
|
||||
entityType: entityType as string,
|
||||
entityId: entityId as string
|
||||
},
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'fullName', 'email']
|
||||
}],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: limitNum,
|
||||
offset
|
||||
});
|
||||
|
||||
// Format the response with human-readable descriptions
|
||||
const formattedLogs = logs.map((log: any) => {
|
||||
const logData = log.toJSON ? log.toJSON() : log;
|
||||
return {
|
||||
id: logData.id,
|
||||
action: logData.action,
|
||||
description: ACTION_DESCRIPTIONS[logData.action] || logData.action,
|
||||
entityType: logData.entityType,
|
||||
entityId: logData.entityId,
|
||||
userName: logData.user?.fullName || 'System',
|
||||
userEmail: logData.user?.email || null,
|
||||
oldData: logData.oldData,
|
||||
newData: logData.newData,
|
||||
changes: formatChanges(logData.oldData, logData.newData),
|
||||
ipAddress: logData.ipAddress,
|
||||
timestamp: logData.createdAt
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: formattedLogs,
|
||||
pagination: {
|
||||
total: count,
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
totalPages: Math.ceil(count / limitNum)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get audit logs error:', error);
|
||||
res.status(500).json({ success: false, message: 'Error fetching audit logs' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get audit log summary/stats for an entity
|
||||
*/
|
||||
export const getAuditSummary = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { entityType, entityId } = req.query;
|
||||
|
||||
if (!entityType || !entityId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'entityType and entityId are required'
|
||||
});
|
||||
}
|
||||
|
||||
const totalLogs = await AuditLog.count({
|
||||
where: {
|
||||
entityType: entityType as string,
|
||||
entityId: entityId as string
|
||||
}
|
||||
});
|
||||
|
||||
const latestLog = await AuditLog.findOne({
|
||||
where: {
|
||||
entityType: entityType as string,
|
||||
entityId: entityId as string
|
||||
},
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'fullName']
|
||||
}],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalEntries: totalLogs,
|
||||
lastActivity: latestLog ? {
|
||||
action: (latestLog as any).action,
|
||||
description: ACTION_DESCRIPTIONS[(latestLog as any).action] || (latestLog as any).action,
|
||||
user: (latestLog as any).user?.fullName || 'System',
|
||||
timestamp: (latestLog as any).createdAt
|
||||
} : null
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get audit summary error:', error);
|
||||
res.status(500).json({ success: false, message: 'Error fetching audit summary' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Format changes between old and new data into a readable list
|
||||
*/
|
||||
function formatChanges(oldData: any, newData: any): string[] {
|
||||
const changes: string[] = [];
|
||||
|
||||
if (!oldData && newData) {
|
||||
// New record created
|
||||
if (typeof newData === 'object') {
|
||||
Object.entries(newData).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
changes.push(`${formatFieldName(key)}: ${value}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
if (oldData && newData && typeof oldData === 'object' && typeof newData === 'object') {
|
||||
// Compare old vs new
|
||||
const allKeys = new Set([...Object.keys(oldData), ...Object.keys(newData)]);
|
||||
allKeys.forEach(key => {
|
||||
const oldVal = oldData[key];
|
||||
const newVal = newData[key];
|
||||
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
||||
if (oldVal !== undefined && newVal !== undefined) {
|
||||
changes.push(`${formatFieldName(key)}: "${oldVal}" → "${newVal}"`);
|
||||
} else if (newVal !== undefined) {
|
||||
changes.push(`${formatFieldName(key)} set to "${newVal}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert camelCase field names to human-readable format
|
||||
*/
|
||||
function formatFieldName(field: string): string {
|
||||
return field
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase())
|
||||
.trim();
|
||||
}
|
||||
15
src/modules/audit/audit.routes.ts
Normal file
15
src/modules/audit/audit.routes.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import * as auditController from './audit.controller.js';
|
||||
import { authenticate } from '../../common/middleware/auth.js';
|
||||
|
||||
// All audit routes require authentication
|
||||
router.use(authenticate as any);
|
||||
|
||||
// GET /api/audit/logs?entityType=application&entityId=<uuid>&page=1&limit=50
|
||||
router.get('/logs', auditController.getAuditLogs);
|
||||
|
||||
// GET /api/audit/summary?entityType=application&entityId=<uuid>
|
||||
router.get('/summary', auditController.getAuditSummary);
|
||||
|
||||
export default router;
|
||||
@ -1,14 +1,27 @@
|
||||
import { Response } from 'express';
|
||||
import db from '../../database/models/index.js';
|
||||
const { Worknote, User, WorkNoteTag, WorkNoteAttachment, Document, DocumentVersion, RequestParticipant, Application } = db;
|
||||
const { Worknote, User, WorkNoteTag, WorkNoteAttachment, Document, DocumentVersion, RequestParticipant, Application, AuditLog } = db;
|
||||
import { AuthRequest } from '../../types/express.types.js';
|
||||
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
||||
import * as EmailService from '../../common/utils/email.service.js';
|
||||
import { getIO } from '../../common/utils/socket.js';
|
||||
import * as NotificationService from '../../common/utils/notification.service.js';
|
||||
import logger from '../../common/utils/logger.js';
|
||||
|
||||
// --- Worknotes ---
|
||||
|
||||
export const addWorknote = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { requestId, requestType, noteText, noteType, tags, attachmentDocIds } = req.body; // tags: ['Urgent'], attachmentDocIds: [uuid]
|
||||
const { requestId, requestType, noteText, noteType, tags, attachmentDocIds } = req.body;
|
||||
logger.info(`Adding worknote for ${requestType} ${requestId}. Body:`, { noteText, tags, attachmentDocIds });
|
||||
|
||||
// Debug: Log participants
|
||||
const participants = await db.RequestParticipant.findAll({
|
||||
where: { requestId, requestType },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }]
|
||||
});
|
||||
const simplifiedParticipants = participants.map((p: any) => ({ id: p.user?.id, name: p.user?.fullName }));
|
||||
logger.info(`Participants for ${requestId}:`, simplifiedParticipants);
|
||||
|
||||
const worknote = await Worknote.create({
|
||||
requestId,
|
||||
@ -21,13 +34,13 @@ export const addWorknote = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
for (const tag of tags) {
|
||||
await WorkNoteTag.create({ workNoteId: worknote.id, tag });
|
||||
await WorkNoteTag.create({ noteId: worknote.id, tagName: tag });
|
||||
}
|
||||
}
|
||||
|
||||
if (attachmentDocIds && attachmentDocIds.length > 0) {
|
||||
for (const docId of attachmentDocIds) {
|
||||
await WorkNoteAttachment.create({ workNoteId: worknote.id, documentId: docId });
|
||||
await WorkNoteAttachment.create({ noteId: worknote.id, documentId: docId });
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +59,79 @@ export const addWorknote = async (req: AuthRequest, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({ success: true, message: 'Worknote added', data: worknote });
|
||||
// Reload with associations for the response and socket emission
|
||||
const fullWorknote = await Worknote.findByPk(worknote.id, {
|
||||
include: [
|
||||
{ model: User, as: 'author', attributes: [['fullName', 'name'], 'email', ['roleCode', 'role']] },
|
||||
{ model: WorkNoteTag, as: 'tags' },
|
||||
{ model: Document, as: 'attachments' }
|
||||
]
|
||||
});
|
||||
|
||||
// --- Real-time & Notifications ---
|
||||
try {
|
||||
const io = getIO();
|
||||
io.to(requestId).emit('new_worknote', fullWorknote);
|
||||
|
||||
// Handle Mentions/Notifications
|
||||
const notifiedUserIds = new Set<string>();
|
||||
|
||||
// 1. Check structured mentions in noteText
|
||||
logger.info(`Checking worknote for mentions. Text: "${noteText}"`);
|
||||
if (noteText && typeof noteText === 'string' && noteText.includes('@')) {
|
||||
const mentionRegex = /@\[([^\]]+)\]\(user:([^\)]+)\)/g;
|
||||
let match;
|
||||
while ((match = mentionRegex.exec(noteText)) !== null) {
|
||||
const userId = match[2];
|
||||
if (userId && userId !== req.user?.id) {
|
||||
notifiedUserIds.add(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check tags (fallback/robustness)
|
||||
if (tags && Array.isArray(tags)) {
|
||||
tags.forEach((tag: any) => {
|
||||
// If tag is a UUID-like string and not the current user
|
||||
if (typeof tag === 'string' && tag.length > 20 && tag !== req.user?.id) {
|
||||
notifiedUserIds.add(tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Send Notifications
|
||||
for (const userId of notifiedUserIds) {
|
||||
logger.info(`Sending notification to user ID: ${userId}`);
|
||||
try {
|
||||
await NotificationService.sendNotification({
|
||||
userId,
|
||||
title: 'New Mention',
|
||||
message: `${req.user?.fullName || 'Someone'} mentioned you in a worknote.`,
|
||||
type: 'info',
|
||||
link: `/applications/${requestId}?tab=worknotes`
|
||||
});
|
||||
} catch (notifyErr) {
|
||||
logger.warn(`Failed to send notification to ${userId}:`, notifyErr);
|
||||
}
|
||||
}
|
||||
|
||||
if (notifiedUserIds.size === 0 && noteText && noteText.includes('@')) {
|
||||
logger.warn('Worknote contains "@" but no mentions were successfully identified.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Real-time notification failed:', err);
|
||||
}
|
||||
|
||||
// Audit log for worknote
|
||||
await AuditLog.create({
|
||||
userId: req.user?.id,
|
||||
action: AUDIT_ACTIONS.WORKNOTE_ADDED,
|
||||
entityType: 'application',
|
||||
entityId: requestId,
|
||||
newData: { noteType: noteType || 'General', hasAttachments: !!(attachmentDocIds?.length) }
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, message: 'Worknote added', data: fullWorknote });
|
||||
} catch (error) {
|
||||
console.error('Add worknote error:', error);
|
||||
res.status(500).json({ success: false, message: 'Error adding worknote' });
|
||||
@ -62,7 +147,7 @@ export const getWorknotes = async (req: AuthRequest, res: Response) => {
|
||||
include: [
|
||||
{ model: User, as: 'author', attributes: [['fullName', 'name'], 'email', ['roleCode', 'role']] },
|
||||
{ model: WorkNoteTag, as: 'tags' },
|
||||
{ model: WorkNoteAttachment, as: 'attachments', include: ['document'] }
|
||||
{ model: Document, as: 'attachments' }
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
@ -73,6 +158,63 @@ export const getWorknotes = async (req: AuthRequest, res: Response) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadWorknoteAttachment = async (req: any, res: Response) => {
|
||||
try {
|
||||
const file = req.file;
|
||||
const { requestId, requestType } = req.body;
|
||||
|
||||
if (!file) {
|
||||
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
||||
}
|
||||
|
||||
const document = await Document.create({
|
||||
requestId: requestId || null,
|
||||
requestType: requestType || null,
|
||||
documentType: 'Worknote Attachment',
|
||||
fileName: file.originalname,
|
||||
filePath: file.path,
|
||||
mimeType: file.mimetype,
|
||||
fileSize: file.size,
|
||||
uploadedBy: req.user?.id,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// Create initial version
|
||||
await DocumentVersion.create({
|
||||
documentId: document.id,
|
||||
versionNumber: 1,
|
||||
filePath: file.path,
|
||||
uploadedBy: req.user?.id,
|
||||
changeReason: 'Initial Upload'
|
||||
});
|
||||
|
||||
// Audit log for attachment upload
|
||||
if (requestId) {
|
||||
await AuditLog.create({
|
||||
userId: req.user?.id,
|
||||
action: AUDIT_ACTIONS.ATTACHMENT_UPLOADED,
|
||||
entityType: requestType || 'application',
|
||||
entityId: requestId,
|
||||
newData: { fileName: file.originalname, mimeType: file.mimetype }
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Attachment uploaded',
|
||||
data: {
|
||||
id: document.id,
|
||||
fileName: document.fileName,
|
||||
filePath: document.filePath,
|
||||
mimeType: document.mimeType
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload worknote attachment error:', error);
|
||||
res.status(500).json({ success: false, message: 'Error uploading attachment' });
|
||||
}
|
||||
};
|
||||
|
||||
// --- Documents ---
|
||||
|
||||
export const uploadDocument = async (req: AuthRequest, res: Response) => {
|
||||
@ -98,6 +240,15 @@ export const uploadDocument = async (req: AuthRequest, res: Response) => {
|
||||
changeReason: 'Initial Upload'
|
||||
});
|
||||
|
||||
// Audit log for document upload
|
||||
await AuditLog.create({
|
||||
userId: req.user?.id,
|
||||
action: AUDIT_ACTIONS.DOCUMENT_UPLOADED,
|
||||
entityType: 'application',
|
||||
entityId: applicationId,
|
||||
newData: { docType, fileName }
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, message: 'Document uploaded', data: document });
|
||||
} catch (error) {
|
||||
console.error('Upload document error:', error);
|
||||
@ -163,6 +314,15 @@ export const addParticipant = async (req: AuthRequest, res: Response) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Audit log for participant added
|
||||
await AuditLog.create({
|
||||
userId: req.user?.id,
|
||||
action: AUDIT_ACTIONS.PARTICIPANT_ADDED,
|
||||
entityType: requestType || 'application',
|
||||
entityId: requestId,
|
||||
newData: { addedUserId: userId, participantType: participantType || 'contributor', userName: user?.fullName }
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, message: 'Participant added', data: participant });
|
||||
} catch (error) {
|
||||
console.error('Add participant error:', error);
|
||||
@ -173,7 +333,20 @@ export const addParticipant = async (req: AuthRequest, res: Response) => {
|
||||
export const removeParticipant = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const participant = await RequestParticipant.findByPk(id);
|
||||
await RequestParticipant.destroy({ where: { id } });
|
||||
|
||||
// Audit log for participant removed
|
||||
if (participant) {
|
||||
await AuditLog.create({
|
||||
userId: req.user?.id,
|
||||
action: AUDIT_ACTIONS.PARTICIPANT_REMOVED,
|
||||
entityType: (participant as any).requestType || 'application',
|
||||
entityId: (participant as any).requestId,
|
||||
newData: { removedUserId: (participant as any).userId }
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Participant removed' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: 'Error removing participant' });
|
||||
|
||||
@ -2,12 +2,14 @@ import express from 'express';
|
||||
const router = express.Router();
|
||||
import * as collaborationController from './collaboration.controller.js';
|
||||
import { authenticate } from '../../common/middleware/auth.js';
|
||||
import { uploadSingle, handleUploadError } from '../../common/middleware/upload.js';
|
||||
|
||||
router.use(authenticate as any);
|
||||
|
||||
// Worknotes
|
||||
router.get('/worknotes', collaborationController.getWorknotes);
|
||||
router.post('/worknotes', collaborationController.addWorknote);
|
||||
router.post('/upload', uploadSingle, handleUploadError, collaborationController.uploadWorknoteAttachment);
|
||||
|
||||
// Participants
|
||||
router.post('/participants', collaborationController.addParticipant);
|
||||
|
||||
@ -35,3 +35,52 @@ export const getNotifications = async (req: AuthRequest, res: Response) => {
|
||||
res.status(500).json({ success: false, message: 'Error fetching notifications' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePushSubscription = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { subscription } = req.body;
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!subscription || !subscription.endpoint) {
|
||||
return res.status(400).json({ success: false, message: 'Invalid subscription data' });
|
||||
}
|
||||
|
||||
const { endpoint, keys } = subscription;
|
||||
|
||||
await db.PushSubscription.upsert({
|
||||
userId,
|
||||
endpoint,
|
||||
p256dh: keys.p256dh,
|
||||
auth: keys.auth,
|
||||
userAgent: req.get('user-agent')
|
||||
});
|
||||
|
||||
res.json({ success: true, message: 'Push subscription updated' });
|
||||
} catch (error) {
|
||||
console.error('Push subscription error:', error);
|
||||
res.status(500).json({ success: false, message: 'Error updating push subscription' });
|
||||
}
|
||||
};
|
||||
|
||||
export const markAsRead = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await Notification.update({ isRead: true }, {
|
||||
where: { id, userId: req.user?.id }
|
||||
});
|
||||
res.json({ success: true, message: 'Notification marked as read' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: 'Error updating notification' });
|
||||
}
|
||||
};
|
||||
|
||||
export const markAllAsRead = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
await Notification.update({ isRead: true }, {
|
||||
where: { userId: req.user?.id, isRead: false }
|
||||
});
|
||||
res.json({ success: true, message: 'All notifications marked as read' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: 'Error updating notifications' });
|
||||
}
|
||||
};
|
||||
|
||||
@ -13,5 +13,8 @@ router.post('/templates', checkRole([ROLES.SUPER_ADMIN]) as any, commController.
|
||||
|
||||
// Notifications
|
||||
router.get('/notifications', commController.getNotifications);
|
||||
router.patch('/notifications/:id/read', commController.markAsRead);
|
||||
router.patch('/notifications/read-all', commController.markAllAsRead);
|
||||
router.post('/notifications/subscribe', commController.updatePushSubscription);
|
||||
|
||||
export default router;
|
||||
|
||||
@ -477,7 +477,7 @@ export const getAreaManagers = async (req: Request, res: Response) => {
|
||||
export const updateArea = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { areaName, city, pincode, isActive, managerId } = req.body;
|
||||
const { areaName, city, pincode, isActive, managerId, districtId, activeFrom, activeTo } = req.body;
|
||||
const area = await Area.findByPk(id);
|
||||
if (!area) return res.status(404).json({ success: false, message: 'Area not found' });
|
||||
|
||||
@ -486,8 +486,38 @@ export const updateArea = async (req: Request, res: Response) => {
|
||||
if (city) updates.city = city;
|
||||
if (pincode) updates.pincode = pincode;
|
||||
if (isActive !== undefined) updates.isActive = isActive;
|
||||
if (activeFrom !== undefined) updates.activeFrom = activeFrom || null;
|
||||
if (activeTo !== undefined) updates.activeTo = activeTo || null;
|
||||
if (managerId !== undefined) updates.managerId = managerId; // Legacy support
|
||||
|
||||
// If district is changed, update the entire hierarchy (State, Zone, Region)
|
||||
if (districtId && districtId !== area.districtId) {
|
||||
updates.districtId = districtId;
|
||||
|
||||
const district = await District.findByPk(districtId, {
|
||||
include: [{
|
||||
model: State,
|
||||
as: 'state',
|
||||
include: [
|
||||
{ model: Zone, as: 'zone' },
|
||||
{ model: Region, as: 'region' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
if (district) {
|
||||
updates.stateId = district.stateId;
|
||||
if (district.state) {
|
||||
if (district.state.zone) {
|
||||
updates.zoneId = district.state.zone.id;
|
||||
}
|
||||
if (district.state.region) {
|
||||
updates.regionId = district.state.region.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await area.update(updates);
|
||||
|
||||
// Handle AreaManager Table Update
|
||||
@ -533,7 +563,7 @@ export const updateArea = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Area updated' });
|
||||
res.json({ success: true, message: 'Area updated successfully' });
|
||||
} catch (error) {
|
||||
console.error('Update area error:', error);
|
||||
res.status(500).json({ success: false, message: 'Error updating area' });
|
||||
|
||||
@ -193,7 +193,7 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
|
||||
{
|
||||
model: db.RequestParticipant,
|
||||
as: 'participants',
|
||||
include: [{ model: db.User, as: 'user', attributes: [['fullName', 'name'], 'email', ['roleCode', 'role']] }]
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', ['fullName', 'name'], 'email', ['roleCode', 'role']] }]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
15
src/scripts/drop_worknote_constraint.ts
Normal file
15
src/scripts/drop_worknote_constraint.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { sequelize } from '../database/models/index.js';
|
||||
|
||||
async function dropConstraint() {
|
||||
try {
|
||||
console.log('Dropping worknotes_requestId_fkey constraint...');
|
||||
await sequelize.query('ALTER TABLE "worknotes" DROP CONSTRAINT IF EXISTS "worknotes_requestId_fkey"');
|
||||
console.log('Constraint dropped successfully (if it existed).');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error dropping constraint:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
dropConstraint();
|
||||
@ -6,6 +6,8 @@ import compression from 'compression';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createServer } from 'http';
|
||||
import { initSocket } from './common/utils/socket.js';
|
||||
|
||||
// Import database
|
||||
import db from './database/models/index.js';
|
||||
@ -31,6 +33,7 @@ import eorRoutes from './modules/eor/eor.routes.js';
|
||||
import dealerRoutes from './modules/dealer/dealer.routes.js';
|
||||
import slaRoutes from './modules/sla/sla.routes.js';
|
||||
import communicationRoutes from './modules/communication/communication.routes.js';
|
||||
import auditRoutes from './modules/audit/audit.routes.js';
|
||||
import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js';
|
||||
import prospectiveLoginRoutes from './modules/prospective-login/prospective-login.routes.js';
|
||||
|
||||
@ -43,9 +46,22 @@ const __dirname = path.dirname(__filename);
|
||||
|
||||
// Initialize Express app
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
|
||||
// Initialize Socket.io
|
||||
initSocket(httpServer);
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
||||
"frame-ancestors": ["'self'", process.env.FRONTEND_URL || 'http://localhost:5173'],
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: false,
|
||||
crossOriginResourcePolicy: { policy: "cross-origin" }
|
||||
}));
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||
credentials: true
|
||||
@ -106,6 +122,7 @@ app.use('/api/eor', eorRoutes);
|
||||
app.use('/api/dealer', dealerRoutes);
|
||||
app.use('/api/sla', slaRoutes);
|
||||
app.use('/api/communication', communicationRoutes);
|
||||
app.use('/api/audit', auditRoutes);
|
||||
app.use('/api/questionnaire', questionnaireRoutes);
|
||||
app.use('/api/prospective-login', prospectiveLoginRoutes);
|
||||
|
||||
@ -153,10 +170,11 @@ const startServer = async () => {
|
||||
}
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
httpServer.listen(PORT, () => {
|
||||
logger.info(`🚀 Server running on port ${PORT}`);
|
||||
logger.info(`📍 Environment: ${process.env.NODE_ENV}`);
|
||||
logger.info(`🔗 API Base URL: http://localhost:${PORT}/api`);
|
||||
logger.info(`🔌 Socket.io initialized`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Unable to start server:', error);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user