feat: Sprint 1 Complete - Security, Logging & Performance ✅
Task 3 & 4: Structured Logging + PII Sanitization ✅ - Installed winston and nest-winston packages - Created winston.config.ts with comprehensive logging setup - Features: * Console transport (development) * File transports (error.log, combined.log, audit.log) * Exception & rejection handlers * PII sanitization (email, phone, SSN, CC, IP addresses) * Production-ready configuration * Log rotation (5MB max, 5-30 file retention) * Structured JSON logging for parsing - Integrated Winston into NestJS (main.ts, app.module.ts) - Created logs/ directory with .gitignore - PII auto-redaction in all logs except audit logs Task 5: Database Table Partitioning ✅ - Created V009 migration for activities table partitioning - Partitioned by month using PostgreSQL RANGE partitioning - Auto-creates 13 partitions (6 past + current + 6 future months) - Features: * Automatic partition creation function * Inherited indexes for all partitions * Foreign key constraints maintained * Data migration from old table * Updated_at trigger * Optimized for time-series queries - Performance benefits: * Faster queries (scans only relevant partitions) * Easier maintenance (drop old partitions) * Better vacuum performance * Parallel partition scanning Sprint 1 Results: ✅ All 5 tasks complete (100%) ✅ Estimated: 11-16 hours ✅ Security hardened ✅ Logging production-ready ✅ Database optimized for scale 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
243
maternal-app/maternal-app-backend/package-lock.json
generated
243
maternal-app/maternal-app-backend/package-lock.json
generated
@@ -52,6 +52,7 @@
|
||||
"langchain": "^0.3.35",
|
||||
"mailgun.js": "^12.1.0",
|
||||
"multer": "^2.0.2",
|
||||
"nest-winston": "^1.10.2",
|
||||
"node-fetch": "^2.7.0",
|
||||
"openai": "^6.0.1",
|
||||
"otplib": "^12.0.1",
|
||||
@@ -67,7 +68,8 @@
|
||||
"sharp": "^0.34.4",
|
||||
"socket.io": "^4.8.1",
|
||||
"typeorm": "^0.3.27",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^13.0.0",
|
||||
"winston": "^3.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
@@ -2084,6 +2086,17 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@dabh/diagnostics": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
|
||||
"integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@so-ric/colorspace": "^1.1.6",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
|
||||
@@ -5896,6 +5909,16 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@so-ric/colorspace": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
|
||||
"integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^5.0.2",
|
||||
"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",
|
||||
@@ -6408,6 +6431,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/triple-beam": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
@@ -7149,6 +7178,12 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async-function": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||
@@ -8089,6 +8124,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz",
|
||||
"integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^3.0.1",
|
||||
"color-string": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -8107,6 +8155,48 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz",
|
||||
"integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-string/node_modules/color-name": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz",
|
||||
"integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-convert": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz",
|
||||
"integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-name": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz",
|
||||
"integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -8683,6 +8773,12 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -9459,6 +9555,12 @@
|
||||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
@@ -9589,6 +9691,12 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
@@ -10511,7 +10619,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -11560,6 +11667,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/langchain": {
|
||||
"version": "0.3.35",
|
||||
"resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.35.tgz",
|
||||
@@ -11927,6 +12040,32 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/logform/node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/loglevel": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
|
||||
@@ -12263,6 +12402,19 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nest-winston": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/nest-winston/-/nest-winston-1.10.2.tgz",
|
||||
"integrity": "sha512-Z9IzL/nekBOF/TEwBHUJDiDPMaXUcFquUQOFavIRet6xF0EbuWnOzslyN/ksgzG+fITNgXhMdrL/POp9SdaFxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-safe-stringify": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"winston": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.77.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz",
|
||||
@@ -12419,6 +12571,15 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fn.name": "1.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
@@ -13821,6 +13982,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -14327,6 +14497,15 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@@ -14890,6 +15069,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/thirty-two": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
|
||||
@@ -15000,6 +15185,15 @@
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
@@ -15820,6 +16014,51 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.18.3",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz",
|
||||
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.8",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston/node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"langchain": "^0.3.35",
|
||||
"mailgun.js": "^12.1.0",
|
||||
"multer": "^2.0.2",
|
||||
"nest-winston": "^1.10.2",
|
||||
"node-fetch": "^2.7.0",
|
||||
"openai": "^6.0.1",
|
||||
"otplib": "^12.0.1",
|
||||
@@ -79,7 +80,8 @@
|
||||
"sharp": "^0.34.4",
|
||||
"socket.io": "^4.8.1",
|
||||
"typeorm": "^0.3.27",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^13.0.0",
|
||||
"winston": "^3.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
|
||||
@@ -4,6 +4,8 @@ import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { GraphQLModule } from '@nestjs/graphql';
|
||||
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
||||
import { APP_GUARD, APP_FILTER } from '@nestjs/core';
|
||||
import { WinstonModule } from 'nest-winston';
|
||||
import { winstonConfig } from './common/logger/winston.config';
|
||||
import { join } from 'path';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
@@ -33,6 +35,7 @@ import { HealthController } from './common/controllers/health.controller';
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
WinstonModule.forRoot(winstonConfig),
|
||||
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
inject: [ConfigService],
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { WinstonModuleOptions } from 'nest-winston';
|
||||
import * as winston from 'winston';
|
||||
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
||||
|
||||
// Custom format to sanitize PII
|
||||
const piiSanitizer = winston.format((info) => {
|
||||
const piiPatterns = {
|
||||
email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
||||
phone: /(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g,
|
||||
ssn: /\b\d{3}-?\d{2}-?\d{4}\b/g,
|
||||
creditCard: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
|
||||
ipv4: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
||||
};
|
||||
|
||||
const sanitize = (text: string): string => {
|
||||
if (typeof text !== 'string') return text;
|
||||
|
||||
let sanitized = text;
|
||||
sanitized = sanitized.replace(piiPatterns.email, '[EMAIL_REDACTED]');
|
||||
sanitized = sanitized.replace(piiPatterns.phone, '[PHONE_REDACTED]');
|
||||
sanitized = sanitized.replace(piiPatterns.ssn, '[SSN_REDACTED]');
|
||||
sanitized = sanitized.replace(piiPatterns.creditCard, '[CC_REDACTED]');
|
||||
sanitized = sanitized.replace(piiPatterns.ipv4, '[IP_REDACTED]');
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
// Sanitize message
|
||||
if (info.message) {
|
||||
info.message = sanitize(info.message);
|
||||
}
|
||||
|
||||
// Sanitize metadata
|
||||
if (info.context && typeof info.context === 'object') {
|
||||
Object.keys(info.context).forEach((key) => {
|
||||
if (typeof info.context[key] === 'string') {
|
||||
info.context[key] = sanitize(info.context[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return info;
|
||||
});
|
||||
|
||||
export const winstonConfig: WinstonModuleOptions = {
|
||||
transports: [
|
||||
// Console transport for development
|
||||
new winston.transports.Console({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.ms(),
|
||||
piiSanitizer(),
|
||||
nestWinstonModuleUtilities.format.nestLike('MaternalApp', {
|
||||
colors: true,
|
||||
prettyPrint: true,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
|
||||
// File transport for errors
|
||||
new winston.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
piiSanitizer(),
|
||||
winston.format.json(),
|
||||
),
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5,
|
||||
}),
|
||||
|
||||
// File transport for all logs
|
||||
new winston.transports.File({
|
||||
filename: 'logs/combined.log',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
piiSanitizer(),
|
||||
winston.format.json(),
|
||||
),
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 10,
|
||||
}),
|
||||
|
||||
// File transport for audit logs (no PII sanitization)
|
||||
new winston.transports.File({
|
||||
filename: 'logs/audit.log',
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json(),
|
||||
winston.format((info) => {
|
||||
// Only include audit-related logs
|
||||
return info.context?.isAudit ? info : false;
|
||||
})(),
|
||||
),
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 30, // Keep longer for compliance
|
||||
}),
|
||||
],
|
||||
|
||||
// Global exception handler
|
||||
exceptionHandlers: [
|
||||
new winston.transports.File({
|
||||
filename: 'logs/exceptions.log',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
piiSanitizer(),
|
||||
winston.format.json(),
|
||||
),
|
||||
}),
|
||||
],
|
||||
|
||||
// Global rejection handler
|
||||
rejectionHandlers: [
|
||||
new winston.transports.File({
|
||||
filename: 'logs/rejections.log',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
piiSanitizer(),
|
||||
winston.format.json(),
|
||||
),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
// Production configuration (can be enabled via environment variable)
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// Remove console transport in production
|
||||
winstonConfig.transports = winstonConfig.transports.filter(
|
||||
(transport) => !(transport instanceof winston.transports.Console),
|
||||
);
|
||||
|
||||
// Add production-specific transports (e.g., CloudWatch, Elasticsearch)
|
||||
// Example: CloudWatch
|
||||
// winstonConfig.transports.push(
|
||||
// new WinstonCloudWatch({
|
||||
// logGroupName: process.env.CLOUDWATCH_LOG_GROUP,
|
||||
// logStreamName: `${process.env.CLOUDWATCH_LOG_STREAM}-${Date.now()}`,
|
||||
// awsRegion: process.env.AWS_REGION,
|
||||
// jsonMessage: true,
|
||||
// })
|
||||
// );
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
-- Migration V009: Activity Table Partitioning
|
||||
-- Created: 2025-10-03
|
||||
-- Description: Convert activities table to partitioned table by month for better performance at scale
|
||||
|
||||
-- Step 1: Create new partitioned table
|
||||
CREATE TABLE IF NOT EXISTS activities_partitioned (
|
||||
id UUID NOT NULL,
|
||||
child_id UUID NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
activity_type VARCHAR(50) NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
start_time TIMESTAMP,
|
||||
end_time TIMESTAMP,
|
||||
duration_minutes INTEGER,
|
||||
amount DECIMAL(10, 2),
|
||||
unit VARCHAR(20),
|
||||
notes TEXT,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id, timestamp)
|
||||
) PARTITION BY RANGE (timestamp);
|
||||
|
||||
-- Step 2: Create partitions for the last 6 months and next 6 months
|
||||
-- Format: activities_YYYY_MM
|
||||
DO $$
|
||||
DECLARE
|
||||
start_date DATE;
|
||||
end_date DATE;
|
||||
partition_name TEXT;
|
||||
partition_start DATE;
|
||||
partition_end DATE;
|
||||
BEGIN
|
||||
-- Create partitions for last 6 months
|
||||
FOR i IN -6..6 LOOP
|
||||
partition_start := DATE_TRUNC('month', CURRENT_DATE + (i || ' months')::INTERVAL);
|
||||
partition_end := partition_start + INTERVAL '1 month';
|
||||
partition_name := 'activities_' || TO_CHAR(partition_start, 'YYYY_MM');
|
||||
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS %I PARTITION OF activities_partitioned
|
||||
FOR VALUES FROM (%L) TO (%L)',
|
||||
partition_name,
|
||||
partition_start,
|
||||
partition_end
|
||||
);
|
||||
|
||||
RAISE NOTICE 'Created partition: %', partition_name;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- Step 3: Create indexes on partitions (will be inherited by all partitions)
|
||||
CREATE INDEX IF NOT EXISTS idx_activities_partitioned_child_id
|
||||
ON activities_partitioned(child_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_activities_partitioned_user_id
|
||||
ON activities_partitioned(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_activities_partitioned_activity_type
|
||||
ON activities_partitioned(activity_type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_activities_partitioned_timestamp
|
||||
ON activities_partitioned(timestamp DESC);
|
||||
|
||||
-- Step 4: Create foreign key constraints
|
||||
ALTER TABLE activities_partitioned
|
||||
ADD CONSTRAINT fk_activities_partitioned_child
|
||||
FOREIGN KEY (child_id) REFERENCES children(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE activities_partitioned
|
||||
ADD CONSTRAINT fk_activities_partitioned_user
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
-- Step 5: Migrate existing data (if any) from old activities table to partitioned table
|
||||
INSERT INTO activities_partitioned
|
||||
SELECT * FROM activities
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Step 6: Rename old table and replace with partitioned table
|
||||
ALTER TABLE activities RENAME TO activities_old;
|
||||
ALTER TABLE activities_partitioned RENAME TO activities;
|
||||
|
||||
-- Step 7: Create trigger for updated_at
|
||||
CREATE OR REPLACE FUNCTION update_activities_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_activities_updated_at
|
||||
BEFORE UPDATE ON activities
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_activities_updated_at();
|
||||
|
||||
-- Step 8: Create function to automatically create future partitions
|
||||
CREATE OR REPLACE FUNCTION create_monthly_partition()
|
||||
RETURNS void AS $$
|
||||
DECLARE
|
||||
partition_date DATE;
|
||||
partition_name TEXT;
|
||||
partition_start DATE;
|
||||
partition_end DATE;
|
||||
BEGIN
|
||||
-- Create partition for next month if it doesn't exist
|
||||
partition_date := DATE_TRUNC('month', CURRENT_DATE + INTERVAL '2 months');
|
||||
partition_name := 'activities_' || TO_CHAR(partition_date, 'YYYY_MM');
|
||||
|
||||
-- Check if partition exists
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_class WHERE relname = partition_name
|
||||
) THEN
|
||||
partition_start := partition_date;
|
||||
partition_end := partition_start + INTERVAL '1 month';
|
||||
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS %I PARTITION OF activities
|
||||
FOR VALUES FROM (%L) TO (%L)',
|
||||
partition_name,
|
||||
partition_start,
|
||||
partition_end
|
||||
);
|
||||
|
||||
RAISE NOTICE 'Auto-created partition: %', partition_name;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 9: Create scheduled job to create partitions (optional - can be done via cron or app)
|
||||
-- This would typically be called monthly by a cron job or scheduled task
|
||||
-- Example: SELECT create_monthly_partition();
|
||||
|
||||
-- Step 10: Add comments
|
||||
COMMENT ON TABLE activities IS 'Partitioned activities table - partitioned by month for performance';
|
||||
COMMENT ON FUNCTION create_monthly_partition() IS 'Auto-creates next month partition if it does not exist';
|
||||
|
||||
-- Note: To drop old activities_old table after verification:
|
||||
-- DROP TABLE IF EXISTS activities_old CASCADE;
|
||||
@@ -2,10 +2,14 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
import helmet from 'helmet';
|
||||
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// Replace default logger with Winston
|
||||
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
|
||||
|
||||
// Security headers with Helmet
|
||||
app.use(
|
||||
helmet({
|
||||
|
||||
Reference in New Issue
Block a user