From 7395157e54fef17e028d5886c2364bf2e07cd1e3 Mon Sep 17 00:00:00 2001 From: Andrei Date: Fri, 3 Oct 2025 21:27:49 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Sprint=201=20Complete=20-=20Security,?= =?UTF-8?q?=20Logging=20&=20Performance=20=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../maternal-app-backend/package-lock.json | 243 +++++++++++++++++- .../maternal-app-backend/package.json | 4 +- .../maternal-app-backend/src/app.module.ts | 3 + .../src/common/logger/winston.config.ts | 145 +++++++++++ .../V009_create_activity_partitions.sql | 139 ++++++++++ maternal-app/maternal-app-backend/src/main.ts | 4 + 6 files changed, 535 insertions(+), 3 deletions(-) create mode 100644 maternal-app/maternal-app-backend/src/common/logger/winston.config.ts create mode 100644 maternal-app/maternal-app-backend/src/database/migrations/V009_create_activity_partitions.sql diff --git a/maternal-app/maternal-app-backend/package-lock.json b/maternal-app/maternal-app-backend/package-lock.json index 18449ca..7b5a064 100644 --- a/maternal-app/maternal-app-backend/package-lock.json +++ b/maternal-app/maternal-app-backend/package-lock.json @@ -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", diff --git a/maternal-app/maternal-app-backend/package.json b/maternal-app/maternal-app-backend/package.json index fc8ce2f..63c60ef 100644 --- a/maternal-app/maternal-app-backend/package.json +++ b/maternal-app/maternal-app-backend/package.json @@ -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", diff --git a/maternal-app/maternal-app-backend/src/app.module.ts b/maternal-app/maternal-app-backend/src/app.module.ts index f2590f9..0b1b8ea 100644 --- a/maternal-app/maternal-app-backend/src/app.module.ts +++ b/maternal-app/maternal-app-backend/src/app.module.ts @@ -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({ driver: ApolloDriver, inject: [ConfigService], diff --git a/maternal-app/maternal-app-backend/src/common/logger/winston.config.ts b/maternal-app/maternal-app-backend/src/common/logger/winston.config.ts new file mode 100644 index 0000000..df0a609 --- /dev/null +++ b/maternal-app/maternal-app-backend/src/common/logger/winston.config.ts @@ -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, + // }) + // ); +} diff --git a/maternal-app/maternal-app-backend/src/database/migrations/V009_create_activity_partitions.sql b/maternal-app/maternal-app-backend/src/database/migrations/V009_create_activity_partitions.sql new file mode 100644 index 0000000..382251a --- /dev/null +++ b/maternal-app/maternal-app-backend/src/database/migrations/V009_create_activity_partitions.sql @@ -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; diff --git a/maternal-app/maternal-app-backend/src/main.ts b/maternal-app/maternal-app-backend/src/main.ts index 4477257..1e74014 100644 --- a/maternal-app/maternal-app-backend/src/main.ts +++ b/maternal-app/maternal-app-backend/src/main.ts @@ -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({