From 81c0efb0b84a3b3955138d3869836d7db01f5f0b Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 16:25:59 +0900 Subject: [PATCH 01/18] Remove example environment configuration files and update Dockerfile for production dependency installation - Deleted .env.dev.example and .env.production.example files to streamline configuration management. - Updated Dockerfile to install production dependencies recursively, ensuring all necessary packages are included during the build process. --- .env.dev.example | 103 ----------------- .env.example | 135 ++++++++++++---------- .env.plesk | 7 ++ .env.production.example | 117 ------------------- .github/workflows/deploy.yml | 93 +++++++++++++++ ARCHITECTURE_RECOMMENDATION.md | 128 --------------------- DEPLOYMENT-GUIDE.md | 141 +++++++++++++++++++++++ DEPLOYMENT.md | 75 ++++++++++++ FINAL_CODE_QUALITY_REPORT.md | 181 ----------------------------- SECURITY_AUDIT_REPORT.md | 202 --------------------------------- SECURITY_FIXES_REQUIRED.md | 169 --------------------------- VALIDATION_AUDIT_REPORT.md | 125 -------------------- apps/bff/Dockerfile | 2 +- compose-plesk.yaml | 113 ++++++++++++++++++ package.json | 1 + 15 files changed, 509 insertions(+), 1083 deletions(-) delete mode 100644 .env.dev.example create mode 100644 .env.plesk delete mode 100644 .env.production.example create mode 100644 .github/workflows/deploy.yml delete mode 100644 ARCHITECTURE_RECOMMENDATION.md create mode 100644 DEPLOYMENT-GUIDE.md create mode 100644 DEPLOYMENT.md delete mode 100644 FINAL_CODE_QUALITY_REPORT.md delete mode 100644 SECURITY_AUDIT_REPORT.md delete mode 100644 SECURITY_FIXES_REQUIRED.md delete mode 100644 VALIDATION_AUDIT_REPORT.md create mode 100644 compose-plesk.yaml diff --git a/.env.dev.example b/.env.dev.example deleted file mode 100644 index 20084b04..00000000 --- a/.env.dev.example +++ /dev/null @@ -1,103 +0,0 @@ -# ๐Ÿš€ Customer Portal - Development Environment -# Copy this file to .env for local development -# This configuration is optimized for development with hot-reloading - -# ============================================================================= -# ๐ŸŒ APPLICATION CONFIGURATION -# ============================================================================= -NODE_ENV=development -APP_NAME=customer-portal-bff -BFF_PORT=4000 -APP_BASE_URL=http://localhost:3000 - -# ============================================================================= -# ๐Ÿ” SECURITY CONFIGURATION (Development) -# ============================================================================= -# Development JWT secret (OK to use simple secret for local dev) -JWT_SECRET=dev_secret_for_local_development_minimum_32_chars_long -JWT_EXPIRES_IN=7d - -# Password Hashing (Minimum rounds for security compliance) -BCRYPT_ROUNDS=10 - -# ============================================================================= -# ๐Ÿ—„๏ธ DATABASE & CACHE (Development) -# ============================================================================= -# Local Docker services -DATABASE_URL=postgresql://dev:dev@localhost:5432/portal_dev?schema=public -REDIS_URL=redis://localhost:6379 - -# ============================================================================= -# ๐ŸŒ NETWORK & CORS (Development) -# ============================================================================= -# Allow local frontend -CORS_ORIGIN=http://localhost:3000 -TRUST_PROXY=false - -# ============================================================================= -# ๐Ÿšฆ RATE LIMITING (Development) -# ============================================================================= -# Relaxed rate limiting for development -RATE_LIMIT_TTL=60000 -RATE_LIMIT_LIMIT=100 -AUTH_RATE_LIMIT_TTL=900000 -AUTH_RATE_LIMIT_LIMIT=3 - -# ============================================================================= -# ๐Ÿข EXTERNAL INTEGRATIONS (Development) -# ============================================================================= -# WHMCS Integration (Demo/Test Environment) -WHMCS_BASE_URL=https://demo.whmcs.com -WHMCS_API_IDENTIFIER=your_demo_identifier -WHMCS_API_SECRET=your_demo_secret -WHMCS_WEBHOOK_SECRET=your_dev_webhook_secret - -# Salesforce Integration (Sandbox Environment) -SF_LOGIN_URL=https://test.salesforce.com -SF_CLIENT_ID=your_dev_client_id -SF_PRIVATE_KEY_PATH=./secrets/sf-dev.key -SF_USERNAME=dev@yourcompany.com.sandbox -SF_WEBHOOK_SECRET=your_dev_webhook_secret - -# ============================================================================= -# ๐Ÿ“Š LOGGING (Development) -# ============================================================================= -# Verbose logging for development -LOG_LEVEL=debug - -# ============================================================================= -# ๐ŸŽฏ FRONTEND CONFIGURATION (Development) -# ============================================================================= -# NEXT_PUBLIC_ variables are exposed to browser -NEXT_PUBLIC_APP_NAME=Customer Portal (Dev) -NEXT_PUBLIC_APP_VERSION=1.0.0-dev -NEXT_PUBLIC_API_BASE=http://localhost:4000 -NEXT_PUBLIC_ENABLE_DEVTOOLS=true - -# ============================================================================= -# ๐ŸŽ›๏ธ DEVELOPMENT OPTIONS -# ============================================================================= -# Node.js options for development -NODE_OPTIONS=--no-deprecation - -# ============================================================================= -# โœ‰๏ธ EMAIL (SendGrid) - Development -# ============================================================================= -SENDGRID_API_KEY= -EMAIL_FROM=no-reply@localhost.test -EMAIL_FROM_NAME=Assist Solutions (Dev) -EMAIL_ENABLED=true -EMAIL_USE_QUEUE=true -SENDGRID_SANDBOX=true -# Optional: dynamic template IDs (use {{resetUrl}} in reset template) -EMAIL_TEMPLATE_RESET= -EMAIL_TEMPLATE_WELCOME= - -# ============================================================================= -# ๐Ÿš€ QUICK START (Development) -# ============================================================================= -# 1. Copy this template: cp .env.dev.example .env -# 2. Edit .env with your development values -# 3. Start services: pnpm dev:start -# 4. Start apps: pnpm dev -# 5. Access: Frontend http://localhost:3000, Backend http://localhost:4000 diff --git a/.env.example b/.env.example index 252dc20e..702afd19 100644 --- a/.env.example +++ b/.env.example @@ -1,72 +1,93 @@ -# ====== Core ====== -NODE_ENV=production +plesk# ๐Ÿš€ Customer Portal - Development Environment Example +# Copy this file to .env for local development +# This configuration is optimized for development with hot-reloading -# ====== Frontend (Next.js) ====== -NEXT_PUBLIC_APP_NAME=Customer Portal -NEXT_PUBLIC_APP_VERSION=1.0.0 -# If using Plesk single domain with /api proxied to backend, set to your main domain -# Example: https://portal.example.com or https://example.com -NEXT_PUBLIC_API_BASE=https://CHANGE_THIS +# ============================================================================= +# ๐Ÿ—„๏ธ DATABASE CONFIGURATION (Development) +# ============================================================================= +DATABASE_URL="postgresql://dev:dev@localhost:5432/portal_dev?schema=public" -# ====== Backend (NestJS BFF) ====== +# ============================================================================= +# ๐Ÿ”ด REDIS CONFIGURATION (Development) +# ============================================================================= +REDIS_URL="redis://localhost:6379" + +# ============================================================================= +# ๐ŸŒ APPLICATION CONFIGURATION (Development) +# ============================================================================= +# Backend Configuration BFF_PORT=4000 -APP_BASE_URL=https://CHANGE_THIS +APP_NAME="customer-portal-bff" +NODE_ENV="development" -# ====== Database (PostgreSQL) ====== -POSTGRES_DB=portal_prod -POSTGRES_USER=portal -POSTGRES_PASSWORD=CHANGE_THIS +# Frontend Configuration (NEXT_PUBLIC_ variables are exposed to browser) +NEXT_PORT=3000 +NEXT_PUBLIC_APP_NAME="Customer Portal (Dev)" +NEXT_PUBLIC_APP_VERSION="1.0.0-dev" +NEXT_PUBLIC_API_BASE="http://localhost:4000" +NEXT_PUBLIC_ENABLE_DEVTOOLS="true" -# Prisma style DATABASE_URL for Postgres inside Compose network -# For Plesk Compose, hostname is the service name 'database' -DATABASE_URL=postgresql://portal:${POSTGRES_PASSWORD}@database:5432/${POSTGRES_DB}?schema=public +# ============================================================================= +# ๐Ÿ” SECURITY CONFIGURATION (Development) +# ============================================================================= +# JWT Secret (Development - OK to use simple secret) +JWT_SECRET="HjHsUyTE3WhPn5N07iSvurdV4hk2VEkIuN+lIflHhVQ=" +JWT_EXPIRES_IN="7d" -# ====== Redis ====== -REDIS_URL=redis://cache:6379/0 +# Password Hashing (Minimum rounds for security compliance) +BCRYPT_ROUNDS=10 -# ====== Security ====== -JWT_SECRET=CHANGE_THIS -JWT_EXPIRES_IN=7d -BCRYPT_ROUNDS=12 +# CORS (Allow local frontend) +CORS_ORIGIN="http://localhost:3000" -# ====== CORS ====== -# If portal: https://portal.example.com ; if root domain: https://example.com -CORS_ORIGIN=https://CHANGE_THIS +# ============================================================================= +# ๐Ÿข EXTERNAL API CONFIGURATION (Development) +# ============================================================================= +# WHMCS Integration (use your actual credentials) +WHMCS_BASE_URL="https://accounts.asolutions.co.jp" +WHMCS_API_IDENTIFIER="your_whmcs_api_identifier" +WHMCS_API_SECRET="your_whmcs_api_secret" -# ====== External APIs (optional) ====== -WHMCS_BASE_URL= -WHMCS_API_IDENTIFIER= -WHMCS_API_SECRET= -SF_LOGIN_URL= -SF_CLIENT_ID= -SF_PRIVATE_KEY_PATH=/app/secrets/salesforce.key -SF_USERNAME= +# Salesforce Integration (use your actual credentials) +SF_LOGIN_URL="https://asolutions.my.salesforce.com" +SF_CLIENT_ID="your_salesforce_client_id" +SF_PRIVATE_KEY_PATH="./secrets/sf-private.key" +SF_USERNAME="your_salesforce_username" -# ====== Salesforce Pricing ====== -# Portal Pricebook ID for product pricing (defaults to Portal pricebook) -PORTAL_PRICEBOOK_ID=01sTL000008eLVlYAM +# Salesforce Pricing +PORTAL_PRICEBOOK_ID="01sTL000008eLVlYAM" -# ====== Logging ====== -LOG_LEVEL=info -LOG_FORMAT=json +# ============================================================================= +# ๐Ÿ“Š LOGGING CONFIGURATION (Development) +# ============================================================================= +LOG_LEVEL="debug" +LOG_FORMAT="pretty" -# ====== Email (SendGrid) ====== -# API key: https://app.sendgrid.com/settings/api_keys -SENDGRID_API_KEY= -# From address for outbound email -EMAIL_FROM=no-reply@yourdomain.com -EMAIL_FROM_NAME=Assist Solutions -# Master email switch -EMAIL_ENABLED=true -# Queue emails for async delivery (recommended) -EMAIL_USE_QUEUE=true -# Enable SendGrid sandbox mode (use true in non-prod to avoid delivery) -SENDGRID_SANDBOX=false -# Optional: dynamic template IDs (use {{resetUrl}} for reset template) -EMAIL_TEMPLATE_RESET= -EMAIL_TEMPLATE_WELCOME= +# ============================================================================= +# ๐Ÿ“ง EMAIL CONFIGURATION (Development) +# ============================================================================= +# SendGrid (optional for development) +SENDGRID_API_KEY="" +EMAIL_FROM="no-reply@yourdomain.com" +EMAIL_FROM_NAME="Assist Solutions" +EMAIL_ENABLED=false +EMAIL_USE_QUEUE=false +SENDGRID_SANDBOX=true +EMAIL_TEMPLATE_RESET="" +EMAIL_TEMPLATE_WELCOME="" -# ====== Node options ====== -NODE_OPTIONS=--max-old-space-size=512 +# ============================================================================= +# ๐ŸŽ›๏ธ DEVELOPMENT CONFIGURATION +# ============================================================================= +# Node.js options for development +NODE_OPTIONS="--no-deprecation" + +# ============================================================================= +# ๐Ÿณ DOCKER DEVELOPMENT NOTES +# ============================================================================= +# For Docker development services (PostgreSQL + Redis only): +# 1. Run: pnpm dev:start +# 2. Frontend and Backend run locally (outside containers) for hot-reloading +# 3. Only database and cache services run in containers diff --git a/.env.plesk b/.env.plesk new file mode 100644 index 00000000..1778ac6a --- /dev/null +++ b/.env.plesk @@ -0,0 +1,7 @@ + +# GitHub Container Registry Authentication +# Replace with your actual GitHub personal access token (with read:packages scope) +GITHUB_TOKEN=your_github_personal_access_token_here + +# Security note: Keep this file secure and don't commit it to Git +# This token allows pulling private images from GitHub Container Registry diff --git a/.env.production.example b/.env.production.example deleted file mode 100644 index d5ad59da..00000000 --- a/.env.production.example +++ /dev/null @@ -1,117 +0,0 @@ -# ๐Ÿš€ Customer Portal - Production Environment -# Copy this file to .env for production deployment -# This configuration is optimized for production with security and performance - -# ============================================================================= -# ๐ŸŒ APPLICATION CONFIGURATION -# ============================================================================= -NODE_ENV=production -APP_NAME=customer-portal-bff -BFF_PORT=4000 -APP_BASE_URL=https://portal.yourdomain.com - -# ============================================================================= -# ๐Ÿ” SECURITY CONFIGURATION (Production) -# ============================================================================= -# CRITICAL: Generate with: openssl rand -base64 32 -JWT_SECRET=GENERATE_SECURE_JWT_SECRET_HERE_MINIMUM_32_CHARS -JWT_EXPIRES_IN=7d - -# Password Hashing (High rounds for security) -BCRYPT_ROUNDS=12 - -# ============================================================================= -# ๐Ÿ—„๏ธ DATABASE & CACHE (Production) -# ============================================================================= -# Docker internal networking (container names as hostnames) -DATABASE_URL=postgresql://portal:YOUR_SECURE_DB_PASSWORD@database:5432/portal_prod?schema=public -REDIS_URL=redis://cache:6379 - -# ============================================================================= -# ๐ŸŒ NETWORK & CORS (Production) -# ============================================================================= -# Your production domain -CORS_ORIGIN=https://yourdomain.com -TRUST_PROXY=true - -# ============================================================================= -# ๐Ÿšฆ RATE LIMITING (Production) -# ============================================================================= -# Strict rate limiting for production -RATE_LIMIT_TTL=60000 -RATE_LIMIT_LIMIT=100 -AUTH_RATE_LIMIT_TTL=900000 -AUTH_RATE_LIMIT_LIMIT=3 - -# ============================================================================= -# ๐Ÿข EXTERNAL INTEGRATIONS (Production) -# ============================================================================= -# WHMCS Integration (Production Environment) -WHMCS_BASE_URL=https://your-whmcs-domain.com -WHMCS_API_IDENTIFIER=your_production_identifier -WHMCS_API_SECRET=your_production_secret -WHMCS_WEBHOOK_SECRET=your_whmcs_webhook_secret - -# Salesforce Integration (Production Environment) -SF_LOGIN_URL=https://login.salesforce.com -SF_CLIENT_ID=your_production_client_id -SF_PRIVATE_KEY_PATH=/app/secrets/sf-prod.key -SF_USERNAME=production@yourcompany.com -SF_WEBHOOK_SECRET=your_salesforce_webhook_secret - -# ============================================================================= -# ๐Ÿ“Š LOGGING (Production) -# ============================================================================= -# Production logging level -LOG_LEVEL=info - -# ============================================================================= -# ๐ŸŽฏ FRONTEND CONFIGURATION (Production) -# ============================================================================= -# NEXT_PUBLIC_ variables are exposed to browser -NEXT_PUBLIC_APP_NAME=Customer Portal -NEXT_PUBLIC_APP_VERSION=1.0.0 -NEXT_PUBLIC_API_BASE=https://yourdomain.com -NEXT_PUBLIC_ENABLE_DEVTOOLS=false - -# ============================================================================= -# ๐ŸŽ›๏ธ PRODUCTION OPTIONS -# ============================================================================= -# Node.js options for production -NODE_OPTIONS=--max-old-space-size=2048 - -# ============================================================================= -# โœ‰๏ธ EMAIL (SendGrid) - Production -# ============================================================================= -# Create and store securely (e.g., KMS/Secrets Manager) -SENDGRID_API_KEY= -EMAIL_FROM=no-reply@yourdomain.com -EMAIL_FROM_NAME=Assist Solutions -EMAIL_ENABLED=true -EMAIL_USE_QUEUE=true -SENDGRID_SANDBOX=false -# Optional: Dynamic Template IDs (recommended) -EMAIL_TEMPLATE_RESET= -EMAIL_TEMPLATE_WELCOME= - -# ============================================================================= -# ๐Ÿ”’ PRODUCTION SECURITY CHECKLIST -# ============================================================================= -# โœ… Replace ALL default/demo values with real credentials -# โœ… Use strong, unique passwords and secrets (minimum 32 characters for JWT) -# โœ… Ensure SF_PRIVATE_KEY_PATH points to actual key file -# โœ… Set correct CORS_ORIGIN for your domain -# โœ… Use HTTPS URLs for all external services -# โœ… Verify DATABASE_URL password matches docker-compose.yml -# โœ… Test all integrations before going live -# โœ… Configure webhook secrets for security -# โœ… Set appropriate rate limiting values -# โœ… Enable trust proxy if behind reverse proxy - -# ============================================================================= -# ๐Ÿš€ QUICK START (Production) -# ============================================================================= -# 1. Copy this template: cp .env.production.example .env -# 2. Edit .env with your production values (REQUIRED!) -# 3. Deploy: pnpm prod:deploy -# 4. Access: https://yourdomain.com diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..49a65b86 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,93 @@ +name: Build & Push Images + +on: + push: + branches: [main] + workflow_dispatch: # Allow manual triggers + +env: + REGISTRY: ghcr.io + IMAGE_NAME_PREFIX: ntumurbars/customer-portal + +jobs: + build-and-push: + name: Build & Push Docker Images + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for frontend + id: meta-frontend + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_PREFIX }}-frontend + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix=main- + + - name: Extract metadata for backend + id: meta-backend + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_PREFIX }}-backend + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix=main- + + - name: Build and push frontend image + uses: docker/build-push-action@v5 + with: + context: . + file: ./apps/portal/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta-frontend.outputs.tags }} + labels: ${{ steps.meta-frontend.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and push backend image + uses: docker/build-push-action@v5 + with: + context: . + file: ./apps/bff/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta-backend.outputs.tags }} + labels: ${{ steps.meta-backend.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build Summary + run: | + echo "## ๐Ÿš€ Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Frontend Image:** \`${{ steps.meta-frontend.outputs.tags }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Backend Image:** \`${{ steps.meta-backend.outputs.tags }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ“ฆ Images Built:" >> $GITHUB_STEP_SUMMARY + echo "- **Frontend**: [ghcr.io/${{ env.IMAGE_NAME_PREFIX }}-frontend](https://github.com/NTumurbars/customer-portal/pkgs/container/customer-portal-frontend)" >> $GITHUB_STEP_SUMMARY + echo "- **Backend**: [ghcr.io/${{ env.IMAGE_NAME_PREFIX }}-backend](https://github.com/NTumurbars/customer-portal/pkgs/container/customer-portal-backend)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿš€ Next Steps:" >> $GITHUB_STEP_SUMMARY + echo "1. **SSH to Plesk server** and run:" >> $GITHUB_STEP_SUMMARY + echo " \`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo " docker compose -f compose-plesk.yaml pull" >> $GITHUB_STEP_SUMMARY + echo " docker compose -f compose-plesk.yaml up -d" >> $GITHUB_STEP_SUMMARY + echo " \`\`\`" >> $GITHUB_STEP_SUMMARY + echo "2. **Or update via Plesk UI**: Docker โ†’ Stacks โ†’ customer-portal โ†’ Pull โ†’ Up" >> $GITHUB_STEP_SUMMARY diff --git a/ARCHITECTURE_RECOMMENDATION.md b/ARCHITECTURE_RECOMMENDATION.md deleted file mode 100644 index f2994454..00000000 --- a/ARCHITECTURE_RECOMMENDATION.md +++ /dev/null @@ -1,128 +0,0 @@ -# Order Services Architecture Recommendation - -## Recommended Structure: Enhanced Separation of Concerns - -### 1. **Controller Layer** (`orders.controller.ts`) -**Responsibility**: API contract and basic validation -- DTO validation (format, types, required fields) -- Authentication/authorization -- HTTP response handling -- Minimal business logic - -### 2. **Orchestrator Layer** (`order-orchestrator.service.ts`) -**Responsibility**: Workflow coordination and transaction management -- Coordinates the order creation flow -- Manages transaction boundaries -- Handles high-level error scenarios -- Calls other services in correct sequence - -### 3. **Validator Layer** (`order-validator.service.ts`) -**Responsibility**: ALL validation logic (business + technical) -```typescript -class OrderValidator { - // API-level validation (move from DTO) - validateRequestFormat(body: any): CreateOrderBody - - // Business validation (current) - validateUserMapping(userId: string): Promise - validatePaymentMethod(userId: string, clientId: number): Promise - validateSKUs(skus: string[], pricebookId: string): Promise - validateBusinessRules(orderType: string, skus: string[]): void - validateInternetDuplication(userId: string, clientId: number): Promise - - // Complete validation (orchestrates all checks) - async validateCompleteOrder(userId: string, body: any): Promise<{ - validatedBody: CreateOrderBody, - userMapping: UserMapping - }> -} -``` - -### 4. **Builder Layer** (`order-builder.service.ts`) -**Responsibility**: Data transformation and mapping -- Transform business data to Salesforce format -- Apply business rules to field mapping -- Handle conditional field logic - -### 5. **ItemBuilder Layer** (`order-item-builder.service.ts`) -**Responsibility**: Order item creation and pricing -- Create order line items -- Handle pricing calculations -- Manage product metadata - -## Benefits of This Structure: - -### โœ… **Single Responsibility Principle** -- Each service has one clear purpose -- Easy to test and maintain -- Clear boundaries - -### โœ… **Validator as Single Source of Truth** -- All validation logic in one place -- Easy to find and modify validation rules -- Consistent error handling - -### โœ… **Orchestrator for Workflow Management** -- Clear sequence of operations -- Transaction management -- Error recovery logic - -### โœ… **Testability** -- Each layer can be unit tested independently -- Mock dependencies easily -- Clear input/output contracts - -## Implementation Changes: - -### Move DTO validation to Validator: -```typescript -// Before: Controller has DTO validation -@Body() body: CreateOrderDto - -// After: Controller accepts any, Validator validates -@Body() body: any -``` - -### Enhanced Validator: -```typescript -async validateCompleteOrder(userId: string, rawBody: any) { - // 1. Format validation (was DTO) - const body = this.validateRequestFormat(rawBody); - - // 2. Business validation (current) - const userMapping = await this.validateUserMapping(userId); - await this.validatePaymentMethod(userId, userMapping.whmcsClientId); - - // 3. SKU validation (move here) - const pricebookId = await this.findPricebookId(); - await this.validateSKUs(body.skus, pricebookId); - this.validateBusinessRules(body.orderType, body.skus); - - // 4. Order-specific validation - if (body.orderType === "Internet") { - await this.validateInternetDuplication(userId, userMapping.whmcsClientId); - } - - return { validatedBody: body, userMapping, pricebookId }; -} -``` - -### Simplified Orchestrator: -```typescript -async createOrder(userId: string, rawBody: any) { - // 1. Complete validation - const { validatedBody, userMapping, pricebookId } = - await this.validator.validateCompleteOrder(userId, rawBody); - - // 2. Build order - const orderFields = this.builder.buildOrderFields(validatedBody, userMapping, pricebookId); - - // 3. Create in Salesforce - const created = await this.sf.sobject("Order").create(orderFields); - - // 4. Create items - await this.itemBuilder.createOrderItemsFromSKUs(created.id, validatedBody.skus, pricebookId); - - return { sfOrderId: created.id, status: "Created" }; -} -``` diff --git a/DEPLOYMENT-GUIDE.md b/DEPLOYMENT-GUIDE.md new file mode 100644 index 00000000..f4919d86 --- /dev/null +++ b/DEPLOYMENT-GUIDE.md @@ -0,0 +1,141 @@ +# ๐Ÿš€ Pre-built Images Deployment Guide + +This guide shows how to deploy using pre-built Docker images instead of building on Plesk. + +## Benefits +- โœ… No build failures on Plesk +- โœ… Faster deployments (no compilation time) +- โœ… Consistent images across environments +- โœ… Better security (build in controlled environment) +- โœ… Easy rollbacks and version control + +## Prerequisites + +1. **GitHub Account** (for free container registry) +2. **Docker installed locally** (for building images) +3. **Plesk with Docker extension** + +## Step 1: Setup GitHub Container Registry + +1. Go to GitHub โ†’ Settings โ†’ Developer settings โ†’ Personal access tokens โ†’ Tokens (classic) +2. Create a new token with these permissions: + - `write:packages` (to push images) + - `read:packages` (to pull images) +3. Save the token securely + +## Step 2: Login to GitHub Container Registry + +```bash +# Replace YOUR_USERNAME and YOUR_TOKEN +echo "YOUR_TOKEN" | docker login ghcr.io -u YOUR_USERNAME --password-stdin +``` + +## Step 3: Update Build Script + +Edit `scripts/build-and-push.sh`: +```bash +# Change this line: +NAMESPACE="your-github-username" # Replace with your actual GitHub username +``` + +## Step 4: Build and Push Images + +```bash +# Build and push with version tag +./scripts/build-and-push.sh v1.0.0 + +# Or build and push as latest +./scripts/build-and-push.sh +``` + +## Step 5: Update Plesk Compose File + +Edit `compose-plesk.yaml` and replace: +```yaml +image: ghcr.io/your-github-username/portal-frontend:latest +image: ghcr.io/your-github-username/portal-backend:latest +``` + +With your actual GitHub username. + +## Step 6: Deploy to Plesk + +1. **Upload compose-plesk.yaml** to your Plesk server +2. **Plesk โ†’ Docker โ†’ Add Stack** +3. **Paste the contents** of `compose-plesk.yaml` +4. **Deploy** + +## Step 7: Configure Plesk Reverse Proxy + +1. **Plesk โ†’ Domains โ†’ your-domain.com โ†’ Apache & Nginx Settings** +2. **Add to "Additional directives for HTTP":** +```nginx +location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; +} + +location /api { + proxy_pass http://127.0.0.1:4000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +## Step 8: Secure Database Access + +Add to Plesk Firewall: +``` +# Allow Docker bridge network +ACCEPT from 172.17.0.0/16 to any port 5432 +ACCEPT from 172.17.0.0/16 to any port 6379 + +# Deny external access to database +DROP from any to any port 5432 +DROP from any to any port 6379 +``` + +## Updating Your Application + +1. **Make code changes** +2. **Build and push new images:** + ```bash + ./scripts/build-and-push.sh v1.0.1 + ``` +3. **Update compose-plesk.yaml** with new version tag +4. **Redeploy in Plesk** + +## Troubleshooting + +### Images not found +- Check if you're logged in: `docker login ghcr.io` +- Verify image names match your GitHub username +- Ensure images are public or Plesk can authenticate + +### Build failures +- Run locally first: `docker build -f apps/portal/Dockerfile .` +- Check Docker logs for specific errors +- Ensure all dependencies are in package.json + +### Connection issues +- Verify firewall allows Docker bridge network (172.17.0.0/16) +- Check that DATABASE_URL uses correct IP (172.17.0.1) +- Test database connection from backend container + +## Security Notes + +- Database is only accessible from Docker bridge network +- Backend API is only accessible via reverse proxy +- Use strong passwords and JWT secrets +- Consider using Docker secrets for sensitive data +- Regularly update base images for security patches diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 00000000..46cccc86 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,75 @@ +# ๐Ÿš€ Deployment Guide + +## ๐Ÿ“ **Environment Files Overview** + +### **Development:** +- `.env` - Your local development environment (active) +- `.env.example` - Development template for new developers + +### **Production:** +- `.env.production` - Production environment for Plesk deployment +- `compose-plesk.yaml` - Docker Stack definition + +## ๐Ÿ”ง **Plesk Deployment Steps** + +### **Step 1: Authenticate Docker (One-time)** +```bash +# SSH to Plesk server +echo "YOUR_GITHUB_TOKEN" | docker login ghcr.io -u ntumurbars --password-stdin +``` + +### **Step 2: Upload Files to Plesk** +Upload these files to your domain directory: +1. `compose-plesk.yaml` - Docker Stack definition +2. `.env.production` - Environment variables (rename to `.env`) + +### **Step 3: Deploy Stack** +1. **Plesk โ†’ Docker โ†’ Stacks โ†’ Add Stack** +2. **Project name**: `customer-portal` +3. **Method**: Upload file or paste `compose-plesk.yaml` content +4. **Deploy** + +### **Step 4: Configure Nginx Proxy** +1. **Plesk โ†’ Websites & Domains โ†’ yourdomain.com โ†’ Docker Proxy Rules** +2. **Add rule**: `/` โ†’ `portal-frontend` โ†’ port `3000` +3. **Add rule**: `/api` โ†’ `portal-backend` โ†’ port `4000` + +## ๐Ÿ”„ **Update Workflow** + +### **When You Push Code:** +1. **GitHub Actions** builds new images automatically +2. **SSH to Plesk** and update: + ```bash + cd /var/www/vhosts/yourdomain.com/httpdocs/ + docker compose -f compose-plesk.yaml pull + docker compose -f compose-plesk.yaml up -d + ``` + +## ๐Ÿ” **Environment Variables** + +Your compose file uses these key variables from `.env.production`: + +### **Database:** +- `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` +- `DATABASE_URL` - Full connection string + +### **Application:** +- `JWT_SECRET`, `CORS_ORIGIN` +- `NEXT_PUBLIC_API_BASE`, `NEXT_PUBLIC_APP_NAME` + +### **External APIs:** +- `WHMCS_BASE_URL`, `WHMCS_API_IDENTIFIER`, `WHMCS_API_SECRET` +- `SF_LOGIN_URL`, `SF_CLIENT_ID`, `SF_USERNAME` + +### **Email & Logging:** +- `SENDGRID_API_KEY`, `EMAIL_FROM` +- `LOG_LEVEL`, `LOG_FORMAT` + +## โœ… **Ready to Deploy!** + +Your setup is clean and production-ready: +- โœ… Environment variables properly configured +- โœ… Docker secrets via environment variables +- โœ… Database and Redis secured (localhost only) +- โœ… Automated image building +- โœ… Clean file structure diff --git a/FINAL_CODE_QUALITY_REPORT.md b/FINAL_CODE_QUALITY_REPORT.md deleted file mode 100644 index 7f2b2179..00000000 --- a/FINAL_CODE_QUALITY_REPORT.md +++ /dev/null @@ -1,181 +0,0 @@ -# ๐ŸŽฏ Final Code Quality & Documentation Compliance Report - -## ๐Ÿ† **Overall Assessment: EXCELLENT** - -The order system demonstrates **enterprise-grade code quality** with proper architecture, maintainable patterns, and full documentation compliance. - ---- - -## โœ… **Architecture Quality: A+** - -### **Clean Architecture Implementation** -```typescript -Controller (Thin API Layer) - โ†“ -OrderValidator (Complete Validation) - โ†“ -OrderOrchestrator (Workflow Coordination) - โ†“ -OrderBuilder + OrderItemBuilder (Data Transformation) - โ†“ -Salesforce (External System) -``` - -**โœ… Strengths:** -- **Single Responsibility Principle**: Each service has one clear purpose -- **Dependency Injection**: Proper NestJS patterns throughout -- **Separation of Concerns**: API, validation, business logic, and data layers clearly separated -- **Testability**: Each component can be unit tested independently - ---- - -## โœ… **Field Mapping: A+** - -### **No Hardcoded Salesforce Fields** -```typescript -// โœ… GOOD: Using field mapping -orderFields[fields.order.internetPlanTier] = serviceProduct.internetPlanTier; -orderFields[fields.order.accessMode] = config.accessMode; - -// โŒ BAD: Hardcoded (eliminated) -// orderFields.Internet_Plan_Tier__c = serviceProduct.internetPlanTier; -``` - -**โœ… Benefits:** -- **Environment Configurable**: All field names can be overridden via `process.env` -- **Maintainable**: Single source of truth in `field-map.ts` -- **Flexible**: Easy to adapt to different Salesforce orgs -- **Type Safe**: Full TypeScript support with proper interfaces - ---- - -## โœ… **Validation Logic: A+** - -### **Comprehensive Validation Pipeline** -```typescript -validateCompleteOrder() { - 1. Format Validation (replaces DTO) - 2. User Mapping Validation - 3. Payment Method Validation - 4. SKU Existence Validation - 5. Business Rules Validation - 6. Order-specific Validation -} -``` - -**โœ… Validation Coverage:** -- **Format**: Field types, required fields, enum values -- **Business**: User mapping, payment methods, duplicate orders -- **Data**: SKU existence in Salesforce, business rule compliance -- **Security**: Proper error handling without sensitive data exposure [[memory:6689308]] - ---- - -## โœ… **Documentation Compliance: A** - -### **Salesforce Order Fields - 100% Compliant** - -| Documentation Requirement | Implementation Status | -|---------------------------|----------------------| -| **Core Fields (5)** | โœ… `AccountId`, `EffectiveDate`, `Status`, `Pricebook2Id`, `Order_Type__c` | -| **Activation Fields (3)** | โœ… `Activation_Type__c`, `Activation_Scheduled_At__c`, `Activation_Status__c` | -| **Internet Fields (5)** | โœ… `Internet_Plan_Tier__c`, `Installation_Type__c`, `Weekend_Install__c`, `Access_Mode__c`, `Hikari_Denwa__c` | -| **SIM Fields (4+11)** | โœ… `SIM_Type__c`, `EID__c`, `SIM_Voice_Mail__c`, `SIM_Call_Waiting__c` + all MNP fields | -| **VPN Fields (1)** | โœ… `VPN_Region__c` | - -### **API Requirements - Compliant** -- โœ… **Server-side checks**: WHMCS mapping โœ“, payment method โœ“ -- โœ… **Order status**: Creates "Pending Review" status โœ“ -- โœ… **Return format**: `{ sfOrderId, status }` โœ“ - -### **โš ๏ธ Minor Documentation Discrepancy** -**Issue**: Documentation shows item-based API structure, implementation uses SKU-based structure. - -**Documentation:** -```json -{ "items": [{ "productId": "...", "billingCycle": "..." }] } -``` - -**Implementation:** -```json -{ "orderType": "Internet", "skus": ["INTERNET-SILVER-HOME-1G"] } -``` - -**Recommendation**: Update documentation to match the superior SKU-based implementation. - ---- - -## โœ… **Code Quality Standards: A+** - -### **Error Handling** -```typescript -// โœ… Proper error handling with context -this.logger.error({ error, orderFields }, "Failed to create Salesforce Order"); -throw new BadRequestException("Order creation failed"); -``` - -### **Logging** -```typescript -// โœ… Structured logging throughout -this.logger.log({ userId, orderType, skuCount }, "Order validation completed"); -``` - -### **Type Safety** -```typescript -// โœ… Strong typing everywhere -async validateCompleteOrder(userId: string, rawBody: any): Promise<{ - validatedBody: CreateOrderBody; - userMapping: UserMapping; - pricebookId: string; -}> -``` - ---- - -## โœ… **Production Readiness: A+** - -### **Security** [[memory:6689308]] -- โœ… **Input validation**: Comprehensive DTO validation -- โœ… **Error handling**: No sensitive data exposure -- โœ… **Authentication**: JWT guards on all endpoints -- โœ… **Authorization**: User-specific data access - -### **Performance** -- โœ… **Efficient validation**: Single validation pipeline -- โœ… **Database optimization**: Proper SOQL queries -- โœ… **Error recovery**: Graceful handling of external API failures - -### **Maintainability** -- โœ… **Modular design**: Easy to extend and modify -- โœ… **Clear interfaces**: Well-defined contracts between layers -- โœ… **Consistent patterns**: Uniform error handling and logging -- โœ… **Documentation**: Comprehensive inline documentation - ---- - -## ๐ŸŽฏ **Final Recommendations** - -### **Immediate Actions: None Required** โœ… -The code is production-ready as-is. - -### **Future Enhancements (Optional)** -1. **API Documentation Update**: Align docs with SKU-based implementation -2. **Integration Tests**: Add end-to-end order flow tests -3. **Monitoring**: Add business metrics for order success rates - ---- - -## ๐Ÿ† **Summary** - -This order system represents **exemplary enterprise software development**: - -- โœ… **Clean Architecture**: Proper separation of concerns -- โœ… **Maintainable Code**: No hardcoded values, configurable fields -- โœ… **Production Ready**: Comprehensive validation, error handling, security -- โœ… **Documentation Compliant**: All Salesforce fields properly mapped -- โœ… **Type Safe**: Full TypeScript coverage -- โœ… **Testable**: Modular design enables comprehensive testing - -**Grade: A+ (Excellent)** - -The system is ready for production deployment with confidence! ๐Ÿš€ diff --git a/SECURITY_AUDIT_REPORT.md b/SECURITY_AUDIT_REPORT.md deleted file mode 100644 index 0d67853b..00000000 --- a/SECURITY_AUDIT_REPORT.md +++ /dev/null @@ -1,202 +0,0 @@ -# ๐Ÿ”’ COMPREHENSIVE SECURITY AUDIT REPORT - -**Date**: August 28, 2025 -**Auditor**: AI Security Assistant -**Scope**: Complete NestJS BFF Application Security Review -**Status**: โœ… **PRODUCTION READY** - -## ๐ŸŽฏ **EXECUTIVE SUMMARY** - -The application has been upgraded to implement **2025 NestJS Security Best Practices** with a comprehensive **Global Authentication Architecture**. All critical security vulnerabilities have been addressed and the system is now **ENTERPRISE-GRADE SECURE**. - -### **๐Ÿ† SECURITY GRADE: A+** - -## ๐Ÿ›ก๏ธ **SECURITY ARCHITECTURE OVERVIEW** - -### **Global Authentication Guard (2025 Standard)** -- โœ… **Single Point of Control**: All authentication handled by `GlobalAuthGuard` -- โœ… **JWT Validation**: Automatic token signature and expiration checking -- โœ… **Token Blacklist Integration**: Real-time revoked token checking -- โœ… **Decorator-Based Public Routes**: Clean `@Public()` decorator system -- โœ… **Comprehensive Logging**: Security event tracking and monitoring - -### **Authentication Flow** -```typescript -Request โ†’ GlobalAuthGuard โ†’ @Public() Check โ†’ JWT Validation โ†’ Blacklist Check โ†’ Route Handler -``` - -## ๐Ÿ” **DETAILED SECURITY AUDIT** - -### **1. Authentication & Authorization** โœ… **SECURE** - -| Component | Status | Details | -|-----------|--------|---------| -| JWT Strategy | โœ… SECURE | Proper signature validation, no body parsing interference | -| Token Blacklist | โœ… SECURE | Redis-based, automatic cleanup, logout integration | -| Global Guard | โœ… SECURE | Centralized, comprehensive, production-ready | -| Public Routes | โœ… SECURE | Properly marked, validated, minimal exposure | -| Admin Routes | โœ… SECURE | Additional AdminGuard protection | - -### **2. Public Route Security** โœ… **VALIDATED** - -| Route | Purpose | Security Measures | -|-------|---------|-------------------| -| `POST /auth/signup` | User registration | Rate limiting, input validation | -| `POST /auth/login` | User authentication | Rate limiting, LocalAuthGuard | -| `POST /auth/request-password-reset` | Password reset | Rate limiting, email validation | -| `POST /auth/reset-password` | Password reset | Rate limiting, token validation | -| `POST /auth/link-whmcs` | WHMCS linking | Rate limiting, input validation | -| `POST /auth/set-password` | Password setting | Rate limiting, input validation | -| `POST /auth/check-password-needed` | Password status | Input validation | -| `GET /health` | Health checks | No sensitive data exposure | -| `POST /webhooks/*` | Webhook endpoints | HMAC signature verification | - -### **3. Protected Route Security** โœ… **VALIDATED** - -| Route Category | Protection Level | Validation | -|----------------|------------------|------------| -| User Management (`/api/me`) | JWT + Blacklist | โœ… Tested | -| Orders (`/api/orders`) | JWT + Blacklist | โœ… Tested | -| Catalog (`/api/catalog`) | JWT + Blacklist | โœ… Tested | -| Subscriptions (`/api/subscriptions`) | JWT + Blacklist | โœ… Tested | -| Invoices (`/api/invoices`) | JWT + Blacklist | โœ… Tested | -| Admin (`/api/auth/admin`) | JWT + Blacklist + AdminGuard | โœ… Tested | - -### **4. Webhook Security** โœ… **ENTERPRISE-GRADE** - -- โœ… **HMAC-SHA256 Signature Verification**: All webhooks require valid signatures -- โœ… **Rate Limiting**: Prevents webhook abuse -- โœ… **Public Route Marking**: Properly excluded from JWT authentication -- โœ… **Separate Authentication**: Uses signature-based auth instead of JWT - -### **5. Input Validation & Sanitization** โœ… **COMPREHENSIVE** - -- โœ… **Global ValidationPipe**: Whitelist mode, forbid unknown values -- โœ… **DTO Validation**: class-validator decorators on all inputs -- โœ… **Request Size Limits**: Helmet.js protection -- โœ… **Production Error Handling**: Sanitized error messages - -### **6. Security Headers & CORS** โœ… **HARDENED** - -- โœ… **Helmet.js**: Comprehensive security headers -- โœ… **CSP**: Content Security Policy configured -- โœ… **CORS**: Restrictive origin validation -- โœ… **Security Headers**: X-Frame-Options, X-Content-Type-Options, etc. - -## ๐Ÿงช **SECURITY TESTING RESULTS** - -### **Authentication Tests** โœ… **PASSED** - -| Test Case | Expected | Actual | Status | -|-----------|----------|--------|--------| -| Public route without auth | 200/400 (validation) | โœ… 400 (validation) | PASS | -| Protected route without auth | 401 Unauthorized | โœ… 401 Unauthorized | PASS | -| Protected route with valid JWT | 200 + data | โœ… 200 + data | PASS | -| Webhook without signature | 401 Unauthorized | โœ… 401 Unauthorized | PASS | -| Password reset public access | 200 + message | โœ… 200 + message | PASS | - -### **Edge Case Tests** โœ… **PASSED** - -- โœ… **Malformed JWT**: Properly rejected -- โœ… **Expired JWT**: Properly rejected -- โœ… **Missing Authorization Header**: Properly rejected -- โœ… **Invalid Webhook Signature**: Properly rejected -- โœ… **Rate Limit Exceeded**: Properly throttled - -## ๐Ÿšจ **SECURITY VULNERABILITIES FIXED** - -### **Critical Issues Resolved** โœ… - -1. **Missing @Public Decorators**: - - โŒ **BEFORE**: Auth routes required JWT (impossible to login) - - โœ… **AFTER**: Proper public route marking - -2. **Inconsistent Guard Usage**: - - โŒ **BEFORE**: Manual guards on each controller (error-prone) - - โœ… **AFTER**: Global guard with decorator-based exceptions - -3. **Token Blacklist Gaps**: - - โŒ **BEFORE**: Separate guard implementation (complex) - - โœ… **AFTER**: Integrated into global guard (seamless) - -4. **Webhook Security**: - - โŒ **BEFORE**: Would require JWT (breaking webhooks) - - โœ… **AFTER**: Proper signature-based authentication - -## ๐ŸŽฏ **SECURITY RECOMMENDATIONS IMPLEMENTED** - -### **2025 Best Practices** โœ… **IMPLEMENTED** - -1. โœ… **Global Authentication Guard**: Single point of control -2. โœ… **Decorator-Based Public Routes**: Clean architecture -3. โœ… **Token Blacklisting**: Proper logout functionality -4. โœ… **Comprehensive Logging**: Security event monitoring -5. โœ… **Rate Limiting**: Abuse prevention -6. โœ… **Input Validation**: XSS and injection prevention -7. โœ… **Security Headers**: Browser-level protection -8. โœ… **CORS Configuration**: Origin validation - -## ๐Ÿ“Š **SECURITY METRICS** - -| Metric | Value | Status | -|--------|-------|--------| -| Protected Endpoints | 100% | โœ… SECURE | -| Public Endpoints | 8 routes | โœ… VALIDATED | -| Authentication Coverage | 100% | โœ… COMPLETE | -| Token Blacklist Coverage | 100% | โœ… COMPLETE | -| Input Validation Coverage | 100% | โœ… COMPLETE | -| Rate Limiting Coverage | 100% | โœ… COMPLETE | -| Security Headers | All configured | โœ… COMPLETE | - -## ๐Ÿ”ง **TECHNICAL IMPLEMENTATION** - -### **Global Guard Architecture** -```typescript -@Injectable() -export class GlobalAuthGuard extends AuthGuard('jwt') { - // 1. Check @Public() decorator - // 2. Validate JWT if not public - // 3. Check token blacklist - // 4. Log security events - // 5. Allow/deny access -} -``` - -### **Security Features** -- **JWT Validation**: Signature, expiration, format -- **Token Blacklisting**: Redis-based, automatic cleanup -- **Public Route Handling**: Decorator-based exceptions -- **Comprehensive Logging**: Debug, warn, error levels -- **Error Handling**: Production-safe messages - -## ๐ŸŽ‰ **CONCLUSION** - -### **โœ… SECURITY STATUS: PRODUCTION READY** - -The application now implements **enterprise-grade security** following **2025 NestJS best practices**: - -1. **๐Ÿ”’ Authentication**: Bulletproof JWT + blacklist system -2. **๐Ÿ›ก๏ธ Authorization**: Proper role-based access control -3. **๐Ÿšซ Input Validation**: Comprehensive XSS/injection prevention -4. **โšก Rate Limiting**: Abuse and DoS protection -5. **๐Ÿ” Security Headers**: Browser-level security -6. **๐Ÿ“ Audit Logging**: Complete security event tracking -7. **๐ŸŒ CORS**: Proper origin validation -8. **๐Ÿ”ง Webhook Security**: HMAC signature verification - -### **๐Ÿ† ACHIEVEMENTS** - -- โœ… **Zero Security Vulnerabilities** -- โœ… **100% Authentication Coverage** -- โœ… **Modern Architecture (2025 Standards)** -- โœ… **Production-Ready Implementation** -- โœ… **Comprehensive Testing Validated** - -### **๐Ÿš€ READY FOR PRODUCTION DEPLOYMENT** - -The security implementation is now **enterprise-grade** and ready for production use with confidence. - ---- - -**Security Audit Completed**: August 28, 2025 -**Next Review**: Recommended in 6 months or after major changes diff --git a/SECURITY_FIXES_REQUIRED.md b/SECURITY_FIXES_REQUIRED.md deleted file mode 100644 index da22ec82..00000000 --- a/SECURITY_FIXES_REQUIRED.md +++ /dev/null @@ -1,169 +0,0 @@ -# ๐Ÿšจ CRITICAL SECURITY FIXES REQUIRED - -## **IMMEDIATE ACTION NEEDED** - -The ESLint scan revealed **204 ERRORS** and **479 WARNINGS** with critical security vulnerabilities: - -### **๐Ÿ”ด CRITICAL SECURITY ISSUES** - -1. **Unsafe `any` Types** - 50+ instances - - **Risk**: Type safety bypass, potential injection attacks - - **Impact**: HIGH - Can lead to runtime errors and security vulnerabilities - -2. **Unsafe Member Access** - 100+ instances - - **Risk**: Accessing properties on potentially undefined objects - - **Impact**: HIGH - Runtime errors, potential crashes - -3. **No Type Validation** - Salesforce responses not validated - - **Risk**: Malformed data can crash the application - - **Impact**: MEDIUM - Stability and reliability issues - -## **๐Ÿ›ก๏ธ MODERN SECURITY FIXES IMPLEMENTED** - -### **1. Type Safety Enhancement** -```typescript -// โŒ BEFORE (UNSAFE) -async createOrder(userId: string, rawBody: any) { - const result = await this.sf.query(sql) as any; - return result.records[0].Id; // Unsafe! -} - -// โœ… AFTER (SECURE) -async createOrder(userId: string, rawBody: unknown) { - const result = await this.sf.query(sql) as SalesforceQueryResult; - if (!isSalesforceQueryResult(result, isSalesforceOrder)) { - throw new BadRequestException('Invalid Salesforce response'); - } - return result.records[0]?.Id; -} -``` - -### **2. Runtime Type Validation** -```typescript -// โœ… NEW: Type Guards for Security -export function isSalesforceOrder(obj: unknown): obj is SalesforceOrder { - return ( - typeof obj === 'object' && - obj !== null && - typeof (obj as SalesforceOrder).Id === 'string' && - typeof (obj as SalesforceOrder).OrderNumber === 'string' - ); -} -``` - -### **3. Proper Error Handling** -```typescript -// โœ… NEW: Secure Error Handling -try { - const validatedBody = this.validateRequestFormat(rawBody); - // Process with type safety -} catch (error) { - this.logger.error('Validation failed', { error: error.message }); - throw new BadRequestException('Invalid request format'); -} -``` - -## **๐Ÿ“‹ FIXES APPLIED** - -### **โœ… Completed** -1. Created `SalesforceOrder` and `SalesforceOrderItem` types -2. Added type guards for runtime validation -3. Replaced critical `any` types with `unknown` -4. Enhanced GlobalAuthGuard with proper logging -5. Fixed public route security - -### **๐Ÿ”„ In Progress** -1. Replacing all `any` types with proper interfaces -2. Adding runtime validation for all external data -3. Implementing proper error boundaries -4. Adding comprehensive type checking - -### **โณ Remaining** -1. Fix all ESLint errors (204 remaining) -2. Add comprehensive input validation -3. Implement data sanitization -4. Add security headers validation - -## **๐ŸŽฏ NEXT STEPS** - -### **Immediate (Critical)** -1. **Fix Type Safety**: Replace all `any` with proper types -2. **Add Validation**: Validate all external API responses -3. **Secure Error Handling**: Sanitize all error messages - -### **Short Term (Important)** -1. **Run ESLint Fix**: `npm run lint:fix` -2. **Add Unit Tests**: Test all type guards and validation -3. **Security Audit**: Review all external integrations - -### **Long Term (Maintenance)** -1. **Automated Security Scanning**: Add to CI/CD -2. **Regular Type Audits**: Monthly type safety reviews -3. **Security Training**: Team education on TypeScript security - -## **๐Ÿš€ RECOMMENDED APPROACH** - -### **Phase 1: Critical Security (Now)** -```bash -# 1. Fix immediate type safety issues -npm run lint:fix - -# 2. Add proper types for all Salesforce interactions -# 3. Implement runtime validation for all external data -# 4. Add comprehensive error handling -``` - -### **Phase 2: Comprehensive Security (This Week)** -```bash -# 1. Complete type safety overhaul -# 2. Add comprehensive input validation -# 3. Implement security testing -# 4. Add monitoring and alerting -``` - -## **๐Ÿ’ก MODERN NESTJS PATTERNS** - -### **Use Proper DTOs with Validation** -```typescript -// โœ… Modern NestJS Pattern -export class CreateOrderDto { - @IsString() - @IsNotEmpty() - @IsIn(['Internet', 'SIM', 'VPN', 'Other']) - orderType: 'Internet' | 'SIM' | 'VPN' | 'Other'; - - @IsArray() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - skus: string[]; -} -``` - -### **Use Type Guards for External Data** -```typescript -// โœ… Secure External Data Handling -function validateSalesforceResponse( - data: unknown, - validator: (obj: unknown) => obj is T -): T { - if (!validator(data)) { - throw new BadRequestException('Invalid external data format'); - } - return data; -} -``` - -## **๐Ÿ”’ SECURITY COMPLIANCE** - -After implementing these fixes, the application will be: -- โœ… **Type Safe**: No `any` types, full TypeScript compliance -- โœ… **Runtime Safe**: All external data validated -- โœ… **Error Safe**: Proper error handling and sanitization -- โœ… **Modern**: Following 2025 NestJS best practices -- โœ… **Secure**: Production-ready security implementation - ---- - -**Status**: ๐Ÿ”ด **CRITICAL FIXES IN PROGRESS** -**ETA**: 2-4 hours for complete security overhaul -**Priority**: **HIGHEST** - Security vulnerabilities must be fixed before production diff --git a/VALIDATION_AUDIT_REPORT.md b/VALIDATION_AUDIT_REPORT.md deleted file mode 100644 index 70798143..00000000 --- a/VALIDATION_AUDIT_REPORT.md +++ /dev/null @@ -1,125 +0,0 @@ -# Order Validation & Salesforce Field Mapping Audit Report - -## ๐Ÿ” **Audit Summary** - -### โœ… **What's Working Correctly:** - -1. **Core Order Fields** - All documented fields are properly mapped: - - `AccountId`, `EffectiveDate`, `Status`, `Pricebook2Id`, `Order_Type__c` โœ… - -2. **Activation Fields** - Correctly implemented: - - `Activation_Type__c`, `Activation_Scheduled_At__c`, `Activation_Status__c` โœ… - -3. **Internet Fields** - All documented fields mapped: - - `Internet_Plan_Tier__c`, `Installation_Type__c`, `Weekend_Install__c`, `Access_Mode__c`, `Hikari_Denwa__c` โœ… - -4. **SIM Fields** - All documented fields mapped: - - `SIM_Type__c`, `EID__c`, `SIM_Voice_Mail__c`, `SIM_Call_Waiting__c` + MNP fields โœ… - -5. **VPN Fields** - Correctly implemented: - - `VPN_Region__c` โœ… - -### โš ๏ธ **Issues Found:** - -## **Issue 1: Field Mapping Not Used in Order Builder** - -**Problem**: Our `order-builder.service.ts` is hardcoding field names instead of using the field mapping configuration. - -**Current Implementation:** -```typescript -// Hardcoded field names -orderFields.Internet_Plan_Tier__c = serviceProduct.internetPlanTier; -orderFields.Access_Mode__c = config.accessMode; -orderFields.Installation_Type__c = installType; -``` - -**Should Be:** -```typescript -// Using field mapping -const fields = getSalesforceFieldMap(); -orderFields[fields.order.internetPlanTier] = serviceProduct.internetPlanTier; -orderFields[fields.order.accessMode] = config.accessMode; -orderFields[fields.order.installationType] = installType; -``` - -## **Issue 2: Missing Documentation Alignment** - -**Problem**: Documentation shows different API structure than implementation. - -**Documentation Says:** -```json -{ - "items": [ - { "productId": "...", "billingCycle": "...", "configOptions": {...} } - ], - "promoCode": "...", - "notes": "..." -} -``` - -**Current Implementation:** -```json -{ - "orderType": "Internet", - "skus": ["INTERNET-SILVER-HOME-1G", "..."], - "configurations": { "accessMode": "PPPoE" } -} -``` - -## **Issue 3: Validation Logic vs Documentation** - -**Problem**: Our validation doesn't match documented requirements exactly. - -**Documentation Requirements:** -- "Server-side checks: require WHMCS mapping; require `hasPaymentMethod=true`" -- "Create Salesforce Order (Pending Review)" - -**Current Implementation:** โœ… Correctly implemented - -## **Issue 4: Missing Order Status Progression** - -**Documentation Shows:** -- Initial Status: "Pending Review" -- After Approval: "Provisioned" -- Error States: "Failed" - -**Current Implementation:** โœ… Sets "Pending Review" correctly - -## **Issue 5: MNP Field Mapping Inconsistency** - -**Problem**: Some MNP fields use different patterns. - -**Field Map Shows:** -```typescript -mnp: { - application: "MNP_Application__c", - reservationNumber: "MNP_Reservation_Number__c", - // ... -} -``` - -**Order Builder Uses:** -```typescript -orderFields.MNP_Application__c = true; // โœ… Correct -orderFields.MNP_Reservation_Number__c = config.mnpNumber; // โœ… Correct -``` - -## **Recommendations:** - -### 1. **Fix Field Mapping Usage** (High Priority) -Update `order-builder.service.ts` to use the field mapping configuration instead of hardcoded field names. - -### 2. **API Structure Alignment** (Medium Priority) -Decide whether to: -- Update documentation to match current SKU-based implementation -- OR update implementation to match item-based documentation - -### 3. **Add Field Validation** (Medium Priority) -Add validation to ensure all required Salesforce fields are present before order creation. - -### 4. **Environment Configuration** (Low Priority) -Ensure all field mappings can be overridden via environment variables for different Salesforce orgs. - -## **Overall Assessment: ๐ŸŸก MOSTLY CORRECT** - -The core functionality is working correctly, but we need to fix the field mapping usage for better maintainability and environment flexibility. diff --git a/apps/bff/Dockerfile b/apps/bff/Dockerfile index fa92c8e5..03c6bc10 100644 --- a/apps/bff/Dockerfile +++ b/apps/bff/Dockerfile @@ -79,7 +79,7 @@ COPY apps/bff/package.json ./apps/bff/ # Install only production dependencies; skip lifecycle scripts to avoid Husky prepare # Prisma client and native assets are generated in the builder stage and copied below ENV HUSKY=0 -RUN pnpm install --frozen-lockfile --prod --ignore-scripts +RUN pnpm install --recursive --frozen-lockfile --prod --ignore-scripts # Copy built applications and Prisma client COPY --from=builder /app/packages/shared/dist ./packages/shared/dist diff --git a/compose-plesk.yaml b/compose-plesk.yaml new file mode 100644 index 00000000..7995888b --- /dev/null +++ b/compose-plesk.yaml @@ -0,0 +1,113 @@ +# ๐Ÿš€ Customer Portal - Plesk Docker Stack +# Deploy via: Plesk โ†’ Docker โ†’ Stacks โ†’ Add Stack +# Project name: customer-portal + +services: + frontend: + image: ghcr.io/ntumurbars/customer-portal-frontend:latest + container_name: portal-frontend + network_mode: bridge + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - PORT=3000 + - HOSTNAME=0.0.0.0 + - NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE} + - NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME} + - NEXT_PUBLIC_APP_VERSION=${NEXT_PUBLIC_APP_VERSION} + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + start_period: 40s + retries: 3 + + backend: + image: ghcr.io/ntumurbars/customer-portal-backend:latest + container_name: portal-backend + network_mode: bridge + ports: + - "127.0.0.1:4000:4000" + environment: + - NODE_ENV=production + - PORT=4000 + - DATABASE_URL=${DATABASE_URL} + - REDIS_URL=${REDIS_URL} + - JWT_SECRET=${JWT_SECRET} + - JWT_EXPIRES_IN=${JWT_EXPIRES_IN} + - BCRYPT_ROUNDS=${BCRYPT_ROUNDS} + - CORS_ORIGIN=${CORS_ORIGIN} + - TRUST_PROXY=${TRUST_PROXY} + - WHMCS_BASE_URL=${WHMCS_BASE_URL} + - WHMCS_API_IDENTIFIER=${WHMCS_API_IDENTIFIER} + - WHMCS_API_SECRET=${WHMCS_API_SECRET} + - SF_LOGIN_URL=${SF_LOGIN_URL} + - SF_CLIENT_ID=${SF_CLIENT_ID} + - SF_PRIVATE_KEY_PATH=${SF_PRIVATE_KEY_PATH} + - SF_USERNAME=${SF_USERNAME} + - PORTAL_PRICEBOOK_ID=${PORTAL_PRICEBOOK_ID} + - LOG_LEVEL=${LOG_LEVEL} + - LOG_FORMAT=${LOG_FORMAT} + - SENDGRID_API_KEY=${SENDGRID_API_KEY} + - EMAIL_FROM=${EMAIL_FROM} + - EMAIL_FROM_NAME=${EMAIL_FROM_NAME} + - EMAIL_ENABLED=${EMAIL_ENABLED} + - EMAIL_USE_QUEUE=${EMAIL_USE_QUEUE} + - SENDGRID_SANDBOX=${SENDGRID_SANDBOX} + - EMAIL_TEMPLATE_RESET=${EMAIL_TEMPLATE_RESET} + - EMAIL_TEMPLATE_WELCOME=${EMAIL_TEMPLATE_WELCOME} + - NODE_OPTIONS=${NODE_OPTIONS} + restart: unless-stopped + depends_on: + - database + - cache + command: sh -c "pnpm prisma migrate deploy && node dist/main" + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4000/health"] + interval: 30s + timeout: 10s + start_period: 60s + retries: 3 + + database: + image: postgres:17-alpine + container_name: portal-database + network_mode: bridge + ports: + - "127.0.0.1:5432:5432" # Only accessible from localhost for security + environment: + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C + volumes: + - postgres_data:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U portal_user -d portal_db"] + interval: 10s + timeout: 5s + retries: 5 + + cache: + image: redis:7-alpine + container_name: portal-cache + network_mode: bridge + ports: + - "127.0.0.1:6379:6379" # Only accessible from localhost for security + volumes: + - redis_data:/data + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + driver: local + redis_data: + driver: local diff --git a/package.json b/package.json index 5ec54bf6..2b447931 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "update:safe": "pnpm update --recursive && pnpm audit && pnpm type-check" }, "devDependencies": { + "@eslint/js": "^9.13.0", "@eslint/eslintrc": "^3.3.1", "@types/node": "^24.3.0", "eslint": "^9.33.0", From 26ccf6369e8edda8bbfac1e72eb2bd59bd1cbe38 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 16:48:06 +0900 Subject: [PATCH 02/18] Update pnpm-lock.yaml to include '@eslint/js' dependency for improved linting capabilities --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f152b1a2..aed4a288 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: '@eslint/eslintrc': specifier: ^3.3.1 version: 3.3.1 + '@eslint/js': + specifier: ^9.13.0 + version: 9.33.0 '@types/node': specifier: ^24.3.0 version: 24.3.0 From b1844d7017b56572b7645e58f878c47e8553e912 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 16:50:49 +0900 Subject: [PATCH 03/18] Add Node.js and pnpm setup in GitHub Actions workflow for improved dependency management --- .github/workflows/deploy.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 49a65b86..6ca529d7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,6 +21,20 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'pnpm' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.15.0 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From 3270069324b845bdb3e7b4f7e4a06b3bee521da5 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 16:52:02 +0900 Subject: [PATCH 04/18] Refactor GitHub Actions workflow to remove duplicate pnpm setup step for cleaner configuration --- .github/workflows/deploy.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6ca529d7..d66fbcf0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,17 +21,17 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.15.0 + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: 'pnpm' - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.15.0 - - name: Install dependencies run: pnpm install --frozen-lockfile From d7a4d9f24aad16230574331c3aef090dd46bad8f Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 17:27:08 +0900 Subject: [PATCH 05/18] Update Docker Compose configuration and remove obsolete GitHub Actions workflows - Changed image references in compose-plesk.yaml to use local images for frontend and backend services. - Deleted outdated CI and test workflow files to streamline the repository and reduce maintenance overhead. --- .github/workflows/ci.yml | 46 ------------------- .github/workflows/test.yml | 94 -------------------------------------- compose-plesk.yaml | 4 +- 3 files changed, 2 insertions(+), 142 deletions(-) delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 2e3fd0ed..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: "pnpm" - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.15.0 - - - name: Install deps - run: pnpm install --frozen-lockfile - - - name: Build Shared (needed for type refs) - run: pnpm --filter @customer-portal/shared run build - - - name: Generate Prisma client - run: pnpm --filter @customer-portal/bff run db:generate - - - name: Type check (workspace) - run: pnpm --recursive run type-check - - - name: Lint (workspace) - run: pnpm --recursive run lint - - - name: Build BFF - run: pnpm --filter @customer-portal/bff run build - - - name: Build Portal - run: pnpm --filter @customer-portal/portal run build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index e044ab40..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Test & Lint - -on: - push: - branches: [main, develop] - pull_request: - branches: [main, develop] - -env: - NODE_VERSION: "22" - PNPM_VERSION: "10.15.0" - -jobs: - test: - name: Test & Lint - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:17 - env: - POSTGRES_PASSWORD: test - POSTGRES_DB: portal_test - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - redis: - image: redis:8-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 6379:6379 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Enable Corepack and install pnpm - run: | - corepack enable - corepack prepare pnpm@${{ env.PNPM_VERSION }} --activate - - - name: Cache pnpm dependencies - uses: actions/cache@v4 - with: - path: ~/.pnpm-store - key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm- - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build shared package (needed for type refs) - run: pnpm --filter @customer-portal/shared run build - - - name: Generate Prisma client - run: pnpm --filter @customer-portal/bff run db:generate - - - name: Type check - run: pnpm type-check - - - name: Lint - run: pnpm lint - - - name: Test shared package - run: pnpm --filter @customer-portal/shared run test - if: success() || failure() - - - name: Test BFF package - run: pnpm --filter @customer-portal/bff run test - env: - DATABASE_URL: postgresql://postgres:test@localhost:5432/portal_test - REDIS_URL: redis://localhost:6379 - if: success() || failure() - - - name: Build applications - run: pnpm build - env: - NEXT_PUBLIC_API_BASE: http://localhost:4000 - NEXT_PUBLIC_APP_NAME: Customer Portal Test diff --git a/compose-plesk.yaml b/compose-plesk.yaml index 7995888b..955f06a6 100644 --- a/compose-plesk.yaml +++ b/compose-plesk.yaml @@ -4,7 +4,7 @@ services: frontend: - image: ghcr.io/ntumurbars/customer-portal-frontend:latest + image: portal-frontend:latest container_name: portal-frontend network_mode: bridge ports: @@ -25,7 +25,7 @@ services: retries: 3 backend: - image: ghcr.io/ntumurbars/customer-portal-backend:latest + image: portal-backend:latest container_name: portal-backend network_mode: bridge ports: From 11f57dfd56cc2d6eaf59e91a3dc6da0a7bc1eccc Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 17:41:04 +0900 Subject: [PATCH 06/18] Update GitHub Actions workflow and add archive files to .gitignore - Modify deploy.yml workflow configuration - Add *.tar, *.tar.gz, *.zip to .gitignore to prevent large file commits --- .github/workflows/deploy.yml | 6 +++--- .gitignore | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d66fbcf0..a80ea8a6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,9 +1,9 @@ name: Build & Push Images on: - push: - branches: [main] - workflow_dispatch: # Allow manual triggers + workflow_dispatch: # Only allow manual triggers + # push: + # branches: [main] # Commented out - no auto-trigger env: REGISTRY: ghcr.io diff --git a/.gitignore b/.gitignore index 3d093c3d..9221220d 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,8 @@ temp/ # Prisma prisma/migrations/dev.db* + +# Large archive files +*.tar +*.tar.gz +*.zip From 8b855ca371c8d5f980ef90761757a7a01a781d9a Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 18:15:05 +0900 Subject: [PATCH 07/18] Update Docker Compose configuration for security and functionality - Restrict frontend service port to localhost for enhanced security. - Add volume mapping for secrets to the application container. - Update healthcheck command for the database service to reflect new user and database names. --- compose-plesk.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compose-plesk.yaml b/compose-plesk.yaml index 955f06a6..5b689a2a 100644 --- a/compose-plesk.yaml +++ b/compose-plesk.yaml @@ -8,7 +8,7 @@ services: container_name: portal-frontend network_mode: bridge ports: - - "3000:3000" + - "127.0.0.1:3000:3000" # Only accessible from localhost for security environment: - NODE_ENV=production - PORT=3000 @@ -59,6 +59,8 @@ services: - EMAIL_TEMPLATE_RESET=${EMAIL_TEMPLATE_RESET} - EMAIL_TEMPLATE_WELCOME=${EMAIL_TEMPLATE_WELCOME} - NODE_OPTIONS=${NODE_OPTIONS} + volumes: + - /var/www/vhosts/asolutions.jp/httpdocs/secrets:/app/secrets:ro restart: unless-stopped depends_on: - database @@ -86,7 +88,7 @@ services: - postgres_data:/var/lib/postgresql/data restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "pg_isready -U portal_user -d portal_db"] + test: ["CMD-SHELL", "pg_isready -U portal -d portal_prod"] interval: 10s timeout: 5s retries: 5 From 638638ea89f39a121ec0d1286587e3ae59d6e7ea Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 18:33:30 +0900 Subject: [PATCH 08/18] Refactor Docker Compose configuration to enhance network management - Removed network_mode settings for services and replaced them with a dedicated portal-network. - Ensured all services are connected to the new bridge network for improved communication and organization. --- compose-plesk.yaml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/compose-plesk.yaml b/compose-plesk.yaml index 5b689a2a..706d3ab9 100644 --- a/compose-plesk.yaml +++ b/compose-plesk.yaml @@ -6,9 +6,10 @@ services: frontend: image: portal-frontend:latest container_name: portal-frontend - network_mode: bridge ports: - - "127.0.0.1:3000:3000" # Only accessible from localhost for security + - "127.0.0.1:3000:3000" + networks: + - portal-network environment: - NODE_ENV=production - PORT=3000 @@ -27,9 +28,10 @@ services: backend: image: portal-backend:latest container_name: portal-backend - network_mode: bridge ports: - "127.0.0.1:4000:4000" + networks: + - portal-network environment: - NODE_ENV=production - PORT=4000 @@ -76,9 +78,8 @@ services: database: image: postgres:17-alpine container_name: portal-database - network_mode: bridge - ports: - - "127.0.0.1:5432:5432" # Only accessible from localhost for security + networks: + - portal-network environment: - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} @@ -96,9 +97,8 @@ services: cache: image: redis:7-alpine container_name: portal-cache - network_mode: bridge - ports: - - "127.0.0.1:6379:6379" # Only accessible from localhost for security + networks: + - portal-network volumes: - redis_data:/data restart: unless-stopped @@ -113,3 +113,8 @@ volumes: driver: local redis_data: driver: local + +networks: + portal-network: + driver: bridge + name: portal-network From ab7492f2abbb763d37d8f6d2c395fca36cf5037c Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 18:35:12 +0900 Subject: [PATCH 09/18] Update Docker Compose configuration to disable IPv6 for portal-network - Set enable_ipv6 to false for the portal-network to enhance compatibility and reduce potential network issues. - Added driver options to explicitly disable IPv6 support in the Docker network configuration. --- compose-plesk-fallback.yaml | 114 ++++++++++++++++++++++++++++++++++++ compose-plesk.yaml | 3 + 2 files changed, 117 insertions(+) create mode 100644 compose-plesk-fallback.yaml diff --git a/compose-plesk-fallback.yaml b/compose-plesk-fallback.yaml new file mode 100644 index 00000000..a2691138 --- /dev/null +++ b/compose-plesk-fallback.yaml @@ -0,0 +1,114 @@ +# ๐Ÿš€ Customer Portal - Plesk Docker Stack (Fallback - Default Bridge) +# Deploy via: Plesk โ†’ Docker โ†’ Stacks โ†’ Add Stack +# Project name: customer-portal + +services: + frontend: + image: portal-frontend:latest + container_name: portal-frontend + ports: + - "127.0.0.1:3000:3000" + environment: + - NODE_ENV=production + - PORT=3000 + - HOSTNAME=0.0.0.0 + - NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE} + - NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME} + - NEXT_PUBLIC_APP_VERSION=${NEXT_PUBLIC_APP_VERSION} + restart: unless-stopped + depends_on: + - backend + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + start_period: 40s + retries: 3 + + backend: + image: portal-backend:latest + container_name: portal-backend + ports: + - "127.0.0.1:4000:4000" + environment: + - NODE_ENV=production + - PORT=4000 + - DATABASE_URL=${DATABASE_URL} + - REDIS_URL=${REDIS_URL} + - JWT_SECRET=${JWT_SECRET} + - JWT_EXPIRES_IN=${JWT_EXPIRES_IN} + - BCRYPT_ROUNDS=${BCRYPT_ROUNDS} + - CORS_ORIGIN=${CORS_ORIGIN} + - TRUST_PROXY=${TRUST_PROXY} + - WHMCS_BASE_URL=${WHMCS_BASE_URL} + - WHMCS_API_IDENTIFIER=${WHMCS_API_IDENTIFIER} + - WHMCS_API_SECRET=${WHMCS_API_SECRET} + - SF_LOGIN_URL=${SF_LOGIN_URL} + - SF_CLIENT_ID=${SF_CLIENT_ID} + - SF_PRIVATE_KEY_PATH=${SF_PRIVATE_KEY_PATH} + - SF_USERNAME=${SF_USERNAME} + - PORTAL_PRICEBOOK_ID=${PORTAL_PRICEBOOK_ID} + - LOG_LEVEL=${LOG_LEVEL} + - LOG_FORMAT=${LOG_FORMAT} + - SENDGRID_API_KEY=${SENDGRID_API_KEY} + - EMAIL_FROM=${EMAIL_FROM} + - EMAIL_FROM_NAME=${EMAIL_FROM_NAME} + - EMAIL_ENABLED=${EMAIL_ENABLED} + - EMAIL_USE_QUEUE=${EMAIL_USE_QUEUE} + - SENDGRID_SANDBOX=${SENDGRID_SANDBOX} + - EMAIL_TEMPLATE_RESET=${EMAIL_TEMPLATE_RESET} + - EMAIL_TEMPLATE_WELCOME=${EMAIL_TEMPLATE_WELCOME} + - NODE_OPTIONS=${NODE_OPTIONS} + volumes: + - /var/www/vhosts/asolutions.jp/httpdocs/secrets:/app/secrets:ro + restart: unless-stopped + depends_on: + database: + condition: service_healthy + cache: + condition: service_healthy + links: + - database + - cache + command: sh -c "pnpm prisma migrate deploy && node dist/main" + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4000/health"] + interval: 30s + timeout: 10s + start_period: 60s + retries: 3 + + database: + image: postgres:17-alpine + container_name: portal-database + environment: + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C + volumes: + - postgres_data:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U portal -d portal_prod"] + interval: 10s + timeout: 5s + retries: 5 + + cache: + image: redis:7-alpine + container_name: portal-cache + volumes: + - redis_data:/data + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + driver: local + redis_data: + driver: local diff --git a/compose-plesk.yaml b/compose-plesk.yaml index 706d3ab9..5ee418da 100644 --- a/compose-plesk.yaml +++ b/compose-plesk.yaml @@ -118,3 +118,6 @@ networks: portal-network: driver: bridge name: portal-network + enable_ipv6: false + driver_opts: + com.docker.network.enable_ipv6: "false" From c625b978d9e837d56128145cb519a6937063fa70 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 18:37:41 +0900 Subject: [PATCH 10/18] Refactor Docker Compose configuration to improve service dependencies and remove unused network - Removed the portal-network configuration and associated network settings from services. - Added service health checks for database and cache dependencies in the backend service. - Updated links for backend service to connect directly to database and cache services. --- compose-plesk.yaml | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/compose-plesk.yaml b/compose-plesk.yaml index 5ee418da..90711702 100644 --- a/compose-plesk.yaml +++ b/compose-plesk.yaml @@ -8,8 +8,6 @@ services: container_name: portal-frontend ports: - "127.0.0.1:3000:3000" - networks: - - portal-network environment: - NODE_ENV=production - PORT=3000 @@ -30,8 +28,9 @@ services: container_name: portal-backend ports: - "127.0.0.1:4000:4000" - networks: - - portal-network + links: + - database + - cache environment: - NODE_ENV=production - PORT=4000 @@ -65,8 +64,10 @@ services: - /var/www/vhosts/asolutions.jp/httpdocs/secrets:/app/secrets:ro restart: unless-stopped depends_on: - - database - - cache + database: + condition: service_healthy + cache: + condition: service_healthy command: sh -c "pnpm prisma migrate deploy && node dist/main" healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4000/health"] @@ -78,8 +79,6 @@ services: database: image: postgres:17-alpine container_name: portal-database - networks: - - portal-network environment: - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} @@ -97,8 +96,6 @@ services: cache: image: redis:7-alpine container_name: portal-cache - networks: - - portal-network volumes: - redis_data:/data restart: unless-stopped @@ -113,11 +110,3 @@ volumes: driver: local redis_data: driver: local - -networks: - portal-network: - driver: bridge - name: portal-network - enable_ipv6: false - driver_opts: - com.docker.network.enable_ipv6: "false" From fd4bef3ffec746d34206bf44d7849e84ff4300c2 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 18:41:35 +0900 Subject: [PATCH 11/18] Update Docker Compose configuration to use host network mode for services - Changed network configuration for frontend, backend, database, and cache services to use host network mode. - Removed port mappings to enhance service communication and simplify network management. --- compose-plesk.yaml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/compose-plesk.yaml b/compose-plesk.yaml index 90711702..48ae6a6b 100644 --- a/compose-plesk.yaml +++ b/compose-plesk.yaml @@ -6,8 +6,7 @@ services: frontend: image: portal-frontend:latest container_name: portal-frontend - ports: - - "127.0.0.1:3000:3000" + network_mode: host environment: - NODE_ENV=production - PORT=3000 @@ -26,11 +25,7 @@ services: backend: image: portal-backend:latest container_name: portal-backend - ports: - - "127.0.0.1:4000:4000" - links: - - database - - cache + network_mode: host environment: - NODE_ENV=production - PORT=4000 @@ -79,6 +74,7 @@ services: database: image: postgres:17-alpine container_name: portal-database + network_mode: host environment: - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} @@ -96,6 +92,7 @@ services: cache: image: redis:7-alpine container_name: portal-cache + network_mode: host volumes: - redis_data:/data restart: unless-stopped From e9c8d193b7e5ea4b50b9a85e6596634ac8e36238 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 18:46:20 +0900 Subject: [PATCH 12/18] Update Dockerfile to streamline production dependencies and build process - Simplified the installation of production dependencies by copying node_modules from the builder stage. - Enhanced the Dockerfile by removing unnecessary environment variable settings and comments for clarity. - Ensured all necessary built applications and dependencies are included in the final image. --- apps/bff/Dockerfile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/bff/Dockerfile b/apps/bff/Dockerfile index 03c6bc10..018e0727 100644 --- a/apps/bff/Dockerfile +++ b/apps/bff/Dockerfile @@ -71,21 +71,21 @@ RUN corepack enable && corepack prepare pnpm@10.15.0 --activate WORKDIR /app -# Copy workspace configuration for production install +# Copy workspace configuration COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ COPY packages/shared/package.json ./packages/shared/ COPY apps/bff/package.json ./apps/bff/ -# Install only production dependencies; skip lifecycle scripts to avoid Husky prepare -# Prisma client and native assets are generated in the builder stage and copied below -ENV HUSKY=0 -RUN pnpm install --recursive --frozen-lockfile --prod --ignore-scripts - -# Copy built applications and Prisma client +# Copy built applications and dependencies from builder COPY --from=builder /app/packages/shared/dist ./packages/shared/dist COPY --from=builder /app/apps/bff/dist ./apps/bff/dist COPY --from=builder /app/apps/bff/prisma ./apps/bff/prisma +# Copy node_modules from builder (includes all dependencies) +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/packages/shared/node_modules ./packages/shared/node_modules +COPY --from=builder /app/apps/bff/node_modules ./apps/bff/node_modules + # Generate Prisma client in the production image (ensures engines and client are present) WORKDIR /app/apps/bff RUN pnpm prisma generate From c025993384d56396bf65881eaff5def9da0bb98f Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 18:52:31 +0900 Subject: [PATCH 13/18] Refactor Dockerfile to optimize production dependency installation - Updated the Dockerfile to install only production dependencies, skipping unnecessary scripts to streamline the build process. - Removed the copying of node_modules from the builder stage to reduce image size and improve efficiency. - Ensured necessary postinstall scripts are executed for essential packages like Prisma and bcrypt. --- apps/bff/Dockerfile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/bff/Dockerfile b/apps/bff/Dockerfile index 018e0727..38d85671 100644 --- a/apps/bff/Dockerfile +++ b/apps/bff/Dockerfile @@ -76,16 +76,19 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ COPY packages/shared/package.json ./packages/shared/ COPY apps/bff/package.json ./apps/bff/ -# Copy built applications and dependencies from builder +# Install ONLY production dependencies (no dev dependencies) +# Skip scripts to avoid Husky, but allow other necessary postinstall scripts later +ENV HUSKY=0 +RUN pnpm install --frozen-lockfile --prod --ignore-scripts + +# Run only necessary postinstall scripts (Prisma, bcrypt, etc.) +RUN pnpm rebuild bcrypt @prisma/client @prisma/engines + +# Copy built applications from builder COPY --from=builder /app/packages/shared/dist ./packages/shared/dist COPY --from=builder /app/apps/bff/dist ./apps/bff/dist COPY --from=builder /app/apps/bff/prisma ./apps/bff/prisma -# Copy node_modules from builder (includes all dependencies) -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/packages/shared/node_modules ./packages/shared/node_modules -COPY --from=builder /app/apps/bff/node_modules ./apps/bff/node_modules - # Generate Prisma client in the production image (ensures engines and client are present) WORKDIR /app/apps/bff RUN pnpm prisma generate From 6f1924065b060126951f3642722a26ad12f0e312 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 19:05:34 +0900 Subject: [PATCH 14/18] Remove obsolete fallback Docker Compose configuration for Plesk - Deleted the compose-plesk-fallback.yaml file as it is no longer needed. - This cleanup helps streamline the project by removing unused configurations. --- compose-plesk-fallback.yaml | 114 ------------------------------------ compose-plesk.yaml | 10 +++- 2 files changed, 8 insertions(+), 116 deletions(-) delete mode 100644 compose-plesk-fallback.yaml diff --git a/compose-plesk-fallback.yaml b/compose-plesk-fallback.yaml deleted file mode 100644 index a2691138..00000000 --- a/compose-plesk-fallback.yaml +++ /dev/null @@ -1,114 +0,0 @@ -# ๐Ÿš€ Customer Portal - Plesk Docker Stack (Fallback - Default Bridge) -# Deploy via: Plesk โ†’ Docker โ†’ Stacks โ†’ Add Stack -# Project name: customer-portal - -services: - frontend: - image: portal-frontend:latest - container_name: portal-frontend - ports: - - "127.0.0.1:3000:3000" - environment: - - NODE_ENV=production - - PORT=3000 - - HOSTNAME=0.0.0.0 - - NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE} - - NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME} - - NEXT_PUBLIC_APP_VERSION=${NEXT_PUBLIC_APP_VERSION} - restart: unless-stopped - depends_on: - - backend - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] - interval: 30s - timeout: 10s - start_period: 40s - retries: 3 - - backend: - image: portal-backend:latest - container_name: portal-backend - ports: - - "127.0.0.1:4000:4000" - environment: - - NODE_ENV=production - - PORT=4000 - - DATABASE_URL=${DATABASE_URL} - - REDIS_URL=${REDIS_URL} - - JWT_SECRET=${JWT_SECRET} - - JWT_EXPIRES_IN=${JWT_EXPIRES_IN} - - BCRYPT_ROUNDS=${BCRYPT_ROUNDS} - - CORS_ORIGIN=${CORS_ORIGIN} - - TRUST_PROXY=${TRUST_PROXY} - - WHMCS_BASE_URL=${WHMCS_BASE_URL} - - WHMCS_API_IDENTIFIER=${WHMCS_API_IDENTIFIER} - - WHMCS_API_SECRET=${WHMCS_API_SECRET} - - SF_LOGIN_URL=${SF_LOGIN_URL} - - SF_CLIENT_ID=${SF_CLIENT_ID} - - SF_PRIVATE_KEY_PATH=${SF_PRIVATE_KEY_PATH} - - SF_USERNAME=${SF_USERNAME} - - PORTAL_PRICEBOOK_ID=${PORTAL_PRICEBOOK_ID} - - LOG_LEVEL=${LOG_LEVEL} - - LOG_FORMAT=${LOG_FORMAT} - - SENDGRID_API_KEY=${SENDGRID_API_KEY} - - EMAIL_FROM=${EMAIL_FROM} - - EMAIL_FROM_NAME=${EMAIL_FROM_NAME} - - EMAIL_ENABLED=${EMAIL_ENABLED} - - EMAIL_USE_QUEUE=${EMAIL_USE_QUEUE} - - SENDGRID_SANDBOX=${SENDGRID_SANDBOX} - - EMAIL_TEMPLATE_RESET=${EMAIL_TEMPLATE_RESET} - - EMAIL_TEMPLATE_WELCOME=${EMAIL_TEMPLATE_WELCOME} - - NODE_OPTIONS=${NODE_OPTIONS} - volumes: - - /var/www/vhosts/asolutions.jp/httpdocs/secrets:/app/secrets:ro - restart: unless-stopped - depends_on: - database: - condition: service_healthy - cache: - condition: service_healthy - links: - - database - - cache - command: sh -c "pnpm prisma migrate deploy && node dist/main" - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4000/health"] - interval: 30s - timeout: 10s - start_period: 60s - retries: 3 - - database: - image: postgres:17-alpine - container_name: portal-database - environment: - - POSTGRES_DB=${POSTGRES_DB} - - POSTGRES_USER=${POSTGRES_USER} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C - volumes: - - postgres_data:/var/lib/postgresql/data - restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "pg_isready -U portal -d portal_prod"] - interval: 10s - timeout: 5s - retries: 5 - - cache: - image: redis:7-alpine - container_name: portal-cache - volumes: - - redis_data:/data - restart: unless-stopped - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - driver: local - redis_data: - driver: local diff --git a/compose-plesk.yaml b/compose-plesk.yaml index 48ae6a6b..26322bf2 100644 --- a/compose-plesk.yaml +++ b/compose-plesk.yaml @@ -4,7 +4,10 @@ services: frontend: - image: portal-frontend:latest + build: + context: /var/www/vhosts/asolutions.jp/httpdocs + dockerfile: apps/portal/Dockerfile + target: production container_name: portal-frontend network_mode: host environment: @@ -23,7 +26,10 @@ services: retries: 3 backend: - image: portal-backend:latest + build: + context: /var/www/vhosts/asolutions.jp/httpdocs + dockerfile: apps/bff/Dockerfile + target: production container_name: portal-backend network_mode: host environment: From cc7235e79c067feae98329d2e817e49a9ffe4e27 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Sat, 30 Aug 2025 10:49:53 +0900 Subject: [PATCH 15/18] Refactor Docker Compose configuration to use pre-built images for services - Updated the frontend and backend services to use pre-built images instead of build contexts. - Set pull_policy to never to prevent automatic image pulls, ensuring consistent deployments. --- compose-plesk.yaml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/compose-plesk.yaml b/compose-plesk.yaml index 26322bf2..143aef38 100644 --- a/compose-plesk.yaml +++ b/compose-plesk.yaml @@ -4,12 +4,10 @@ services: frontend: - build: - context: /var/www/vhosts/asolutions.jp/httpdocs - dockerfile: apps/portal/Dockerfile - target: production + image: portal-frontend container_name: portal-frontend network_mode: host + pull_policy: never environment: - NODE_ENV=production - PORT=3000 @@ -26,12 +24,10 @@ services: retries: 3 backend: - build: - context: /var/www/vhosts/asolutions.jp/httpdocs - dockerfile: apps/bff/Dockerfile - target: production + image: portal-backend container_name: portal-backend network_mode: host + pull_policy: never environment: - NODE_ENV=production - PORT=4000 From e13f63cf0c88dcb573264de7f79cf3e4f954a812 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Sat, 30 Aug 2025 10:58:31 +0900 Subject: [PATCH 16/18] Update Dockerfile to install all dependencies for build and optimize production setup - Changed the installation command to include all dependencies necessary for the build process. - Updated comments for clarity regarding the installation of production dependencies and rebuilding critical native modules. --- apps/bff/Dockerfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/bff/Dockerfile b/apps/bff/Dockerfile index 38d85671..31293d06 100644 --- a/apps/bff/Dockerfile +++ b/apps/bff/Dockerfile @@ -21,7 +21,7 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ COPY packages/shared/package.json ./packages/shared/ COPY apps/bff/package.json ./apps/bff/ -# Install dependencies with frozen lockfile +# Install ALL dependencies (needed for build) RUN pnpm install --frozen-lockfile --prefer-offline # ===================================================== @@ -76,12 +76,11 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ COPY packages/shared/package.json ./packages/shared/ COPY apps/bff/package.json ./apps/bff/ -# Install ONLY production dependencies (no dev dependencies) -# Skip scripts to avoid Husky, but allow other necessary postinstall scripts later +# Install ONLY production dependencies (lightweight) ENV HUSKY=0 RUN pnpm install --frozen-lockfile --prod --ignore-scripts -# Run only necessary postinstall scripts (Prisma, bcrypt, etc.) +# Rebuild only critical native modules RUN pnpm rebuild bcrypt @prisma/client @prisma/engines # Copy built applications from builder From 5e21d2840a800b8848e16cddc4a7f6a486be41ce Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Sat, 30 Aug 2025 11:02:02 +0900 Subject: [PATCH 17/18] Update backend service image to optimized version in Docker Compose configuration --- compose-plesk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose-plesk.yaml b/compose-plesk.yaml index 143aef38..6b908658 100644 --- a/compose-plesk.yaml +++ b/compose-plesk.yaml @@ -24,7 +24,7 @@ services: retries: 3 backend: - image: portal-backend + image: portal-backend:optimized container_name: portal-backend network_mode: host pull_policy: never From d055ba34d82a44906e1887465887fa940f2cd50a Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Sat, 30 Aug 2025 15:47:48 +0900 Subject: [PATCH 18/18] Remove obsolete deployment guides and ESLint report files to streamline project documentation and improve clarity. Updated package configurations and scripts for better development experience and logging integration. --- DEPLOYMENT-GUIDE.md | 141 ----------- DEPLOYMENT.md | 75 ------ apps/bff/nest-cli.json | 3 +- apps/bff/package.json | 4 +- apps/bff/src/common/logging/logging.config.ts | 234 ------------------ apps/bff/src/common/logging/logging.module.ts | 4 +- apps/bff/src/main.ts | 1 + apps/bff/tsconfig.build.json | 15 ++ apps/portal/package.json | 5 +- apps/portal/src/lib/logger.ts | 135 +--------- apps/portal/tsconfig.json | 21 +- eslint-report.json | 1 - package.json | 13 +- packages/shared/package.json | 15 ++ packages/shared/src/logging/index.ts | 2 + .../shared/src/logging/nest-logger.config.ts | 126 ++++++++++ packages/shared/src/logging/pino-logger.ts | 178 +++++++++++++ pnpm-lock.yaml | 4 + scripts/dev/manage.sh | 11 +- tsconfig.json | 6 +- 20 files changed, 383 insertions(+), 611 deletions(-) delete mode 100644 DEPLOYMENT-GUIDE.md delete mode 100644 DEPLOYMENT.md delete mode 100644 apps/bff/src/common/logging/logging.config.ts create mode 100644 apps/bff/tsconfig.build.json delete mode 100644 eslint-report.json create mode 100644 packages/shared/src/logging/nest-logger.config.ts create mode 100644 packages/shared/src/logging/pino-logger.ts diff --git a/DEPLOYMENT-GUIDE.md b/DEPLOYMENT-GUIDE.md deleted file mode 100644 index f4919d86..00000000 --- a/DEPLOYMENT-GUIDE.md +++ /dev/null @@ -1,141 +0,0 @@ -# ๐Ÿš€ Pre-built Images Deployment Guide - -This guide shows how to deploy using pre-built Docker images instead of building on Plesk. - -## Benefits -- โœ… No build failures on Plesk -- โœ… Faster deployments (no compilation time) -- โœ… Consistent images across environments -- โœ… Better security (build in controlled environment) -- โœ… Easy rollbacks and version control - -## Prerequisites - -1. **GitHub Account** (for free container registry) -2. **Docker installed locally** (for building images) -3. **Plesk with Docker extension** - -## Step 1: Setup GitHub Container Registry - -1. Go to GitHub โ†’ Settings โ†’ Developer settings โ†’ Personal access tokens โ†’ Tokens (classic) -2. Create a new token with these permissions: - - `write:packages` (to push images) - - `read:packages` (to pull images) -3. Save the token securely - -## Step 2: Login to GitHub Container Registry - -```bash -# Replace YOUR_USERNAME and YOUR_TOKEN -echo "YOUR_TOKEN" | docker login ghcr.io -u YOUR_USERNAME --password-stdin -``` - -## Step 3: Update Build Script - -Edit `scripts/build-and-push.sh`: -```bash -# Change this line: -NAMESPACE="your-github-username" # Replace with your actual GitHub username -``` - -## Step 4: Build and Push Images - -```bash -# Build and push with version tag -./scripts/build-and-push.sh v1.0.0 - -# Or build and push as latest -./scripts/build-and-push.sh -``` - -## Step 5: Update Plesk Compose File - -Edit `compose-plesk.yaml` and replace: -```yaml -image: ghcr.io/your-github-username/portal-frontend:latest -image: ghcr.io/your-github-username/portal-backend:latest -``` - -With your actual GitHub username. - -## Step 6: Deploy to Plesk - -1. **Upload compose-plesk.yaml** to your Plesk server -2. **Plesk โ†’ Docker โ†’ Add Stack** -3. **Paste the contents** of `compose-plesk.yaml` -4. **Deploy** - -## Step 7: Configure Plesk Reverse Proxy - -1. **Plesk โ†’ Domains โ†’ your-domain.com โ†’ Apache & Nginx Settings** -2. **Add to "Additional directives for HTTP":** -```nginx -location / { - proxy_pass http://127.0.0.1:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; -} - -location /api { - proxy_pass http://127.0.0.1:4000; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; -} -``` - -## Step 8: Secure Database Access - -Add to Plesk Firewall: -``` -# Allow Docker bridge network -ACCEPT from 172.17.0.0/16 to any port 5432 -ACCEPT from 172.17.0.0/16 to any port 6379 - -# Deny external access to database -DROP from any to any port 5432 -DROP from any to any port 6379 -``` - -## Updating Your Application - -1. **Make code changes** -2. **Build and push new images:** - ```bash - ./scripts/build-and-push.sh v1.0.1 - ``` -3. **Update compose-plesk.yaml** with new version tag -4. **Redeploy in Plesk** - -## Troubleshooting - -### Images not found -- Check if you're logged in: `docker login ghcr.io` -- Verify image names match your GitHub username -- Ensure images are public or Plesk can authenticate - -### Build failures -- Run locally first: `docker build -f apps/portal/Dockerfile .` -- Check Docker logs for specific errors -- Ensure all dependencies are in package.json - -### Connection issues -- Verify firewall allows Docker bridge network (172.17.0.0/16) -- Check that DATABASE_URL uses correct IP (172.17.0.1) -- Test database connection from backend container - -## Security Notes - -- Database is only accessible from Docker bridge network -- Backend API is only accessible via reverse proxy -- Use strong passwords and JWT secrets -- Consider using Docker secrets for sensitive data -- Regularly update base images for security patches diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md deleted file mode 100644 index 46cccc86..00000000 --- a/DEPLOYMENT.md +++ /dev/null @@ -1,75 +0,0 @@ -# ๐Ÿš€ Deployment Guide - -## ๐Ÿ“ **Environment Files Overview** - -### **Development:** -- `.env` - Your local development environment (active) -- `.env.example` - Development template for new developers - -### **Production:** -- `.env.production` - Production environment for Plesk deployment -- `compose-plesk.yaml` - Docker Stack definition - -## ๐Ÿ”ง **Plesk Deployment Steps** - -### **Step 1: Authenticate Docker (One-time)** -```bash -# SSH to Plesk server -echo "YOUR_GITHUB_TOKEN" | docker login ghcr.io -u ntumurbars --password-stdin -``` - -### **Step 2: Upload Files to Plesk** -Upload these files to your domain directory: -1. `compose-plesk.yaml` - Docker Stack definition -2. `.env.production` - Environment variables (rename to `.env`) - -### **Step 3: Deploy Stack** -1. **Plesk โ†’ Docker โ†’ Stacks โ†’ Add Stack** -2. **Project name**: `customer-portal` -3. **Method**: Upload file or paste `compose-plesk.yaml` content -4. **Deploy** - -### **Step 4: Configure Nginx Proxy** -1. **Plesk โ†’ Websites & Domains โ†’ yourdomain.com โ†’ Docker Proxy Rules** -2. **Add rule**: `/` โ†’ `portal-frontend` โ†’ port `3000` -3. **Add rule**: `/api` โ†’ `portal-backend` โ†’ port `4000` - -## ๐Ÿ”„ **Update Workflow** - -### **When You Push Code:** -1. **GitHub Actions** builds new images automatically -2. **SSH to Plesk** and update: - ```bash - cd /var/www/vhosts/yourdomain.com/httpdocs/ - docker compose -f compose-plesk.yaml pull - docker compose -f compose-plesk.yaml up -d - ``` - -## ๐Ÿ” **Environment Variables** - -Your compose file uses these key variables from `.env.production`: - -### **Database:** -- `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` -- `DATABASE_URL` - Full connection string - -### **Application:** -- `JWT_SECRET`, `CORS_ORIGIN` -- `NEXT_PUBLIC_API_BASE`, `NEXT_PUBLIC_APP_NAME` - -### **External APIs:** -- `WHMCS_BASE_URL`, `WHMCS_API_IDENTIFIER`, `WHMCS_API_SECRET` -- `SF_LOGIN_URL`, `SF_CLIENT_ID`, `SF_USERNAME` - -### **Email & Logging:** -- `SENDGRID_API_KEY`, `EMAIL_FROM` -- `LOG_LEVEL`, `LOG_FORMAT` - -## โœ… **Ready to Deploy!** - -Your setup is clean and production-ready: -- โœ… Environment variables properly configured -- โœ… Docker secrets via environment variables -- โœ… Database and Redis secured (localhost only) -- โœ… Automated image building -- โœ… Clean file structure diff --git a/apps/bff/nest-cli.json b/apps/bff/nest-cli.json index ad492172..1a89e095 100644 --- a/apps/bff/nest-cli.json +++ b/apps/bff/nest-cli.json @@ -5,6 +5,7 @@ "compilerOptions": { "deleteOutDir": true, "watchAssets": true, - "assets": ["**/*.prisma"] + "assets": ["**/*.prisma"], + "tsConfigPath": "tsconfig.build.json" } } diff --git a/apps/bff/package.json b/apps/bff/package.json index ea91285a..07648d3e 100644 --- a/apps/bff/package.json +++ b/apps/bff/package.json @@ -6,7 +6,7 @@ "private": true, "license": "UNLICENSED", "scripts": { - "build": "nest build", + "build": "nest build -c tsconfig.build.json", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "dev": "NODE_OPTIONS=\"--no-deprecation\" nest start --watch", @@ -82,7 +82,7 @@ "source-map-support": "^0.5.21", "supertest": "^7.1.4", "ts-jest": "^29.4.1", - "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.9.2" diff --git a/apps/bff/src/common/logging/logging.config.ts b/apps/bff/src/common/logging/logging.config.ts deleted file mode 100644 index 42cf1015..00000000 --- a/apps/bff/src/common/logging/logging.config.ts +++ /dev/null @@ -1,234 +0,0 @@ -import type { Params } from "nestjs-pino"; -import type { Options as PinoHttpOptions } from "pino-http"; -import type { IncomingMessage, ServerResponse } from "http"; -import type { ConfigService } from "@nestjs/config"; -import { join } from "path"; -import { mkdir } from "fs/promises"; - -export class LoggingConfig { - static async createPinoConfig(configService: ConfigService): Promise { - const nodeEnv = configService.get("NODE_ENV", "development"); - const logLevel = configService.get("LOG_LEVEL", "info"); - const appName = configService.get("APP_NAME", "customer-portal-bff"); - - // Ensure logs directory exists for production - if (nodeEnv === "production") { - try { - await mkdir("logs", { recursive: true }); - } catch { - // Directory might already exist - } - } - - // Base Pino configuration - const pinoConfig: PinoHttpOptions = { - level: logLevel, - name: appName, - base: { - service: appName, - environment: nodeEnv, - pid: typeof process !== "undefined" ? process.pid : 0, - }, - timestamp: true, - // Ensure sensitive fields are redacted across all logs - redact: { - paths: [ - // Common headers - "req.headers.authorization", - "req.headers.cookie", - // Auth - "password", - "password2", - "token", - "secret", - "jwt", - "apiKey", - // Custom params that may carry secrets - "params.password", - "params.password2", - "params.secret", - "params.token", - ], - remove: true, - }, - formatters: { - level: (label: string) => ({ level: label }), - bindings: () => ({}), // Remove default hostname/pid from every log - }, - serializers: { - // Keep logs concise: omit headers by default - req: (req: { - method?: string; - url?: string; - remoteAddress?: string; - remotePort?: number; - }) => ({ - method: req.method, - url: req.url, - remoteAddress: req.remoteAddress, - remotePort: req.remotePort, - }), - res: (res: { statusCode: number }) => ({ - statusCode: res.statusCode, - }), - err: (err: { - constructor: { name: string }; - message: string; - stack?: string; - code?: string; - status?: number; - }) => ({ - type: err.constructor.name, - message: err.message, - stack: err.stack, - ...(err.code && { code: err.code }), - ...(err.status && { status: err.status }), - }), - }, - }; - - // Development: Pretty printing - if (nodeEnv === "development") { - pinoConfig.transport = { - target: "pino-pretty", - options: { - colorize: true, - translateTime: "yyyy-mm-dd HH:MM:ss", - ignore: "pid,hostname", - singleLine: false, - hideObject: false, - }, - }; - } - - // Production: File logging with rotation - if (nodeEnv === "production") { - pinoConfig.transport = { - targets: [ - // Console output for container logs - { - target: "pino/file", - level: logLevel, - options: { destination: 1 }, // stdout - }, - // Combined log file - { - target: "pino/file", - level: "info", - options: { - destination: join("logs", `${appName}-combined.log`), - mkdir: true, - }, - }, - // Error log file - { - target: "pino/file", - level: "error", - options: { - destination: join("logs", `${appName}-error.log`), - mkdir: true, - }, - }, - ], - }; - } - - return { - pinoHttp: { - ...pinoConfig, - // Auto-generate correlation IDs - genReqId: (req: IncomingMessage, res: ServerResponse) => { - const existingIdHeader = req.headers["x-correlation-id"]; - const existingId = Array.isArray(existingIdHeader) - ? existingIdHeader[0] - : existingIdHeader; - if (existingId) return existingId; - - const correlationId = LoggingConfig.generateCorrelationId(); - res.setHeader("x-correlation-id", correlationId); - return correlationId; - }, - // Custom log levels: only warn on 4xx and error on 5xx - customLogLevel: (_req: IncomingMessage, res: ServerResponse, err?: unknown) => { - if (res.statusCode >= 400 && res.statusCode < 500) return "warn"; - if (res.statusCode >= 500 || err) return "error"; - return "silent" as unknown as - | "error" - | "warn" - | "info" - | "debug" - | "trace" - | "fatal" - | "silent"; - }, - // Suppress success messages entirely - customSuccessMessage: () => "", - customErrorMessage: ( - req: IncomingMessage, - res: ServerResponse, - err: { message?: string } - ) => { - const method = req.method ?? ""; - const url = req.url ?? ""; - return `${method} ${url} ${res.statusCode} - ${err.message ?? "error"}`; - }, - }, - }; - } - - /** - * Sanitize headers to remove sensitive information - */ - private static sanitizeHeaders( - headers: Record | undefined | null - ): Record | undefined | null { - if (!headers || typeof headers !== "object") { - return headers; - } - - const sensitiveKeys = [ - "authorization", - "cookie", - "set-cookie", - "x-api-key", - "x-auth-token", - "password", - "secret", - "token", - "jwt", - "bearer", - ]; - - const sanitized: Record = { ...headers } as Record; - - Object.keys(sanitized).forEach(key => { - if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive.toLowerCase()))) { - sanitized[key] = "[REDACTED]"; - } - }); - - return sanitized; - } - - /** - * Generate correlation ID - */ - private static generateCorrelationId(): string { - return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - } - - /** - * Get log levels for different environments - */ - static getLogLevels(level: string): string[] { - const logLevels: Record = { - error: ["error"], - warn: ["error", "warn"], - info: ["error", "warn", "info"], - debug: ["error", "warn", "info", "debug"], - verbose: ["error", "warn", "info", "debug", "verbose"], - }; - - return logLevels[level] || logLevels.info; - } -} diff --git a/apps/bff/src/common/logging/logging.module.ts b/apps/bff/src/common/logging/logging.module.ts index 2503fc96..03f5f837 100644 --- a/apps/bff/src/common/logging/logging.module.ts +++ b/apps/bff/src/common/logging/logging.module.ts @@ -1,7 +1,7 @@ import { Global, Module } from "@nestjs/common"; import { ConfigModule, ConfigService } from "@nestjs/config"; import { LoggerModule } from "nestjs-pino"; -import { LoggingConfig } from "./logging.config"; +import { createNestPinoConfig } from "@customer-portal/shared"; @Global() @Module({ @@ -10,7 +10,7 @@ import { LoggingConfig } from "./logging.config"; imports: [ConfigModule], inject: [ConfigService], useFactory: async (configService: ConfigService) => - await LoggingConfig.createPinoConfig(configService), + await createNestPinoConfig(configService), }), ], exports: [LoggerModule], diff --git a/apps/bff/src/main.ts b/apps/bff/src/main.ts index cd82b151..0a148811 100644 --- a/apps/bff/src/main.ts +++ b/apps/bff/src/main.ts @@ -139,6 +139,7 @@ async function bootstrap() { logger.log( `๐Ÿ—„๏ธ Database: ${configService.get("DATABASE_URL", "postgresql://dev:dev@localhost:5432/portal_dev")}` ); + logger.log(`๐Ÿ”— Prisma Studio: http://localhost:5555`); logger.log(`๐Ÿ”ด Redis: ${configService.get("REDIS_URL", "redis://localhost:6379")}`); if (configService.get("NODE_ENV") !== "production") { diff --git a/apps/bff/tsconfig.build.json b/apps/bff/tsconfig.build.json new file mode 100644 index 00000000..8166da9d --- /dev/null +++ b/apps/bff/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "incremental": true, + "tsBuildInfoFile": "./tsconfig.build.tsbuildinfo", + "outDir": "./dist", + "sourceMap": true, + "declaration": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*.spec.ts"] +} + + diff --git a/apps/portal/package.json b/apps/portal/package.json index 491d7353..a6d4b07e 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev -p ${NEXT_PORT:-3000} --turbopack", - "build": "next build", + "dev": "next dev -p ${NEXT_PORT:-3000}", + "build": "next build --turbopack", "build:turbo": "next build --turbopack", "start": "next start -p ${NEXT_PORT:-3000}", "lint": "eslint .", @@ -37,7 +37,6 @@ "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "tailwindcss": "^4.1.12", - "tw-animate-css": "^1.3.7", "typescript": "^5.9.2" } } diff --git a/apps/portal/src/lib/logger.ts b/apps/portal/src/lib/logger.ts index 4d113c08..b799506a 100644 --- a/apps/portal/src/lib/logger.ts +++ b/apps/portal/src/lib/logger.ts @@ -1,134 +1,5 @@ -/** - * Application logger utility - * Provides structured logging with appropriate levels for development and production - * Compatible with backend logging standards - */ +import { createPinoLogger, getSharedLogger } from "@customer-portal/shared"; -type LogLevel = "debug" | "info" | "warn" | "error"; - -interface LogEntry { - level: LogLevel; - message: string; - data?: unknown; - timestamp: string; - service: string; - environment: string; -} - -class Logger { - private isDevelopment = process.env.NODE_ENV === "development"; - private service = "customer-portal-frontend"; - - private formatMessage(level: LogLevel, message: string, data?: unknown): LogEntry { - return { - level, - message, - data, - timestamp: new Date().toISOString(), - service: this.service, - environment: process.env.NODE_ENV || "development", - }; - } - - private log(level: LogLevel, message: string, data?: unknown): void { - const entry = this.formatMessage(level, message, data); - - if (this.isDevelopment) { - const safeData = - data instanceof Error - ? { - name: data.name, - message: data.message, - stack: data.stack, - } - : data; - - const logData = { - timestamp: entry.timestamp, - level: entry.level.toUpperCase(), - service: entry.service, - message: entry.message, - ...(safeData != null ? { data: safeData } : {}), - }; - - try { - console.log(logData); - } catch { - // no-op - } - } else { - // In production, structured logging for external services - const logData = { - ...entry, - ...(data != null ? { data } : {}), - }; - - // For production, you might want to send to a logging service - // For now, only log errors and warnings to console - if (level === "error" || level === "warn") { - try { - console[level](JSON.stringify(logData)); - } catch { - // no-op - } - } - } - } - - debug(message: string, data?: unknown): void { - this.log("debug", message, data); - } - - info(message: string, data?: unknown): void { - this.log("info", message, data); - } - - warn(message: string, data?: unknown): void { - this.log("warn", message, data); - } - - error(message: string, data?: unknown): void { - this.log("error", message, data); - } - - // Structured logging methods for better integration - logApiCall( - endpoint: string, - method: string, - status: number, - duration: number, - data?: unknown - ): void { - this.info(`API ${method} ${endpoint}`, { - endpoint, - method, - status, - duration: `${duration}ms`, - ...(data != null ? { data } : {}), - }); - } - - logUserAction(userId: string, action: string, data?: unknown): void { - this.info(`User action: ${action}`, { - userId, - action, - ...(data != null ? { data } : {}), - }); - } - - logError(error: Error, context?: string, data?: unknown): void { - this.error(`Error${context ? ` in ${context}` : ""}: ${error.message}`, { - error: { - name: error.name, - message: error.message, - stack: error.stack, - }, - context, - ...(data != null ? { data } : {}), - }); - } -} - -// Export singleton instance -export const logger = new Logger(); +// Prefer a shared singleton so logs share correlationId/userId across modules +export const logger = getSharedLogger(); export default logger; diff --git a/apps/portal/tsconfig.json b/apps/portal/tsconfig.json index 9afbb2bc..0a70949c 100644 --- a/apps/portal/tsconfig.json +++ b/apps/portal/tsconfig.json @@ -9,12 +9,23 @@ "name": "next" } ], - // Path mappings "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + // Enforce TS-only in portal and keep strict mode explicit (inherits from root) + "allowJs": false, + "strict": true }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/eslint-report.json b/eslint-report.json deleted file mode 100644 index 45efdbd4..00000000 --- a/eslint-report.json +++ /dev/null @@ -1 +0,0 @@ -[{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/app.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/auth-admin.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/auth.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/auth.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/auth.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/auth.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/dto/link-whmcs.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/dto/login.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/dto/request-password-reset.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/dto/reset-password.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/dto/set-password.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/dto/signup.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/guards/admin.guard.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/guards/auth-throttle.guard.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/guards/jwt-auth.guard.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/guards/local-auth.guard.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/services/token-blacklist.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/strategies/jwt.strategy.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/auth/strategies/local.strategy.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/cases/cases.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/cases/cases.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/cases/cases.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/catalog/catalog.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/catalog/catalog.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/catalog/catalog.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/audit/audit.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/audit/audit.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/cache/cache.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/cache/cache.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/config/env.validation.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/email/email.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/email/email.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/email/providers/sendgrid.provider.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/email/queue/email.processor.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/email/queue/email.queue.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/filters/http-exception.filter.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/logging/logging.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/logging/logging.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/prisma/prisma.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/prisma/prisma.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/redis/redis.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/common/utils/error.util.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/health/health.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/health/health.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/invoices/invoices.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/invoices/invoices.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/invoices/invoices.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/jobs/jobs.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/jobs/jobs.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/jobs/reconcile.processor.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/main.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/mappings/cache/mapping-cache.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/mappings/mappings.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/mappings/mappings.service.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":576,"column":13,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":576,"endColumn":59}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import {\n Injectable,\n NotFoundException,\n ConflictException,\n BadRequestException,\n Inject,\n} from \"@nestjs/common\";\nimport { Logger } from \"nestjs-pino\";\nimport { PrismaService } from \"../common/prisma/prisma.service\";\nimport { getErrorMessage } from \"../common/utils/error.util\";\nimport { MappingCacheService } from \"./cache/mapping-cache.service\";\nimport { MappingValidatorService } from \"./validation/mapping-validator.service\";\nimport {\n UserIdMapping,\n CreateMappingRequest,\n UpdateMappingRequest,\n MappingSearchFilters,\n MappingStats,\n BulkMappingResult,\n} from \"./types/mapping.types\";\n\n@Injectable()\nexport class MappingsService {\n constructor(\n private readonly prisma: PrismaService,\n private readonly cacheService: MappingCacheService,\n private readonly validator: MappingValidatorService,\n @Inject(Logger) private readonly logger: Logger\n ) {}\n\n /**\n * Create a new user mapping\n */\n async createMapping(request: CreateMappingRequest): Promise {\n try {\n // Validate request\n const validation = this.validator.validateCreateRequest(request);\n this.validator.logValidationResult(\"Create mapping\", validation, {\n userId: request.userId,\n });\n\n if (!validation.isValid) {\n throw new BadRequestException(`Invalid mapping data: ${validation.errors.join(\", \")}`);\n }\n\n // Sanitize input\n const sanitizedRequest = this.validator.sanitizeCreateRequest(request);\n\n // Check for conflicts\n const existingMappings = await this.getAllMappingsFromDb();\n const conflictValidation = this.validator.validateNoConflicts(\n sanitizedRequest,\n existingMappings\n );\n\n if (!conflictValidation.isValid) {\n throw new ConflictException(`Mapping conflict: ${conflictValidation.errors.join(\", \")}`);\n }\n\n // Create in database\n const created = await this.prisma.idMapping.create({\n data: sanitizedRequest,\n });\n\n const mapping: UserIdMapping = {\n userId: created.userId,\n whmcsClientId: created.whmcsClientId,\n sfAccountId: created.sfAccountId || undefined,\n createdAt: created.createdAt,\n updatedAt: created.updatedAt,\n };\n\n // Cache the new mapping\n await this.cacheService.setMapping(mapping);\n\n this.logger.log(`Created mapping for user ${mapping.userId}`, {\n whmcsClientId: mapping.whmcsClientId,\n sfAccountId: mapping.sfAccountId,\n warnings: validation.warnings,\n });\n\n return mapping;\n } catch (error) {\n this.logger.error(`Failed to create mapping for user ${request.userId}`, {\n error: getErrorMessage(error),\n request: this.sanitizeForLog(request),\n });\n throw error;\n }\n }\n\n /**\n * Find mapping by user ID\n */\n async findByUserId(userId: string): Promise {\n try {\n // Validate user ID\n if (!userId) {\n throw new BadRequestException(\"User ID is required\");\n }\n\n // Try cache first\n const cached = await this.cacheService.getByUserId(userId);\n if (cached) {\n this.logger.debug(`Cache hit for user mapping: ${userId}`);\n return cached;\n }\n\n // Fetch from database\n const dbMapping = await this.prisma.idMapping.findUnique({\n where: { userId },\n });\n\n if (!dbMapping) {\n this.logger.debug(`No mapping found for user ${userId}`);\n return null;\n }\n\n const mapping: UserIdMapping = {\n userId: dbMapping.userId,\n whmcsClientId: dbMapping.whmcsClientId,\n sfAccountId: dbMapping.sfAccountId || undefined,\n createdAt: dbMapping.createdAt,\n updatedAt: dbMapping.updatedAt,\n };\n\n // Cache the result\n await this.cacheService.setMapping(mapping);\n\n this.logger.debug(`Found mapping for user ${userId}`, {\n whmcsClientId: mapping.whmcsClientId,\n sfAccountId: mapping.sfAccountId,\n });\n\n return mapping;\n } catch (error) {\n this.logger.error(`Failed to find mapping for user ${userId}`, {\n error: getErrorMessage(error),\n });\n throw error;\n }\n }\n\n /**\n * Find mapping by WHMCS client ID\n */\n async findByWhmcsClientId(whmcsClientId: number): Promise {\n try {\n // Validate WHMCS client ID\n if (!whmcsClientId || whmcsClientId < 1) {\n throw new BadRequestException(\"Valid WHMCS client ID is required\");\n }\n\n // Try cache first\n const cached = await this.cacheService.getByWhmcsClientId(whmcsClientId);\n if (cached) {\n this.logger.debug(`Cache hit for WHMCS client mapping: ${whmcsClientId}`);\n return cached;\n }\n\n // Fetch from database\n const dbMapping = await this.prisma.idMapping.findUnique({\n where: { whmcsClientId },\n });\n\n if (!dbMapping) {\n this.logger.debug(`No mapping found for WHMCS client ${whmcsClientId}`);\n return null;\n }\n\n const mapping: UserIdMapping = {\n userId: dbMapping.userId,\n whmcsClientId: dbMapping.whmcsClientId,\n sfAccountId: dbMapping.sfAccountId || undefined,\n createdAt: dbMapping.createdAt,\n updatedAt: dbMapping.updatedAt,\n };\n\n // Cache the result\n await this.cacheService.setMapping(mapping);\n\n this.logger.debug(`Found mapping for WHMCS client ${whmcsClientId}`, {\n userId: mapping.userId,\n sfAccountId: mapping.sfAccountId,\n });\n\n return mapping;\n } catch (error) {\n this.logger.error(`Failed to find mapping for WHMCS client ${whmcsClientId}`, {\n error: getErrorMessage(error),\n });\n throw error;\n }\n }\n\n /**\n * Find mapping by Salesforce account ID\n */\n async findBySfAccountId(sfAccountId: string): Promise {\n try {\n // Validate Salesforce account ID\n if (!sfAccountId) {\n throw new BadRequestException(\"Salesforce account ID is required\");\n }\n\n // Try cache first\n const cached = await this.cacheService.getBySfAccountId(sfAccountId);\n if (cached) {\n this.logger.debug(`Cache hit for SF account mapping: ${sfAccountId}`);\n return cached;\n }\n\n // Fetch from database\n const dbMapping = await this.prisma.idMapping.findFirst({\n where: { sfAccountId },\n });\n\n if (!dbMapping) {\n this.logger.debug(`No mapping found for SF account ${sfAccountId}`);\n return null;\n }\n\n const mapping: UserIdMapping = {\n userId: dbMapping.userId,\n whmcsClientId: dbMapping.whmcsClientId,\n sfAccountId: dbMapping.sfAccountId || undefined,\n createdAt: dbMapping.createdAt,\n updatedAt: dbMapping.updatedAt,\n };\n\n // Cache the result\n await this.cacheService.setMapping(mapping);\n\n this.logger.debug(`Found mapping for SF account ${sfAccountId}`, {\n userId: mapping.userId,\n whmcsClientId: mapping.whmcsClientId,\n });\n\n return mapping;\n } catch (error) {\n this.logger.error(`Failed to find mapping for SF account ${sfAccountId}`, {\n error: getErrorMessage(error),\n });\n throw error;\n }\n }\n\n /**\n * Update an existing mapping\n */\n async updateMapping(userId: string, updates: UpdateMappingRequest): Promise {\n try {\n // Validate request\n const validation = this.validator.validateUpdateRequest(userId, updates);\n this.validator.logValidationResult(\"Update mapping\", validation, {\n userId,\n });\n\n if (!validation.isValid) {\n throw new BadRequestException(`Invalid update data: ${validation.errors.join(\", \")}`);\n }\n\n // Get existing mapping\n const existing = await this.findByUserId(userId);\n if (!existing) {\n throw new NotFoundException(`Mapping not found for user ${userId}`);\n }\n\n // Sanitize input\n const sanitizedUpdates = this.validator.sanitizeUpdateRequest(updates);\n\n // Check for conflicts if WHMCS client ID is being changed\n if (\n sanitizedUpdates.whmcsClientId &&\n sanitizedUpdates.whmcsClientId !== existing.whmcsClientId\n ) {\n const conflictingMapping = await this.findByWhmcsClientId(sanitizedUpdates.whmcsClientId);\n if (conflictingMapping && conflictingMapping.userId !== userId) {\n throw new ConflictException(\n `WHMCS client ${sanitizedUpdates.whmcsClientId} is already mapped to user ${conflictingMapping.userId}`\n );\n }\n }\n\n // Update in database\n const updated = await this.prisma.idMapping.update({\n where: { userId },\n data: sanitizedUpdates,\n });\n\n const newMapping: UserIdMapping = {\n userId: updated.userId,\n whmcsClientId: updated.whmcsClientId,\n sfAccountId: updated.sfAccountId || undefined,\n createdAt: updated.createdAt,\n updatedAt: updated.updatedAt,\n };\n\n // Update cache\n await this.cacheService.updateMapping(existing, newMapping);\n\n this.logger.log(`Updated mapping for user ${userId}`, {\n changes: sanitizedUpdates,\n warnings: validation.warnings,\n });\n\n return newMapping;\n } catch (error) {\n this.logger.error(`Failed to update mapping for user ${userId}`, {\n error: getErrorMessage(error),\n updates: this.sanitizeForLog(updates),\n });\n throw error;\n }\n }\n\n /**\n * Delete a mapping\n */\n async deleteMapping(userId: string): Promise {\n try {\n // Get existing mapping\n const existing = await this.findByUserId(userId);\n if (!existing) {\n throw new NotFoundException(`Mapping not found for user ${userId}`);\n }\n\n // Validate deletion\n const validation = this.validator.validateDeletion(existing);\n this.validator.logValidationResult(\"Delete mapping\", validation, {\n userId,\n });\n\n // Delete from database\n await this.prisma.idMapping.delete({\n where: { userId },\n });\n\n // Remove from cache\n await this.cacheService.deleteMapping(existing);\n\n this.logger.log(`Deleted mapping for user ${userId}`, {\n whmcsClientId: existing.whmcsClientId,\n sfAccountId: existing.sfAccountId,\n warnings: validation.warnings,\n });\n } catch (error) {\n this.logger.error(`Failed to delete mapping for user ${userId}`, {\n error: getErrorMessage(error),\n });\n throw error;\n }\n }\n\n /**\n * Search mappings with filters\n */\n async searchMappings(filters: MappingSearchFilters): Promise {\n try {\n const whereClause: Record = {};\n\n if (filters.userId) {\n whereClause.userId = filters.userId;\n }\n\n if (filters.whmcsClientId) {\n whereClause.whmcsClientId = filters.whmcsClientId;\n }\n\n if (filters.sfAccountId) {\n whereClause.sfAccountId = filters.sfAccountId;\n }\n\n if (filters.hasWhmcsMapping !== undefined) {\n whereClause.whmcsClientId = filters.hasWhmcsMapping ? { not: null } : null;\n }\n\n if (filters.hasSfMapping !== undefined) {\n if (filters.hasSfMapping) {\n whereClause.sfAccountId = { not: null };\n } else {\n whereClause.sfAccountId = null;\n }\n }\n\n const dbMappings = await this.prisma.idMapping.findMany({\n where: whereClause,\n orderBy: { createdAt: \"desc\" },\n });\n\n const mappings: UserIdMapping[] = dbMappings.map(mapping => ({\n userId: mapping.userId,\n whmcsClientId: mapping.whmcsClientId,\n sfAccountId: mapping.sfAccountId || undefined,\n createdAt: mapping.createdAt,\n updatedAt: mapping.updatedAt,\n }));\n\n this.logger.debug(`Found ${mappings.length} mappings matching filters`, filters);\n return mappings;\n } catch (error) {\n this.logger.error(\"Failed to search mappings\", {\n error: getErrorMessage(error),\n filters,\n });\n throw error;\n }\n }\n\n /**\n * Get mapping statistics\n */\n async getMappingStats(): Promise {\n try {\n const [totalCount, whmcsCount, sfCount, completeCount] = await Promise.all([\n this.prisma.idMapping.count(),\n // whmcsClientId is non-nullable; this count equals total mappings\n this.prisma.idMapping.count(),\n this.prisma.idMapping.count({\n where: { sfAccountId: { not: null } },\n }),\n // Complete mappings are those with a non-null sfAccountId (whmcsClientId is always present)\n this.prisma.idMapping.count({\n where: { sfAccountId: { not: null } },\n }),\n ]);\n\n const stats: MappingStats = {\n totalMappings: totalCount,\n whmcsMappings: whmcsCount,\n salesforceMappings: sfCount,\n completeMappings: completeCount,\n orphanedMappings: 0, // Would need to check against actual user records\n };\n\n this.logger.debug(\"Generated mapping statistics\", stats);\n return stats;\n } catch (error) {\n this.logger.error(\"Failed to get mapping statistics\", {\n error: getErrorMessage(error),\n });\n throw error;\n }\n }\n\n /**\n * Bulk create mappings\n */\n async bulkCreateMappings(mappings: CreateMappingRequest[]): Promise {\n const result: BulkMappingResult = {\n successful: 0,\n failed: 0,\n errors: [],\n };\n\n try {\n // Validate all mappings first\n const validations = this.validator.validateBulkMappings(mappings);\n\n for (let i = 0; i < mappings.length; i++) {\n const mapping = mappings[i];\n const validation = validations[i].validation;\n\n try {\n if (!validation.isValid) {\n throw new Error(validation.errors.join(\", \"));\n }\n\n await this.createMapping(mapping);\n result.successful++;\n } catch (error) {\n result.failed++;\n result.errors.push({\n index: i,\n error: getErrorMessage(error),\n data: mapping,\n });\n }\n }\n\n this.logger.log(\n `Bulk create completed: ${result.successful} successful, ${result.failed} failed`\n );\n return result;\n } catch (error) {\n this.logger.error(\"Bulk create mappings failed\", {\n error: getErrorMessage(error),\n });\n throw error;\n }\n }\n\n /**\n * Check if user has mapping\n */\n async hasMapping(userId: string): Promise {\n try {\n // Try cache first\n const cached = await this.cacheService.getByUserId(userId);\n if (cached) {\n return true;\n }\n\n // Check database\n const mapping = await this.prisma.idMapping.findUnique({\n where: { userId },\n select: { userId: true },\n });\n\n return mapping !== null;\n } catch (error) {\n this.logger.error(`Failed to check mapping for user ${userId}`, {\n error: getErrorMessage(error),\n });\n return false;\n }\n }\n\n /**\n * Invalidate cache for a user\n */\n async invalidateCache(userId: string): Promise {\n // Get the current mapping to invalidate all related cache keys\n const mapping = await this.cacheService.getByUserId(userId);\n if (mapping) {\n await this.cacheService.deleteMapping(mapping);\n }\n this.logger.log(`Invalidated mapping cache for user ${userId}`);\n }\n\n /**\n * Health check\n */\n async healthCheck(): Promise<{ status: string; details: Record }> {\n try {\n // Test database connectivity\n await this.prisma.idMapping.count();\n\n return {\n status: \"healthy\",\n details: {\n database: \"connected\",\n cache: \"available\",\n timestamp: new Date().toISOString(),\n },\n };\n } catch (error) {\n this.logger.error(\"Mapping service health check failed\", {\n error: getErrorMessage(error),\n });\n return {\n status: \"unhealthy\",\n details: {\n error: getErrorMessage(error),\n timestamp: new Date().toISOString(),\n },\n };\n }\n }\n\n // Private helper methods\n\n private async getAllMappingsFromDb(): Promise {\n const dbMappings = await this.prisma.idMapping.findMany();\n return dbMappings.map(mapping => ({\n userId: mapping.userId,\n whmcsClientId: mapping.whmcsClientId,\n sfAccountId: mapping.sfAccountId || undefined,\n createdAt: mapping.createdAt,\n updatedAt: mapping.updatedAt,\n }));\n }\n\n private sanitizeForLog(data: unknown): Record {\n try {\n const plain = JSON.parse(JSON.stringify(data ?? {}));\n if (plain && typeof plain === \"object\" && !Array.isArray(plain)) {\n return plain as Record;\n }\n return { value: plain } as Record;\n } catch {\n return { value: String(data) } as Record;\n }\n }\n\n /**\n * Legacy method support (for backward compatibility)\n */\n async create(data: CreateMappingRequest): Promise {\n this.logger.warn(\"Using legacy create method - please update to createMapping\");\n return this.createMapping(data);\n }\n\n /**\n * Legacy method support (for backward compatibility)\n */\n async createMappingLegacy(data: CreateMappingRequest): Promise {\n this.logger.warn(\"Using legacy createMapping method - please update to createMapping\");\n return this.createMapping(data);\n }\n\n /**\n * Legacy method support (for backward compatibility)\n */\n async updateMappingLegacy(userId: string, updates: UpdateMappingRequest): Promise {\n this.logger.warn(\"Using legacy updateMapping method - please update to updateMapping\");\n return this.updateMapping(userId, updates);\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/mappings/types/mapping.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/mappings/validation/mapping-validator.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/orders/orders.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/orders/orders.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/orders/orders.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/subscriptions/subscriptions.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/subscriptions/subscriptions.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/subscriptions/subscriptions.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/users/dto/update-billing.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/users/dto/update-user.dto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/users/users.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/users/users.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/users/users.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/salesforce/salesforce.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/salesforce/salesforce.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/salesforce/services/salesforce-account.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/salesforce/services/salesforce-case.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/salesforce/services/salesforce-connection.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/vendors.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/cache/whmcs-cache.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/services/whmcs-client.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/services/whmcs-connection.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/services/whmcs-invoice.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/services/whmcs-payment.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/services/whmcs-sso.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/services/whmcs-subscription.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/transformers/whmcs-data.transformer.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/types/whmcs-api.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/whmcs.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/vendors/whmcs/whmcs.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/webhooks/guards/webhook-signature.guard.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/webhooks/schemas/salesforce.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/webhooks/schemas/whmcs.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/webhooks/webhooks.controller.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/webhooks/webhooks.module.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/bff/src/webhooks/webhooks.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/next.config.mjs","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/postcss.config.mjs","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/account/profile/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/api/health/route.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/forgot-password/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/link-whmcs/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/login/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/reset-password/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/set-password/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/signup/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/billing/invoices/[id]/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/billing/invoices/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/billing/payments/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/catalog/esim/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/catalog/internet/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/catalog/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/catalog/vpn/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/checkout/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/dashboard/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/orders/[id]/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/subscriptions/[id]/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/subscriptions/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/support/cases/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/support/new/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/auth/auth-layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/auth/session-timeout-warning.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/layout/dashboard-layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/layout/page-layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/button.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/data-table.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/input.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/label.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/search-filter-bar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/InvoiceItemRow.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/InvoiceStatusBadge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/hooks/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/AccountStatusCard.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/DashboardActivityItem.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/QuickAction.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/StatCard.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/hooks/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/components/SubscriptionStatusBadge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/components/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/hooks/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/hooks/useDashboard.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/hooks/useInvoices.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/hooks/useSubscriptions.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/api.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/auth/api.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/auth/store.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/env.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/logger.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/query-client.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/providers/query-provider.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/utils/currency.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/eslint.config.mjs","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/array-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/case.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/common.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/invoice.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/logging/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/logging/logger.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/logging/logger.interface.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/order.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/payment.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/skus.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":98,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":98,"endColumn":35}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Central SKU registry for Product2 <-> portal mappings.\n// Replace the placeholder codes with your actual Product2.SKU__c values.\n\nexport type InternetTier = \"Platinum_Gold\" | \"Silver\";\nexport type AccessMode = \"IPoE-HGW\" | \"IPoE-BYOR\" | \"PPPoE\";\nexport type InstallPlan = \"One-time\" | \"12-Month\" | \"24-Month\";\n\nconst INTERNET_SKU: Record> = {\n Platinum_Gold: {\n \"IPoE-HGW\": \"INT-1G-PLAT-HGW\",\n \"IPoE-BYOR\": \"INT-1G-PLAT-BYOR\",\n PPPoE: \"INT-1G-PLAT-PPPOE\",\n },\n Silver: {\n \"IPoE-HGW\": \"INT-1G-SILV-HGW\",\n \"IPoE-BYOR\": \"INT-1G-SILV-BYOR\",\n PPPoE: \"INT-1G-SILV-PPPOE\",\n },\n};\n\nconst INSTALL_SKU: Record = {\n \"One-time\": \"INT-INSTALL-ONETIME\",\n \"12-Month\": \"INT-INSTALL-12M\",\n \"24-Month\": \"INT-INSTALL-24M\",\n};\n\nconst VPN_SKU: Record = {\n \"USA-SF\": \"VPN-USA-SF\",\n \"UK-London\": \"VPN-UK-LON\",\n};\n\nexport function getInternetServiceSku(tier: InternetTier, mode: AccessMode): string {\n return INTERNET_SKU[tier][mode];\n}\n\nexport function getInternetInstallSku(plan: InstallPlan): string {\n return INSTALL_SKU[plan];\n}\n\nexport function getVpnServiceSku(region: string): string {\n return VPN_SKU[region] || \"\";\n}\n\nexport function getVpnActivationSku(): string {\n return \"VPN-ACTIVATION\";\n}\n\n// ===== SIM / eSIM =====\nexport type SimFormat = \"eSIM\" | \"Physical\";\nexport type SimPlanType = \"DataOnly\" | \"DataSmsVoice\" | \"VoiceOnly\";\nexport type SimDataSize = \"5GB\" | \"10GB\" | \"25GB\" | \"50GB\" | \"None\";\n\ntype SimSkuMap = Record>>>;\n\n// Default mapping. Replace with your actual Product2.SKU__c codes in Salesforce.\n// Can be overridden at runtime via NEXT_PUBLIC_SIM_SKU_MAP (JSON with the same shape as SimSkuMap).\nconst DEFAULT_SIM_SKU_MAP: SimSkuMap = {\n eSIM: {\n DataOnly: {\n \"5GB\": \"SIM-ESIM-DATA-5GB\",\n \"10GB\": \"SIM-ESIM-DATA-10GB\",\n \"25GB\": \"SIM-ESIM-DATA-25GB\",\n \"50GB\": \"SIM-ESIM-DATA-50GB\",\n },\n DataSmsVoice: {\n \"5GB\": \"SIM-ESIM-VOICE-5GB\",\n \"10GB\": \"SIM-ESIM-VOICE-10GB\",\n \"25GB\": \"SIM-ESIM-VOICE-25GB\",\n \"50GB\": \"SIM-ESIM-VOICE-50GB\",\n },\n VoiceOnly: {\n None: \"SIM-ESIM-VOICEONLY\",\n },\n },\n Physical: {\n DataOnly: {\n \"5GB\": \"SIM-PHYS-DATA-5GB\",\n \"10GB\": \"SIM-PHYS-DATA-10GB\",\n \"25GB\": \"SIM-PHYS-DATA-25GB\",\n \"50GB\": \"SIM-PHYS-DATA-50GB\",\n },\n DataSmsVoice: {\n \"5GB\": \"SIM-PHYS-VOICE-5GB\",\n \"10GB\": \"SIM-PHYS-VOICE-10GB\",\n \"25GB\": \"SIM-PHYS-VOICE-25GB\",\n \"50GB\": \"SIM-PHYS-VOICE-50GB\",\n },\n VoiceOnly: {\n None: \"SIM-PHYS-VOICEONLY\",\n },\n },\n};\n\nfunction getEnvSimSkuMap(): SimSkuMap | null {\n try {\n const raw = process.env.NEXT_PUBLIC_SIM_SKU_MAP;\n if (!raw) return null;\n const parsed = JSON.parse(raw);\n return parsed as SimSkuMap;\n } catch {\n return null;\n }\n}\n\n/**\n * Returns the Product2 SKU for the given SIM configuration.\n * If an environment override is present, it takes precedence.\n */\nexport function getSimServiceSku(\n format: SimFormat,\n planType: SimPlanType,\n dataSize: SimDataSize\n): string {\n const map = getEnvSimSkuMap() || DEFAULT_SIM_SKU_MAP;\n const byFormat = map[format] || {};\n const byType = (byFormat[planType] || {}) as Record;\n return typeof byType[dataSize] === \"string\" ? byType[dataSize] : \"\";\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/status.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/subscription.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/user.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/packages/shared/src/validation.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]}] \ No newline at end of file diff --git a/package.json b/package.json index 2b447931..e4f5f45a 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,18 @@ }, "packageManager": "pnpm@10.15.0", "scripts": { - "dev": "pnpm --parallel --recursive run dev", - "build": "pnpm --recursive run build", - "start": "pnpm --parallel --filter portal --filter @customer-portal/bff run start", + "predev": "pnpm --filter @customer-portal/shared build", + "dev": "./scripts/dev/manage.sh apps", + "dev:all": "pnpm --parallel --filter @customer-portal/shared --filter @customer-portal/portal --filter @customer-portal/bff run dev", + "build": "pnpm --recursive -w --if-present run build", + "start": "pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run start", "test": "pnpm --recursive run test", "lint": "pnpm --recursive run lint", "lint:fix": "pnpm --recursive run lint:fix", "format": "prettier -w .", "format:check": "prettier -c .", "prepare": "husky", - "type-check": "pnpm --recursive run type-check", + "type-check": "pnpm --filter @customer-portal/shared build && pnpm --recursive run type-check", "clean": "pnpm --recursive run clean", "dev:start": "./scripts/dev/manage.sh start", "dev:stop": "./scripts/dev/manage.sh stop", @@ -44,7 +46,8 @@ "db:reset": "pnpm --filter @customer-portal/bff run db:reset", "update:check": "pnpm outdated --recursive", "update:all": "pnpm update --recursive --latest && pnpm audit && pnpm type-check", - "update:safe": "pnpm update --recursive && pnpm audit && pnpm type-check" + "update:safe": "pnpm update --recursive && pnpm audit && pnpm type-check", + "dev:watch": "pnpm --parallel --filter @customer-portal/shared --filter @customer-portal/portal --filter @customer-portal/bff run dev" }, "devDependencies": { "@eslint/js": "^9.13.0", diff --git a/packages/shared/package.json b/packages/shared/package.json index a1499d78..2d7b3be1 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -5,14 +5,29 @@ "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "private": true, + "sideEffects": false, + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, "scripts": { "build": "tsc", + "dev": "tsc -w --preserveWatchOutput", "clean": "rm -rf dist", "type-check": "tsc --noEmit", "test": "echo \"No tests specified for shared package\"", "lint": "eslint .", "lint:fix": "eslint . --fix" }, + "dependencies": { + "pino": "^9.9.0" + }, "devDependencies": { "typescript": "^5.9.2" } diff --git a/packages/shared/src/logging/index.ts b/packages/shared/src/logging/index.ts index 21029d82..1dd90060 100644 --- a/packages/shared/src/logging/index.ts +++ b/packages/shared/src/logging/index.ts @@ -5,3 +5,5 @@ export * from "./logger.config.js"; export * from "./logger.interface.js"; +export * from "./pino-logger.js"; +export * from "./nest-logger.config.js"; diff --git a/packages/shared/src/logging/nest-logger.config.ts b/packages/shared/src/logging/nest-logger.config.ts new file mode 100644 index 00000000..2aacb33b --- /dev/null +++ b/packages/shared/src/logging/nest-logger.config.ts @@ -0,0 +1,126 @@ +// Lightweight, framework-agnostic factory that returns an object compatible +// with nestjs-pino's LoggerModule.forRoot({ pinoHttp: {...} }) shape without importing types. +import { join } from "path"; +import { mkdir } from "fs/promises"; + +export async function createNestPinoConfig(configService: { + get(key: string, defaultValue?: T): T; +}) { + const nodeEnv = configService.get("NODE_ENV", "development"); + const logLevel = configService.get("LOG_LEVEL", "info"); + const appName = configService.get("APP_NAME", "customer-portal-bff"); + + if (nodeEnv === "production") { + try { + await mkdir("logs", { recursive: true }); + } catch { + // ignore + } + } + + const pinoConfig: Record = { + level: logLevel, + name: appName, + base: { + service: appName, + environment: nodeEnv, + pid: typeof process !== "undefined" ? process.pid : 0, + }, + timestamp: true, + redact: { + paths: [ + "req.headers.authorization", + "req.headers.cookie", + "password", + "password2", + "token", + "secret", + "jwt", + "apiKey", + "params.password", + "params.password2", + "params.secret", + "params.token", + ], + remove: true, + }, + formatters: { + level: (label: string) => ({ level: label }), + bindings: () => ({}), + }, + serializers: { + req: (req: { method?: string; url?: string; remoteAddress?: string; remotePort?: number }) => ({ + method: req.method, + url: req.url, + remoteAddress: req.remoteAddress, + remotePort: req.remotePort, + }), + res: (res: { statusCode: number }) => ({ statusCode: res.statusCode }), + err: (err: { constructor: { name: string }; message: string; stack?: string; code?: string; status?: number }) => ({ + type: err.constructor.name, + message: err.message, + stack: err.stack, + ...(err.code && { code: err.code }), + ...(err.status && { status: err.status }), + }), + }, + }; + + if (nodeEnv === "development") { + (pinoConfig as any).transport = { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "yyyy-mm-dd HH:MM:ss", + ignore: "pid,hostname", + singleLine: false, + hideObject: false, + }, + }; + } + + if (nodeEnv === "production") { + (pinoConfig as any).transport = { + targets: [ + { target: "pino/file", level: logLevel, options: { destination: 1 } }, + { + target: "pino/file", + level: "info", + options: { destination: join("logs", `${appName}-combined.log`), mkdir: true }, + }, + { + target: "pino/file", + level: "error", + options: { destination: join("logs", `${appName}-error.log`), mkdir: true }, + }, + ], + }; + } + + return { + pinoHttp: { + ...(pinoConfig as any), + genReqId: (req: any, res: any) => { + const existingIdHeader = req.headers?.["x-correlation-id"]; + const existingId = Array.isArray(existingIdHeader) ? existingIdHeader[0] : existingIdHeader; + if (existingId) return existingId; + const correlationId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + res.setHeader?.("x-correlation-id", correlationId); + return correlationId; + }, + customLogLevel: (_req: any, res: any, err?: unknown) => { + if (res.statusCode >= 400 && res.statusCode < 500) return "warn"; + if (res.statusCode >= 500 || err) return "error"; + return "silent" as any; + }, + customSuccessMessage: () => "", + customErrorMessage: (req: any, res: any, err: { message?: string }) => { + const method = req.method ?? ""; + const url = req.url ?? ""; + return `${method} ${url} ${res.statusCode} - ${err.message ?? "error"}`; + }, + }, + }; +} + + diff --git a/packages/shared/src/logging/pino-logger.ts b/packages/shared/src/logging/pino-logger.ts new file mode 100644 index 00000000..911cc13a --- /dev/null +++ b/packages/shared/src/logging/pino-logger.ts @@ -0,0 +1,178 @@ +import pino from "pino"; +import { DEFAULT_LOG_CONFIG, formatLogEntry, sanitizeLogData } from "./logger.config.js"; +import type { ILogger, LoggerOptions } from "./logger.interface.js"; + +/** + * Create a cross-platform Pino-based logger that implements ILogger + * Works in Node and browser environments + */ +export function createPinoLogger(options: LoggerOptions = {}): ILogger { + const level = options.level ?? DEFAULT_LOG_CONFIG.level; + const service = options.service ?? DEFAULT_LOG_CONFIG.service; + const environment = options.environment ?? DEFAULT_LOG_CONFIG.environment; + + // Context that flows with the logger instance + let correlationId: string | undefined = options.context?.correlationId; + let userId: string | undefined = options.context?.userId; + let requestId: string | undefined = options.context?.requestId; + + // Configure pino for both Node and browser + const isBrowser = typeof window !== "undefined"; + const pinoLogger = pino({ + level, + name: service, + base: { + service, + environment, + }, + // Pretty output only in development for Node; browsers format via console + ...(isBrowser + ? { browser: { asObject: true } } + : {}), + formatters: { + level: (label: string) => ({ level: label }), + bindings: () => ({}), + }, + redact: { + paths: [ + "req.headers.authorization", + "req.headers.cookie", + "password", + "password2", + "token", + "secret", + "jwt", + "apiKey", + "params.password", + "params.password2", + "params.secret", + "params.token", + ], + remove: true, + }, + }); + + function withContext(data?: unknown): Record | undefined { + if (data == null) return undefined; + const sanitized = sanitizeLogData(data); + return { + ...(correlationId ? { correlationId } : {}), + ...(userId ? { userId } : {}), + ...(requestId ? { requestId } : {}), + data: sanitized, + } as Record; + } + + const api: ILogger = { + debug(message, data) { + pinoLogger.debug(withContext(data), message); + }, + info(message, data) { + pinoLogger.info(withContext(data), message); + }, + warn(message, data) { + pinoLogger.warn(withContext(data), message); + }, + error(message, data) { + pinoLogger.error(withContext(data), message); + }, + trace(message, data) { + pinoLogger.trace(withContext(data), message); + }, + + logApiCall(endpoint, method, status, duration, data) { + pinoLogger.info( + withContext({ endpoint, method, status, duration: `${duration}ms`, ...(data ? { data } : {}) }), + `API ${method} ${endpoint}` + ); + }, + logUserAction(user, action, data) { + pinoLogger.info(withContext({ userId: user, action, ...(data ? { data } : {}) }), "User action"); + }, + logError(error, context, data) { + pinoLogger.error( + withContext({ + error: { name: error.name, message: error.message, stack: error.stack }, + ...(context ? { context } : {}), + ...(data ? { data } : {}), + }), + `Error${context ? ` in ${context}` : ""}: ${error.message}` + ); + }, + logRequest(req, data) { + pinoLogger.info(withContext({ req, ...(data ? { data } : {}) }), "Request"); + }, + logResponse(res, data) { + pinoLogger.info(withContext({ res, ...(data ? { data } : {} ) }), "Response"); + }, + + setCorrelationId(id) { + correlationId = id; + }, + setUserId(id) { + userId = id; + }, + setRequestId(id) { + requestId = id; + }, + + child(context) { + const child = pinoLogger.child(context); + const childLogger = createPinoLogger({ + level, + service, + environment, + context: { + correlationId, + userId, + requestId, + ...context, + }, + }); + // Bind methods to use child pino instance + // We cannot replace the underlying pino instance easily, so we wrap methods + return { + ...childLogger, + debug(message, data) { + child.debug(withContext(data), message); + }, + info(message, data) { + child.info(withContext(data), message); + }, + warn(message, data) { + child.warn(withContext(data), message); + }, + error(message, data) { + child.error(withContext(data), message); + }, + trace(message, data) { + child.trace(withContext(data), message); + }, + } as ILogger; + }, + + async flush() { + // Flushing is typically relevant in Node streams; browsers are no-ops + try { + if (typeof (pinoLogger as unknown as { flush?: () => void }).flush === "function") { + (pinoLogger as unknown as { flush?: () => void }).flush?.(); + } + } catch { + // no-op + } + }, + }; + + return api; +} + +// Default singleton for convenience +let defaultLogger: ILogger | undefined; +export function getSharedLogger(): ILogger { + if (!defaultLogger) { + defaultLogger = createPinoLogger(); + } + return defaultLogger; +} + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aed4a288..0884bb82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -295,6 +295,10 @@ importers: version: 5.9.2 packages/shared: + dependencies: + pino: + specifier: ^9.9.0 + version: 9.9.0 devDependencies: typescript: specifier: ^5.9.2 diff --git a/scripts/dev/manage.sh b/scripts/dev/manage.sh index 10163432..d5d69a46 100755 --- a/scripts/dev/manage.sh +++ b/scripts/dev/manage.sh @@ -15,13 +15,11 @@ PROJECT_NAME="portal-dev" # Colors GREEN='\033[0;32m' YELLOW='\033[1;33m' -BLUE='\033[0;34m' RED='\033[0;31m' NC='\033[0m' log() { echo -e "${GREEN}[DEV] $1${NC}"; } warn() { echo -e "${YELLOW}[DEV] $1${NC}"; } -info() { echo -e "${BLUE}[DEV] $1${NC}"; } error() { echo -e "${RED}[DEV] ERROR: $1${NC}"; exit 1; } # Change to project root @@ -111,10 +109,13 @@ start_apps() { log "๐Ÿ”— Database: postgresql://dev:dev@localhost:5432/portal_dev" log "๐Ÿ”— Redis: redis://localhost:6379" log "๐Ÿ“š API Docs: http://localhost:${BFF_PORT:-4000}/api/docs" - log "" log "Starting apps with hot-reload..." - - pnpm dev + + # Start Prisma Studio (opens browser) + (cd "$PROJECT_ROOT/apps/bff" && pnpm db:studio &) + + # Start apps (portal + bff) with hot reload in parallel + pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run dev } # Reset environment diff --git a/tsconfig.json b/tsconfig.json index 45b1fdb8..462f3c77 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,13 +26,9 @@ // Performance and compatibility "skipLibCheck": true, - "allowJs": true, // Build settings - "incremental": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true + "incremental": true }, // This is a workspace root - individual packages extend this