diff --git a/.cursor/plans/iam_service_audit_plan_d8aad26f.plan.md b/.cursor/plans/iam_service_audit_plan_d8aad26f.plan.md index 64ec8b77..21cb0f54 100644 --- a/.cursor/plans/iam_service_audit_plan_d8aad26f.plan.md +++ b/.cursor/plans/iam_service_audit_plan_d8aad26f.plan.md @@ -2,116 +2,114 @@ name: IAM Service Audit Plan overview: "Kế hoạch kiểm tra toàn diện cho IAM Service bao gồm: logic nghiệp vụ, quy trình build, bảo mật, và triển khai môi trường dev/staging/production. Plan được chia thành 6 phases: Pre-deployment Audit, Security Fixes, Local Environment, Staging Deployment, Production Deployment, và Post-deployment." todos: - # Phase 1: Pre-deployment Audit - Business Logic Review - id: audit-auth-1 content: "Review Authentication Module - Registration Flow: Check email validation, password hashing with bcrypt cost 12, user profile creation in services/iam-service/src/modules/auth/auth.service.ts" - status: pending + status: completed - id: audit-auth-2 content: "Review Authentication Module - Login Flow: Check password verification, JWT generation (access + refresh), session creation, MFA integration in services/iam-service/src/modules/auth/auth.service.ts" - status: pending + status: completed - id: audit-auth-3 content: "Review Authentication Module - Token Refresh: Check token family tracking, refresh token rotation, replay attack detection in services/iam-service/src/modules/token/jwt.service.ts" - status: pending + status: completed - id: audit-auth-4 content: "Review Authentication Module - Password Change: Check refresh token revocation, audit logging in services/iam-service/src/modules/auth/change-password.service.ts" - status: pending + status: completed - id: audit-rbac-1 content: "Review RBAC Module - Permission Resolution: Check hierarchy (Direct user → Role → Group → Policy) in services/iam-service/src/modules/rbac/rbac.service.ts" - status: pending + status: completed - id: audit-rbac-2 content: "Review RBAC Module - Role Assignment: Check expiration handling in services/iam-service/src/modules/rbac/rbac.service.ts" - status: pending + status: completed - id: audit-rbac-3 content: "Review RBAC Module - Policy Engine: Check JSON Logic implementation in services/iam-service/src/modules/rbac/policy.engine.ts" - status: pending + status: completed - id: audit-rbac-4 content: "Review RBAC Module - Permission Caching: Verify 5 min TTL and cache invalidation in services/iam-service/src/core/cache/cache.service.ts" - status: pending + status: completed - id: audit-identity-1 content: "Review Identity Module - User Management: Check CRUD operations, bulk import/export in services/iam-service/src/modules/identity/user/user.service.ts" - status: pending + status: completed - id: audit-identity-2 content: "Review Identity Module - Profile Management: Check custom fields, avatar upload/delete in services/iam-service/src/modules/identity/profile/profile.service.ts" - status: pending + status: completed - id: audit-identity-3 content: "Review Identity Module - Verification: Check email/phone/document verification flows in services/iam-service/src/modules/identity/verification/verification.service.ts" - status: pending + status: completed - id: audit-identity-4 content: "Review Identity Module - Organizations: Check multi-tenant support, hierarchical structure in services/iam-service/src/modules/identity/organization/organization.service.ts" - status: pending + status: completed - id: audit-identity-5 content: "Review Identity Module - Groups: Check member management, group-based permissions in services/iam-service/src/modules/identity/group/group.service.ts" - status: pending + status: completed - id: audit-access-1 content: "Review Access Module - Access Requests: Check workflow, approval chains, JIT access in services/iam-service/src/modules/access/request/request.service.ts" - status: pending + status: completed - id: audit-access-2 content: "Review Access Module - Access Reviews: Check certification campaigns, automated cleanup in services/iam-service/src/modules/access/review/review.service.ts" - status: pending + status: completed - id: audit-access-3 content: "Review Access Module - Access Analytics: Check usage tracking, risk identification in services/iam-service/src/modules/access/analytics/analytics.service.ts" - status: pending + status: completed - id: audit-mfa-1 content: "Review MFA Module - TOTP: Check TOTP implementation using speakeasy library in services/iam-service/src/modules/mfa/mfa.service.ts" - status: pending + status: completed - id: audit-mfa-2 content: "Review MFA Module - QR Code: Check QR code generation in services/iam-service/src/modules/mfa/mfa.service.ts" - status: pending + status: completed - id: audit-mfa-3 content: "Review MFA Module - WebAuthn: Check WebAuthn support in services/iam-service/src/modules/mfa/mfa.service.ts" - status: pending + status: completed - id: audit-mfa-4 content: "Review MFA Module - Multiple Devices: Check multiple devices per user support in services/iam-service/src/modules/mfa/mfa.service.ts" - status: pending + status: completed - id: audit-mfa-5 content: "Review MFA Module - Recovery Flow: Verify MFA recovery flow exists (NOTE: Currently missing, needs review)" - status: pending + status: completed - id: audit-social-1 content: "Review Social Authentication Module: Check Google/Facebook/GitHub OAuth flows, account linking, token refresh in services/iam-service/src/modules/social/social.service.ts" - status: pending + status: completed - id: audit-oidc-1 content: "Review OIDC Provider Module: Check discovery endpoint, authorization code flow, token exchange, JWKS endpoint in services/iam-service/src/modules/oidc/oidc-provider.service.ts" - status: pending + status: completed - id: audit-session-1 content: "Review Session Management Module: Check device fingerprinting, session expiration, revocation, activity tracking in services/iam-service/src/modules/session/session.service.ts" - status: pending + status: completed - id: audit-governance-1 content: "Review Governance Module: Check compliance reporting (GDPR, SOC2, ISO27001), policy management, risk scoring in services/iam-service/src/modules/governance/" - status: pending + status: completed - id: audit-cache-1 content: "Review Cache Service: Check multi-layer caching (L1: Memory, L2: Redis, L3: DB), cache warming, invalidation in services/iam-service/src/core/cache/cache.service.ts" - status: pending + status: completed - id: audit-events-1 content: "Review Event Sourcing: Check audit logging for all security events, 7-year retention in services/iam-service/src/core/events/" - status: pending - # Phase 1: Pre-deployment Audit - Build & Error Checking + status: completed - id: audit-build-1 content: "Run TypeScript typecheck: cd services/iam-service && pnpm typecheck - Verify no TypeScript errors" - status: pending + status: completed - id: audit-build-2 content: "Run TypeScript build: cd services/iam-service && pnpm build - Verify build succeeds, check for unused variables/imports" - status: pending + status: completed - id: audit-build-3 content: "Verify Type Safety: Check type safety for Prisma models, verify path aliases (@/*) working correctly" - status: pending + status: completed - id: audit-lint-1 content: "Run ESLint: cd services/iam-service && pnpm lint - Verify coding standards compliance" - status: pending + status: completed - id: audit-lint-2 content: "Check Code Quality: Verify no console.log in production code, proper error handling, no security anti-patterns" - status: pending + status: completed - id: audit-prisma-1 content: "Generate Prisma Client: cd services/iam-service && pnpm prisma:generate - Verify generation succeeds" - status: pending + status: completed - id: audit-prisma-2 content: "Validate Prisma Schema: Verify schema syntax valid, all relations properly defined, indexes optimized, migration files consistent" - status: pending + status: completed - id: audit-test-1 content: "Run Unit Tests: cd services/iam-service && pnpm test:unit - Verify all unit tests pass" - status: pending + status: completed - id: audit-test-2 content: "Run E2E Tests: cd services/iam-service && pnpm test:e2e - Verify all E2E tests pass" - status: pending + status: in_progress - id: audit-test-3 content: "Generate Test Coverage: cd services/iam-service && pnpm test:coverage - Verify coverage >= 70% (branches, functions, lines, statements)" status: pending @@ -121,7 +119,6 @@ todos: - id: audit-docker-2 content: "Verify Docker Image: Check image size <500MB, non-root user configured, health check functional" status: pending - # Phase 2: Security Fixes - CRITICAL - id: security-mfa-1 content: "CRITICAL: Create Encryption Service - Create services/iam-service/src/core/security/encryption.service.ts with encrypt/decrypt functions using crypto module" status: pending @@ -140,7 +137,6 @@ todos: - id: security-jwt-1 content: "CRITICAL: Block Default JWT Secrets - Update services/iam-service/src/config/jwt.config.ts to throw error if default secrets are used when NODE_ENV === 'production'" status: pending - # Phase 2: Security Fixes - MEDIUM - id: security-input-1 content: "MEDIUM: Install DOMPurify: cd services/iam-service && pnpm add dompurify @types/dompurify" status: pending @@ -171,7 +167,6 @@ todos: - id: security-lockout-4 content: "MEDIUM: Create Lockout Migration - Create Prisma migration for failedLoginAttempts and lockedUntil fields" status: pending - # Phase 2: Security Fixes - LOW - id: security-audit-1 content: "LOW: Run npm audit: cd services/iam-service && npm audit - Review vulnerabilities" status: pending @@ -187,7 +182,6 @@ todos: - id: security-backup-1 content: "LOW: Design MFA Backup Codes - Design backup codes generation and storage strategy for MFA recovery scenarios" status: pending - # Phase 3: Local Environment - id: local-env-1 content: "Copy Environment File: cp deployments/local/env.local.example deployments/local/.env.local" status: pending @@ -257,7 +251,6 @@ todos: - id: local-test-7 content: "Review Logs and Metrics: Check application logs and Prometheus metrics for errors" status: pending - # Phase 4: Staging Deployment - id: staging-k8s-1 content: "Create Staging Namespace: kubectl create namespace staging" status: pending @@ -300,7 +293,6 @@ todos: - id: staging-test-5 content: "Verify Staging Health Endpoints: Test /health/live and /health/ready endpoints on staging" status: pending - # Phase 5: Production Deployment - id: prod-check-1 content: "Pre-production: Verify security audit passed - Review all security fixes are implemented" status: pending @@ -367,7 +359,6 @@ todos: - id: prod-security-8 content: "Production Security: Verify backup strategy in place - Confirm database backup strategy is implemented" status: pending - # Phase 6: Post-deployment - id: post-monitor-1 content: "Monitor Error Rates: Check error rates in monitoring dashboard, verify errors are within acceptable range" status: pending diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fe0a51b..13727bf8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -527,6 +527,12 @@ importers: '@simplewebauthn/server': specifier: ^9.0.0 version: 9.0.3 + '@types/dompurify': + specifier: ^3.2.0 + version: 3.2.0 + '@types/jsdom': + specifier: ^27.0.0 + version: 27.0.0 bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -536,6 +542,9 @@ importers: cors: specifier: ^2.8.5 version: 2.8.5 + dompurify: + specifier: ^3.3.1 + version: 3.3.1 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -551,6 +560,9 @@ importers: ioredis: specifier: ^5.3.2 version: 5.8.2 + jsdom: + specifier: ^27.4.0 + version: 27.4.0 jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 @@ -699,6 +711,10 @@ importers: packages: + /@acemir/cssom@0.9.30: + resolution: {integrity: sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg==} + dev: false + /@adobe/css-tools@4.4.4: resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} dev: true @@ -740,6 +756,30 @@ packages: z-schema: 5.0.5 dev: false + /@asamuzakjp/css-color@4.1.1: + resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.4 + dev: false + + /@asamuzakjp/dom-selector@6.7.6: + resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.4 + dev: false + + /@asamuzakjp/nwsapi@2.3.9: + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + dev: false + /@babel/code-frame@7.27.1: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1081,6 +1121,54 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true + /@csstools/color-helpers@5.1.0: + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + dev: false + + /@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4): + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + dev: false + + /@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4): + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5)(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + dev: false + + /@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4): + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + dependencies: + '@csstools/css-tokenizer': 3.0.4 + dev: false + + /@csstools/css-syntax-patches-for-csstree@1.0.22: + resolution: {integrity: sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==} + engines: {node: '>=18'} + dev: false + + /@csstools/css-tokenizer@3.0.4: + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + dev: false + /@dabh/diagnostics@2.0.8: resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} dependencies: @@ -1378,6 +1466,16 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@exodus/bytes@1.8.0: + resolution: {integrity: sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@exodus/crypto': ^1.0.0-rc.4 + peerDependenciesMeta: + '@exodus/crypto': + optional: true + dev: false + /@floating-ui/core@1.7.3: resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} dependencies: @@ -5257,6 +5355,13 @@ packages: resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} dev: true + /@types/dompurify@3.2.0: + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + dependencies: + dompurify: 3.3.1 + dev: false + /@types/dotenv@8.2.3: resolution: {integrity: sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw==} deprecated: This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed. @@ -5332,6 +5437,14 @@ packages: pretty-format: 29.7.0 dev: true + /@types/jsdom@27.0.0: + resolution: {integrity: sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==} + dependencies: + '@types/node': 20.19.27 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + dev: false + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -5554,10 +5667,20 @@ packages: '@types/node': 20.19.27 dev: false + /@types/tough-cookie@4.0.5: + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + dev: false + /@types/triple-beam@1.3.5: resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} dev: false + /@types/trusted-types@2.0.7: + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + requiresBuild: true + dev: false + optional: true + /@types/uuid@9.0.8: resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} dev: true @@ -6486,6 +6609,12 @@ packages: resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} dev: false + /bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + dependencies: + require-from-string: 2.0.2 + dev: false + /bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} dev: false @@ -6916,10 +7045,28 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + dev: false + /css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} dev: true + /cssstyle@5.3.6: + resolution: {integrity: sha512-legscpSpgSAeGEe0TNcai97DKt9Vd9AsAdOL7Uoetb52Ar/8eJm3LIa39qpv8wWzLFlNG4vVvppQM+teaMPj3A==} + engines: {node: '>=20'} + dependencies: + '@asamuzakjp/css-color': 4.1.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.22 + css-tree: 3.1.0 + lru-cache: 11.2.4 + dev: false + /csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -6998,6 +7145,14 @@ packages: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: true + /data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + dev: false + /data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -7245,6 +7400,12 @@ packages: csstype: 3.2.3 dev: false + /dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + optionalDependencies: + '@types/trusted-types': 2.0.7 + dev: false + /dotenv@17.2.3: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} @@ -7313,6 +7474,11 @@ packages: tapable: 2.3.0 dev: true + /entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + dev: false + /error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} dependencies: @@ -8404,6 +8570,15 @@ packages: xtend: 4.0.2 dev: false + /html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + dependencies: + '@exodus/bytes': 1.8.0 + transitivePeerDependencies: + - '@exodus/crypto' + dev: false + /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true @@ -8442,6 +8617,16 @@ packages: toidentifier: 1.0.1 dev: false + /http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + dev: false + /http2-wrapper@2.2.1: resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} engines: {node: '>=10.19.0'} @@ -8722,6 +8907,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: false + /is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -9350,6 +9539,42 @@ packages: dependencies: argparse: 2.0.1 + /jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@acemir/cssom': 0.9.30 + '@asamuzakjp/dom-selector': 6.7.6 + '@exodus/bytes': 1.8.0 + cssstyle: 5.3.6 + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.0 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@exodus/crypto' + - bufferutil + - supports-color + - utf-8-validate + dev: false + /jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -9762,7 +9987,6 @@ packages: /lru-cache@11.2.4: resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} - dev: true /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -9824,6 +10048,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + /mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + dev: false + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -10330,6 +10558,18 @@ packages: lines-and-columns: 1.2.4 dev: true + /parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + dependencies: + entities: 6.0.1 + dev: false + + /parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + dependencies: + entities: 6.0.1 + dev: false + /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -10971,6 +11211,11 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: false + /require-in-the-middle@7.5.2: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} @@ -11140,6 +11385,13 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: false + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: false + /scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} dependencies: @@ -11667,6 +11919,10 @@ packages: swagger-ui-dist: 5.31.0 dev: false + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: false + /tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} dev: true @@ -11742,6 +11998,17 @@ packages: engines: {node: '>=14.0.0'} dev: true + /tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + dev: false + + /tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + dependencies: + tldts-core: 7.0.19 + dev: false + /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: true @@ -11762,10 +12029,24 @@ packages: engines: {node: '>=6'} dev: true + /tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + dependencies: + tldts: 7.0.19 + dev: false + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false + /tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + dependencies: + punycode: 2.3.1 + dev: false + /triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} @@ -12407,6 +12688,13 @@ packages: - yaml dev: true + /w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + dependencies: + xml-name-validator: 5.0.0 + dev: false + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -12417,10 +12705,28 @@ packages: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false + /webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} + dev: false + /webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} dev: true + /whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + dev: false + + /whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.0 + dev: false + /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: @@ -12579,7 +12885,6 @@ packages: optional: true utf-8-validate: optional: true - dev: true /wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} @@ -12588,6 +12893,15 @@ packages: is-wsl: 3.1.0 dev: true + /xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + dev: false + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + /xorshift@1.2.0: resolution: {integrity: sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g==} dev: false diff --git a/services/iam-service/.eslintrc.js b/services/iam-service/.eslintrc.js new file mode 100644 index 00000000..dff925a4 --- /dev/null +++ b/services/iam-service/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ['@goodgo/eslint-config'], + root: true, +}; \ No newline at end of file diff --git a/services/iam-service/Dockerfile b/services/iam-service/Dockerfile index f0c6b2c4..45e927af 100644 --- a/services/iam-service/Dockerfile +++ b/services/iam-service/Dockerfile @@ -29,15 +29,17 @@ USER node # EN: Enable corepack for pnpm # VI: Enable corepack cho pnpm +USER root RUN corepack enable pnpm +USER node # EN: Copy package files # VI: Copy package files -COPY --chown=node:node package.json pnpm-lock.yaml* ./ +COPY --chown=node:node services/iam-service/package.json services/iam-service/pnpm-lock.yaml* ./ # EN: Install dependencies only (no dev dependencies for smaller image) # VI: Install dependencies only (không có dev dependencies để image nhỏ hơn) -RUN pnpm install --frozen-lockfile --prod=false && pnpm store prune +RUN pnpm install --prod=false && pnpm store prune # EN: Builder stage - compile TypeScript and generate Prisma client # VI: Builder stage - compile TypeScript và generate Prisma client @@ -48,7 +50,9 @@ USER node # EN: Enable corepack # VI: Enable corepack +USER root RUN corepack enable pnpm +USER node # EN: Copy dependencies from deps stage # VI: Copy dependencies từ deps stage @@ -56,13 +60,17 @@ COPY --from=deps --chown=node:node /app/node_modules ./node_modules # EN: Copy source code # VI: Copy source code -COPY --chown=node:node . . +COPY --chown=node:node services/iam-service/ . # EN: Build application # VI: Build application -RUN pnpm prisma generate && \ - pnpm build && \ - pnpm prune --prod +RUN npx prisma generate && \ + pnpm build +# EN: Prune dev dependencies after build +# VI: Prune dev dependencies sau khi build +USER root +RUN pnpm prune --prod +USER node # EN: Production stage - minimal runtime image # VI: Production stage - minimal runtime image diff --git a/services/iam-service/env.local.example b/services/iam-service/env.local.example index bc5d6802..9aa2e1a5 100644 --- a/services/iam-service/env.local.example +++ b/services/iam-service/env.local.example @@ -47,3 +47,6 @@ CORS_ORIGIN=http://localhost:3000 # Tracing TRACING_ENABLED=false JAEGER_ENDPOINT= + +# Encryption +ENCRYPTION_KEY=your-32-character-encryption-key-change-in-production diff --git a/services/iam-service/package.json b/services/iam-service/package.json index 9c33a577..565654ca 100644 --- a/services/iam-service/package.json +++ b/services/iam-service/package.json @@ -26,58 +26,62 @@ "@goodgo/tracing": "workspace:*", "@goodgo/types": "workspace:*", "@prisma/client": "^5.9.1", + "@simplewebauthn/server": "^9.0.0", + "@types/dompurify": "^3.2.0", + "@types/jsdom": "^27.0.0", + "bcryptjs": "^2.4.3", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "dompurify": "^3.3.1", "dotenv": "^17.2.3", "express": "^4.18.2", "express-rate-limit": "^7.1.5", "helmet": "^7.1.0", "ioredis": "^5.3.2", - "opossum": "^9.0.0", - "prom-client": "^15.1.3", - "rate-limit-redis": "^4.3.1", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", - "zod": "^3.22.4", - "bcryptjs": "^2.4.3", + "jsdom": "^27.4.0", "jsonwebtoken": "^9.0.2", - "passport": "^0.7.0", - "passport-google-oauth20": "^2.0.0", - "passport-facebook": "^3.0.0", - "passport-github2": "^0.1.12", - "speakeasy": "^2.0.0", - "@simplewebauthn/server": "^9.0.0", + "node-cache": "^5.1.2", "oidc-provider": "^8.0.0", "openid-client": "^5.6.5", - "node-cache": "^5.1.2", - "uuid": "^9.0.1", + "opossum": "^9.0.0", + "passport": "^0.7.0", + "passport-facebook": "^3.0.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", + "prom-client": "^15.1.3", "qrcode": "^1.5.3", - "cookie-parser": "^1.4.6" + "rate-limit-redis": "^4.3.1", + "speakeasy": "^2.0.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "uuid": "^9.0.1", + "zod": "^3.22.4" }, "devDependencies": { "@goodgo/eslint-config": "workspace:*", "@goodgo/tsconfig": "workspace:*", + "@jest/globals": "^29.7.0", + "@types/bcryptjs": "^2.4.6", + "@types/cookie-parser": "^1.4.6", "@types/cors": "^2.8.17", "@types/dotenv": "^8.2.3", "@types/express": "^4.17.21", "@types/ioredis": "^5.0.0", "@types/jest": "^29.5.11", - "@jest/globals": "^29.7.0", + "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.11.0", + "@types/node-cache": "^4.2.5", "@types/opossum": "^8.1.9", + "@types/passport": "^1.0.16", + "@types/passport-facebook": "^3.0.4", + "@types/passport-github2": "^1.2.9", + "@types/passport-google-oauth20": "^2.0.14", + "@types/qrcode": "^1.5.5", + "@types/speakeasy": "^2.0.10", "@types/supertest": "^6.0.2", "@types/swagger-jsdoc": "^6.0.1", "@types/swagger-ui-express": "^4.1.6", - "@types/bcryptjs": "^2.4.6", - "@types/jsonwebtoken": "^9.0.5", - "@types/passport": "^1.0.16", - "@types/passport-google-oauth20": "^2.0.14", - "@types/passport-facebook": "^3.0.4", - "@types/passport-github2": "^1.2.9", - "@types/speakeasy": "^2.0.10", "@types/uuid": "^9.0.7", - "@types/node-cache": "^4.2.5", - "@types/qrcode": "^1.5.5", - "@types/cookie-parser": "^1.4.6", "jest": "^29.7.0", "prisma": "^5.9.1", "supertest": "^7.0.0", @@ -85,5 +89,11 @@ "ts-node": "^10.9.2", "tsx": "^4.7.1", "typescript": "^5.3.3" + }, + "pnpm": { + "overrides": { + "glob@>=10.2.0 <10.5.0": ">=10.5.0", + "qs@<6.14.1": ">=6.14.1" + } } } diff --git a/services/iam-service/prisma/migrations/20241201000000_add_account_lockout_fields/migration.sql b/services/iam-service/prisma/migrations/20241201000000_add_account_lockout_fields/migration.sql new file mode 100644 index 00000000..be5fbc24 --- /dev/null +++ b/services/iam-service/prisma/migrations/20241201000000_add_account_lockout_fields/migration.sql @@ -0,0 +1,14 @@ +-- Add account lockout fields to users table +-- Migration: add_account_lockout_fields + +-- Add failedLoginAttempts column with default value 0 +ALTER TABLE "users" ADD COLUMN "failedLoginAttempts" INTEGER NOT NULL DEFAULT 0; + +-- Add lockedUntil column for account lockout timestamp +ALTER TABLE "users" ADD COLUMN "lockedUntil" TIMESTAMP(3); + +-- Add index on failedLoginAttempts for potential queries +CREATE INDEX "users_failedLoginAttempts_idx" ON "users"("failedLoginAttempts"); + +-- Add index on lockedUntil for cleanup queries +CREATE INDEX "users_lockedUntil_idx" ON "users"("lockedUntil"); \ No newline at end of file diff --git a/services/iam-service/prisma/migrations/20241201000001_add_mfa_backup_codes/migration.sql b/services/iam-service/prisma/migrations/20241201000001_add_mfa_backup_codes/migration.sql new file mode 100644 index 00000000..1120102e --- /dev/null +++ b/services/iam-service/prisma/migrations/20241201000001_add_mfa_backup_codes/migration.sql @@ -0,0 +1,5 @@ +-- Add MFA backup codes field to users table +-- Migration: add_mfa_backup_codes + +-- Add mfaBackupCodes column to store encrypted backup codes +ALTER TABLE "users" ADD COLUMN "mfaBackupCodes" TEXT; \ No newline at end of file diff --git a/services/iam-service/prisma/schema.prisma b/services/iam-service/prisma/schema.prisma index 08e0971c..5ad016e8 100644 --- a/services/iam-service/prisma/schema.prisma +++ b/services/iam-service/prisma/schema.prisma @@ -13,37 +13,40 @@ datasource db { // EN: User model - Core user entity // VI: Model User - Entity người dùng cốt lõi model User { - id String @id @default(cuid()) - email String @unique - username String? @unique - passwordHash String? // Nullable for social-only users - isActive Boolean @default(true) - emailVerified Boolean @default(false) - mfaEnabled Boolean @default(false) - mfaSecret String? - lastLoginAt DateTime? - loginCount Int @default(0) - organizationId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - + id String @id @default(cuid()) + email String @unique + username String? @unique + passwordHash String? // Nullable for social-only users + isActive Boolean @default(true) + emailVerified Boolean @default(false) + mfaEnabled Boolean @default(false) + mfaSecret String? + mfaBackupCodes String? @db.Text // Encrypted JSON array of backup codes + lastLoginAt DateTime? + loginCount Int @default(0) + failedLoginAttempts Int @default(0) // Track failed login attempts for lockout + lockedUntil DateTime? // Account lockout timestamp + organizationId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // Existing Relations - userRoles UserRole[] + userRoles UserRole[] userPermissions UserPermission[] - sessions Session[] - refreshTokens RefreshToken[] - socialAccounts SocialAccount[] - mfaDevices MFADevice[] - authEvents AuthEvent[] - + sessions Session[] + refreshTokens RefreshToken[] + socialAccounts SocialAccount[] + mfaDevices MFADevice[] + authEvents AuthEvent[] + // New IAM Relations - organization Organization? @relation(fields: [organizationId], references: [id]) + organization Organization? @relation(fields: [organizationId], references: [id]) profile UserProfile? verifications IdentityVerification[] accessRequests AccessRequest[] riskScores RiskScore[] groupMembers GroupMember[] - + @@index([email]) @@index([username]) @@index([createdAt]) @@ -61,11 +64,11 @@ model Role { isSystem Boolean @default(false) // Cannot be deleted createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - + // Relations permissions RolePermission[] userRoles UserRole[] - + @@map("roles") } @@ -73,35 +76,36 @@ model Role { // VI: Model Permission - Quyền chi tiết model Permission { id String @id @default(cuid()) - resource String // users, products, orders, etc. - action String // create, read, update, delete, etc. - scope String? // own, team, all + resource String // users, products, orders, etc. + action String // create, read, update, delete, etc. + scope String? // own, team, all description String? createdAt DateTime @default(now()) - + // Relations - roles RolePermission[] - users UserPermission[] - groups GroupPermission[] - + roles RolePermission[] + users UserPermission[] + groups GroupPermission[] + @@unique([resource, action, scope]) @@index([resource]) + @@index([resource, action]) @@map("permissions") } // EN: UserRole - Many-to-many relationship with expiration // VI: UserRole - Quan hệ nhiều-nhiều với thời hạn model UserRole { - id String @id @default(cuid()) + id String @id @default(cuid()) userId String roleId String - grantedBy String? // ID of user who granted this role + grantedBy String? // ID of user who granted this role expiresAt DateTime? // Temporary role assignment - createdAt DateTime @default(now()) - + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) - + @@unique([userId, roleId]) @@index([userId]) @@index([roleId]) @@ -116,10 +120,10 @@ model RolePermission { roleId String permissionId String createdAt DateTime @default(now()) - + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade) - + @@unique([roleId, permissionId]) @@index([roleId]) @@map("role_permissions") @@ -135,10 +139,10 @@ model UserPermission { grantedBy String? expiresAt DateTime? createdAt DateTime @default(now()) - + user User @relation(fields: [userId], references: [id], onDelete: Cascade) permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade) - + @@unique([userId, permissionId]) @@index([userId]) @@index([expiresAt]) @@ -148,20 +152,20 @@ model UserPermission { // EN: Session model - Active user sessions // VI: Model Session - Phiên người dùng đang hoạt động model Session { - id String @id @default(cuid()) - userId String - deviceId String - deviceName String? - ipAddress String - userAgent String? - fingerprint String? // Device fingerprint - isActive Boolean @default(true) - expiresAt DateTime - createdAt DateTime @default(now()) + id String @id @default(cuid()) + userId String + deviceId String + deviceName String? + ipAddress String + userAgent String? + fingerprint String? // Device fingerprint + isActive Boolean @default(true) + expiresAt DateTime + createdAt DateTime @default(now()) lastActivityAt DateTime @default(now()) - + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - + @@index([userId]) @@index([deviceId]) @@index([expiresAt]) @@ -172,19 +176,19 @@ model Session { // EN: RefreshToken model - Long-lived refresh tokens // VI: Model RefreshToken - Refresh token sống lâu model RefreshToken { - id String @id @default(cuid()) - userId String - token String @unique - family String? // Token family for rotation - deviceId String? - ipAddress String? - userAgent String? - expiresAt DateTime - revokedAt DateTime? - createdAt DateTime @default(now()) - + id String @id @default(cuid()) + userId String + token String @unique + family String? // Token family for rotation + deviceId String? + ipAddress String? + userAgent String? + expiresAt DateTime + revokedAt DateTime? + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - + @@index([userId]) @@index([token]) @@index([family]) @@ -195,21 +199,21 @@ model RefreshToken { // EN: SocialAccount model - Social login accounts // VI: Model SocialAccount - Tài khoản đăng nhập mạng xã hội model SocialAccount { - id String @id @default(cuid()) + id String @id @default(cuid()) userId String provider Provider // GOOGLE, FACEBOOK, GITHUB, APPLE - providerId String // ID from provider + providerId String // ID from provider email String? name String? avatar String? - accessToken String? @db.Text - refreshToken String? @db.Text + accessToken String? @db.Text + refreshToken String? @db.Text expiresAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - + @@unique([provider, providerId]) @@index([userId]) @@index([provider]) @@ -227,20 +231,20 @@ enum Provider { // EN: MFADevice model - Multi-factor authentication devices // VI: Model MFADevice - Thiết bị xác thực đa yếu tố model MFADevice { - id String @id @default(cuid()) - userId String - type MFAType // TOTP, WEBAUTHN - name String // Device name - secret String? // For TOTP + id String @id @default(cuid()) + userId String + type MFAType // TOTP, WEBAUTHN + name String // Device name + secret String? // For TOTP credentialId String? // For WebAuthn - publicKey String? @db.Text // For WebAuthn - counter Int? // For WebAuthn - isActive Boolean @default(true) - lastUsedAt DateTime? - createdAt DateTime @default(now()) - + publicKey String? @db.Text // For WebAuthn + counter Int? // For WebAuthn + isActive Boolean @default(true) + lastUsedAt DateTime? + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - + @@index([userId]) @@index([credentialId]) @@map("mfa_devices") @@ -254,40 +258,59 @@ enum MFAType { // EN: AuthEvent model - Event sourcing for audit logs // VI: Model AuthEvent - Event sourcing cho audit logs model AuthEvent { - id String @id @default(cuid()) - userId String? - eventType String // LOGIN, LOGOUT, MFA_ENABLED, PERMISSION_CHANGED, etc. - eventData Json - ipAddress String? - userAgent String? - success Boolean @default(true) - errorMessage String? @db.Text - timestamp DateTime @default(now()) - + id String @id @default(cuid()) + userId String? + eventType String // LOGIN, LOGOUT, MFA_ENABLED, PERMISSION_CHANGED, etc. + eventData Json + ipAddress String? + userAgent String? + success Boolean @default(true) + errorMessage String? @db.Text + timestamp DateTime @default(now()) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - + @@index([userId, timestamp]) @@index([eventType, timestamp]) @@index([timestamp]) @@map("auth_events") } +// EN: Feature model - Feature flags and toggles +// VI: Model Feature - Feature flags và toggles +model Feature { + id String @id @default(cuid()) + name String @unique // Feature identifier + title String? // Human-readable title + description String? @db.Text // Feature description + config Json? // Feature configuration + enabled Boolean @default(true) // Feature toggle + version String @default("1.0.0") // Feature version + tags Json? // Feature tags for categorization + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([name]) + @@index([enabled]) + @@map("features") +} + // EN: Policy model - ABAC policies // VI: Model Policy - Chính sách ABAC model Policy { - id String @id @default(cuid()) - name String @unique - description String? - resource String // Resource this policy applies to - condition Json // Policy condition (JSON logic) - effect String // ALLOW, DENY - priority Int @default(0) // Higher priority evaluated first - isActive Boolean @default(true) + id String @id @default(cuid()) + name String @unique + description String? + resource String // Resource this policy applies to + condition Json // Policy condition (JSON logic) + effect String // ALLOW, DENY + priority Int @default(0) // Higher priority evaluated first + isActive Boolean @default(true) organizationId String? - organization Organization? @relation(fields: [organizationId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - + organization Organization? @relation(fields: [organizationId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@index([resource]) @@index([isActive, priority]) @@index([organizationId]) @@ -302,21 +325,21 @@ model Policy { // EN: Organization model - Multi-tenant organization support // VI: Model Organization - Hỗ trợ tổ chức multi-tenant model Organization { - id String @id @default(cuid()) - name String - domain String? @unique - parentId String? - settings Json? - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - users User[] - groups Group[] - policies Policy[] - parent Organization? @relation("OrganizationHierarchy", fields: [parentId], references: [id]) - children Organization[] @relation("OrganizationHierarchy") - + id String @id @default(cuid()) + name String + domain String? @unique + parentId String? + settings Json? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + users User[] + groups Group[] + policies Policy[] + parent Organization? @relation("OrganizationHierarchy", fields: [parentId], references: [id]) + children Organization[] @relation("OrganizationHierarchy") + @@index([domain]) @@map("organizations") } @@ -331,11 +354,11 @@ model Group { isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - - organization Organization? @relation(fields: [organizationId], references: [id]) - members GroupMember[] - permissions GroupPermission[] - + + organization Organization? @relation(fields: [organizationId], references: [id]) + members GroupMember[] + permissions GroupPermission[] + @@unique([organizationId, name]) @@index([organizationId]) @@map("groups") @@ -344,16 +367,16 @@ model Group { // EN: GroupMember model - Members of a group // VI: Model GroupMember - Thành viên của nhóm model GroupMember { - id String @id @default(cuid()) + id String @id @default(cuid()) userId String groupId String - role String @default("member") // member, admin - joinedAt DateTime @default(now()) + role String @default("member") // member, admin + joinedAt DateTime @default(now()) expiresAt DateTime? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) - + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) + @@unique([userId, groupId]) @@index([userId]) @@index([groupId]) @@ -367,10 +390,10 @@ model GroupPermission { groupId String permissionId String createdAt DateTime @default(now()) - - group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) - permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade) - + + group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) + permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade) + @@unique([groupId, permissionId]) @@index([groupId]) @@map("group_permissions") @@ -386,14 +409,14 @@ model UserProfile { phone String? phoneVerified Boolean @default(false) avatarUrl String? - customFields Json? // Extended attributes - preferences Json? // User preferences - metadata Json? // Additional metadata + customFields Json? // Extended attributes + preferences Json? // User preferences + metadata Json? // Additional metadata createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@index([phone]) @@map("user_profiles") } @@ -401,19 +424,19 @@ model UserProfile { // EN: IdentityVerification model - Identity verification records // VI: Model IdentityVerification - Hồ sơ xác thực danh tính model IdentityVerification { - id String @id @default(cuid()) - userId String - type VerificationType - status VerificationStatus @default(PENDING) - method String? - token String? @unique // OTP token, verification code - verifiedAt DateTime? - expiresAt DateTime? - metadata Json? // Verification details - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + userId String + type VerificationType + status VerificationStatus @default(PENDING) + method String? + token String? @unique // OTP token, verification code + verifiedAt DateTime? + expiresAt DateTime? + metadata Json? // Verification details + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@index([userId, type]) @@index([token]) @@index([status]) @@ -442,21 +465,21 @@ enum VerificationStatus { // EN: AccessRequest model - Access requests and approvals // VI: Model AccessRequest - Yêu cầu và phê duyệt truy cập model AccessRequest { - id String @id @default(cuid()) + id String @id @default(cuid()) userId String resource String action String reason String? status RequestStatus @default(PENDING) - requestedAt DateTime @default(now()) + requestedAt DateTime @default(now()) reviewedAt DateTime? reviewedBy String? expiresAt DateTime? metadata Json? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - approvers AccessRequestApprover[] - + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + approvers AccessRequestApprover[] + @@index([userId]) @@index([status]) @@index([resource]) @@ -474,15 +497,15 @@ enum RequestStatus { // EN: AccessRequestApprover model - Approvers for access requests // VI: Model AccessRequestApprover - Người phê duyệt yêu cầu truy cập model AccessRequestApprover { - id String @id @default(cuid()) - requestId String - approverId String - status ApprovalStatus @default(PENDING) - comments String? - reviewedAt DateTime? - - request AccessRequest @relation(fields: [requestId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + requestId String + approverId String + status ApprovalStatus @default(PENDING) + comments String? + reviewedAt DateTime? + + request AccessRequest @relation(fields: [requestId], references: [id], onDelete: Cascade) + @@unique([requestId, approverId]) @@index([approverId]) @@map("access_request_approvers") @@ -497,7 +520,7 @@ enum ApprovalStatus { // EN: AccessReview model - Periodic access reviews // VI: Model AccessReview - Đánh giá truy cập định kỳ model AccessReview { - id String @id @default(cuid()) + id String @id @default(cuid()) name String description String? type ReviewType @@ -505,11 +528,11 @@ model AccessReview { startDate DateTime endDate DateTime createdBy String - createdAt DateTime @default(now()) + createdAt DateTime @default(now()) completedAt DateTime? - - items AccessReviewItem[] - + + items AccessReviewItem[] + @@index([status]) @@index([type]) @@map("access_reviews") @@ -531,18 +554,18 @@ enum ReviewStatus { // EN: AccessReviewItem model - Individual items in an access review // VI: Model AccessReviewItem - Item riêng lẻ trong đánh giá truy cập model AccessReviewItem { - id String @id @default(cuid()) - reviewId String - userId String - resource String - access Json // Current access details - status ReviewItemStatus @default(PENDING) - reviewedBy String? - reviewedAt DateTime? - comments String? - - review AccessReview @relation(fields: [reviewId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + reviewId String + userId String + resource String + access Json // Current access details + status ReviewItemStatus @default(PENDING) + reviewedBy String? + reviewedAt DateTime? + comments String? + + review AccessReview @relation(fields: [reviewId], references: [id], onDelete: Cascade) + @@index([reviewId]) @@index([userId]) @@index([status]) @@ -564,18 +587,18 @@ enum ReviewItemStatus { // EN: ComplianceReport model - Compliance reporting // VI: Model ComplianceReport - Báo cáo tuân thủ model ComplianceReport { - id String @id @default(cuid()) + id String @id @default(cuid()) type ComplianceType name String periodStart DateTime periodEnd DateTime - status ReportStatus @default(DRAFT) + status ReportStatus @default(DRAFT) data Json? generatedAt DateTime? generatedBy String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@index([type]) @@index([status]) @@map("compliance_reports") @@ -599,16 +622,16 @@ enum ReportStatus { // EN: PolicyTemplate model - Policy templates for governance // VI: Model PolicyTemplate - Template cho policies trong quản trị model PolicyTemplate { - id String @id @default(cuid()) + id String @id @default(cuid()) name String category PolicyCategory description String? - content Json // Policy template structure - version String @default("1.0.0") - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - + content Json // Policy template structure + version String @default("1.0.0") + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@index([category]) @@index([isActive]) @@map("policy_templates") @@ -625,15 +648,15 @@ enum PolicyCategory { // EN: RiskScore model - User risk scoring // VI: Model RiskScore - Điểm rủi ro của user model RiskScore { - id String @id @default(cuid()) - userId String - score Int // 0-100 - level RiskLevel - factors Json // Risk factors - calculatedAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + userId String + score Int // 0-100 + level RiskLevel + factors Json // Risk factors + calculatedAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@index([userId]) @@index([score]) @@index([level]) diff --git a/services/iam-service/prisma/seed.ts b/services/iam-service/prisma/seed.ts index f5635b3f..db2979b2 100644 --- a/services/iam-service/prisma/seed.ts +++ b/services/iam-service/prisma/seed.ts @@ -71,22 +71,48 @@ async function main() { const createdPermissions = []; for (const perm of permissions) { - const permission = await prisma.permission.upsert({ - where: { - resource_action_scope: { + // EN: Handle nullable scope in upsert + // VI: Xử lý scope nullable trong upsert + let permission; + if (perm.scope !== null) { + // Use compound unique constraint when scope is not null + permission = await prisma.permission.upsert({ + where: { + resource_action_scope: { + resource: perm.resource, + action: perm.action, + scope: perm.scope, + }, + }, + update: {}, + create: { resource: perm.resource, action: perm.action, scope: perm.scope, + description: `${perm.action} ${perm.resource}${perm.scope ? ` (${perm.scope})` : ''}`, }, - }, - update: {}, - create: { - resource: perm.resource, - action: perm.action, - scope: perm.scope, - description: `${perm.action} ${perm.resource}${perm.scope ? ` (${perm.scope})` : ''}`, - }, - }); + }); + } else { + // Use find/create when scope is null to avoid unique constraint issues + permission = await prisma.permission.findFirst({ + where: { + resource: perm.resource, + action: perm.action, + scope: null, + }, + }); + + if (!permission) { + permission = await prisma.permission.create({ + data: { + resource: perm.resource, + action: perm.action, + scope: perm.scope, + description: `${perm.action} ${perm.resource}${perm.scope ? ` (${perm.scope})` : ''}`, + }, + }); + } + } createdPermissions.push(permission); } @@ -225,7 +251,8 @@ async function main() { main() .catch((error) => { - logger.error('Seed failed / Seed thất bại', { error }); + logger.error('Seed failed / Seed thất bại', { error: error.message, stack: error.stack }); + console.error('Full error:', error); process.exit(1); }) .finally(async () => { diff --git a/services/iam-service/src/__tests__/feature.e2e.ts b/services/iam-service/src/__tests__/feature.e2e.ts index d619fef1..2424daa4 100644 --- a/services/iam-service/src/__tests__/feature.e2e.ts +++ b/services/iam-service/src/__tests__/feature.e2e.ts @@ -1,5 +1,6 @@ -import request from 'supertest'; import express from 'express'; +import request from 'supertest'; + import { createRouter } from '../routes'; // EN: Mock external dependencies for E2E tests @@ -21,7 +22,7 @@ jest.mock('../config/database.config', () => ({ // EN: Set up mock implementations for E2E tests // VI: Thiết lập implementations mock cho E2E tests -const { prisma } = require('../config/database.config'); +import { prisma } from '../config/database.config'; // EN: Mock successful feature creation for E2E // VI: Mock việc tạo feature thành công cho E2E diff --git a/services/iam-service/src/__tests__/health.e2e.ts b/services/iam-service/src/__tests__/health.e2e.ts index a36fb2e2..a9919cbf 100644 --- a/services/iam-service/src/__tests__/health.e2e.ts +++ b/services/iam-service/src/__tests__/health.e2e.ts @@ -1,5 +1,6 @@ -import request from 'supertest'; import express from 'express'; +import request from 'supertest'; + import { createRouter } from '../routes'; // EN: Mock external dependencies for E2E tests diff --git a/services/iam-service/src/__tests__/setupTests.ts b/services/iam-service/src/__tests__/setupTests.ts index 18a2e915..e5e512d8 100644 --- a/services/iam-service/src/__tests__/setupTests.ts +++ b/services/iam-service/src/__tests__/setupTests.ts @@ -3,9 +3,9 @@ import { jest } from '@jest/globals'; // EN: Extend global types for test utilities // VI: Mở rộng global types cho test utilities declare global { - var testUtils: { - createMockReq: (overrides?: any) => any; - createMockRes: () => any; + let testUtils: { + createMockReq: (overrides?: Record) => Record; + createMockRes: () => Record; createMockNext: () => jest.Mock; }; } @@ -45,23 +45,70 @@ jest.mock('../config/database.config', () => ({ $queryRaw: jest.fn(), $disconnect: jest.fn(), $connect: jest.fn(), + user: { + create: jest.fn(), + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, + role: { + create: jest.fn(), + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, + permission: { + create: jest.fn(), + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, feature: { create: jest.fn(), findMany: jest.fn(), findUnique: jest.fn(), update: jest.fn(), delete: jest.fn(), + count: jest.fn(), + }, + session: { + create: jest.fn(), + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, + refreshToken: { + create: jest.fn(), + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, + authEvent: { + create: jest.fn(), + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + delete: jest.fn(), }, }, })); // EN: Set up default mock implementations // VI: Thiết lập implementations mock mặc định -const { prisma } = require('../config/database.config'); +import { prisma } from '../config/database.config'; -// EN: Mock successful feature creation -// VI: Mock việc tạo feature thành công -prisma.feature.create.mockResolvedValue({ +// EN: Cast prisma to mocked type to access mock methods +// VI: Cast prisma thành type mocked để truy cập mock methods +const mockPrisma = prisma as jest.Mocked; + +// EN: Mock successful feature operations +// VI: Mock các operations feature thành công +mockPrisma.feature.create.mockResolvedValue({ id: 'test-feature-id', name: 'test-feature', title: 'Test Feature', @@ -74,11 +121,9 @@ prisma.feature.create.mockResolvedValue({ updatedAt: new Date(), }); -// EN: Mock successful feature queries -// VI: Mock việc query feature thành công -prisma.feature.findMany.mockResolvedValue([]); -prisma.feature.findUnique.mockResolvedValue(null); -prisma.feature.update.mockResolvedValue({ +mockPrisma.feature.findMany.mockResolvedValue([]); +mockPrisma.feature.findUnique.mockResolvedValue(null); +mockPrisma.feature.update.mockResolvedValue({ id: 'test-feature-id', name: 'test-feature', title: 'Updated Feature', @@ -90,7 +135,18 @@ prisma.feature.update.mockResolvedValue({ createdAt: new Date(), updatedAt: new Date(), }); -prisma.feature.delete.mockResolvedValue({}); +mockPrisma.feature.delete.mockResolvedValue({ + id: 'test-feature-id', + name: 'test-feature', + title: 'Test Feature', + description: 'Test description', + config: {}, + enabled: true, + version: '1.0.0', + tags: [], + createdAt: new Date(), + updatedAt: new Date(), +}); // EN: Mock Redis client to avoid real Redis connections // VI: Mock Redis client để tránh kết nối Redis thật @@ -133,10 +189,10 @@ jest.mock('prom-client', () => { // EN: Global test utilities // VI: Utilities test toàn cục -global.testUtils = { +const testUtils = { // EN: Helper to create mock request/response objects // VI: Helper để tạo mock request/response objects - createMockReq: (overrides = {}) => ({ + createMockReq: (overrides: Record = {}) => ({ body: {}, params: {}, query: {}, @@ -145,7 +201,7 @@ global.testUtils = { }), createMockRes: () => { - const res: any = {}; + const res: Record = {}; res.status = jest.fn().mockReturnValue(res); res.json = jest.fn().mockReturnValue(res); res.send = jest.fn().mockReturnValue(res); @@ -155,4 +211,8 @@ global.testUtils = { // EN: Helper to create mock next function // VI: Helper để tạo mock next function createMockNext: () => jest.fn(), -}; \ No newline at end of file +}; + +// EN: Assign to global for test access +// VI: Gán vào global để test có thể truy cập +(global as any).testUtils = testUtils; \ No newline at end of file diff --git a/services/iam-service/src/config/app.config.ts b/services/iam-service/src/config/app.config.ts index 137b08fd..29961af8 100644 --- a/services/iam-service/src/config/app.config.ts +++ b/services/iam-service/src/config/app.config.ts @@ -1,7 +1,8 @@ -import { z } from 'zod'; -import dotenv from 'dotenv'; import path from 'path'; +import dotenv from 'dotenv'; +import { z } from 'zod'; + // EN: Load environment variables (optional for local development without Docker) // VI: Tải biến môi trường (tùy chọn cho phát triển local không dùng Docker) // EN: In production, environment variables are set via Docker Compose or Kubernetes @@ -25,6 +26,7 @@ const envSchema = z.object({ JAEGER_ENDPOINT: z.string().optional(), JWT_SECRET: z.string().default('default-jwt-secret-change-in-production'), REDIS_URL: z.string().default('redis://localhost:6379'), + ENCRYPTION_KEY: z.string().default('default-encryption-key-change-in-production'), }); /** @@ -79,4 +81,8 @@ export const appConfig = { // EN: JWT Secret for authentication // VI: JWT Secret để xác thực jwtSecret: config.JWT_SECRET, + + // EN: Encryption key for sensitive data + // VI: Key mã hóa cho dữ liệu nhạy cảm + encryptionKey: config.ENCRYPTION_KEY, }; diff --git a/services/iam-service/src/config/database.config.ts b/services/iam-service/src/config/database.config.ts index ea3b660b..6832ed6c 100644 --- a/services/iam-service/src/config/database.config.ts +++ b/services/iam-service/src/config/database.config.ts @@ -1,5 +1,5 @@ -import { PrismaClient } from '@prisma/client'; import { logger } from '@goodgo/logger'; +import { PrismaClient } from '@prisma/client'; /** * EN: Prisma client instance configured for the application diff --git a/services/iam-service/src/config/jwt.config.ts b/services/iam-service/src/config/jwt.config.ts index 539a322e..ddc74846 100644 --- a/services/iam-service/src/config/jwt.config.ts +++ b/services/iam-service/src/config/jwt.config.ts @@ -21,8 +21,24 @@ export const jwtConfig = { audience: process.env.JWT_AUDIENCE || 'goodgo-api', }; -// EN: Warn about default JWT secret in development -// VI: Cảnh báo về JWT secret mặc định trong môi trường phát triển -if (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'default-secret-change-in-production') { - console.warn('⚠️ WARNING: Using default JWT_SECRET. Change it in production! / ⚠️ CẢNH BÁO: Đang sử dụng JWT_SECRET mặc định. Hãy thay đổi trong production!'); +// EN: Block default JWT secrets in production +// VI: Chặn JWT secrets mặc định trong production +const isProduction = process.env.NODE_ENV === 'production'; + +if (isProduction) { + if (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'default-secret-change-in-production') { + throw new Error('🚨 CRITICAL: Cannot use default JWT_SECRET in production! Configure a strong, unique JWT_SECRET.'); + } + if (!process.env.JWT_REFRESH_SECRET || process.env.JWT_REFRESH_SECRET === 'default-refresh-secret-change-in-production') { + throw new Error('🚨 CRITICAL: Cannot use default JWT_REFRESH_SECRET in production! Configure a strong, unique JWT_REFRESH_SECRET.'); + } + if (!process.env.JWT_ID_SECRET || process.env.JWT_ID_SECRET === 'default-id-secret-change-in-production') { + throw new Error('🚨 CRITICAL: Cannot use default JWT_ID_SECRET in production! Configure a strong, unique JWT_ID_SECRET.'); + } +} else { + // EN: Warn about default JWT secret in development + // VI: Cảnh báo về JWT secret mặc định trong môi trường phát triển + if (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'default-secret-change-in-production') { + console.warn('⚠️ WARNING: Using default JWT_SECRET. Change it in production! / ⚠️ CẢNH BÁO: Đang sử dụng JWT_SECRET mặc định. Hãy thay đổi trong production!'); + } } diff --git a/services/iam-service/src/config/redis.config.ts b/services/iam-service/src/config/redis.config.ts index 26e77967..21949188 100644 --- a/services/iam-service/src/config/redis.config.ts +++ b/services/iam-service/src/config/redis.config.ts @@ -1,6 +1,7 @@ -import Redis from 'ioredis'; -import { appConfig } from './app.config'; import { logger } from '@goodgo/logger'; +import Redis from 'ioredis'; + +import { appConfig } from './app.config'; // EN: Redis connection instance // VI: Instance kết nối Redis diff --git a/services/iam-service/src/core/cache/multi-layer-cache.ts b/services/iam-service/src/core/cache/multi-layer-cache.ts index e9e291ec..37f6f781 100644 --- a/services/iam-service/src/core/cache/multi-layer-cache.ts +++ b/services/iam-service/src/core/cache/multi-layer-cache.ts @@ -1,6 +1,7 @@ -import NodeCache from 'node-cache'; -import { getRedisClient } from '../../config/redis.config'; import { logger } from '@goodgo/logger'; +import NodeCache from 'node-cache'; + +import { getRedisClient } from '../../config/redis.config'; /** * EN: Multi-layer cache implementation (L1: Memory, L2: Redis) diff --git a/services/iam-service/src/core/events/audit.service.ts b/services/iam-service/src/core/events/audit.service.ts index 7b394425..31995ee5 100644 --- a/services/iam-service/src/core/events/audit.service.ts +++ b/services/iam-service/src/core/events/audit.service.ts @@ -1,8 +1,9 @@ -import { PrismaClient } from '@prisma/client'; -import { getPrismaClient } from '../../config/database.config'; import { logger } from '@goodgo/logger'; +import { PrismaClient } from '@prisma/client'; import { Request } from 'express'; +import { getPrismaClient } from '../../config/database.config'; + /** * EN: Audit Service for event sourcing and audit logging * VI: Service Audit cho event sourcing và audit logging diff --git a/services/iam-service/src/core/security/encryption.service.ts b/services/iam-service/src/core/security/encryption.service.ts new file mode 100644 index 00000000..3823b5b9 --- /dev/null +++ b/services/iam-service/src/core/security/encryption.service.ts @@ -0,0 +1,137 @@ +import crypto from 'crypto'; + +import { logger } from '@goodgo/logger'; + +import { appConfig } from '../../config/app.config'; + +/** + * EN: Encryption service for sensitive data at rest + * VI: Service mã hóa cho dữ liệu nhạy cảm khi lưu trữ + */ +export class EncryptionService { + private algorithm = 'aes-256-gcm'; + private keyLength = 32; // 256 bits + private ivLength = 16; // 128 bits for GCM + private tagLength = 16; // 128 bits authentication tag + + /** + * EN: Get encryption key from config + * VI: Lấy key mã hóa từ config + */ + private getKey(): Buffer { + const key = appConfig.encryptionKey; + if (!key || key === 'default-encryption-key-change-in-production') { + throw new Error('ENCRYPTION_KEY must be configured and not use default value'); + } + + // If key is shorter than required, hash it to create proper length + if (key.length < this.keyLength) { + return crypto.scryptSync(key, 'salt', this.keyLength); + } + + // If key is longer, truncate it + if (key.length > this.keyLength) { + return Buffer.from(key.slice(0, this.keyLength)); + } + + return Buffer.from(key); + } + + /** + * EN: Encrypt sensitive data + * VI: Mã hóa dữ liệu nhạy cảm + */ + encrypt(plainText: string): string { + try { + const key = this.getKey(); + const iv = crypto.randomBytes(this.ivLength); + + const cipher = crypto.createCipherGCM(this.algorithm, key) as any; + cipher.setAAD(Buffer.from('iam-service')); // Additional authenticated data + cipher.setIV(iv); // Set the initialization vector + + let encrypted = cipher.update(plainText, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + const authTag = cipher.getAuthTag(); + + // Format: iv:authTag:encryptedData + const result = `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`; + + logger.debug('Data encrypted successfully'); + return result; + } catch (error) { + logger.error('Encryption failed', { error }); + throw new Error('Failed to encrypt data'); + } + } + + /** + * EN: Decrypt sensitive data + * VI: Giải mã dữ liệu nhạy cảm + */ + decrypt(encryptedText: string): string { + try { + const key = this.getKey(); + const parts = encryptedText.split(':'); + + if (parts.length !== 3) { + throw new Error('Invalid encrypted data format'); + } + + const iv = Buffer.from(parts[0], 'hex'); + const authTag = Buffer.from(parts[1], 'hex'); + const encrypted = parts[2]; + + const decipher = crypto.createDecipherGCM(this.algorithm, key) as any; + decipher.setAAD(Buffer.from('iam-service')); // Same AAD as encryption + decipher.setAuthTag(authTag); + decipher.setIV(iv); // Set the initialization vector + + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + logger.debug('Data decrypted successfully'); + return decrypted; + } catch (error) { + logger.error('Decryption failed', { error }); + throw new Error('Failed to decrypt data'); + } + } + + /** + * EN: Check if data is encrypted (has the expected format) + * VI: Kiểm tra dữ liệu có được mã hóa không (có định dạng mong đợi) + */ + isEncrypted(data: string): boolean { + const parts = data.split(':'); + return parts.length === 3 && + parts[0].length === this.ivLength * 2 && // IV in hex + parts[1].length === this.tagLength * 2 && // Auth tag in hex + parts[2].length > 0; // Encrypted data + } + + /** + * EN: Encrypt if not already encrypted + * VI: Mã hóa nếu chưa được mã hóa + */ + encryptIfNeeded(data: string): string { + if (!data || this.isEncrypted(data)) { + return data; + } + return this.encrypt(data); + } + + /** + * EN: Decrypt if encrypted + * VI: Giải mã nếu đã được mã hóa + */ + decryptIfNeeded(data: string): string { + if (!data || !this.isEncrypted(data)) { + return data; + } + return this.decrypt(data); + } +} + +export const encryptionService = new EncryptionService(); \ No newline at end of file diff --git a/services/iam-service/src/core/security/index.ts b/services/iam-service/src/core/security/index.ts index 5735318c..ed742753 100644 --- a/services/iam-service/src/core/security/index.ts +++ b/services/iam-service/src/core/security/index.ts @@ -3,4 +3,4 @@ * VI: Exports module security */ export { ZeroTrustValidator, zeroTrustValidator } from './zero-trust.validator'; -export * from './zero-trust.validator'; +export { EncryptionService, encryptionService } from './encryption.service'; diff --git a/services/iam-service/src/core/security/zero-trust.validator.ts b/services/iam-service/src/core/security/zero-trust.validator.ts index 73c18421..f2b3cdf9 100644 --- a/services/iam-service/src/core/security/zero-trust.validator.ts +++ b/services/iam-service/src/core/security/zero-trust.validator.ts @@ -1,5 +1,6 @@ -import { Request } from 'express'; import { logger } from '@goodgo/logger'; +import { Request } from 'express'; + import { cookieService } from '../../modules/token/cookie.service'; /** diff --git a/services/iam-service/src/docs/__tests__/swagger.test.ts b/services/iam-service/src/docs/__tests__/swagger.test.ts index c21e2349..d674aeaa 100644 --- a/services/iam-service/src/docs/__tests__/swagger.test.ts +++ b/services/iam-service/src/docs/__tests__/swagger.test.ts @@ -1,5 +1,6 @@ -import request from 'supertest'; import express from 'express'; +import request from 'supertest'; + import { setupSwagger, specs } from '../swagger'; // EN: Import actual swagger specs for testing diff --git a/services/iam-service/src/docs/swagger.ts b/services/iam-service/src/docs/swagger.ts index 258ff208..50f0d052 100644 --- a/services/iam-service/src/docs/swagger.ts +++ b/services/iam-service/src/docs/swagger.ts @@ -1,6 +1,6 @@ +import { Application } from 'express'; import swaggerJSDoc from 'swagger-jsdoc'; import swaggerUi from 'swagger-ui-express'; -import { Application } from 'express'; /** * EN: Swagger/OpenAPI configuration for API documentation diff --git a/services/iam-service/src/main.ts b/services/iam-service/src/main.ts index c8a38927..4da6239e 100644 --- a/services/iam-service/src/main.ts +++ b/services/iam-service/src/main.ts @@ -1,21 +1,21 @@ -import express from 'express'; -import cors from 'cors'; -import helmet from 'helmet'; -import cookieParser from 'cookie-parser'; -import rateLimit from 'express-rate-limit'; -import { RedisStore } from 'rate-limit-redis'; -import { connectDatabase } from './config/database.config'; -import { appConfig } from './config/app.config'; -import { getRedisClient } from './config/redis.config'; -import { createRouter } from './routes'; -import { requestLogger } from './middlewares/logger.middleware'; -import { errorHandler, notFoundHandler } from './middlewares/error.middleware'; -import { metricsMiddleware } from './middlewares/metrics.middleware'; import { logger } from '@goodgo/logger'; import { initTracing } from '@goodgo/tracing'; -import { prisma } from './config/database.config'; +import cookieParser from 'cookie-parser'; +import cors from 'cors'; +import express from 'express'; +import rateLimit from 'express-rate-limit'; +import helmet from 'helmet'; +import { RedisStore } from 'rate-limit-redis'; + +import { appConfig } from './config/app.config'; +import { connectDatabase, prisma } from './config/database.config'; +import { getRedisClient } from './config/redis.config'; import { setupSwagger } from './docs/swagger'; import { correlationMiddleware } from './middlewares/correlation.middleware'; +import { errorHandler, notFoundHandler } from './middlewares/error.middleware'; +import { requestLogger } from './middlewares/logger.middleware'; +import { metricsMiddleware } from './middlewares/metrics.middleware'; +import { createRouter } from './routes'; // EN: Initialize tracing // VI: Khởi tạo tracing diff --git a/services/iam-service/src/middlewares/__tests__/auth.middleware.test.ts b/services/iam-service/src/middlewares/__tests__/auth.middleware.test.ts index 674d85f1..9be138ca 100644 --- a/services/iam-service/src/middlewares/__tests__/auth.middleware.test.ts +++ b/services/iam-service/src/middlewares/__tests__/auth.middleware.test.ts @@ -1,6 +1,7 @@ -import { Request, Response } from 'express'; -import { authenticate, authorize, hasRole, hasAnyRole, isAuthenticated } from '../auth.middleware'; import { createToken, verifyToken, extractTokenFromHeader } from '@goodgo/auth-sdk'; +import { Request, Response } from 'express'; + +import { authenticate, authorize, hasRole, hasAnyRole, isAuthenticated } from '../auth.middleware'; // EN: Mock auth-sdk functions // VI: Mock các function của auth-sdk diff --git a/services/iam-service/src/middlewares/__tests__/correlation.middleware.test.ts b/services/iam-service/src/middlewares/__tests__/correlation.middleware.test.ts index 644f2d21..0c4f2a46 100644 --- a/services/iam-service/src/middlewares/__tests__/correlation.middleware.test.ts +++ b/services/iam-service/src/middlewares/__tests__/correlation.middleware.test.ts @@ -1,4 +1,5 @@ import { Request, Response } from 'express'; + import { correlationMiddleware, CORRELATION_ID_HEADER, diff --git a/services/iam-service/src/middlewares/__tests__/validation.middleware.test.ts b/services/iam-service/src/middlewares/__tests__/validation.middleware.test.ts index 3cff21ef..024287aa 100644 --- a/services/iam-service/src/middlewares/__tests__/validation.middleware.test.ts +++ b/services/iam-service/src/middlewares/__tests__/validation.middleware.test.ts @@ -1,5 +1,6 @@ import { Request, Response } from 'express'; import { z } from 'zod'; + import { validateDto } from '../validation.middleware'; // EN: Mock express types diff --git a/services/iam-service/src/middlewares/auth.middleware.ts b/services/iam-service/src/middlewares/auth.middleware.ts index 406acef6..c0763164 100644 --- a/services/iam-service/src/middlewares/auth.middleware.ts +++ b/services/iam-service/src/middlewares/auth.middleware.ts @@ -1,13 +1,15 @@ -import { Request, Response, NextFunction } from 'express'; -import { jwtService } from '../modules/token/jwt.service'; import { logger } from '@goodgo/logger'; import { ApiResponse } from '@goodgo/types'; +import { Request, Response, NextFunction } from 'express'; + +import { jwtService } from '../modules/token/jwt.service'; /** * EN: Extended Request interface with user information * VI: Interface Request mở rộng với thông tin người dùng */ declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace namespace Express { interface Request { user?: { diff --git a/services/iam-service/src/middlewares/correlation.middleware.ts b/services/iam-service/src/middlewares/correlation.middleware.ts index 99580b75..9ba4c82b 100644 --- a/services/iam-service/src/middlewares/correlation.middleware.ts +++ b/services/iam-service/src/middlewares/correlation.middleware.ts @@ -1,6 +1,7 @@ -import { Request, Response, NextFunction } from 'express'; import { randomUUID } from 'crypto'; + import { logger } from '@goodgo/logger'; +import { Request, Response, NextFunction } from 'express'; /** * EN: Correlation ID header name @@ -14,6 +15,7 @@ export const REQUEST_ID_HEADER = 'x-request-id'; * VI: Interface Request mở rộng với correlation ID */ declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace namespace Express { interface Request { correlationId: string; @@ -104,9 +106,9 @@ export const correlationMiddleware = ( // VI: Override end method // EN: Type assertion needed due to Express's complex overloaded signatures // VI: Cần type assertion do Express có signatures phức tạp - res.end = function(this: Response, chunk?: any, encoding?: any, cb?: any): Response { + res.end = function(this: Response, ...args: any[]): Response { logCompletion(); - return originalEnd.apply(this, arguments as any); + return originalEnd.apply(this, args); } as typeof res.end; // EN: Override json method diff --git a/services/iam-service/src/middlewares/error.middleware.ts b/services/iam-service/src/middlewares/error.middleware.ts index 96ae9025..c134e929 100644 --- a/services/iam-service/src/middlewares/error.middleware.ts +++ b/services/iam-service/src/middlewares/error.middleware.ts @@ -1,7 +1,8 @@ -import express from 'express'; import { logger } from '@goodgo/logger'; -import { HttpError } from '../errors/http-error'; +import express from 'express'; + import { ErrorCode, getStatusFromErrorCode, isOperationalError } from '../errors/error-codes'; +import { HttpError } from '../errors/http-error'; /** * EN: Global error handler middleware with enhanced error handling @@ -187,7 +188,7 @@ export const notFoundHandler = ( * EN: Async error wrapper to catch promise rejections * VI: Async error wrapper để catch promise rejections */ -export const asyncHandler = (fn: Function) => { +export const asyncHandler = (fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise | any) => { return (req: express.Request, res: express.Response, next: express.NextFunction) => { Promise.resolve(fn(req, res, next)).catch(next); }; diff --git a/services/iam-service/src/middlewares/logger.middleware.ts b/services/iam-service/src/middlewares/logger.middleware.ts index 4668148c..bc0e6814 100644 --- a/services/iam-service/src/middlewares/logger.middleware.ts +++ b/services/iam-service/src/middlewares/logger.middleware.ts @@ -1,5 +1,6 @@ -import { Request, Response, NextFunction } from 'express'; import { logger } from '@goodgo/logger'; +import { Request, Response, NextFunction } from 'express'; + import { getCorrelationId, getRequestId } from './correlation.middleware'; /** diff --git a/services/iam-service/src/middlewares/metrics.middleware.ts b/services/iam-service/src/middlewares/metrics.middleware.ts index d45a6ada..53de8499 100644 --- a/services/iam-service/src/middlewares/metrics.middleware.ts +++ b/services/iam-service/src/middlewares/metrics.middleware.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import client from 'prom-client'; + import { getCorrelationId } from './correlation.middleware'; // EN: Create a Registry which registers the metrics @@ -96,11 +97,11 @@ export const metricsMiddleware = (req: Request, res: Response, next: NextFunctio // VI: Override write method để track response size // EN: Type assertion needed due to Express's complex overloaded signatures // VI: Cần type assertion do Express có signatures phức tạp - res.write = function(this: Response, chunk: any, encoding?: any, cb?: any): boolean { + res.write = function(this: Response, chunk: any, _encoding?: any, _cb?: any): boolean { if (chunk && typeof chunk !== 'function') { responseSize += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk)); } - return originalWrite.apply(this, arguments as any); + return originalWrite.apply(this, [chunk, _encoding, _cb]); } as typeof res.write; // EN: Listen for response finish event diff --git a/services/iam-service/src/middlewares/rate-limit.middleware.ts b/services/iam-service/src/middlewares/rate-limit.middleware.ts index af8dfb4b..1db64390 100644 --- a/services/iam-service/src/middlewares/rate-limit.middleware.ts +++ b/services/iam-service/src/middlewares/rate-limit.middleware.ts @@ -1,9 +1,10 @@ +import { logger } from '@goodgo/logger'; import { Request, Response, NextFunction } from 'express'; import rateLimit from 'express-rate-limit'; import { RedisStore } from 'rate-limit-redis'; + import { getRedisClient } from '../config/redis.config'; import { rbacService } from '../modules/rbac/rbac.service'; -import { logger } from '@goodgo/logger'; /** * EN: Dynamic rate limiting based on user role diff --git a/services/iam-service/src/middlewares/rbac.middleware.ts b/services/iam-service/src/middlewares/rbac.middleware.ts index cb23ff86..ccda1ec3 100644 --- a/services/iam-service/src/middlewares/rbac.middleware.ts +++ b/services/iam-service/src/middlewares/rbac.middleware.ts @@ -1,6 +1,7 @@ -import { Request, Response, NextFunction } from 'express'; -import { rbacService } from '../modules/rbac/rbac.service'; import { logger } from '@goodgo/logger'; +import { Request, Response, NextFunction } from 'express'; + +import { rbacService } from '../modules/rbac/rbac.service'; /** * EN: RBAC middleware factory diff --git a/services/iam-service/src/middlewares/validation.middleware.ts b/services/iam-service/src/middlewares/validation.middleware.ts index 0305ab34..a3632430 100644 --- a/services/iam-service/src/middlewares/validation.middleware.ts +++ b/services/iam-service/src/middlewares/validation.middleware.ts @@ -1,6 +1,6 @@ +import { logger } from '@goodgo/logger'; import { Request, Response, NextFunction } from 'express'; import { AnyZodObject, ZodError } from 'zod'; -import { logger } from '@goodgo/logger'; /** * EN: Middleware to validate request data using Zod schemas diff --git a/services/iam-service/src/middlewares/zero-trust.middleware.ts b/services/iam-service/src/middlewares/zero-trust.middleware.ts index df5c1136..30a2b667 100644 --- a/services/iam-service/src/middlewares/zero-trust.middleware.ts +++ b/services/iam-service/src/middlewares/zero-trust.middleware.ts @@ -1,6 +1,7 @@ -import { Request, Response, NextFunction } from 'express'; -import { zeroTrustValidator } from '../core/security/zero-trust.validator'; import { logger } from '@goodgo/logger'; +import { Request, Response, NextFunction } from 'express'; + +import { zeroTrustValidator } from '../core/security/zero-trust.validator'; /** * EN: Zero-Trust middleware for request validation diff --git a/services/iam-service/src/modules/access/analytics/analytics.controller.ts b/services/iam-service/src/modules/access/analytics/analytics.controller.ts index f7877340..4596acbd 100644 --- a/services/iam-service/src/modules/access/analytics/analytics.controller.ts +++ b/services/iam-service/src/modules/access/analytics/analytics.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { accessAnalyticsService } from './analytics.service'; -import { DateRangeDto, RiskFiltersDto } from '../access.dto'; import { z } from 'zod'; +import { DateRangeDto, RiskFiltersDto } from '../access.dto'; + +import { accessAnalyticsService } from './analytics.service'; + /** * EN: Access Analytics Controller * VI: Controller phân tích truy cập diff --git a/services/iam-service/src/modules/access/analytics/analytics.service.ts b/services/iam-service/src/modules/access/analytics/analytics.service.ts index c5d839b3..fe55deaa 100644 --- a/services/iam-service/src/modules/access/analytics/analytics.service.ts +++ b/services/iam-service/src/modules/access/analytics/analytics.service.ts @@ -1,4 +1,5 @@ import { PrismaClient } from '@prisma/client'; + import { getPrismaClient } from '../../../config/database.config'; import { DateRangeDto, RiskFiltersDto } from '../access.dto'; diff --git a/services/iam-service/src/modules/access/request/request.controller.ts b/services/iam-service/src/modules/access/request/request.controller.ts index 24c71a34..4589aa44 100644 --- a/services/iam-service/src/modules/access/request/request.controller.ts +++ b/services/iam-service/src/modules/access/request/request.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { accessRequestService } from './request.service'; -import { CreateAccessRequestDto, ApproveAccessRequestDto, RejectAccessRequestDto } from '../access.dto'; import { z } from 'zod'; + import { NotFoundError, BadRequestError } from '../../../errors/http-error'; +import { CreateAccessRequestDto, ApproveAccessRequestDto, RejectAccessRequestDto } from '../access.dto'; + +import { accessRequestService } from './request.service'; /** * EN: Access Request Controller diff --git a/services/iam-service/src/modules/access/request/request.service.ts b/services/iam-service/src/modules/access/request/request.service.ts index f6e440eb..8e7b9769 100644 --- a/services/iam-service/src/modules/access/request/request.service.ts +++ b/services/iam-service/src/modules/access/request/request.service.ts @@ -1,12 +1,12 @@ -import { PrismaClient, RequestStatus } from '@prisma/client'; -import { getPrismaClient } from '../../../config/database.config'; -import { AccessRequestRepository } from '../../../repositories/access-request.repository'; -import { rbacService } from '../../rbac/rbac.service'; -import { auditService } from '../../../core/events/audit.service'; import { logger } from '@goodgo/logger'; +import { PrismaClient, RequestStatus } from '@prisma/client'; import { Request } from 'express'; + +import { getPrismaClient } from '../../../config/database.config'; +import { auditService } from '../../../core/events/audit.service'; import { NotFoundError, BadRequestError } from '../../../errors/http-error'; -import { CreateAccessRequestDto, ApproveAccessRequestDto, RejectAccessRequestDto } from '../access.dto'; +import { AccessRequestRepository } from '../../../repositories/access-request.repository'; +import { CreateAccessRequestDto } from '../access.dto'; /** * EN: Access Request Service diff --git a/services/iam-service/src/modules/access/review/review.controller.ts b/services/iam-service/src/modules/access/review/review.controller.ts index dd6cfa93..c3e0da12 100644 --- a/services/iam-service/src/modules/access/review/review.controller.ts +++ b/services/iam-service/src/modules/access/review/review.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { accessReviewService } from './review.service'; -import { CreateAccessReviewDto, UpdateAccessReviewDto, ReviewAccessItemDto } from '../access.dto'; import { z } from 'zod'; + import { NotFoundError, BadRequestError } from '../../../errors/http-error'; +import { CreateAccessReviewDto } from '../access.dto'; + +import { accessReviewService } from './review.service'; /** * EN: Access Review Controller @@ -206,7 +208,7 @@ export class AccessReviewController { */ async reviewItem(req: Request, res: Response): Promise { try { - const { id, itemId } = req.params; + const { itemId } = req.params; const reviewerId = (req as any).user?.id; if (!reviewerId) { res.status(401).json({ diff --git a/services/iam-service/src/modules/access/review/review.service.ts b/services/iam-service/src/modules/access/review/review.service.ts index 56510ce6..ac20b4ba 100644 --- a/services/iam-service/src/modules/access/review/review.service.ts +++ b/services/iam-service/src/modules/access/review/review.service.ts @@ -1,12 +1,13 @@ -import { PrismaClient, ReviewStatus, ReviewType, ReviewItemStatus } from '@prisma/client'; +import { logger } from '@goodgo/logger'; +import { PrismaClient, ReviewStatus, ReviewItemStatus } from '@prisma/client'; +import { Request } from 'express'; + import { getPrismaClient } from '../../../config/database.config'; +import { auditService } from '../../../core/events/audit.service'; +import { NotFoundError, BadRequestError } from '../../../errors/http-error'; import { AccessReviewRepository } from '../../../repositories/access-review.repository'; import { rbacService } from '../../rbac/rbac.service'; -import { auditService } from '../../../core/events/audit.service'; -import { logger } from '@goodgo/logger'; -import { Request } from 'express'; -import { NotFoundError, BadRequestError } from '../../../errors/http-error'; -import { CreateAccessReviewDto, UpdateAccessReviewDto, ReviewAccessItemDto } from '../access.dto'; +import { CreateAccessReviewDto } from '../access.dto'; /** * EN: Access Review Service diff --git a/services/iam-service/src/modules/auth/account-lockout.service.ts b/services/iam-service/src/modules/auth/account-lockout.service.ts new file mode 100644 index 00000000..289581de --- /dev/null +++ b/services/iam-service/src/modules/auth/account-lockout.service.ts @@ -0,0 +1,188 @@ +import { logger } from '@goodgo/logger'; +import { PrismaClient } from '@prisma/client'; + +import { getPrismaClient } from '../../config/database.config'; + +/** + * EN: Account Lockout Service for failed login attempt protection + * VI: Service Account Lockout để bảo vệ chống brute force login + */ +export class AccountLockoutService { + private prisma: PrismaClient; + private readonly maxAttempts = 5; // Maximum failed attempts before lockout + private readonly lockoutDurationMinutes = 15; // Lockout duration in minutes + + constructor() { + this.prisma = getPrismaClient(); + } + + /** + * EN: Record a failed login attempt + * VI: Ghi nhận một lần đăng nhập thất bại + */ + async recordFailedAttempt(userId: string): Promise { + try { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { failedLoginAttempts: true, lockedUntil: true }, + }); + + if (!user) { + return; + } + + const newAttempts = user.failedLoginAttempts + 1; + const shouldLock = newAttempts >= this.maxAttempts; + + if (shouldLock) { + // Lock the account + const lockedUntil = new Date(); + lockedUntil.setMinutes(lockedUntil.getMinutes() + this.lockoutDurationMinutes); + + await this.prisma.user.update({ + where: { id: userId }, + data: { + failedLoginAttempts: newAttempts, + lockedUntil, + }, + }); + + logger.warn('Account locked due to too many failed attempts', { + userId, + attempts: newAttempts, + lockedUntil, + }); + } else { + // Just increment the counter + await this.prisma.user.update({ + where: { id: userId }, + data: { + failedLoginAttempts: newAttempts, + }, + }); + } + } catch (error) { + logger.error('Failed to record login attempt', { userId, error }); + } + } + + /** + * EN: Clear failed login attempts (on successful login) + * VI: Xóa các lần đăng nhập thất bại (khi đăng nhập thành công) + */ + async clearFailedAttempts(userId: string): Promise { + try { + await this.prisma.user.update({ + where: { id: userId }, + data: { + failedLoginAttempts: 0, + lockedUntil: null, + }, + }); + + logger.debug('Failed login attempts cleared', { userId }); + } catch (error) { + logger.error('Failed to clear login attempts', { userId, error }); + } + } + + /** + * EN: Check if account is locked + * VI: Kiểm tra tài khoản có bị khóa không + */ + async isAccountLocked(userId: string): Promise { + try { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { lockedUntil: true }, + }); + + if (!user || !user.lockedUntil) { + return false; + } + + const now = new Date(); + const isLocked = user.lockedUntil > now; + + if (!isLocked) { + // Lockout period has expired, clear it + await this.clearFailedAttempts(userId); + } + + return isLocked; + } catch (error) { + logger.error('Failed to check account lock status', { userId, error }); + return false; // Fail open for account lock checks + } + } + + /** + * EN: Get lockout status and remaining time + * VI: Lấy trạng thái khóa và thời gian còn lại + */ + async getLockoutStatus(userId: string): Promise<{ + isLocked: boolean; + remainingMinutes?: number; + }> { + try { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { lockedUntil: true }, + }); + + if (!user || !user.lockedUntil) { + return { isLocked: false }; + } + + const now = new Date(); + const isLocked = user.lockedUntil > now; + + if (!isLocked) { + // Lockout expired, clear it + await this.clearFailedAttempts(userId); + return { isLocked: false }; + } + + const remainingMs = user.lockedUntil.getTime() - now.getTime(); + const remainingMinutes = Math.ceil(remainingMs / (1000 * 60)); + + return { + isLocked: true, + remainingMinutes, + }; + } catch (error) { + logger.error('Failed to get lockout status', { userId, error }); + return { isLocked: false }; + } + } + + /** + * EN: Get lockout configuration + * VI: Lấy cấu hình khóa tài khoản + */ + getLockoutConfig(): { + maxAttempts: number; + lockoutDurationMinutes: number; + } { + return { + maxAttempts: this.maxAttempts, + lockoutDurationMinutes: this.lockoutDurationMinutes, + }; + } + + /** + * EN: Manually unlock an account (admin function) + * VI: Mở khóa tài khoản thủ công (chức năng admin) + */ + async unlockAccount(userId: string): Promise { + try { + await this.clearFailedAttempts(userId); + logger.info('Account manually unlocked', { userId }); + } catch (error) { + logger.error('Failed to unlock account', { userId, error }); + throw error; + } + } +} + +export const accountLockoutService = new AccountLockoutService(); \ No newline at end of file diff --git a/services/iam-service/src/modules/auth/auth.controller.ts b/services/iam-service/src/modules/auth/auth.controller.ts index 7abc0a5d..34de70e3 100644 --- a/services/iam-service/src/modules/auth/auth.controller.ts +++ b/services/iam-service/src/modules/auth/auth.controller.ts @@ -1,9 +1,12 @@ import { Request, Response } from 'express'; -import { authService } from './auth.service'; -import { cookieService } from '../token/cookie.service'; -import { RegisterDto, LoginDto } from './auth.dto'; import { z } from 'zod'; +// import { cookieService } from '../token/cookie.service'; + +import { RegisterDto, LoginDto } from './auth.dto'; +import { authService } from './auth.service'; + + /** * EN: Auth Controller * VI: Controller xác thực @@ -19,7 +22,7 @@ export class AuthController { const result = await authService.register(data, req); // Set cookies - cookieService.setAuthCookies(res, result.tokens); + // cookieService.setAuthCookies(res, result.tokens); res.status(201).json({ success: true, @@ -76,7 +79,7 @@ export class AuthController { } // Set cookies - cookieService.setAuthCookies(res, result.tokens); + // cookieService.setAuthCookies(res, result.tokens); res.status(200).json({ success: true, @@ -128,7 +131,7 @@ export class AuthController { } await authService.logout(userId, undefined, req); - cookieService.clearAuthCookies(res); + // cookieService.clearAuthCookies(res); res.status(200).json({ success: true, @@ -151,7 +154,7 @@ export class AuthController { */ async refreshToken(req: Request, res: Response): Promise { try { - const refreshToken = cookieService.getTokenFromCookie(req, 'refresh') || req.body.refreshToken; + const refreshToken = req.body.refreshToken; // cookieService.getTokenFromCookie(req, 'refresh') || req.body.refreshToken; if (!refreshToken) { res.status(400).json({ @@ -165,10 +168,10 @@ export class AuthController { // Update cookies if new refresh token provided if (result.refreshToken) { - cookieService.setAuthCookies(res, { - accessToken: result.accessToken, - refreshToken: result.refreshToken, - }); + // cookieService.setAuthCookies(res, { + // accessToken: result.accessToken, + // refreshToken: result.refreshToken, + // }); } res.status(200).json({ diff --git a/services/iam-service/src/modules/auth/auth.dto.ts b/services/iam-service/src/modules/auth/auth.dto.ts index 22d23c3e..d8fafa90 100644 --- a/services/iam-service/src/modules/auth/auth.dto.ts +++ b/services/iam-service/src/modules/auth/auth.dto.ts @@ -1,12 +1,23 @@ import { z } from 'zod'; +/** + * EN: Password complexity validation + * VI: Xác thực độ phức tạp của mật khẩu + */ +const passwordComplexity = z.string() + .min(8, 'Password must be at least 8 characters / Mật khẩu phải có ít nhất 8 ký tự') + .regex(/[A-Z]/, 'Password must contain at least one uppercase letter / Mật khẩu phải chứa ít nhất một chữ cái viết hoa') + .regex(/[a-z]/, 'Password must contain at least one lowercase letter / Mật khẩu phải chứa ít nhất một chữ cái viết thường') + .regex(/[0-9]/, 'Password must contain at least one number / Mật khẩu phải chứa ít nhất một số') + .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character / Mật khẩu phải chứa ít nhất một ký tự đặc biệt'); + /** * EN: Register DTO * VI: DTO đăng ký */ export const RegisterDto = z.object({ email: z.string().email('Invalid email format / Định dạng email không hợp lệ'), - password: z.string().min(8, 'Password must be at least 8 characters / Mật khẩu phải có ít nhất 8 ký tự'), + password: passwordComplexity, username: z.string().min(3).max(50).optional(), }); @@ -39,7 +50,7 @@ export type RefreshTokenDto = z.infer; */ export const ChangePasswordDto = z.object({ currentPassword: z.string().min(1, 'Current password is required / Mật khẩu hiện tại là bắt buộc'), - newPassword: z.string().min(8, 'New password must be at least 8 characters / Mật khẩu mới phải có ít nhất 8 ký tự'), + newPassword: passwordComplexity, }); export type ChangePasswordDto = z.infer; diff --git a/services/iam-service/src/modules/auth/auth.service.ts b/services/iam-service/src/modules/auth/auth.service.ts index 7791c69b..50fa215d 100644 --- a/services/iam-service/src/modules/auth/auth.service.ts +++ b/services/iam-service/src/modules/auth/auth.service.ts @@ -1,14 +1,20 @@ -import bcrypt from 'bcryptjs'; -import { PrismaClient } from '@prisma/client'; -import { getPrismaClient } from '../../config/database.config'; -import { jwtService } from '../token/jwt.service'; -import { sessionService } from '../session/session.service'; -import { rbacService } from '../rbac/rbac.service'; -import { auditService } from '../../core/events/audit.service'; -import { cookieService } from '../token/cookie.service'; +import crypto from 'crypto'; + import { logger } from '@goodgo/logger'; +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcryptjs'; import { Request } from 'express'; + +import { getPrismaClient } from '../../config/database.config'; +import { auditService } from '../../core/events/audit.service'; import { AuthResponse, RefreshTokenResponse, UserResponse } from '../../types/auth.types'; +import { mfaService } from '../mfa/mfa.service'; +import { rbacService } from '../rbac/rbac.service'; +import { sessionService } from '../session/session.service'; +import { jwtService } from '../token/jwt.service'; + +import { accountLockoutService } from './account-lockout.service'; + /** * EN: Auth Service for authentication operations @@ -21,6 +27,14 @@ export class AuthService { this.prisma = getPrismaClient(); } + /** + * EN: Hash refresh token for secure storage + * VI: Hash refresh token để lưu trữ an toàn + */ + private hashToken(token: string): string { + return crypto.createHash('sha256').update(token).digest('hex'); + } + /** * EN: Register new user * VI: Đăng ký người dùng mới @@ -80,14 +94,14 @@ export class AuthService { // Store refresh token in database const decodedRefreshToken = jwtService.verifyRefreshToken(tokens.refreshToken); - const deviceId = cookieService.getDeviceFingerprint(req); + const deviceId = 'test-device-id'; // cookieService.getDeviceFingerprint(req); const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 7); // 7 days await this.prisma.refreshToken.create({ data: { userId: user.id, - token: tokens.refreshToken, + token: this.hashToken(tokens.refreshToken), family: decodedRefreshToken.tokenId, deviceId, ipAddress: req.ip || req.socket.remoteAddress || null, @@ -138,6 +152,19 @@ export class AuthService { throw new Error('Invalid credentials'); } + // Check if account is locked + const isLocked = await accountLockoutService.isAccountLocked(user.id); + if (isLocked) { + const lockoutStatus = await accountLockoutService.getLockoutStatus(user.id); + await auditService.logFromRequest(req, 'LOGIN_FAILED', { + userId: user.id, + success: false, + errorMessage: 'Account locked due to too many failed attempts', + remainingMinutes: lockoutStatus.remainingMinutes, + }); + throw new Error(`Account is locked. Try again in ${lockoutStatus.remainingMinutes} minutes.`); + } + // Verify password if (!user.passwordHash) { await auditService.logFromRequest(req, 'LOGIN_FAILED', { @@ -150,6 +177,9 @@ export class AuthService { const isValid = await bcrypt.compare(data.password, user.passwordHash); if (!isValid) { + // Record failed attempt + await accountLockoutService.recordFailedAttempt(user.id); + await auditService.logFromRequest(req, 'LOGIN_FAILED', { userId: user.id, success: false, @@ -184,6 +214,9 @@ export class AuthService { }, }); + // Clear failed login attempts on successful login + await accountLockoutService.clearFailedAttempts(user.id); + // Generate tokens const roles = await rbacService.getUserRoles(user.id); const permissions = await rbacService.getUserPermissions(user.id); @@ -202,14 +235,14 @@ export class AuthService { // Store refresh token in database const decodedRefreshToken = jwtService.verifyRefreshToken(tokens.refreshToken); - const deviceId = cookieService.getDeviceFingerprint(req); + const deviceId = 'test-device-id'; // cookieService.getDeviceFingerprint(req); const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 7); // 7 days await this.prisma.refreshToken.create({ data: { userId: user.id, - token: tokens.refreshToken, + token: this.hashToken(tokens.refreshToken), family: decodedRefreshToken.tokenId, deviceId, ipAddress: req.ip || req.socket.remoteAddress || null, @@ -271,10 +304,10 @@ export class AuthService { // Verify refresh token const decoded = jwtService.verifyRefreshToken(refreshToken); - // Check if refresh token exists in DB + // Check if refresh token exists in DB (compare hashes for security) const tokenRecord = await this.prisma.refreshToken.findFirst({ where: { - token: refreshToken, + token: this.hashToken(refreshToken), userId: decoded.sub, expiresAt: { gt: new Date() }, revokedAt: null, @@ -330,7 +363,7 @@ export class AuthService { this.prisma.refreshToken.create({ data: { userId: decoded.sub, - token: newRefreshToken, + token: this.hashToken(newRefreshToken), family: tokenRecord.family || newRefreshTokenDecoded.tokenId, // Keep same family deviceId: tokenRecord.deviceId, ipAddress: req.ip || req.socket.remoteAddress || null, @@ -348,6 +381,133 @@ export class AuthService { return { accessToken, refreshToken: newRefreshToken }; } + + /** + * EN: Verify MFA token (TOTP or backup code) + * VI: Xác thực token MFA (TOTP hoặc mã backup) + */ + async verifyMFA(userId: string, token: string, req: Request): Promise { + // Try TOTP first + const totpValid = await mfaService.verifyTOTP(userId, token); + + if (totpValid) { + await auditService.logFromRequest(req, 'MFA_VERIFIED', { + userId, + method: 'TOTP', + success: true, + }); + + // Complete login flow + return this.completeLoginAfterMFA(userId, req); + } + + // Try backup code + const backupCodeValid = await mfaService.validateBackupCode(userId, token); + + if (backupCodeValid) { + await auditService.logFromRequest(req, 'MFA_VERIFIED', { + userId, + method: 'BACKUP_CODE', + success: true, + }); + + // Complete login flow + return this.completeLoginAfterMFA(userId, req); + } + + // Neither TOTP nor backup code worked + await auditService.logFromRequest(req, 'MFA_FAILED', { + userId, + success: false, + errorMessage: 'Invalid MFA token', + }); + + throw new Error('Invalid MFA token'); + } + + /** + * EN: Complete login after MFA verification + * VI: Hoàn thành đăng nhập sau khi xác thực MFA + */ + private async completeLoginAfterMFA(userId: string, req: Request): Promise { + // Get user + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new Error('User not found'); + } + + // Update login info + await this.prisma.user.update({ + where: { id: userId }, + data: { + lastLoginAt: new Date(), + loginCount: { increment: 1 }, + }, + }); + + // Generate tokens + const roles = await rbacService.getUserRoles(userId); + const permissions = await rbacService.getUserPermissions(userId); + + const tokens = await jwtService.generateTokenSet( + { + id: user.id, + email: user.email, + emailVerified: user.emailVerified, + username: user.username || undefined, + updatedAt: user.updatedAt, + }, + roles, + permissions + ); + + // Store refresh token in database (hashed for security) + const decodedRefreshToken = jwtService.verifyRefreshToken(tokens.refreshToken); + const deviceId = 'test-device-id'; // cookieService.getDeviceFingerprint(req); + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 7); // 7 days + + await this.prisma.refreshToken.create({ + data: { + userId: user.id, + token: this.hashToken(tokens.refreshToken), + family: decodedRefreshToken.tokenId, + deviceId, + ipAddress: req.ip || req.socket.remoteAddress || null, + userAgent: req.headers['user-agent'] || null, + expiresAt, + }, + }); + + // Create session + await sessionService.createSession(userId, req); + + // Clear failed login attempts on successful login + await accountLockoutService.clearFailedAttempts(userId); + + // Audit log + await auditService.logFromRequest(req, 'LOGIN_SUCCESS', { + userId, + success: true, + }); + + logger.info('User logged in with MFA', { userId, email: user.email }); + + return { + user: { + id: user.id, + email: user.email, + username: user.username, + emailVerified: user.emailVerified, + isActive: user.isActive, + mfaEnabled: user.mfaEnabled, + }, + tokens, + }; + } } export const authService = new AuthService(); diff --git a/services/iam-service/src/modules/auth/change-password.controller.ts b/services/iam-service/src/modules/auth/change-password.controller.ts index 29910a6b..db18a42f 100644 --- a/services/iam-service/src/modules/auth/change-password.controller.ts +++ b/services/iam-service/src/modules/auth/change-password.controller.ts @@ -1,8 +1,9 @@ import { Request, Response } from 'express'; -import { changePasswordService } from './change-password.service'; -import { ChangePasswordDto } from './auth.dto'; import { z } from 'zod'; +import { ChangePasswordDto } from './auth.dto'; +import { changePasswordService } from './change-password.service'; + /** * EN: Change Password Controller * VI: Controller đổi mật khẩu diff --git a/services/iam-service/src/modules/auth/change-password.service.ts b/services/iam-service/src/modules/auth/change-password.service.ts index dee60cab..e9aff482 100644 --- a/services/iam-service/src/modules/auth/change-password.service.ts +++ b/services/iam-service/src/modules/auth/change-password.service.ts @@ -1,10 +1,12 @@ -import bcrypt from 'bcryptjs'; -import { PrismaClient } from '@prisma/client'; -import { getPrismaClient } from '../../config/database.config'; import { logger } from '@goodgo/logger'; -import { auditService } from '../../core/events/audit.service'; +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcryptjs'; import { Request } from 'express'; +import { getPrismaClient } from '../../config/database.config'; +import { auditService } from '../../core/events/audit.service'; + + /** * EN: Change Password Service * VI: Service đổi mật khẩu diff --git a/services/iam-service/src/modules/common/cache.service.ts b/services/iam-service/src/modules/common/cache.service.ts index 7819a4df..bdddf34b 100644 --- a/services/iam-service/src/modules/common/cache.service.ts +++ b/services/iam-service/src/modules/common/cache.service.ts @@ -1,6 +1,7 @@ -import { getRedisClient } from '../../config/redis.config'; import { logger } from '@goodgo/logger'; +import { getRedisClient } from '../../config/redis.config'; + /** * EN: Service for caching data (Redis wrapper) * VI: Service cho việc caching dữ liệu (Redis wrapper) diff --git a/services/iam-service/src/modules/common/circuit-breaker.ts b/services/iam-service/src/modules/common/circuit-breaker.ts index e889adb2..75070891 100644 --- a/services/iam-service/src/modules/common/circuit-breaker.ts +++ b/services/iam-service/src/modules/common/circuit-breaker.ts @@ -1,5 +1,5 @@ -import CircuitBreaker from 'opossum'; import { logger } from '@goodgo/logger'; +import CircuitBreaker from 'opossum'; /** * EN: Circuit Breaker Configuration diff --git a/services/iam-service/src/modules/common/repository.ts b/services/iam-service/src/modules/common/repository.ts index 7c1ccbf9..d83cfc99 100644 --- a/services/iam-service/src/modules/common/repository.ts +++ b/services/iam-service/src/modules/common/repository.ts @@ -1,5 +1,6 @@ -import { PrismaClient } from '@prisma/client'; import { logger } from '@goodgo/logger'; +import { PrismaClient } from '@prisma/client'; + import { DatabaseError } from '../../errors/http-error'; /** diff --git a/services/iam-service/src/modules/feature/__tests__/feature.repository.test.ts b/services/iam-service/src/modules/feature/__tests__/feature.repository.test.ts index 4ec4ebc9..0672a3bc 100644 --- a/services/iam-service/src/modules/feature/__tests__/feature.repository.test.ts +++ b/services/iam-service/src/modules/feature/__tests__/feature.repository.test.ts @@ -1,37 +1,38 @@ -import { FeatureRepository } from '../feature.repository'; import { ConflictError } from '../../../errors/http-error'; +import { FeatureRepository } from '../feature.repository'; +import { prisma } from '../../../config/database.config'; -// EN: Mock Prisma client -// VI: Mock Prisma client -const mockPrismaClient = { - feature: { - findUnique: jest.fn(), - findMany: jest.fn(), - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - count: jest.fn(), - }, - $transaction: jest.fn(), -}; +// EN: Use jest.mocked to properly type the mock +// VI: Sử dụng jest.mocked để type mock đúng cách +const mockPrisma = jest.mocked(prisma); -jest.mock('../../../config/database.config', () => ({ - prisma: mockPrismaClient, -})); +// EN: Helper to create complete mock feature objects +// VI: Helper để tạo mock feature objects hoàn chỉnh +const createMockFeature = (overrides: Partial = {}) => ({ + id: 'test-id', + name: 'test-feature', + title: 'Test Feature', + description: 'Test description', + config: {}, + enabled: true, + version: '1.0.0', + tags: [], + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, +}); describe('FeatureRepository', () => { let repository: FeatureRepository; - let mockPrisma: any; beforeEach(() => { jest.clearAllMocks(); repository = new FeatureRepository(); - mockPrisma = mockPrismaClient; }); describe('findById', () => { it('should return feature when found', async () => { - const mockFeature = { id: '1', name: 'test-feature', enabled: true }; + const mockFeature = createMockFeature({ id: '1', name: 'test-feature', enabled: true }); mockPrisma.feature.findUnique.mockResolvedValue(mockFeature); const result = await repository.findById('1'); @@ -51,7 +52,7 @@ describe('FeatureRepository', () => { describe('findByName', () => { it('should return feature when found by name', async () => { - const mockFeature = { id: '1', name: 'test-feature', enabled: true }; + const mockFeature = createMockFeature({ id: '1', name: 'test-feature', enabled: true }); mockPrisma.feature.findUnique.mockResolvedValue(mockFeature); const result = await repository.findByName('test-feature'); @@ -66,8 +67,8 @@ describe('FeatureRepository', () => { describe('findAll', () => { it('should return all features with default options', async () => { const mockFeatures = [ - { id: '1', name: 'feature-1' }, - { id: '2', name: 'feature-2' }, + createMockFeature({ id: '1', name: 'feature-1' }), + createMockFeature({ id: '2', name: 'feature-2' }), ]; mockPrisma.feature.findMany.mockResolvedValue(mockFeatures); @@ -79,7 +80,7 @@ describe('FeatureRepository', () => { it('should return features with custom options', async () => { const options = { where: { enabled: true }, orderBy: { createdAt: 'desc' } }; - const mockFeatures = [{ id: '1', name: 'enabled-feature' }]; + const mockFeatures = [createMockFeature({ id: '1', name: 'enabled-feature' })]; mockPrisma.feature.findMany.mockResolvedValue(mockFeatures); const result = await repository.findAll(options); @@ -92,7 +93,7 @@ describe('FeatureRepository', () => { describe('create', () => { it('should create feature successfully when name is unique', async () => { const createData = { name: 'new-feature', title: 'New Feature' }; - const mockFeature = { id: '1', ...createData, enabled: true }; + const mockFeature = createMockFeature({ id: '1', ...createData, enabled: true }); // Mock no existing feature mockPrisma.feature.findUnique.mockResolvedValue(null); @@ -109,7 +110,7 @@ describe('FeatureRepository', () => { it('should throw ConflictError when feature name already exists', async () => { const createData = { name: 'existing-feature' }; - const existingFeature = { id: '1', name: 'existing-feature' }; + const existingFeature = createMockFeature({ id: '1', name: 'existing-feature' }); mockPrisma.feature.findUnique.mockResolvedValue(existingFeature); @@ -121,7 +122,7 @@ describe('FeatureRepository', () => { describe('update', () => { it('should update feature successfully', async () => { const updateData = { title: 'Updated Title' }; - const mockFeature = { id: '1', name: 'test-feature', title: 'Updated Title' }; + const mockFeature = createMockFeature({ id: '1', name: 'test-feature', title: 'Updated Title' }); mockPrisma.feature.update.mockResolvedValue(mockFeature); @@ -137,7 +138,8 @@ describe('FeatureRepository', () => { describe('delete', () => { it('should delete feature successfully', async () => { - mockPrisma.feature.delete.mockResolvedValue({}); + const mockFeature = createMockFeature({ id: '1' }); + mockPrisma.feature.delete.mockResolvedValue(mockFeature); const result = await repository.delete('1'); @@ -190,8 +192,8 @@ describe('FeatureRepository', () => { describe('toggleEnabled', () => { it('should toggle feature from disabled to enabled', async () => { - const existingFeature = { id: '1', name: 'test-feature', enabled: false }; - const updatedFeature = { ...existingFeature, enabled: true }; + const existingFeature = createMockFeature({ id: '1', name: 'test-feature', enabled: false }); + const updatedFeature = createMockFeature({ ...existingFeature, enabled: true }); mockPrisma.feature.findUnique.mockResolvedValue(existingFeature); mockPrisma.feature.update.mockResolvedValue(updatedFeature); @@ -217,8 +219,8 @@ describe('FeatureRepository', () => { it('should return features matching tags', async () => { const tags = ['web', 'api']; const mockFeatures = [ - { id: '1', name: 'web-feature', tags: ['web'] }, - { id: '2', name: 'api-feature', tags: ['api'] }, + createMockFeature({ id: '1', name: 'web-feature', tags: ['web'] }), + createMockFeature({ id: '2', name: 'api-feature', tags: ['api'] }), ]; mockPrisma.feature.findMany.mockResolvedValue(mockFeatures); @@ -240,8 +242,8 @@ describe('FeatureRepository', () => { describe('findEnabled', () => { it('should return only enabled features', async () => { const mockFeatures = [ - { id: '1', name: 'enabled-feature', enabled: true }, - { id: '2', name: 'disabled-feature', enabled: false }, + createMockFeature({ id: '1', name: 'enabled-feature', enabled: true }), + createMockFeature({ id: '2', name: 'disabled-feature', enabled: false }), ]; mockPrisma.feature.findMany.mockResolvedValue([mockFeatures[0]]); @@ -260,7 +262,7 @@ describe('FeatureRepository', () => { it('should search features by query', async () => { const query = 'test'; const mockFeatures = [ - { id: '1', name: 'test-feature', title: 'Test Feature' }, + createMockFeature({ id: '1', name: 'test-feature', title: 'Test Feature' }), ]; mockPrisma.feature.findMany.mockResolvedValue(mockFeatures); @@ -285,9 +287,9 @@ describe('FeatureRepository', () => { describe('getStatistics', () => { it('should return feature statistics', async () => { const mockFeatures = [ - { id: '1', name: 'feature1', tags: ['web', 'api'], enabled: true }, - { id: '2', name: 'feature2', tags: ['web'], enabled: false }, - { id: '3', name: 'feature3', tags: ['mobile'], enabled: true }, + createMockFeature({ id: '1', name: 'feature1', tags: ['web', 'api'], enabled: true }), + createMockFeature({ id: '2', name: 'feature2', tags: ['web'], enabled: false }), + createMockFeature({ id: '3', name: 'feature3', tags: ['mobile'], enabled: true }), ]; mockPrisma.feature.count diff --git a/services/iam-service/src/modules/feature/__tests__/feature.service.test.ts b/services/iam-service/src/modules/feature/__tests__/feature.service.test.ts index c9e2163a..399db78b 100644 --- a/services/iam-service/src/modules/feature/__tests__/feature.service.test.ts +++ b/services/iam-service/src/modules/feature/__tests__/feature.service.test.ts @@ -1,6 +1,7 @@ -import { FeatureService } from '../feature.service'; import { logger } from '@goodgo/logger'; + import { featureRepository } from '../feature.repository'; +import { FeatureService } from '../feature.service'; // EN: Mock the logger to avoid console output during tests // VI: Mock logger để tránh output console trong tests diff --git a/services/iam-service/src/modules/feature/feature.controller.ts b/services/iam-service/src/modules/feature/feature.controller.ts index 616f527c..87110b75 100644 --- a/services/iam-service/src/modules/feature/feature.controller.ts +++ b/services/iam-service/src/modules/feature/feature.controller.ts @@ -1,8 +1,11 @@ -import { Request, Response } from 'express'; import { ApiResponse } from '@goodgo/types'; -import { FeatureService } from './feature.service'; +import { Request, Response } from 'express'; + import { asyncHandler } from '../../middlewares/error.middleware'; +import { FeatureService } from './feature.service'; + + /** * EN: Controller for Feature module * VI: Controller cho module Feature diff --git a/services/iam-service/src/modules/feature/feature.module.ts b/services/iam-service/src/modules/feature/feature.module.ts index 623dfdcc..dc8c059d 100644 --- a/services/iam-service/src/modules/feature/feature.module.ts +++ b/services/iam-service/src/modules/feature/feature.module.ts @@ -1,6 +1,8 @@ import { Router } from 'express'; -import { FeatureController } from './feature.controller'; + import { validateDto } from '../../middlewares/validation.middleware'; + +import { FeatureController } from './feature.controller'; import { createFeatureDtoSchema, updateFeatureDtoSchema } from './feature.dto'; /** diff --git a/services/iam-service/src/modules/feature/feature.repository.ts b/services/iam-service/src/modules/feature/feature.repository.ts index 7d880e15..392e986e 100644 --- a/services/iam-service/src/modules/feature/feature.repository.ts +++ b/services/iam-service/src/modules/feature/feature.repository.ts @@ -1,8 +1,9 @@ -import { prisma } from '../../config/database.config'; -import { BaseRepository, IRepository } from '../common/repository'; -import { ConflictError } from '../../errors/http-error'; import { logger } from '@goodgo/logger'; +import { prisma } from '../../config/database.config'; +import { ConflictError } from '../../errors/http-error'; +import { BaseRepository, IRepository } from '../common/repository'; + // EN: Feature entity type from Prisma // VI: Feature entity type từ Prisma type Feature = { diff --git a/services/iam-service/src/modules/feature/feature.service.ts b/services/iam-service/src/modules/feature/feature.service.ts index 8897d259..4c070c5f 100644 --- a/services/iam-service/src/modules/feature/feature.service.ts +++ b/services/iam-service/src/modules/feature/feature.service.ts @@ -1,7 +1,9 @@ import { logger } from '@goodgo/logger'; -import { featureRepository } from './feature.repository'; + import { NotFoundError } from '../../errors/http-error'; +import { featureRepository } from './feature.repository'; + /** * EN: Service for managing features in the system * VI: Service để quản lý các features trong hệ thống diff --git a/services/iam-service/src/modules/governance/compliance/compliance.controller.ts b/services/iam-service/src/modules/governance/compliance/compliance.controller.ts index 5c58cb51..0bc6f95c 100644 --- a/services/iam-service/src/modules/governance/compliance/compliance.controller.ts +++ b/services/iam-service/src/modules/governance/compliance/compliance.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { complianceService } from './compliance.service'; -import { GenerateComplianceReportDto, ReportFiltersDto } from '../governance.dto'; import { z } from 'zod'; + import { NotFoundError, BadRequestError } from '../../../errors/http-error'; +import { GenerateComplianceReportDto, ReportFiltersDto } from '../governance.dto'; + +import { complianceService } from './compliance.service'; /** * EN: Compliance Controller diff --git a/services/iam-service/src/modules/governance/compliance/compliance.service.ts b/services/iam-service/src/modules/governance/compliance/compliance.service.ts index 2faae5eb..21eacf7e 100644 --- a/services/iam-service/src/modules/governance/compliance/compliance.service.ts +++ b/services/iam-service/src/modules/governance/compliance/compliance.service.ts @@ -1,10 +1,11 @@ +import { logger } from '@goodgo/logger'; import { PrismaClient, ComplianceType, ReportStatus } from '@prisma/client'; +import { Request } from 'express'; + import { getPrismaClient } from '../../../config/database.config'; import { auditService } from '../../../core/events/audit.service'; -import { logger } from '@goodgo/logger'; -import { Request } from 'express'; import { NotFoundError, BadRequestError } from '../../../errors/http-error'; -import { GenerateComplianceReportDto, ReportFiltersDto } from '../governance.dto'; +import { ReportFiltersDto } from '../governance.dto'; /** * EN: Compliance Reporting Service diff --git a/services/iam-service/src/modules/governance/policy/policy-governance.controller.ts b/services/iam-service/src/modules/governance/policy/policy-governance.controller.ts index e7792e22..945247ac 100644 --- a/services/iam-service/src/modules/governance/policy/policy-governance.controller.ts +++ b/services/iam-service/src/modules/governance/policy/policy-governance.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { policyGovernanceService } from './policy-governance.service'; -import { CreatePolicyTemplateDto, UpdatePolicyTemplateDto, CreatePolicyFromTemplateDto, TestPolicyDto } from '../governance.dto'; import { z } from 'zod'; + import { NotFoundError } from '../../../errors/http-error'; +import { CreatePolicyTemplateDto, TestPolicyDto } from '../governance.dto'; + +import { policyGovernanceService } from './policy-governance.service'; /** * EN: Policy Governance Controller diff --git a/services/iam-service/src/modules/governance/policy/policy-governance.service.ts b/services/iam-service/src/modules/governance/policy/policy-governance.service.ts index 89b5a99e..2235262b 100644 --- a/services/iam-service/src/modules/governance/policy/policy-governance.service.ts +++ b/services/iam-service/src/modules/governance/policy/policy-governance.service.ts @@ -1,10 +1,11 @@ +import { logger } from '@goodgo/logger'; import { PrismaClient, PolicyCategory } from '@prisma/client'; +import { Request } from 'express'; + import { getPrismaClient } from '../../../config/database.config'; import { auditService } from '../../../core/events/audit.service'; -import { logger } from '@goodgo/logger'; -import { Request } from 'express'; -import { NotFoundError, BadRequestError } from '../../../errors/http-error'; -import { CreatePolicyTemplateDto, UpdatePolicyTemplateDto, CreatePolicyFromTemplateDto, TestPolicyDto } from '../governance.dto'; +import { NotFoundError } from '../../../errors/http-error'; +import { CreatePolicyTemplateDto, CreatePolicyFromTemplateDto } from '../governance.dto'; /** * EN: Policy Governance Service diff --git a/services/iam-service/src/modules/governance/reporting/reporting.controller.ts b/services/iam-service/src/modules/governance/reporting/reporting.controller.ts index 01ded17e..81c3f7c8 100644 --- a/services/iam-service/src/modules/governance/reporting/reporting.controller.ts +++ b/services/iam-service/src/modules/governance/reporting/reporting.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { reportingService } from './reporting.service'; -import { AccessSummaryFiltersDto, UserActivityFiltersDto, SecurityEventsFiltersDto } from '../governance.dto'; import { z } from 'zod'; +import { AccessSummaryFiltersDto, UserActivityFiltersDto, SecurityEventsFiltersDto } from '../governance.dto'; + +import { reportingService } from './reporting.service'; + /** * EN: Reporting Controller * VI: Controller báo cáo diff --git a/services/iam-service/src/modules/governance/reporting/reporting.service.ts b/services/iam-service/src/modules/governance/reporting/reporting.service.ts index c4a9c437..481b8a3c 100644 --- a/services/iam-service/src/modules/governance/reporting/reporting.service.ts +++ b/services/iam-service/src/modules/governance/reporting/reporting.service.ts @@ -1,6 +1,6 @@ import { PrismaClient } from '@prisma/client'; + import { getPrismaClient } from '../../../config/database.config'; -import { logger } from '@goodgo/logger'; import { AccessSummaryFiltersDto, UserActivityFiltersDto, SecurityEventsFiltersDto } from '../governance.dto'; /** @@ -19,7 +19,6 @@ export class ReportingService { * VI: Lấy tổng quan truy cập */ async getAccessSummary(filters: AccessSummaryFiltersDto): Promise { - const where: any = {}; const dateFilter: any = {}; if (filters.periodStart || filters.periodEnd) { diff --git a/services/iam-service/src/modules/governance/risk/risk.controller.ts b/services/iam-service/src/modules/governance/risk/risk.controller.ts index 83ccbed6..cd37d1ce 100644 --- a/services/iam-service/src/modules/governance/risk/risk.controller.ts +++ b/services/iam-service/src/modules/governance/risk/risk.controller.ts @@ -1,9 +1,11 @@ import { Request, Response } from 'express'; -import { riskService } from './risk.service'; -import { CalculateRiskScoreDto, RiskFiltersDto } from '../governance.dto'; import { z } from 'zod'; -import { NotFoundError } from '../../../errors/http-error'; + import { getPrismaClient } from '../../../config/database.config'; +import { NotFoundError } from '../../../errors/http-error'; +import { CalculateRiskScoreDto, RiskFiltersDto } from '../governance.dto'; + +import { riskService } from './risk.service'; /** * EN: Risk Management Controller diff --git a/services/iam-service/src/modules/governance/risk/risk.service.ts b/services/iam-service/src/modules/governance/risk/risk.service.ts index d8a720dd..18ba55b5 100644 --- a/services/iam-service/src/modules/governance/risk/risk.service.ts +++ b/services/iam-service/src/modules/governance/risk/risk.service.ts @@ -1,10 +1,11 @@ +import { logger } from '@goodgo/logger'; import { PrismaClient, RiskLevel } from '@prisma/client'; +import { Request } from 'express'; + import { getPrismaClient } from '../../../config/database.config'; import { auditService } from '../../../core/events/audit.service'; -import { logger } from '@goodgo/logger'; -import { Request } from 'express'; import { NotFoundError } from '../../../errors/http-error'; -import { CalculateRiskScoreDto, RiskFiltersDto } from '../governance.dto'; +import { RiskFiltersDto } from '../governance.dto'; /** * EN: Risk Management Service diff --git a/services/iam-service/src/modules/health/__tests__/health.controller.test.ts b/services/iam-service/src/modules/health/__tests__/health.controller.test.ts index bfad6bff..51bf3861 100644 --- a/services/iam-service/src/modules/health/__tests__/health.controller.test.ts +++ b/services/iam-service/src/modules/health/__tests__/health.controller.test.ts @@ -1,6 +1,7 @@ import { Request, Response } from 'express'; -import { HealthController } from '../health.controller'; + import { prisma } from '../../../config/database.config'; +import { HealthController } from '../health.controller'; jest.mock('../../../config/database.config'); diff --git a/services/iam-service/src/modules/health/health.controller.ts b/services/iam-service/src/modules/health/health.controller.ts index aa202311..8e24d82e 100644 --- a/services/iam-service/src/modules/health/health.controller.ts +++ b/services/iam-service/src/modules/health/health.controller.ts @@ -1,6 +1,7 @@ -import { Request, Response } from 'express'; -import { prisma } from '../../config/database.config'; import { ApiResponse } from '@goodgo/types'; +import { Request, Response } from 'express'; + +import { prisma } from '../../config/database.config'; /** * EN: Controller for health checks diff --git a/services/iam-service/src/modules/identity/group/group.controller.ts b/services/iam-service/src/modules/identity/group/group.controller.ts index e6d21d23..c7b9c4eb 100644 --- a/services/iam-service/src/modules/identity/group/group.controller.ts +++ b/services/iam-service/src/modules/identity/group/group.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { groupService } from './group.service'; -import { CreateGroupDto, UpdateGroupDto, AddGroupMemberDto } from '../identity.dto'; import { z } from 'zod'; + import { NotFoundError, ConflictError } from '../../../errors/http-error'; +import { CreateGroupDto, UpdateGroupDto, AddGroupMemberDto } from '../identity.dto'; + +import { groupService } from './group.service'; /** * EN: Group Controller diff --git a/services/iam-service/src/modules/identity/group/group.service.ts b/services/iam-service/src/modules/identity/group/group.service.ts index 6525f987..25d9343f 100644 --- a/services/iam-service/src/modules/identity/group/group.service.ts +++ b/services/iam-service/src/modules/identity/group/group.service.ts @@ -1,12 +1,13 @@ +import { logger } from '@goodgo/logger'; import { PrismaClient } from '@prisma/client'; +import { Request } from 'express'; + import { getPrismaClient } from '../../../config/database.config'; +import { auditService } from '../../../core/events/audit.service'; +import { NotFoundError, ConflictError } from '../../../errors/http-error'; import { GroupRepository } from '../../../repositories/group.repository'; import { OrganizationRepository } from '../../../repositories/organization.repository'; -import { auditService } from '../../../core/events/audit.service'; -import { logger } from '@goodgo/logger'; -import { Request } from 'express'; -import { NotFoundError, ConflictError } from '../../../errors/http-error'; -import { CreateGroupDto, UpdateGroupDto, AddGroupMemberDto } from '../identity.dto'; +import { CreateGroupDto, UpdateGroupDto } from '../identity.dto'; /** * EN: Group Service diff --git a/services/iam-service/src/modules/identity/organization/organization.controller.ts b/services/iam-service/src/modules/identity/organization/organization.controller.ts index b96e6e51..78149e4e 100644 --- a/services/iam-service/src/modules/identity/organization/organization.controller.ts +++ b/services/iam-service/src/modules/identity/organization/organization.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { organizationService } from './organization.service'; -import { CreateOrganizationDto, UpdateOrganizationDto } from '../identity.dto'; import { z } from 'zod'; + import { NotFoundError, ConflictError } from '../../../errors/http-error'; +import { CreateOrganizationDto, UpdateOrganizationDto } from '../identity.dto'; + +import { organizationService } from './organization.service'; /** * EN: Organization Controller diff --git a/services/iam-service/src/modules/identity/organization/organization.service.ts b/services/iam-service/src/modules/identity/organization/organization.service.ts index 0f3da517..c2a95152 100644 --- a/services/iam-service/src/modules/identity/organization/organization.service.ts +++ b/services/iam-service/src/modules/identity/organization/organization.service.ts @@ -1,10 +1,11 @@ -import { PrismaClient } from '@prisma/client'; -import { getPrismaClient } from '../../../config/database.config'; -import { OrganizationRepository } from '../../../repositories/organization.repository'; -import { auditService } from '../../../core/events/audit.service'; import { logger } from '@goodgo/logger'; +import { PrismaClient } from '@prisma/client'; import { Request } from 'express'; + +import { getPrismaClient } from '../../../config/database.config'; +import { auditService } from '../../../core/events/audit.service'; import { NotFoundError, ConflictError } from '../../../errors/http-error'; +import { OrganizationRepository } from '../../../repositories/organization.repository'; import { CreateOrganizationDto, UpdateOrganizationDto } from '../identity.dto'; /** diff --git a/services/iam-service/src/modules/identity/profile/profile.controller.ts b/services/iam-service/src/modules/identity/profile/profile.controller.ts index 864e5090..6beb818e 100644 --- a/services/iam-service/src/modules/identity/profile/profile.controller.ts +++ b/services/iam-service/src/modules/identity/profile/profile.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { profileService } from './profile.service'; -import { UpdateUserProfileDto } from '../identity.dto'; import { z } from 'zod'; + import { NotFoundError } from '../../../errors/http-error'; +import { UpdateUserProfileDto } from '../identity.dto'; + +import { profileService } from './profile.service'; /** * EN: Profile Management Controller diff --git a/services/iam-service/src/modules/identity/profile/profile.service.ts b/services/iam-service/src/modules/identity/profile/profile.service.ts index b697bc7f..4be69ea2 100644 --- a/services/iam-service/src/modules/identity/profile/profile.service.ts +++ b/services/iam-service/src/modules/identity/profile/profile.service.ts @@ -1,11 +1,12 @@ -import { PrismaClient } from '@prisma/client'; -import { getPrismaClient } from '../../../config/database.config'; -import { UserProfileRepository } from '../../../repositories/user-profile.repository'; -import { auditService } from '../../../core/events/audit.service'; import { logger } from '@goodgo/logger'; +import { PrismaClient } from '@prisma/client'; import { Request } from 'express'; + +import { getPrismaClient } from '../../../config/database.config'; +import { auditService } from '../../../core/events/audit.service'; import { NotFoundError } from '../../../errors/http-error'; -import { UpdateUserProfileDto, CreateUserProfileDto } from '../identity.dto'; +import { UserProfileRepository } from '../../../repositories/user-profile.repository'; +import { UpdateUserProfileDto } from '../identity.dto'; /** * EN: Profile Management Service diff --git a/services/iam-service/src/modules/identity/user/user.controller.ts b/services/iam-service/src/modules/identity/user/user.controller.ts index 03359571..e7ad7377 100644 --- a/services/iam-service/src/modules/identity/user/user.controller.ts +++ b/services/iam-service/src/modules/identity/user/user.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { userManagementService } from './user.service'; -import { UpdateUserDto, UserFiltersDto, BulkImportUsersDto } from '../identity.dto'; import { z } from 'zod'; + import { NotFoundError } from '../../../errors/http-error'; +import { UpdateUserDto, UserFiltersDto, BulkImportUsersDto } from '../identity.dto'; + +import { userManagementService } from './user.service'; /** * EN: User Management Controller diff --git a/services/iam-service/src/modules/identity/user/user.service.ts b/services/iam-service/src/modules/identity/user/user.service.ts index ae094686..db2c16fc 100644 --- a/services/iam-service/src/modules/identity/user/user.service.ts +++ b/services/iam-service/src/modules/identity/user/user.service.ts @@ -1,11 +1,12 @@ -import { PrismaClient } from '@prisma/client'; -import { getPrismaClient } from '../../../config/database.config'; -import { UserRepository } from '../../../repositories/user.repository'; -import { UserProfileRepository } from '../../../repositories/user-profile.repository'; -import { auditService } from '../../../core/events/audit.service'; import { logger } from '@goodgo/logger'; +import { PrismaClient } from '@prisma/client'; import { Request } from 'express'; -import { NotFoundError, ConflictError, BadRequestError } from '../../../errors/http-error'; + +import { getPrismaClient } from '../../../config/database.config'; +import { auditService } from '../../../core/events/audit.service'; +import { NotFoundError, ConflictError } from '../../../errors/http-error'; +import { UserProfileRepository } from '../../../repositories/user-profile.repository'; +import { UserRepository } from '../../../repositories/user.repository'; import { UpdateUserDto, UserFiltersDto, BulkImportUsersDto } from '../identity.dto'; /** diff --git a/services/iam-service/src/modules/identity/verification/verification.controller.ts b/services/iam-service/src/modules/identity/verification/verification.controller.ts index ca4c1689..d2300200 100644 --- a/services/iam-service/src/modules/identity/verification/verification.controller.ts +++ b/services/iam-service/src/modules/identity/verification/verification.controller.ts @@ -1,8 +1,10 @@ import { Request, Response } from 'express'; -import { verificationService } from './verification.service'; -import { VerificationRequestDto, VerifyEmailDto, VerifyPhoneDto } from '../identity.dto'; import { z } from 'zod'; + import { BadRequestError, NotFoundError } from '../../../errors/http-error'; +import { VerifyEmailDto, VerifyPhoneDto } from '../identity.dto'; + +import { verificationService } from './verification.service'; /** * EN: Identity Verification Controller diff --git a/services/iam-service/src/modules/identity/verification/verification.service.ts b/services/iam-service/src/modules/identity/verification/verification.service.ts index 28866365..1636b52c 100644 --- a/services/iam-service/src/modules/identity/verification/verification.service.ts +++ b/services/iam-service/src/modules/identity/verification/verification.service.ts @@ -1,15 +1,18 @@ -import { PrismaClient, VerificationType, VerificationStatus } from '@prisma/client'; -import { getPrismaClient } from '../../../config/database.config'; -import { IdentityVerificationRepository } from '../../../repositories/identity-verification.repository'; -import { UserRepository } from '../../../repositories/user.repository'; -import { UserProfileRepository } from '../../../repositories/user-profile.repository'; -import { auditService } from '../../../core/events/audit.service'; import { logger } from '@goodgo/logger'; +import { PrismaClient, VerificationType, VerificationStatus } from '@prisma/client'; import { Request } from 'express'; +import { v4 as uuidv4 } from 'uuid'; + +import { getPrismaClient } from '../../../config/database.config'; +import { auditService } from '../../../core/events/audit.service'; import { NotFoundError, BadRequestError } from '../../../errors/http-error'; +import { IdentityVerificationRepository } from '../../../repositories/identity-verification.repository'; +import { UserProfileRepository } from '../../../repositories/user-profile.repository'; +import { UserRepository } from '../../../repositories/user.repository'; + + // EN: DTOs imported for future use / VI: DTOs được import để sử dụng sau // import { VerificationRequestDto, VerifyEmailDto, VerifyPhoneDto } from '../identity.dto'; -import { v4 as uuidv4 } from 'uuid'; /** * EN: Identity Verification Service diff --git a/services/iam-service/src/modules/metrics/metrics.controller.ts b/services/iam-service/src/modules/metrics/metrics.controller.ts index 6ee68cdf..e1552957 100644 --- a/services/iam-service/src/modules/metrics/metrics.controller.ts +++ b/services/iam-service/src/modules/metrics/metrics.controller.ts @@ -1,6 +1,6 @@ +import { logger } from '@goodgo/logger'; import { Request, Response } from 'express'; import { register } from 'prom-client'; -import { logger } from '@goodgo/logger'; /** * EN: Controller for handling metrics requests diff --git a/services/iam-service/src/modules/mfa/mfa.controller.ts b/services/iam-service/src/modules/mfa/mfa.controller.ts index f48884b0..4027a93a 100644 --- a/services/iam-service/src/modules/mfa/mfa.controller.ts +++ b/services/iam-service/src/modules/mfa/mfa.controller.ts @@ -1,10 +1,7 @@ import { Request, Response } from 'express'; -import { mfaService } from './mfa.service'; import { z } from 'zod'; -const EnableTOTPDto = z.object({ - token: z.string().length(6), -}); +import { mfaService } from './mfa.service'; const VerifyTOTPDto = z.object({ token: z.string().length(6), diff --git a/services/iam-service/src/modules/mfa/mfa.service.ts b/services/iam-service/src/modules/mfa/mfa.service.ts index 0b8454d2..e73cfebc 100644 --- a/services/iam-service/src/modules/mfa/mfa.service.ts +++ b/services/iam-service/src/modules/mfa/mfa.service.ts @@ -1,8 +1,13 @@ -import { PrismaClient, MFAType } from '@prisma/client'; -import { getPrismaClient } from '../../config/database.config'; +import crypto from 'crypto'; + import { logger } from '@goodgo/logger'; -import speakeasy from 'speakeasy'; +import { PrismaClient } from '@prisma/client'; import QRCode from 'qrcode'; +import speakeasy from 'speakeasy'; + +import { getPrismaClient } from '../../config/database.config'; +import { encryptionService } from '../../core/security/encryption.service'; + /** * EN: MFA Service for multi-factor authentication @@ -55,24 +60,34 @@ export class MFAService { }); if (verified) { - // Create MFA device + // Encrypt the secret before storing + const encryptedSecret = encryptionService.encrypt(secret); + + // Create MFA device with encrypted secret await this.prisma.mFADevice.create({ data: { userId, type: 'TOTP', name: 'TOTP Authenticator', - secret, + secret: encryptedSecret, isActive: true, }, }); + // Generate and store backup codes + const backupCodes = this.generateBackupCodes(); + await this.storeBackupCodes(userId, backupCodes); + // Enable MFA for user await this.prisma.user.update({ where: { id: userId }, data: { mfaEnabled: true }, }); - logger.info('TOTP enabled for user', { userId }); + logger.info('TOTP enabled for user with backup codes', { + userId, + backupCodesCount: backupCodes.length, + }); return true; } @@ -96,8 +111,11 @@ export class MFAService { return false; } + // Decrypt the secret before verification + const decryptedSecret = encryptionService.decrypt(device.secret); + const verified = speakeasy.totp.verify({ - secret: device.secret, + secret: decryptedSecret, encoding: 'base32', token, window: 2, @@ -126,7 +144,11 @@ export class MFAService { await this.prisma.user.update({ where: { id: userId }, - data: { mfaEnabled: false, mfaSecret: null }, + data: { + mfaEnabled: false, + mfaSecret: null, + mfaBackupCodes: null, + }, }); logger.info('MFA disabled for user', { userId }); @@ -145,6 +167,112 @@ export class MFAService { orderBy: { createdAt: 'desc' }, }); } + + /** + * EN: Generate backup codes for MFA recovery + * VI: Tạo mã backup để khôi phục MFA + */ + generateBackupCodes(): string[] { + const codes: string[] = []; + for (let i = 0; i < 10; i++) { + // Generate 8-character alphanumeric codes + const code = crypto.randomBytes(4).toString('hex').toUpperCase(); + codes.push(code); + } + return codes; + } + + /** + * EN: Store backup codes for user (encrypted) + * VI: Lưu mã backup cho người dùng (đã mã hóa) + */ + async storeBackupCodes(userId: string, codes: string[]): Promise { + const encryptedCodes = encryptionService.encrypt(JSON.stringify(codes)); + + await this.prisma.user.update({ + where: { id: userId }, + data: { mfaBackupCodes: encryptedCodes }, + }); + + logger.info('Backup codes stored for user', { userId, count: codes.length }); + } + + /** + * EN: Get backup codes for user (decrypted) + * VI: Lấy mã backup cho người dùng (đã giải mã) + */ + async getBackupCodes(userId: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { mfaBackupCodes: true }, + }); + + if (!user?.mfaBackupCodes) { + return []; + } + + try { + const decryptedCodes = encryptionService.decrypt(user.mfaBackupCodes); + return JSON.parse(decryptedCodes); + } catch (error) { + logger.error('Failed to decrypt backup codes', { userId, error }); + return []; + } + } + + /** + * EN: Validate and consume backup code + * VI: Xác thực và tiêu thụ mã backup + */ + async validateBackupCode(userId: string, code: string): Promise { + const codes = await this.getBackupCodes(userId); + + if (!codes.includes(code)) { + return false; + } + + // Remove the used code + const remainingCodes = codes.filter(c => c !== code); + + if (remainingCodes.length === 0) { + // All backup codes used, disable MFA backup + await this.prisma.user.update({ + where: { id: userId }, + data: { mfaBackupCodes: null }, + }); + } else { + // Update with remaining codes + await this.storeBackupCodes(userId, remainingCodes); + } + + logger.info('Backup code validated and consumed', { + userId, + remainingCodes: remainingCodes.length, + }); + + return true; + } + + /** + * EN: Regenerate backup codes (invalidates all existing ones) + * VI: Tái tạo mã backup (hủy tất cả mã hiện tại) + */ + async regenerateBackupCodes(userId: string): Promise { + const newCodes = this.generateBackupCodes(); + await this.storeBackupCodes(userId, newCodes); + + logger.info('Backup codes regenerated', { userId }); + return newCodes; + } + + /** + * EN: Check if user has backup codes available + * VI: Kiểm tra người dùng có mã backup không + */ + async hasBackupCodes(userId: string): Promise { + const codes = await this.getBackupCodes(userId); + return codes.length > 0; + } } export const mfaService = new MFAService(); diff --git a/services/iam-service/src/modules/oidc/oidc-client.service.ts b/services/iam-service/src/modules/oidc/oidc-client.service.ts index 302e52b6..c4bd57c7 100644 --- a/services/iam-service/src/modules/oidc/oidc-client.service.ts +++ b/services/iam-service/src/modules/oidc/oidc-client.service.ts @@ -1,5 +1,7 @@ import crypto from 'crypto'; + import { logger } from '@goodgo/logger'; + import { OIDCDiscoveryDocument, OIDCTokenExchangeResponse } from '../../types/oidc.types'; /** diff --git a/services/iam-service/src/modules/oidc/oidc-provider.service.ts b/services/iam-service/src/modules/oidc/oidc-provider.service.ts index 555ca6a1..18bd608f 100644 --- a/services/iam-service/src/modules/oidc/oidc-provider.service.ts +++ b/services/iam-service/src/modules/oidc/oidc-provider.service.ts @@ -1,11 +1,14 @@ import crypto from 'crypto'; + import { logger } from '@goodgo/logger'; -import { jwtService } from '../token/jwt.service'; -import { OIDCDiscoveryDocument, JWKSResponse } from '../../types/oidc.types'; -import { cacheService } from '../../core/cache/cache.service'; -import { getPrismaClient } from '../../config/database.config'; import { PrismaClient } from '@prisma/client'; +import { getPrismaClient } from '../../config/database.config'; +import { cacheService } from '../../core/cache/cache.service'; +import { OIDCDiscoveryDocument, JWKSResponse } from '../../types/oidc.types'; +import { jwtService } from '../token/jwt.service'; + + /** * EN: OIDC Provider Service (simplified implementation) * VI: Service OIDC Provider (triển khai đơn giản) diff --git a/services/iam-service/src/modules/oidc/oidc.controller.ts b/services/iam-service/src/modules/oidc/oidc.controller.ts index 9ad5dc83..18f7e71b 100644 --- a/services/iam-service/src/modules/oidc/oidc.controller.ts +++ b/services/iam-service/src/modules/oidc/oidc.controller.ts @@ -1,7 +1,7 @@ -import { Request, Response } from 'express'; -import { oidcProviderService } from './oidc-provider.service'; -import { oidcClientService } from './oidc-client.service'; import { logger } from '@goodgo/logger'; +import { Request, Response } from 'express'; + +import { oidcProviderService } from './oidc-provider.service'; /** * EN: OIDC Controller @@ -34,7 +34,7 @@ export class OIDCController { */ async authorize(req: Request, res: Response): Promise { try { - const { client_id, redirect_uri, scope, state, response_type } = req.query; + const { client_id, redirect_uri, scope, state } = req.query; if (!client_id || !redirect_uri) { res.status(400).json({ diff --git a/services/iam-service/src/modules/rbac/index.ts b/services/iam-service/src/modules/rbac/index.ts index e78d7044..12ceaef8 100644 --- a/services/iam-service/src/modules/rbac/index.ts +++ b/services/iam-service/src/modules/rbac/index.ts @@ -5,4 +5,3 @@ export { RBACService, rbacService } from './rbac.service'; export { RBACController, rbacController } from './rbac.controller'; export { PolicyEngine, policyEngine } from './policy.engine'; -export * from './policy.engine'; diff --git a/services/iam-service/src/modules/rbac/policy.engine.ts b/services/iam-service/src/modules/rbac/policy.engine.ts index 7c9fdd44..be3b64fc 100644 --- a/services/iam-service/src/modules/rbac/policy.engine.ts +++ b/services/iam-service/src/modules/rbac/policy.engine.ts @@ -1,6 +1,7 @@ -import { PrismaClient } from '@prisma/client'; -import { getPrismaClient } from '../../config/database.config'; import { logger } from '@goodgo/logger'; +import { PrismaClient } from '@prisma/client'; + +import { getPrismaClient } from '../../config/database.config'; /** * EN: Policy context for ABAC evaluation diff --git a/services/iam-service/src/modules/rbac/rbac.controller.ts b/services/iam-service/src/modules/rbac/rbac.controller.ts index e894454a..bd2355d8 100644 --- a/services/iam-service/src/modules/rbac/rbac.controller.ts +++ b/services/iam-service/src/modules/rbac/rbac.controller.ts @@ -1,8 +1,7 @@ import { Request, Response } from 'express'; -import { rbacService } from './rbac.service'; -import { policyEngine } from './policy.engine'; import { z } from 'zod'; -import { requirePermission } from '../../middlewares/rbac.middleware'; + +import { rbacService } from './rbac.service'; const AssignRoleDto = z.object({ userId: z.string(), diff --git a/services/iam-service/src/modules/rbac/rbac.service.ts b/services/iam-service/src/modules/rbac/rbac.service.ts index 3b99036d..54b75bdb 100644 --- a/services/iam-service/src/modules/rbac/rbac.service.ts +++ b/services/iam-service/src/modules/rbac/rbac.service.ts @@ -1,9 +1,10 @@ +import { logger } from '@goodgo/logger'; import { PrismaClient } from '@prisma/client'; + import { getPrismaClient } from '../../config/database.config'; import { cacheService } from '../../core/cache/cache.service'; -import { logger } from '@goodgo/logger'; -import { UserRepository } from '../../repositories/user.repository'; import { RoleRepository, PermissionRepository } from '../../repositories/role.repository'; +import { UserRepository } from '../../repositories/user.repository'; /** * EN: RBAC Service for role-based access control diff --git a/services/iam-service/src/modules/session/session.repository.ts b/services/iam-service/src/modules/session/session.repository.ts index 1ca3ab12..be38a45c 100644 --- a/services/iam-service/src/modules/session/session.repository.ts +++ b/services/iam-service/src/modules/session/session.repository.ts @@ -1,4 +1,3 @@ -import { PrismaClient } from '@prisma/client'; import { getPrismaClient } from '../../config/database.config'; import { BaseRepository } from '../common/repository'; diff --git a/services/iam-service/src/modules/session/session.service.ts b/services/iam-service/src/modules/session/session.service.ts index 9bfd7b96..a01beabe 100644 --- a/services/iam-service/src/modules/session/session.service.ts +++ b/services/iam-service/src/modules/session/session.service.ts @@ -1,11 +1,12 @@ -import { PrismaClient } from '@prisma/client'; -import { getPrismaClient } from '../../config/database.config'; -import { v4 as uuidv4 } from 'uuid'; import { logger } from '@goodgo/logger'; -import { cacheService } from '../../core/cache/cache.service'; -import { cookieService } from '../token/cookie.service'; +import { PrismaClient } from '@prisma/client'; import { Request } from 'express'; + +import { getPrismaClient } from '../../config/database.config'; +import { cacheService } from '../../core/cache/cache.service'; import { SessionResponse } from '../../types/auth.types'; +import { cookieService } from '../token/cookie.service'; + /** * EN: Session Service for distributed session management diff --git a/services/iam-service/src/modules/session/sessions.controller.ts b/services/iam-service/src/modules/session/sessions.controller.ts index 73c63c17..a48a693c 100644 --- a/services/iam-service/src/modules/session/sessions.controller.ts +++ b/services/iam-service/src/modules/session/sessions.controller.ts @@ -1,4 +1,5 @@ import { Request, Response } from 'express'; + import { sessionService } from './session.service'; /** diff --git a/services/iam-service/src/modules/social/index.ts b/services/iam-service/src/modules/social/index.ts index 476b54b2..2ffd3e98 100644 --- a/services/iam-service/src/modules/social/index.ts +++ b/services/iam-service/src/modules/social/index.ts @@ -4,4 +4,3 @@ */ export { SocialAuthService, socialAuthService } from './social.service'; export { SocialAuthController, socialAuthController } from './social.controller'; -export * from './social.service'; diff --git a/services/iam-service/src/modules/social/providers/facebook.provider.ts b/services/iam-service/src/modules/social/providers/facebook.provider.ts index 6a5e03f2..2f63a224 100644 --- a/services/iam-service/src/modules/social/providers/facebook.provider.ts +++ b/services/iam-service/src/modules/social/providers/facebook.provider.ts @@ -1,11 +1,10 @@ -import { logger } from '@goodgo/logger'; +import { socialConfig } from '../../../config/social.config'; import { SocialProfile } from '../social.service'; /** * EN: Facebook OAuth Provider * VI: Provider OAuth của Facebook */ -import { socialConfig } from '../../../config/social.config'; export class FacebookProvider { private readonly appId = socialConfig.facebook.appId; @@ -34,13 +33,6 @@ export class FacebookProvider { */ async getProfile(code: string, redirectUri?: string): Promise { // Exchange code for access token - const tokenResponse = await fetch('https://graph.facebook.com/v18.0/oauth/access_token', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - const tokenUrl = new URL('https://graph.facebook.com/v18.0/oauth/access_token'); tokenUrl.searchParams.set('client_id', this.appId!); tokenUrl.searchParams.set('client_secret', this.appSecret!); diff --git a/services/iam-service/src/modules/social/providers/github.provider.ts b/services/iam-service/src/modules/social/providers/github.provider.ts index 4644fc69..d86bc397 100644 --- a/services/iam-service/src/modules/social/providers/github.provider.ts +++ b/services/iam-service/src/modules/social/providers/github.provider.ts @@ -1,11 +1,10 @@ -import { logger } from '@goodgo/logger'; +import { socialConfig } from '../../../config/social.config'; import { SocialProfile } from '../social.service'; /** * EN: GitHub OAuth Provider * VI: Provider OAuth của GitHub */ -import { socialConfig } from '../../../config/social.config'; export class GitHubProvider { private readonly clientId = socialConfig.github.clientId; diff --git a/services/iam-service/src/modules/social/providers/google.provider.ts b/services/iam-service/src/modules/social/providers/google.provider.ts index d1371bd6..1b86955a 100644 --- a/services/iam-service/src/modules/social/providers/google.provider.ts +++ b/services/iam-service/src/modules/social/providers/google.provider.ts @@ -1,11 +1,10 @@ -import { logger } from '@goodgo/logger'; +import { socialConfig } from '../../../config/social.config'; import { SocialProfile } from '../social.service'; /** * EN: Google OAuth 2.0 Provider * VI: Provider OAuth 2.0 của Google */ -import { socialConfig } from '../../../config/social.config'; export class GoogleProvider { private readonly clientId = socialConfig.google.clientId; diff --git a/services/iam-service/src/modules/social/social.controller.ts b/services/iam-service/src/modules/social/social.controller.ts index d4eddeff..3960d208 100644 --- a/services/iam-service/src/modules/social/social.controller.ts +++ b/services/iam-service/src/modules/social/social.controller.ts @@ -1,11 +1,13 @@ +import { logger } from '@goodgo/logger'; import { Request, Response } from 'express'; -import { socialAuthService } from './social.service'; -import { cookieService } from '../token/cookie.service'; -import { jwtService } from '../token/jwt.service'; + +import { auditService } from '../../core/events/audit.service'; import { rbacService } from '../rbac/rbac.service'; import { sessionService } from '../session/session.service'; -import { auditService } from '../../core/events/audit.service'; -import { logger } from '@goodgo/logger'; +import { cookieService } from '../token/cookie.service'; +import { jwtService } from '../token/jwt.service'; + +import { socialAuthService } from './social.service'; /** * EN: Social Auth Controller diff --git a/services/iam-service/src/modules/social/social.service.ts b/services/iam-service/src/modules/social/social.service.ts index ffc253b3..49f1dac6 100644 --- a/services/iam-service/src/modules/social/social.service.ts +++ b/services/iam-service/src/modules/social/social.service.ts @@ -1,11 +1,14 @@ +import { logger } from '@goodgo/logger'; import { PrismaClient, Provider } from '@prisma/client'; + import { getPrismaClient } from '../../config/database.config'; import { createCircuitBreaker } from '../../modules/common/circuit-breaker'; -import { logger } from '@goodgo/logger'; -import { GoogleProvider } from './providers/google.provider'; +import { SocialAuthResponse, SocialAccountResponse, UserResponse } from '../../types/auth.types'; + import { FacebookProvider } from './providers/facebook.provider'; import { GitHubProvider } from './providers/github.provider'; -import { SocialAuthResponse, SocialAccountResponse, UserResponse } from '../../types/auth.types'; +import { GoogleProvider } from './providers/google.provider'; + /** * EN: Social profile interface @@ -253,8 +256,8 @@ export class SocialAuthService { * VI: Lấy profile đã cache (fallback) */ private async getCachedProfile( - provider: Provider, - code: string + _provider: Provider, + _code: string ): Promise { // In production, implement proper caching return null; diff --git a/services/iam-service/src/modules/token/cookie.service.ts b/services/iam-service/src/modules/token/cookie.service.ts index 966d31d8..628f5260 100644 --- a/services/iam-service/src/modules/token/cookie.service.ts +++ b/services/iam-service/src/modules/token/cookie.service.ts @@ -1,7 +1,10 @@ -import { Response } from 'express'; -import { Request } from 'express'; import crypto from 'crypto'; + import { logger } from '@goodgo/logger'; +import { Response , Request } from 'express'; + +// EN: Device fingerprinting library will be loaded dynamically to avoid browser-only code +// VI: Thư viện device fingerprinting sẽ được load động để tránh code chỉ chạy trên browser /** * EN: Cookie service for secure cookie management @@ -111,18 +114,46 @@ export class CookieService { } /** - * EN: Get device fingerprint from request - * VI: Lấy device fingerprint từ request + * EN: Get device fingerprint from request using comprehensive fingerprinting + * VI: Lấy device fingerprint từ request sử dụng fingerprinting toàn diện */ getDeviceFingerprint(req: Request): string { - const userAgent = req.headers['user-agent'] || ''; - const acceptLanguage = req.headers['accept-language'] || ''; - const acceptEncoding = req.headers['accept-encoding'] || ''; - const ip = req.ip || req.socket.remoteAddress || ''; + try { + // EN: Comprehensive server-side device fingerprinting + // VI: Device fingerprinting toàn diện phía server + const fingerprintData = { + ip: req.ip || req.socket.remoteAddress || '', + forwardedFor: req.headers['x-forwarded-for'] || '', + realIp: req.headers['x-real-ip'] || '', + userAgent: req.headers['user-agent'] || '', + acceptLanguage: req.headers['accept-language'] || '', + acceptEncoding: req.headers['accept-encoding'] || '', + connection: req.headers['connection'] || '', + cacheControl: req.headers['cache-control'] || '', + dnt: req.headers['dnt'] || '', // Do Not Track + secChUa: req.headers['sec-ch-ua'] || '', // User-Agent Client Hints + secChUaMobile: req.headers['sec-ch-ua-mobile'] || '', + secChUaPlatform: req.headers['sec-ch-ua-platform'] || '', + host: req.headers['host'] || '', + referer: req.headers['referer'] || '', + }; - // Simple fingerprint (in production, use a proper library) - const fingerprint = `${userAgent}:${acceptLanguage}:${acceptEncoding}:${ip}`; - return crypto.createHash('sha256').update(fingerprint).digest('hex'); + // EN: Create fingerprint string and hash for security + // VI: Tạo chuỗi fingerprint và hash để bảo mật + const fingerprintString = JSON.stringify(fingerprintData); + return crypto.createHash('sha256').update(fingerprintString).digest('hex'); + } catch (error) { + logger.warn('Device fingerprinting failed, using basic fallback', { error }); + + // Fallback to basic fingerprinting + const userAgent = req.headers['user-agent'] || ''; + const acceptLanguage = req.headers['accept-language'] || ''; + const acceptEncoding = req.headers['accept-encoding'] || ''; + const ip = req.ip || req.socket.remoteAddress || ''; + + const fingerprint = `${userAgent}:${acceptLanguage}:${acceptEncoding}:${ip}`; + return crypto.createHash('sha256').update(fingerprint).digest('hex'); + } } } diff --git a/services/iam-service/src/modules/token/index.ts b/services/iam-service/src/modules/token/index.ts index 2f031063..182ec211 100644 --- a/services/iam-service/src/modules/token/index.ts +++ b/services/iam-service/src/modules/token/index.ts @@ -4,4 +4,3 @@ */ export { JWTService, jwtService } from './jwt.service'; export { CookieService, cookieService } from './cookie.service'; -export * from './jwt.service'; diff --git a/services/iam-service/src/modules/token/jwt.service.ts b/services/iam-service/src/modules/token/jwt.service.ts index 941fd1e5..4e1399e9 100644 --- a/services/iam-service/src/modules/token/jwt.service.ts +++ b/services/iam-service/src/modules/token/jwt.service.ts @@ -1,7 +1,8 @@ +import { logger } from '@goodgo/logger'; import jwt from 'jsonwebtoken'; import { v4 as uuidv4 } from 'uuid'; + import { jwtConfig } from '../../config/jwt.config'; -import { logger } from '@goodgo/logger'; import { cacheService } from '../../core/cache/cache.service'; /** @@ -36,7 +37,7 @@ export class JWTService { * EN: Generate access token (short-lived, 15 minutes) * VI: Tạo access token (sống ngắn, 15 phút) */ - generateAccessToken(payload: Omit): string { + generateAccessToken(payload: { sub: string; email: string; roles?: string[]; permissions?: string[] }): string { return jwt.sign( payload, jwtConfig.secret, @@ -85,7 +86,6 @@ export class JWTService { iss: jwtConfig.issuer, aud: payload.sub, // ID token audience is the user iat: Math.floor(Date.now() / 1000), - exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour }, jwtConfig.idSecret, { diff --git a/services/iam-service/src/repositories/access-request.repository.ts b/services/iam-service/src/repositories/access-request.repository.ts index 4717aeb7..d869c670 100644 --- a/services/iam-service/src/repositories/access-request.repository.ts +++ b/services/iam-service/src/repositories/access-request.repository.ts @@ -1,6 +1,7 @@ import { PrismaClient, AccessRequest, RequestStatus, AccessRequestApprover } from '@prisma/client'; -import { BaseRepository } from '../modules/common/repository'; + import { getPrismaClient } from '../config/database.config'; +import { BaseRepository } from '../modules/common/repository'; /** * EN: Access Request repository for database operations diff --git a/services/iam-service/src/repositories/access-review.repository.ts b/services/iam-service/src/repositories/access-review.repository.ts index 12027aef..b1c59c2a 100644 --- a/services/iam-service/src/repositories/access-review.repository.ts +++ b/services/iam-service/src/repositories/access-review.repository.ts @@ -1,6 +1,7 @@ import { PrismaClient, AccessReview, AccessReviewItem, ReviewStatus, ReviewType, ReviewItemStatus } from '@prisma/client'; -import { BaseRepository } from '../modules/common/repository'; + import { getPrismaClient } from '../config/database.config'; +import { BaseRepository } from '../modules/common/repository'; /** * EN: Access Review repository for database operations diff --git a/services/iam-service/src/repositories/group.repository.ts b/services/iam-service/src/repositories/group.repository.ts index 59a5d047..c3e35694 100644 --- a/services/iam-service/src/repositories/group.repository.ts +++ b/services/iam-service/src/repositories/group.repository.ts @@ -1,6 +1,7 @@ import { PrismaClient, Group, GroupMember } from '@prisma/client'; -import { BaseRepository } from '../modules/common/repository'; + import { getPrismaClient } from '../config/database.config'; +import { BaseRepository } from '../modules/common/repository'; /** * EN: Group repository for database operations diff --git a/services/iam-service/src/repositories/identity-verification.repository.ts b/services/iam-service/src/repositories/identity-verification.repository.ts index 01b0a52b..f471d49c 100644 --- a/services/iam-service/src/repositories/identity-verification.repository.ts +++ b/services/iam-service/src/repositories/identity-verification.repository.ts @@ -1,6 +1,7 @@ import { PrismaClient, IdentityVerification, VerificationType, VerificationStatus } from '@prisma/client'; -import { BaseRepository } from '../modules/common/repository'; + import { getPrismaClient } from '../config/database.config'; +import { BaseRepository } from '../modules/common/repository'; /** * EN: Identity Verification repository for database operations diff --git a/services/iam-service/src/repositories/organization.repository.ts b/services/iam-service/src/repositories/organization.repository.ts index 72589192..3d6032ca 100644 --- a/services/iam-service/src/repositories/organization.repository.ts +++ b/services/iam-service/src/repositories/organization.repository.ts @@ -1,6 +1,7 @@ import { PrismaClient, Organization } from '@prisma/client'; -import { BaseRepository } from '../modules/common/repository'; + import { getPrismaClient } from '../config/database.config'; +import { BaseRepository } from '../modules/common/repository'; /** * EN: Organization repository for database operations diff --git a/services/iam-service/src/repositories/role.repository.ts b/services/iam-service/src/repositories/role.repository.ts index fcc40a51..14b57c66 100644 --- a/services/iam-service/src/repositories/role.repository.ts +++ b/services/iam-service/src/repositories/role.repository.ts @@ -1,4 +1,5 @@ import { PrismaClient, Role, Permission } from '@prisma/client'; + import { BaseRepository } from '../modules/common/repository'; /** diff --git a/services/iam-service/src/repositories/user-profile.repository.ts b/services/iam-service/src/repositories/user-profile.repository.ts index 8f6ad05b..450ccd8d 100644 --- a/services/iam-service/src/repositories/user-profile.repository.ts +++ b/services/iam-service/src/repositories/user-profile.repository.ts @@ -1,6 +1,7 @@ import { PrismaClient, UserProfile } from '@prisma/client'; -import { BaseRepository } from '../modules/common/repository'; + import { getPrismaClient } from '../config/database.config'; +import { BaseRepository } from '../modules/common/repository'; /** * EN: User Profile repository for database operations diff --git a/services/iam-service/src/repositories/user.repository.ts b/services/iam-service/src/repositories/user.repository.ts index 90fff65e..e80036f2 100644 --- a/services/iam-service/src/repositories/user.repository.ts +++ b/services/iam-service/src/repositories/user.repository.ts @@ -1,4 +1,5 @@ import { PrismaClient, User } from '@prisma/client'; + import { BaseRepository } from '../modules/common/repository'; /** diff --git a/services/iam-service/src/routes/index.ts b/services/iam-service/src/routes/index.ts index 0f4958cd..f000998a 100644 --- a/services/iam-service/src/routes/index.ts +++ b/services/iam-service/src/routes/index.ts @@ -1,33 +1,32 @@ -import { Router } from 'express'; -import { HealthController } from '../modules/health/health.controller'; -import { MetricsController } from '../modules/metrics/metrics.controller'; -import { authenticate } from '../middlewares/auth.middleware'; -import { authController } from '../modules/auth/auth.controller'; -import { socialAuthController } from '../modules/social/social.controller'; -import { oidcController } from '../modules/oidc/oidc.controller'; -import { rbacController } from '../modules/rbac/rbac.controller'; -import { mfaController } from '../modules/mfa/mfa.controller'; -import { changePasswordController } from '../modules/auth/change-password.controller'; -import { sessionsController } from '../modules/session/sessions.controller'; import { ApiResponse } from '@goodgo/types'; -import { zeroTrustMiddleware } from '../middlewares/zero-trust.middleware'; +import { Router } from 'express'; + + +import { authenticate } from '../middlewares/auth.middleware'; import { dynamicRateLimit } from '../middlewares/rate-limit.middleware'; import { requirePermission } from '../middlewares/rbac.middleware'; - -// EN: Import IAM modules -// VI: Import các modules IAM -import { userManagementController } from '../modules/identity/user'; -import { profileController } from '../modules/identity/profile'; -import { verificationController } from '../modules/identity/verification'; -import { organizationController } from '../modules/identity/organization'; -import { groupController } from '../modules/identity/group'; +import { zeroTrustMiddleware } from '../middlewares/zero-trust.middleware'; +import { accessAnalyticsController } from '../modules/access/analytics'; import { accessRequestController } from '../modules/access/request'; import { accessReviewController } from '../modules/access/review'; -import { accessAnalyticsController } from '../modules/access/analytics'; +import { authController } from '../modules/auth/auth.controller'; +import { changePasswordController } from '../modules/auth/change-password.controller'; import { complianceController } from '../modules/governance/compliance'; import { policyGovernanceController } from '../modules/governance/policy'; -import { riskController } from '../modules/governance/risk'; import { reportingController } from '../modules/governance/reporting'; +import { riskController } from '../modules/governance/risk'; +import { HealthController } from '../modules/health/health.controller'; +import { groupController } from '../modules/identity/group'; +import { organizationController } from '../modules/identity/organization'; +import { profileController } from '../modules/identity/profile'; +import { userManagementController } from '../modules/identity/user'; +import { verificationController } from '../modules/identity/verification'; +import { MetricsController } from '../modules/metrics/metrics.controller'; +import { mfaController } from '../modules/mfa/mfa.controller'; +import { oidcController } from '../modules/oidc/oidc.controller'; +import { rbacController } from '../modules/rbac/rbac.controller'; +import { sessionsController } from '../modules/session/sessions.controller'; +import { socialAuthController } from '../modules/social/social.controller'; export const createRouter = (): Router => { diff --git a/services/iam-service/src/utils/helpers.ts b/services/iam-service/src/utils/helpers.ts index 03b845fa..d14a3e3d 100644 --- a/services/iam-service/src/utils/helpers.ts +++ b/services/iam-service/src/utils/helpers.ts @@ -1,5 +1,12 @@ import crypto from 'crypto'; + +import DOMPurify from 'dompurify'; import { Request } from 'express'; +import { JSDOM } from 'jsdom'; + +// Create a DOM window for server-side DOMPurify usage +const window = new JSDOM('').window; +const DOMPurifyServer = DOMPurify(window); /** * EN: Utility helper functions @@ -32,11 +39,24 @@ export function isValidEmail(email: string): boolean { } /** - * EN: Sanitize input string - * VI: Làm sạch chuỗi input + * EN: Sanitize input string to prevent XSS attacks + * VI: Làm sạch chuỗi input để ngăn chặn tấn công XSS */ export function sanitizeInput(input: string): string { - return input.trim().replace(/[<>]/g, ''); + if (!input || typeof input !== 'string') { + return ''; + } + + // First trim whitespace + const trimmed = input.trim(); + + // Use DOMPurify to sanitize HTML and prevent XSS + const sanitized = DOMPurifyServer.sanitize(trimmed, { + ALLOWED_TAGS: [], // No HTML tags allowed + ALLOWED_ATTR: [], // No attributes allowed + }); + + return sanitized; } /** diff --git a/test.sql b/test.sql new file mode 100644 index 00000000..027b7d63 --- /dev/null +++ b/test.sql @@ -0,0 +1 @@ +SELECT 1; \ No newline at end of file