Compare commits
4 Commits
1bf0b78343
...
68e321a08f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68e321a08f | ||
|
|
22b17ef8c3 | ||
|
|
5d06ad791f | ||
|
|
6a507c003d |
@ -11,7 +11,11 @@
|
|||||||
"Bash(git commit:*)",
|
"Bash(git commit:*)",
|
||||||
"Bash(k6:*)",
|
"Bash(k6:*)",
|
||||||
"Bash(npx playwright:*)",
|
"Bash(npx playwright:*)",
|
||||||
"Bash(npx newman:*)"
|
"Bash(npx newman:*)",
|
||||||
|
"Bash(chmod:*)",
|
||||||
|
"Bash(netstat -ano)",
|
||||||
|
"Bash(findstr \":5432\")",
|
||||||
|
"Bash(findstr \"LISTENING\")"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
241
.github/workflows/docker-build.yml
vendored
Normal file
241
.github/workflows/docker-build.yml
vendored
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
name: Docker Build and Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main # Production builds
|
||||||
|
- develop # Staging builds
|
||||||
|
tags:
|
||||||
|
- 'v*' # Version tags (v1.0.0, v1.2.3, etc.)
|
||||||
|
workflow_dispatch: # Manual trigger
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: docker.io
|
||||||
|
REPO: xpeditis
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ================================================================
|
||||||
|
# Determine Environment
|
||||||
|
# ================================================================
|
||||||
|
prepare:
|
||||||
|
name: Prepare Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
environment: ${{ steps.set-env.outputs.environment }}
|
||||||
|
backend_tag: ${{ steps.set-tags.outputs.backend_tag }}
|
||||||
|
frontend_tag: ${{ steps.set-tags.outputs.frontend_tag }}
|
||||||
|
should_push: ${{ steps.set-push.outputs.should_push }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Determine environment
|
||||||
|
id: set-env
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.ref }}" == "refs/heads/main" || "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||||
|
echo "environment=production" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "environment=staging" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Determine tags
|
||||||
|
id: set-tags
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/v}
|
||||||
|
echo "backend_tag=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
echo "frontend_tag=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||||
|
echo "backend_tag=latest" >> $GITHUB_OUTPUT
|
||||||
|
echo "frontend_tag=latest" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "backend_tag=staging-latest" >> $GITHUB_OUTPUT
|
||||||
|
echo "frontend_tag=staging-latest" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Determine push
|
||||||
|
id: set-push
|
||||||
|
run: |
|
||||||
|
# Push only on main, develop, or tags (not on PRs)
|
||||||
|
if [[ "${{ github.event_name }}" == "push" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||||
|
echo "should_push=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "should_push=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Build and Push Backend Image
|
||||||
|
# ================================================================
|
||||||
|
build-backend:
|
||||||
|
name: Build Backend Docker Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: needs.prepare.outputs.should_push == 'true'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.REPO }}/backend
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ needs.prepare.outputs.backend_tag }}
|
||||||
|
type=raw,value=build-${{ github.run_number }}
|
||||||
|
type=sha,prefix={{branch}}-
|
||||||
|
|
||||||
|
- name: Build and push Backend
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./apps/backend
|
||||||
|
file: ./apps/backend/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: ${{ needs.prepare.outputs.should_push == 'true' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
NODE_ENV=${{ needs.prepare.outputs.environment }}
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo "Backend image digest ${{ steps.build.outputs.digest }}"
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Build and Push Frontend Image
|
||||||
|
# ================================================================
|
||||||
|
build-frontend:
|
||||||
|
name: Build Frontend Docker Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: needs.prepare.outputs.should_push == 'true'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Set environment variables
|
||||||
|
id: env-vars
|
||||||
|
run: |
|
||||||
|
if [[ "${{ needs.prepare.outputs.environment }}" == "production" ]]; then
|
||||||
|
echo "api_url=https://api.xpeditis.com" >> $GITHUB_OUTPUT
|
||||||
|
echo "app_url=https://xpeditis.com" >> $GITHUB_OUTPUT
|
||||||
|
echo "sentry_env=production" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "api_url=https://api-staging.xpeditis.com" >> $GITHUB_OUTPUT
|
||||||
|
echo "app_url=https://staging.xpeditis.com" >> $GITHUB_OUTPUT
|
||||||
|
echo "sentry_env=staging" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.REPO }}/frontend
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ needs.prepare.outputs.frontend_tag }}
|
||||||
|
type=raw,value=build-${{ github.run_number }}
|
||||||
|
type=sha,prefix={{branch}}-
|
||||||
|
|
||||||
|
- name: Build and push Frontend
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./apps/frontend
|
||||||
|
file: ./apps/frontend/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: ${{ needs.prepare.outputs.should_push == 'true' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
NEXT_PUBLIC_API_URL=${{ steps.env-vars.outputs.api_url }}
|
||||||
|
NEXT_PUBLIC_APP_URL=${{ steps.env-vars.outputs.app_url }}
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
|
||||||
|
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ steps.env-vars.outputs.sentry_env }}
|
||||||
|
NEXT_PUBLIC_GA_MEASUREMENT_ID=${{ secrets.NEXT_PUBLIC_GA_MEASUREMENT_ID }}
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo "Frontend image digest ${{ steps.build.outputs.digest }}"
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Security Scan (optional but recommended)
|
||||||
|
# ================================================================
|
||||||
|
security-scan:
|
||||||
|
name: Security Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-backend, build-frontend, prepare]
|
||||||
|
if: needs.prepare.outputs.should_push == 'true'
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
service: [backend, frontend]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Run Trivy vulnerability scanner
|
||||||
|
uses: aquasecurity/trivy-action@master
|
||||||
|
with:
|
||||||
|
image-ref: ${{ env.REGISTRY }}/${{ env.REPO }}/${{ matrix.service }}:${{ matrix.service == 'backend' && needs.prepare.outputs.backend_tag || needs.prepare.outputs.frontend_tag }}
|
||||||
|
format: 'sarif'
|
||||||
|
output: 'trivy-results-${{ matrix.service }}.sarif'
|
||||||
|
|
||||||
|
- name: Upload Trivy results to GitHub Security
|
||||||
|
uses: github/codeql-action/upload-sarif@v2
|
||||||
|
with:
|
||||||
|
sarif_file: 'trivy-results-${{ matrix.service }}.sarif'
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Summary
|
||||||
|
# ================================================================
|
||||||
|
summary:
|
||||||
|
name: Build Summary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [prepare, build-backend, build-frontend]
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build summary
|
||||||
|
run: |
|
||||||
|
echo "## 🐳 Docker Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Environment**: ${{ needs.prepare.outputs.environment }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Branch**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Images Built" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Backend: \`${{ env.REGISTRY }}/${{ env.REPO }}/backend:${{ needs.prepare.outputs.backend_tag }}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Frontend: \`${{ env.REGISTRY }}/${{ env.REPO }}/frontend:${{ needs.prepare.outputs.frontend_tag }}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ "${{ needs.prepare.outputs.should_push }}" == "true" ]]; then
|
||||||
|
echo "✅ Images pushed to Docker Hub" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Deploy with Portainer" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "1. Login to Portainer UI" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "2. Go to Stacks → Select \`xpeditis-${{ needs.prepare.outputs.environment }}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "3. Click \"Editor\"" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "4. Update image tags if needed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "5. Click \"Update the stack\"" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "ℹ️ Images built but not pushed (PR or dry-run)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
746
PHASE4_REMAINING_TASKS.md
Normal file
746
PHASE4_REMAINING_TASKS.md
Normal file
@ -0,0 +1,746 @@
|
|||||||
|
# Phase 4 - Remaining Tasks Analysis
|
||||||
|
|
||||||
|
## 📊 Current Status: 85% COMPLETE
|
||||||
|
|
||||||
|
**Completed**: Security hardening, GDPR compliance, monitoring setup, testing infrastructure, comprehensive documentation
|
||||||
|
|
||||||
|
**Remaining**: Test execution, frontend performance, accessibility, deployment infrastructure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ COMPLETED TASKS (Session 1 & 2)
|
||||||
|
|
||||||
|
### 1. Security Hardening ✅
|
||||||
|
**From TODO.md Lines 1031-1063**
|
||||||
|
|
||||||
|
- ✅ **Security audit preparation**: OWASP Top 10 compliance implemented
|
||||||
|
- ✅ **Data protection**:
|
||||||
|
- Password hashing with bcrypt (12 rounds)
|
||||||
|
- JWT token security configured
|
||||||
|
- Rate limiting per user implemented
|
||||||
|
- Brute-force protection with exponential backoff
|
||||||
|
- Secure file upload validation (MIME, magic numbers, size limits)
|
||||||
|
- ✅ **Infrastructure security**:
|
||||||
|
- Helmet.js security headers configured
|
||||||
|
- CORS properly configured
|
||||||
|
- Response compression (gzip)
|
||||||
|
- Security config centralized
|
||||||
|
|
||||||
|
**Files Created**:
|
||||||
|
- `infrastructure/security/security.config.ts`
|
||||||
|
- `infrastructure/security/security.module.ts`
|
||||||
|
- `application/guards/throttle.guard.ts`
|
||||||
|
- `application/services/brute-force-protection.service.ts`
|
||||||
|
- `application/services/file-validation.service.ts`
|
||||||
|
|
||||||
|
### 2. Compliance & Privacy ✅
|
||||||
|
**From TODO.md Lines 1047-1054**
|
||||||
|
|
||||||
|
- ✅ **Terms & Conditions page** (15 comprehensive sections)
|
||||||
|
- ✅ **Privacy Policy page** (GDPR compliant, 14 sections)
|
||||||
|
- ✅ **GDPR compliance features**:
|
||||||
|
- Data export (JSON + CSV)
|
||||||
|
- Data deletion (with email confirmation)
|
||||||
|
- Consent management (record, withdraw, status)
|
||||||
|
- ✅ **Cookie consent banner** (granular controls for Essential, Functional, Analytics, Marketing)
|
||||||
|
|
||||||
|
**Files Created**:
|
||||||
|
- `apps/frontend/src/pages/terms.tsx`
|
||||||
|
- `apps/frontend/src/pages/privacy.tsx`
|
||||||
|
- `apps/frontend/src/components/CookieConsent.tsx`
|
||||||
|
- `apps/backend/src/application/services/gdpr.service.ts`
|
||||||
|
- `apps/backend/src/application/controllers/gdpr.controller.ts`
|
||||||
|
- `apps/backend/src/application/gdpr/gdpr.module.ts`
|
||||||
|
|
||||||
|
### 3. Backend Performance ✅
|
||||||
|
**From TODO.md Lines 1066-1073**
|
||||||
|
|
||||||
|
- ✅ **API response compression** (gzip) - implemented in main.ts
|
||||||
|
- ✅ **Caching for frequently accessed data** - Redis cache module exists
|
||||||
|
- ✅ **Database connection pooling** - TypeORM configuration
|
||||||
|
|
||||||
|
**Note**: Query optimization and N+1 fixes are ongoing (addressed per-feature)
|
||||||
|
|
||||||
|
### 4. Monitoring Setup ✅
|
||||||
|
**From TODO.md Lines 1090-1095**
|
||||||
|
|
||||||
|
- ✅ **Setup APM** (Sentry with profiling)
|
||||||
|
- ✅ **Configure error tracking** (Sentry with breadcrumbs, filtering)
|
||||||
|
- ✅ **Performance monitoring** (PerformanceMonitoringInterceptor for request tracking)
|
||||||
|
- ✅ **Performance dashboards** (Sentry dashboard configured)
|
||||||
|
- ✅ **Setup alerts** (Sentry alerts for slow requests, errors)
|
||||||
|
|
||||||
|
**Files Created**:
|
||||||
|
- `infrastructure/monitoring/sentry.config.ts`
|
||||||
|
- `infrastructure/monitoring/performance-monitoring.interceptor.ts`
|
||||||
|
|
||||||
|
### 5. Developer Documentation ✅
|
||||||
|
**From TODO.md Lines 1144-1149**
|
||||||
|
|
||||||
|
- ✅ **Architecture decisions** (ARCHITECTURE.md - 5,800+ words with ADRs)
|
||||||
|
- ✅ **API documentation** (OpenAPI/Swagger configured throughout codebase)
|
||||||
|
- ✅ **Deployment process** (DEPLOYMENT.md - 4,500+ words)
|
||||||
|
- ✅ **Test execution guide** (TEST_EXECUTION_GUIDE.md - 400+ lines)
|
||||||
|
|
||||||
|
**Files Created**:
|
||||||
|
- `ARCHITECTURE.md`
|
||||||
|
- `DEPLOYMENT.md`
|
||||||
|
- `TEST_EXECUTION_GUIDE.md`
|
||||||
|
- `PHASE4_SUMMARY.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏳ REMAINING TASKS
|
||||||
|
|
||||||
|
### 🔴 HIGH PRIORITY (Critical for Production Launch)
|
||||||
|
|
||||||
|
#### 1. Security Audit Execution
|
||||||
|
**From TODO.md Lines 1031-1037**
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Run OWASP ZAP security scan
|
||||||
|
- [ ] Test SQL injection vulnerabilities (automated)
|
||||||
|
- [ ] Test XSS prevention
|
||||||
|
- [ ] Verify CSRF protection
|
||||||
|
- [ ] Test authentication & authorization edge cases
|
||||||
|
|
||||||
|
**Estimated Time**: 2-4 hours
|
||||||
|
|
||||||
|
**Prerequisites**:
|
||||||
|
- Backend server running
|
||||||
|
- Test database with data
|
||||||
|
|
||||||
|
**Action Items**:
|
||||||
|
1. Install OWASP ZAP: https://www.zaproxy.org/download/
|
||||||
|
2. Configure ZAP to scan `http://localhost:4000`
|
||||||
|
3. Run automated scan
|
||||||
|
4. Run manual active scan on auth endpoints
|
||||||
|
5. Generate report and fix critical/high issues
|
||||||
|
6. Re-scan to verify fixes
|
||||||
|
|
||||||
|
**Tools**:
|
||||||
|
- OWASP ZAP (free, open source)
|
||||||
|
- SQLMap for SQL injection testing
|
||||||
|
- Burp Suite Community Edition (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Load Testing Execution
|
||||||
|
**From TODO.md Lines 1082-1089**
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Install K6 CLI
|
||||||
|
- [ ] Run k6 load test for rate search endpoint (target: 100 req/s)
|
||||||
|
- [ ] Run k6 load test for booking creation (target: 50 req/s)
|
||||||
|
- [ ] Run k6 load test for dashboard API (target: 200 req/s)
|
||||||
|
- [ ] Identify and fix bottlenecks
|
||||||
|
- [ ] Verify auto-scaling works (if cloud-deployed)
|
||||||
|
|
||||||
|
**Estimated Time**: 4-6 hours (including fixes)
|
||||||
|
|
||||||
|
**Prerequisites**:
|
||||||
|
- K6 CLI installed
|
||||||
|
- Backend + database running
|
||||||
|
- Sufficient test data seeded
|
||||||
|
|
||||||
|
**Action Items**:
|
||||||
|
1. Install K6: https://k6.io/docs/getting-started/installation/
|
||||||
|
```bash
|
||||||
|
# Windows (Chocolatey)
|
||||||
|
choco install k6
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
brew install k6
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
|
||||||
|
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install k6
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run existing rate-search test:
|
||||||
|
```bash
|
||||||
|
cd apps/backend
|
||||||
|
k6 run load-tests/rate-search.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create additional tests for booking and dashboard:
|
||||||
|
- `load-tests/booking-creation.test.js`
|
||||||
|
- `load-tests/dashboard-api.test.js`
|
||||||
|
|
||||||
|
4. Analyze results and optimize (database indexes, caching, query optimization)
|
||||||
|
|
||||||
|
5. Re-run tests to verify improvements
|
||||||
|
|
||||||
|
**Files Already Created**:
|
||||||
|
- ✅ `apps/backend/load-tests/rate-search.test.js`
|
||||||
|
|
||||||
|
**Files to Create**:
|
||||||
|
- [ ] `apps/backend/load-tests/booking-creation.test.js`
|
||||||
|
- [ ] `apps/backend/load-tests/dashboard-api.test.js`
|
||||||
|
|
||||||
|
**Success Criteria**:
|
||||||
|
- Rate search: p95 < 2000ms, failure rate < 1%
|
||||||
|
- Booking creation: p95 < 3000ms, failure rate < 1%
|
||||||
|
- Dashboard: p95 < 1000ms, failure rate < 1%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. E2E Testing Execution
|
||||||
|
**From TODO.md Lines 1101-1112**
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Test: Complete user registration flow
|
||||||
|
- [ ] Test: Login with OAuth (if implemented)
|
||||||
|
- [ ] Test: Search rates and view results
|
||||||
|
- [ ] Test: Complete booking workflow (all 4 steps)
|
||||||
|
- [ ] Test: View booking in dashboard
|
||||||
|
- [ ] Test: Edit booking
|
||||||
|
- [ ] Test: Cancel booking
|
||||||
|
- [ ] Test: User management (invite, change role)
|
||||||
|
- [ ] Test: Organization settings update
|
||||||
|
|
||||||
|
**Estimated Time**: 3-4 hours (running tests + fixing issues)
|
||||||
|
|
||||||
|
**Prerequisites**:
|
||||||
|
- Frontend running on http://localhost:3000
|
||||||
|
- Backend running on http://localhost:4000
|
||||||
|
- Test database with seed data (test user, organization, mock rates)
|
||||||
|
|
||||||
|
**Action Items**:
|
||||||
|
1. Seed test database:
|
||||||
|
```sql
|
||||||
|
-- Test user
|
||||||
|
INSERT INTO users (email, password_hash, first_name, last_name, role)
|
||||||
|
VALUES ('test@example.com', '$2b$12$...', 'Test', 'User', 'MANAGER');
|
||||||
|
|
||||||
|
-- Test organization
|
||||||
|
INSERT INTO organizations (name, type)
|
||||||
|
VALUES ('Test Freight Forwarders Inc', 'FORWARDER');
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start servers:
|
||||||
|
```bash
|
||||||
|
# Terminal 1 - Backend
|
||||||
|
cd apps/backend && npm run start:dev
|
||||||
|
|
||||||
|
# Terminal 2 - Frontend
|
||||||
|
cd apps/frontend && npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run Playwright tests:
|
||||||
|
```bash
|
||||||
|
cd apps/frontend
|
||||||
|
npx playwright test
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run with UI for debugging:
|
||||||
|
```bash
|
||||||
|
npx playwright test --headed --project=chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Generate HTML report:
|
||||||
|
```bash
|
||||||
|
npx playwright show-report
|
||||||
|
```
|
||||||
|
|
||||||
|
**Files Already Created**:
|
||||||
|
- ✅ `apps/frontend/e2e/booking-workflow.spec.ts` (8 test scenarios)
|
||||||
|
- ✅ `apps/frontend/playwright.config.ts` (5 browser configurations)
|
||||||
|
|
||||||
|
**Files to Create** (if time permits):
|
||||||
|
- [ ] `apps/frontend/e2e/user-management.spec.ts`
|
||||||
|
- [ ] `apps/frontend/e2e/organization-settings.spec.ts`
|
||||||
|
|
||||||
|
**Success Criteria**:
|
||||||
|
- All 8+ E2E tests passing on Chrome
|
||||||
|
- Tests passing on Firefox, Safari (desktop)
|
||||||
|
- Tests passing on Mobile Chrome, Mobile Safari
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. API Testing Execution
|
||||||
|
**From TODO.md Lines 1114-1120**
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Run Postman collection with Newman
|
||||||
|
- [ ] Test all API endpoints
|
||||||
|
- [ ] Verify example requests/responses
|
||||||
|
- [ ] Test error scenarios (400, 401, 403, 404, 500)
|
||||||
|
- [ ] Document any API inconsistencies
|
||||||
|
|
||||||
|
**Estimated Time**: 1-2 hours
|
||||||
|
|
||||||
|
**Prerequisites**:
|
||||||
|
- Backend running on http://localhost:4000
|
||||||
|
- Valid JWT token for authenticated endpoints
|
||||||
|
|
||||||
|
**Action Items**:
|
||||||
|
1. Run Newman tests:
|
||||||
|
```bash
|
||||||
|
cd apps/backend
|
||||||
|
npx newman run postman/xpeditis-api.postman_collection.json \
|
||||||
|
--env-var "BASE_URL=http://localhost:4000" \
|
||||||
|
--reporters cli,html \
|
||||||
|
--reporter-html-export newman-report.html
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Review HTML report for failures
|
||||||
|
|
||||||
|
3. Fix any failing tests or API issues
|
||||||
|
|
||||||
|
4. Update Postman collection if needed
|
||||||
|
|
||||||
|
5. Re-run tests to verify all passing
|
||||||
|
|
||||||
|
**Files Already Created**:
|
||||||
|
- ✅ `apps/backend/postman/xpeditis-api.postman_collection.json`
|
||||||
|
|
||||||
|
**Success Criteria**:
|
||||||
|
- All API tests passing (status codes, response structure, business logic)
|
||||||
|
- Response times within acceptable limits
|
||||||
|
- Error scenarios handled gracefully
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. Deployment Infrastructure Setup
|
||||||
|
**From TODO.md Lines 1157-1165**
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Setup production environment (AWS/GCP/Azure)
|
||||||
|
- [ ] Configure CI/CD for production deployment
|
||||||
|
- [ ] Setup database backups (automated daily)
|
||||||
|
- [ ] Configure SSL certificates
|
||||||
|
- [ ] Setup domain and DNS
|
||||||
|
- [ ] Configure email service for production (SendGrid/AWS SES)
|
||||||
|
- [ ] Setup S3 buckets for production
|
||||||
|
|
||||||
|
**Estimated Time**: 8-12 hours (full production setup)
|
||||||
|
|
||||||
|
**Prerequisites**:
|
||||||
|
- Cloud provider account (AWS recommended)
|
||||||
|
- Domain name registered
|
||||||
|
- Payment method configured
|
||||||
|
|
||||||
|
**Action Items**:
|
||||||
|
|
||||||
|
**Option A: AWS Deployment (Recommended)**
|
||||||
|
|
||||||
|
1. **Database (RDS PostgreSQL)**:
|
||||||
|
```bash
|
||||||
|
# Create RDS PostgreSQL instance
|
||||||
|
- Instance type: db.t3.medium (2 vCPU, 4 GB RAM)
|
||||||
|
- Storage: 100 GB SSD (auto-scaling enabled)
|
||||||
|
- Multi-AZ: Yes (for high availability)
|
||||||
|
- Automated backups: 7 days retention
|
||||||
|
- Backup window: 03:00-04:00 UTC
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Cache (ElastiCache Redis)**:
|
||||||
|
```bash
|
||||||
|
# Create Redis cluster
|
||||||
|
- Node type: cache.t3.medium
|
||||||
|
- Number of replicas: 1
|
||||||
|
- Multi-AZ: Yes
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Backend (ECS Fargate)**:
|
||||||
|
```bash
|
||||||
|
# Create ECS cluster
|
||||||
|
- Launch type: Fargate
|
||||||
|
- Task CPU: 1 vCPU
|
||||||
|
- Task memory: 2 GB
|
||||||
|
- Desired count: 2 (for HA)
|
||||||
|
- Auto-scaling: Min 2, Max 10
|
||||||
|
- Target tracking: 70% CPU utilization
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Frontend (Vercel or AWS Amplify)**:
|
||||||
|
- Deploy Next.js app to Vercel (easiest)
|
||||||
|
- Or use AWS Amplify for AWS-native solution
|
||||||
|
- Configure environment variables
|
||||||
|
- Setup custom domain
|
||||||
|
|
||||||
|
5. **Storage (S3)**:
|
||||||
|
```bash
|
||||||
|
# Create S3 buckets
|
||||||
|
- xpeditis-prod-documents (booking documents)
|
||||||
|
- xpeditis-prod-uploads (user uploads)
|
||||||
|
- Enable versioning
|
||||||
|
- Configure lifecycle policies (delete after 7 years)
|
||||||
|
- Setup bucket policies for secure access
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Email (AWS SES)**:
|
||||||
|
```bash
|
||||||
|
# Setup SES
|
||||||
|
- Verify domain
|
||||||
|
- Move out of sandbox mode (request production access)
|
||||||
|
- Configure DKIM, SPF, DMARC
|
||||||
|
- Setup bounce/complaint handling
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **SSL/TLS (AWS Certificate Manager)**:
|
||||||
|
```bash
|
||||||
|
# Request certificate
|
||||||
|
- Request public certificate for xpeditis.com
|
||||||
|
- Add *.xpeditis.com for subdomains
|
||||||
|
- Validate via DNS (Route 53)
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **Load Balancer (ALB)**:
|
||||||
|
```bash
|
||||||
|
# Create Application Load Balancer
|
||||||
|
- Scheme: Internet-facing
|
||||||
|
- Listeners: HTTP (redirect to HTTPS), HTTPS
|
||||||
|
- Target groups: ECS tasks
|
||||||
|
- Health checks: /health endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
9. **DNS (Route 53)**:
|
||||||
|
```bash
|
||||||
|
# Configure Route 53
|
||||||
|
- Create hosted zone for xpeditis.com
|
||||||
|
- A record: xpeditis.com → ALB
|
||||||
|
- A record: api.xpeditis.com → ALB
|
||||||
|
- MX records for email (if custom email)
|
||||||
|
```
|
||||||
|
|
||||||
|
10. **CI/CD (GitHub Actions)**:
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/deploy-production.yml
|
||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: aws-actions/configure-aws-credentials@v2
|
||||||
|
- name: Build and push Docker image
|
||||||
|
run: |
|
||||||
|
docker build -t xpeditis-backend:${{ github.sha }} .
|
||||||
|
docker push $ECR_REPO/xpeditis-backend:${{ github.sha }}
|
||||||
|
- name: Deploy to ECS
|
||||||
|
run: |
|
||||||
|
aws ecs update-service --cluster xpeditis-prod --service backend --force-new-deployment
|
||||||
|
|
||||||
|
deploy-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Deploy to Vercel
|
||||||
|
run: vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Staging Environment First (Recommended)**
|
||||||
|
|
||||||
|
Before production, setup staging environment:
|
||||||
|
- Use smaller instance types (save costs)
|
||||||
|
- Same architecture as production
|
||||||
|
- Test deployment process
|
||||||
|
- Run load tests on staging
|
||||||
|
- Verify monitoring and alerting
|
||||||
|
|
||||||
|
**Files to Create**:
|
||||||
|
- [ ] `.github/workflows/deploy-staging.yml`
|
||||||
|
- [ ] `.github/workflows/deploy-production.yml`
|
||||||
|
- [ ] `infra/terraform/` (optional, for Infrastructure as Code)
|
||||||
|
- [ ] `docs/DEPLOYMENT_RUNBOOK.md`
|
||||||
|
|
||||||
|
**Success Criteria**:
|
||||||
|
- Backend deployed and accessible via API domain
|
||||||
|
- Frontend deployed and accessible via web domain
|
||||||
|
- Database backups running daily
|
||||||
|
- SSL certificate valid
|
||||||
|
- Monitoring and alerting operational
|
||||||
|
- CI/CD pipeline successfully deploying changes
|
||||||
|
|
||||||
|
**Estimated Cost (AWS)**:
|
||||||
|
- RDS PostgreSQL (db.t3.medium): ~$100/month
|
||||||
|
- ElastiCache Redis (cache.t3.medium): ~$50/month
|
||||||
|
- ECS Fargate (2 tasks): ~$50/month
|
||||||
|
- S3 storage: ~$10/month
|
||||||
|
- Data transfer: ~$20/month
|
||||||
|
- **Total**: ~$230/month (staging + production: ~$400/month)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟡 MEDIUM PRIORITY (Important but Not Blocking)
|
||||||
|
|
||||||
|
#### 6. Frontend Performance Optimization
|
||||||
|
**From TODO.md Lines 1074-1080**
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Optimize bundle size (code splitting)
|
||||||
|
- [ ] Implement lazy loading for routes
|
||||||
|
- [ ] Optimize images (WebP, lazy loading)
|
||||||
|
- [ ] Add service worker for offline support (optional)
|
||||||
|
- [ ] Implement skeleton screens (partially done)
|
||||||
|
- [ ] Reduce JavaScript execution time
|
||||||
|
|
||||||
|
**Estimated Time**: 4-6 hours
|
||||||
|
|
||||||
|
**Action Items**:
|
||||||
|
1. Run Lighthouse audit:
|
||||||
|
```bash
|
||||||
|
npx lighthouse http://localhost:3000 --view
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Analyze bundle size:
|
||||||
|
```bash
|
||||||
|
cd apps/frontend
|
||||||
|
npm run build
|
||||||
|
npx @next/bundle-analyzer
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Implement code splitting for large pages
|
||||||
|
|
||||||
|
4. Convert images to WebP format
|
||||||
|
|
||||||
|
5. Add lazy loading for images and components
|
||||||
|
|
||||||
|
6. Re-run Lighthouse and compare scores
|
||||||
|
|
||||||
|
**Target Scores**:
|
||||||
|
- Performance: > 90
|
||||||
|
- Accessibility: > 90
|
||||||
|
- Best Practices: > 90
|
||||||
|
- SEO: > 90
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 7. Accessibility Testing
|
||||||
|
**From TODO.md Lines 1121-1126**
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Run axe-core audits on all pages
|
||||||
|
- [ ] Test keyboard navigation (Tab, Enter, Esc, Arrow keys)
|
||||||
|
- [ ] Test screen reader compatibility (NVDA, JAWS, VoiceOver)
|
||||||
|
- [ ] Ensure WCAG 2.1 AA compliance
|
||||||
|
- [ ] Fix accessibility issues
|
||||||
|
|
||||||
|
**Estimated Time**: 3-4 hours
|
||||||
|
|
||||||
|
**Action Items**:
|
||||||
|
1. Install axe DevTools extension (Chrome/Firefox)
|
||||||
|
|
||||||
|
2. Run audits on key pages:
|
||||||
|
- Login/Register
|
||||||
|
- Rate search
|
||||||
|
- Booking workflow
|
||||||
|
- Dashboard
|
||||||
|
|
||||||
|
3. Test keyboard navigation:
|
||||||
|
- All interactive elements focusable
|
||||||
|
- Focus indicators visible
|
||||||
|
- Logical tab order
|
||||||
|
|
||||||
|
4. Test with screen reader:
|
||||||
|
- Install NVDA (Windows) or use VoiceOver (macOS)
|
||||||
|
- Navigate through app
|
||||||
|
- Verify labels, headings, landmarks
|
||||||
|
|
||||||
|
5. Fix issues identified
|
||||||
|
|
||||||
|
6. Re-run audits to verify fixes
|
||||||
|
|
||||||
|
**Success Criteria**:
|
||||||
|
- Zero critical accessibility errors
|
||||||
|
- All interactive elements keyboard accessible
|
||||||
|
- Proper ARIA labels and roles
|
||||||
|
- Sufficient color contrast (4.5:1 for text)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 8. Browser & Device Testing
|
||||||
|
**From TODO.md Lines 1128-1134**
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Test on Chrome, Firefox, Safari, Edge
|
||||||
|
- [ ] Test on iOS (Safari)
|
||||||
|
- [ ] Test on Android (Chrome)
|
||||||
|
- [ ] Test on different screen sizes (mobile, tablet, desktop)
|
||||||
|
- [ ] Fix cross-browser issues
|
||||||
|
|
||||||
|
**Estimated Time**: 2-3 hours
|
||||||
|
|
||||||
|
**Action Items**:
|
||||||
|
1. Use BrowserStack or LambdaTest (free tier available)
|
||||||
|
|
||||||
|
2. Test matrix:
|
||||||
|
| Browser | Desktop | Mobile |
|
||||||
|
|---------|---------|--------|
|
||||||
|
| Chrome | ✅ | ✅ |
|
||||||
|
| Firefox | ✅ | ❌ |
|
||||||
|
| Safari | ✅ | ✅ |
|
||||||
|
| Edge | ✅ | ❌ |
|
||||||
|
|
||||||
|
3. Test key flows on each platform:
|
||||||
|
- Login
|
||||||
|
- Rate search
|
||||||
|
- Booking creation
|
||||||
|
- Dashboard
|
||||||
|
|
||||||
|
4. Document and fix browser-specific issues
|
||||||
|
|
||||||
|
5. Add polyfills if needed for older browsers
|
||||||
|
|
||||||
|
**Success Criteria**:
|
||||||
|
- Core functionality works on all tested browsers
|
||||||
|
- Layout responsive on all screen sizes
|
||||||
|
- No critical rendering issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟢 LOW PRIORITY (Nice to Have)
|
||||||
|
|
||||||
|
#### 9. User Documentation
|
||||||
|
**From TODO.md Lines 1137-1142**
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Create user guide (how to search rates)
|
||||||
|
- [ ] Create booking guide (step-by-step)
|
||||||
|
- [ ] Create dashboard guide
|
||||||
|
- [ ] Add FAQ section
|
||||||
|
- [ ] Create video tutorials (optional)
|
||||||
|
|
||||||
|
**Estimated Time**: 6-8 hours
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- User documentation portal (can use GitBook, Notion, or custom Next.js site)
|
||||||
|
- Screenshots and annotated guides
|
||||||
|
- FAQ with common questions
|
||||||
|
- Video walkthrough (5-10 minutes)
|
||||||
|
|
||||||
|
**Priority**: Can be done post-launch with real user feedback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 10. Admin Documentation
|
||||||
|
**From TODO.md Lines 1151-1155**
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [ ] Create runbook for common issues
|
||||||
|
- [ ] Document backup/restore procedures
|
||||||
|
- [ ] Document monitoring and alerting
|
||||||
|
- [ ] Create incident response plan
|
||||||
|
|
||||||
|
**Estimated Time**: 4-6 hours
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- `docs/RUNBOOK.md` - Common operational tasks
|
||||||
|
- `docs/INCIDENT_RESPONSE.md` - What to do when things break
|
||||||
|
- `docs/BACKUP_RESTORE.md` - Database backup and restore procedures
|
||||||
|
|
||||||
|
**Priority**: Can be created alongside deployment infrastructure setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Pre-Launch Checklist
|
||||||
|
**From TODO.md Lines 1166-1172**
|
||||||
|
|
||||||
|
Before launching to production, verify:
|
||||||
|
|
||||||
|
- [ ] **Environment variables**: All required env vars set in production
|
||||||
|
- [ ] **Security audit**: Final OWASP ZAP scan complete with no critical issues
|
||||||
|
- [ ] **Load testing**: Production-like environment tested under load
|
||||||
|
- [ ] **Disaster recovery**: Backup/restore procedures tested
|
||||||
|
- [ ] **Monitoring**: Sentry operational, alerts configured and tested
|
||||||
|
- [ ] **SSL certificates**: Valid and auto-renewing
|
||||||
|
- [ ] **Domain/DNS**: Properly configured and propagated
|
||||||
|
- [ ] **Email service**: Production SES/SendGrid configured and verified
|
||||||
|
- [ ] **Database backups**: Automated daily backups enabled and tested
|
||||||
|
- [ ] **CI/CD pipeline**: Successfully deploying to staging and production
|
||||||
|
- [ ] **Error tracking**: Sentry capturing errors correctly
|
||||||
|
- [ ] **Uptime monitoring**: Pingdom or UptimeRobot configured
|
||||||
|
- [ ] **Performance baselines**: Established and monitored
|
||||||
|
- [ ] **Launch communication**: Stakeholders informed of launch date
|
||||||
|
- [ ] **Support infrastructure**: Support email and ticketing system ready
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Summary
|
||||||
|
|
||||||
|
### Completion Status
|
||||||
|
|
||||||
|
| Category | Completed | Remaining | Total |
|
||||||
|
|----------|-----------|-----------|-------|
|
||||||
|
| Security & Compliance | 3/4 (75%) | 1 (audit execution) | 4 |
|
||||||
|
| Performance | 2/3 (67%) | 1 (frontend optimization) | 3 |
|
||||||
|
| Testing | 1/5 (20%) | 4 (load, E2E, API, accessibility) | 5 |
|
||||||
|
| Documentation | 3/5 (60%) | 2 (user docs, admin docs) | 5 |
|
||||||
|
| Deployment | 0/1 (0%) | 1 (production infrastructure) | 1 |
|
||||||
|
| **TOTAL** | **9/18 (50%)** | **9** | **18** |
|
||||||
|
|
||||||
|
**Note**: The 85% completion status in PHASE4_SUMMARY.md refers to the **complexity-weighted progress**, where security hardening, GDPR compliance, and monitoring setup were the most complex tasks and are now complete. The remaining tasks are primarily execution-focused rather than implementation-focused.
|
||||||
|
|
||||||
|
### Time Estimates
|
||||||
|
|
||||||
|
| Priority | Tasks | Estimated Time |
|
||||||
|
|----------|-------|----------------|
|
||||||
|
| 🔴 High | 5 | 18-28 hours |
|
||||||
|
| 🟡 Medium | 3 | 9-13 hours |
|
||||||
|
| 🟢 Low | 2 | 10-14 hours |
|
||||||
|
| **Total** | **10** | **37-55 hours** |
|
||||||
|
|
||||||
|
### Recommended Sequence
|
||||||
|
|
||||||
|
**Week 1** (Critical Path):
|
||||||
|
1. Security audit execution (2-4 hours)
|
||||||
|
2. Load testing execution (4-6 hours)
|
||||||
|
3. E2E testing execution (3-4 hours)
|
||||||
|
4. API testing execution (1-2 hours)
|
||||||
|
|
||||||
|
**Week 2** (Deployment):
|
||||||
|
5. Deployment infrastructure setup - Staging (4-6 hours)
|
||||||
|
6. Deployment infrastructure setup - Production (4-6 hours)
|
||||||
|
7. Pre-launch checklist verification (2-3 hours)
|
||||||
|
|
||||||
|
**Week 3** (Polish):
|
||||||
|
8. Frontend performance optimization (4-6 hours)
|
||||||
|
9. Accessibility testing (3-4 hours)
|
||||||
|
10. Browser & device testing (2-3 hours)
|
||||||
|
|
||||||
|
**Post-Launch**:
|
||||||
|
11. User documentation (6-8 hours)
|
||||||
|
12. Admin documentation (4-6 hours)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
|
||||||
|
1. **Immediate (This Session)**:
|
||||||
|
- Review remaining tasks with stakeholders
|
||||||
|
- Prioritize based on launch timeline
|
||||||
|
- Decide on staging vs direct production deployment
|
||||||
|
|
||||||
|
2. **This Week**:
|
||||||
|
- Execute security audit
|
||||||
|
- Run load tests and fix bottlenecks
|
||||||
|
- Execute E2E and API tests
|
||||||
|
- Fix any critical bugs found
|
||||||
|
|
||||||
|
3. **Next Week**:
|
||||||
|
- Setup staging environment
|
||||||
|
- Deploy to staging
|
||||||
|
- Run full test suite on staging
|
||||||
|
- Setup production infrastructure
|
||||||
|
- Deploy to production
|
||||||
|
|
||||||
|
4. **Week 3**:
|
||||||
|
- Monitor production closely
|
||||||
|
- Performance optimization based on real usage
|
||||||
|
- Gather user feedback
|
||||||
|
- Create user documentation based on feedback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated*: October 14, 2025
|
||||||
|
*Document Version*: 1.0.0
|
||||||
|
*Status*: Phase 4 - 85% Complete, 10 tasks remaining
|
||||||
85
apps/backend/.dockerignore
Normal file
85
apps/backend/.dockerignore
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
*.spec.ts
|
||||||
|
*.test.ts
|
||||||
|
**/__tests__
|
||||||
|
**/__mocks__
|
||||||
|
test
|
||||||
|
tests
|
||||||
|
e2e
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.swn
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
.github
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
docs
|
||||||
|
documentation
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp
|
||||||
|
temp
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.cache
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.yaml
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.travis.yml
|
||||||
|
Jenkinsfile
|
||||||
|
azure-pipelines.yml
|
||||||
|
|
||||||
|
# Other
|
||||||
|
.prettierrc
|
||||||
|
.prettierignore
|
||||||
|
.eslintrc.js
|
||||||
|
.eslintignore
|
||||||
|
tsconfig.build.tsbuildinfo
|
||||||
79
apps/backend/Dockerfile
Normal file
79
apps/backend/Dockerfile
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# ===============================================
|
||||||
|
# Stage 1: Dependencies Installation
|
||||||
|
# ===============================================
|
||||||
|
FROM node:20-alpine AS dependencies
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache python3 make g++ libc6-compat
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY tsconfig*.json ./
|
||||||
|
|
||||||
|
# Install all dependencies (including dev for build)
|
||||||
|
RUN npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
# ===============================================
|
||||||
|
# Stage 2: Build Application
|
||||||
|
# ===============================================
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependencies from previous stage
|
||||||
|
COPY --from=dependencies /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Remove dev dependencies to reduce size
|
||||||
|
RUN npm prune --production --legacy-peer-deps
|
||||||
|
|
||||||
|
# ===============================================
|
||||||
|
# Stage 3: Production Image
|
||||||
|
# ===============================================
|
||||||
|
FROM node:20-alpine AS production
|
||||||
|
|
||||||
|
# Install dumb-init for proper signal handling
|
||||||
|
RUN apk add --no-cache dumb-init
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S nestjs -u 1001
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy built application from builder
|
||||||
|
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist
|
||||||
|
COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder --chown=nestjs:nodejs /app/package*.json ./
|
||||||
|
|
||||||
|
# Create logs directory
|
||||||
|
RUN mkdir -p /app/logs && chown -R nestjs:nodejs /app/logs
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER nestjs
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 4000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
PORT=4000
|
||||||
|
|
||||||
|
# Use dumb-init to handle signals properly
|
||||||
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["node", "dist/main"]
|
||||||
19
apps/backend/docker-compose.yaml
Normal file
19
apps/backend/docker-compose.yaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:latest
|
||||||
|
container_name: xpeditis-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: xpeditis
|
||||||
|
POSTGRES_PASSWORD: xpeditis_dev_password
|
||||||
|
POSTGRES_DB: xpeditis_dev
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
container_name: xpeditis-redis
|
||||||
|
command: redis-server --requirepass xpeditis_redis_password
|
||||||
|
environment:
|
||||||
|
REDIS_PASSWORD: xpeditis_redis_password
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { JwtStrategy } from './jwt.strategy';
|
import { JwtStrategy } from './jwt.strategy';
|
||||||
import { AuthController } from '../controllers/auth.controller';
|
import { AuthController } from '../controllers/auth.controller';
|
||||||
@ -9,18 +10,8 @@ import { AuthController } from '../controllers/auth.controller';
|
|||||||
// Import domain and infrastructure dependencies
|
// Import domain and infrastructure dependencies
|
||||||
import { USER_REPOSITORY } from '../../domain/ports/out/user.repository';
|
import { USER_REPOSITORY } from '../../domain/ports/out/user.repository';
|
||||||
import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
|
import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
|
||||||
|
import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities/user.orm-entity';
|
||||||
|
|
||||||
/**
|
|
||||||
* Authentication Module
|
|
||||||
*
|
|
||||||
* Wires together the authentication system:
|
|
||||||
* - JWT configuration with access/refresh tokens
|
|
||||||
* - Passport JWT strategy
|
|
||||||
* - Auth service and controller
|
|
||||||
* - User repository for database access
|
|
||||||
*
|
|
||||||
* This module should be imported in AppModule.
|
|
||||||
*/
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
// Passport configuration
|
// Passport configuration
|
||||||
@ -37,6 +28,9 @@ import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// 👇 Add this to register TypeORM repository for UserOrmEntity
|
||||||
|
TypeOrmModule.forFeature([UserOrmEntity]),
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Injectable, UnauthorizedException, ConflictException, Logger } from '@nestjs/common';
|
import { Injectable, UnauthorizedException, ConflictException, Logger, Inject } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import { UserRepository } from '../../domain/ports/out/user.repository';
|
import { UserRepository, USER_REPOSITORY } from '../../domain/ports/out/user.repository';
|
||||||
import { User, UserRole } from '../../domain/entities/user.entity';
|
import { User, UserRole } from '../../domain/entities/user.entity';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@ -19,7 +19,8 @@ export class AuthService {
|
|||||||
private readonly logger = new Logger(AuthService.name);
|
private readonly logger = new Logger(AuthService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly userRepository: UserRepository,
|
@Inject(USER_REPOSITORY)
|
||||||
|
private readonly userRepository: UserRepository, // ✅ Correct injection
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
@ -36,14 +37,12 @@ export class AuthService {
|
|||||||
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
||||||
this.logger.log(`Registering new user: ${email}`);
|
this.logger.log(`Registering new user: ${email}`);
|
||||||
|
|
||||||
// Check if user already exists
|
|
||||||
const existingUser = await this.userRepository.findByEmail(email);
|
const existingUser = await this.userRepository.findByEmail(email);
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new ConflictException('User with this email already exists');
|
throw new ConflictException('User with this email already exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash password with Argon2
|
|
||||||
const passwordHash = await argon2.hash(password, {
|
const passwordHash = await argon2.hash(password, {
|
||||||
type: argon2.argon2id,
|
type: argon2.argon2id,
|
||||||
memoryCost: 65536, // 64 MB
|
memoryCost: 65536, // 64 MB
|
||||||
@ -51,7 +50,6 @@ export class AuthService {
|
|||||||
parallelism: 4,
|
parallelism: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create user entity
|
|
||||||
const user = User.create({
|
const user = User.create({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
organizationId,
|
organizationId,
|
||||||
@ -59,13 +57,11 @@ export class AuthService {
|
|||||||
passwordHash,
|
passwordHash,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
role: UserRole.USER, // Default role
|
role: UserRole.USER,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save to database
|
|
||||||
const savedUser = await this.userRepository.save(user);
|
const savedUser = await this.userRepository.save(user);
|
||||||
|
|
||||||
// Generate tokens
|
|
||||||
const tokens = await this.generateTokens(savedUser);
|
const tokens = await this.generateTokens(savedUser);
|
||||||
|
|
||||||
this.logger.log(`User registered successfully: ${email}`);
|
this.logger.log(`User registered successfully: ${email}`);
|
||||||
@ -92,7 +88,6 @@ export class AuthService {
|
|||||||
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
||||||
this.logger.log(`Login attempt for: ${email}`);
|
this.logger.log(`Login attempt for: ${email}`);
|
||||||
|
|
||||||
// Find user by email
|
|
||||||
const user = await this.userRepository.findByEmail(email);
|
const user = await this.userRepository.findByEmail(email);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -103,14 +98,12 @@ export class AuthService {
|
|||||||
throw new UnauthorizedException('User account is inactive');
|
throw new UnauthorizedException('User account is inactive');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password
|
|
||||||
const isPasswordValid = await argon2.verify(user.passwordHash, password);
|
const isPasswordValid = await argon2.verify(user.passwordHash, password);
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
throw new UnauthorizedException('Invalid credentials');
|
throw new UnauthorizedException('Invalid credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate tokens
|
|
||||||
const tokens = await this.generateTokens(user);
|
const tokens = await this.generateTokens(user);
|
||||||
|
|
||||||
this.logger.log(`User logged in successfully: ${email}`);
|
this.logger.log(`User logged in successfully: ${email}`);
|
||||||
@ -133,7 +126,6 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async refreshAccessToken(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
|
async refreshAccessToken(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
|
||||||
try {
|
try {
|
||||||
// Verify refresh token
|
|
||||||
const payload = await this.jwtService.verifyAsync<JwtPayload>(refreshToken, {
|
const payload = await this.jwtService.verifyAsync<JwtPayload>(refreshToken, {
|
||||||
secret: this.configService.get('JWT_SECRET'),
|
secret: this.configService.get('JWT_SECRET'),
|
||||||
});
|
});
|
||||||
@ -142,14 +134,12 @@ export class AuthService {
|
|||||||
throw new UnauthorizedException('Invalid token type');
|
throw new UnauthorizedException('Invalid token type');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user
|
|
||||||
const user = await this.userRepository.findById(payload.sub);
|
const user = await this.userRepository.findById(payload.sub);
|
||||||
|
|
||||||
if (!user || !user.isActive) {
|
if (!user || !user.isActive) {
|
||||||
throw new UnauthorizedException('User not found or inactive');
|
throw new UnauthorizedException('User not found or inactive');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new tokens
|
|
||||||
const tokens = await this.generateTokens(user);
|
const tokens = await this.generateTokens(user);
|
||||||
|
|
||||||
this.logger.log(`Access token refreshed for user: ${user.email}`);
|
this.logger.log(`Access token refreshed for user: ${user.email}`);
|
||||||
|
|||||||
@ -101,17 +101,13 @@ export class UsersController {
|
|||||||
})
|
})
|
||||||
async createUser(
|
async createUser(
|
||||||
@Body() dto: CreateUserDto,
|
@Body() dto: CreateUserDto,
|
||||||
@CurrentUser() user: UserPayload,
|
@CurrentUser() user: UserPayload
|
||||||
): Promise<UserResponseDto> {
|
): Promise<UserResponseDto> {
|
||||||
this.logger.log(
|
this.logger.log(`[User: ${user.email}] Creating user: ${dto.email} (${dto.role})`);
|
||||||
`[User: ${user.email}] Creating user: ${dto.email} (${dto.role})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Authorization: Managers can only create users in their own organization
|
// Authorization: Managers can only create users in their own organization
|
||||||
if (user.role === 'manager' && dto.organizationId !== user.organizationId) {
|
if (user.role === 'manager' && dto.organizationId !== user.organizationId) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException('You can only create users in your own organization');
|
||||||
'You can only create users in your own organization',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
@ -121,8 +117,7 @@ export class UsersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate temporary password if not provided
|
// Generate temporary password if not provided
|
||||||
const tempPassword =
|
const tempPassword = dto.password || this.generateTemporaryPassword();
|
||||||
dto.password || this.generateTemporaryPassword();
|
|
||||||
|
|
||||||
// Hash password with Argon2id
|
// Hash password with Argon2id
|
||||||
const passwordHash = await argon2.hash(tempPassword, {
|
const passwordHash = await argon2.hash(tempPassword, {
|
||||||
@ -153,7 +148,7 @@ export class UsersController {
|
|||||||
|
|
||||||
// TODO: Send invitation email with temporary password
|
// TODO: Send invitation email with temporary password
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`TODO: Send invitation email to ${dto.email} with temp password: ${tempPassword}`,
|
`TODO: Send invitation email to ${dto.email} with temp password: ${tempPassword}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return UserMapper.toDto(savedUser);
|
return UserMapper.toDto(savedUser);
|
||||||
@ -165,8 +160,7 @@ export class UsersController {
|
|||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get user by ID',
|
summary: 'Get user by ID',
|
||||||
description:
|
description: 'Retrieve user details. Users can view users in their org, admins can view any.',
|
||||||
'Retrieve user details. Users can view users in their org, admins can view any.',
|
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@ -183,7 +177,7 @@ export class UsersController {
|
|||||||
})
|
})
|
||||||
async getUser(
|
async getUser(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@CurrentUser() currentUser: UserPayload,
|
@CurrentUser() currentUser: UserPayload
|
||||||
): Promise<UserResponseDto> {
|
): Promise<UserResponseDto> {
|
||||||
this.logger.log(`[User: ${currentUser.email}] Fetching user: ${id}`);
|
this.logger.log(`[User: ${currentUser.email}] Fetching user: ${id}`);
|
||||||
|
|
||||||
@ -193,10 +187,7 @@ export class UsersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authorization: Can only view users in same organization (unless admin)
|
// Authorization: Can only view users in same organization (unless admin)
|
||||||
if (
|
if (currentUser.role !== 'admin' && user.organizationId !== currentUser.organizationId) {
|
||||||
currentUser.role !== 'admin' &&
|
|
||||||
user.organizationId !== currentUser.organizationId
|
|
||||||
) {
|
|
||||||
throw new ForbiddenException('You can only view users in your organization');
|
throw new ForbiddenException('You can only view users in your organization');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,8 +202,7 @@ export class UsersController {
|
|||||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Update user',
|
summary: 'Update user',
|
||||||
description:
|
description: 'Update user details (name, role, status). Admin/manager only.',
|
||||||
'Update user details (name, role, status). Admin/manager only.',
|
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@ -233,7 +223,7 @@ export class UsersController {
|
|||||||
async updateUser(
|
async updateUser(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@Body() dto: UpdateUserDto,
|
@Body() dto: UpdateUserDto,
|
||||||
@CurrentUser() currentUser: UserPayload,
|
@CurrentUser() currentUser: UserPayload
|
||||||
): Promise<UserResponseDto> {
|
): Promise<UserResponseDto> {
|
||||||
this.logger.log(`[User: ${currentUser.email}] Updating user: ${id}`);
|
this.logger.log(`[User: ${currentUser.email}] Updating user: ${id}`);
|
||||||
|
|
||||||
@ -243,13 +233,8 @@ export class UsersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authorization: Managers can only update users in their own organization
|
// Authorization: Managers can only update users in their own organization
|
||||||
if (
|
if (currentUser.role === 'manager' && user.organizationId !== currentUser.organizationId) {
|
||||||
currentUser.role === 'manager' &&
|
throw new ForbiddenException('You can only update users in your own organization');
|
||||||
user.organizationId !== currentUser.organizationId
|
|
||||||
) {
|
|
||||||
throw new ForbiddenException(
|
|
||||||
'You can only update users in your own organization',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update fields
|
// Update fields
|
||||||
@ -308,7 +293,7 @@ export class UsersController {
|
|||||||
})
|
})
|
||||||
async deleteUser(
|
async deleteUser(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@CurrentUser() currentUser: UserPayload,
|
@CurrentUser() currentUser: UserPayload
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.log(`[Admin: ${currentUser.email}] Deactivating user: ${id}`);
|
this.logger.log(`[Admin: ${currentUser.email}] Deactivating user: ${id}`);
|
||||||
|
|
||||||
@ -360,21 +345,17 @@ export class UsersController {
|
|||||||
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
|
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
|
||||||
@Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number,
|
@Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number,
|
||||||
@Query('role') role: string | undefined,
|
@Query('role') role: string | undefined,
|
||||||
@CurrentUser() currentUser: UserPayload,
|
@CurrentUser() currentUser: UserPayload
|
||||||
): Promise<UserListResponseDto> {
|
): Promise<UserListResponseDto> {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[User: ${currentUser.email}] Listing users: page=${page}, pageSize=${pageSize}, role=${role}`,
|
`[User: ${currentUser.email}] Listing users: page=${page}, pageSize=${pageSize}, role=${role}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch users by organization
|
// Fetch users by organization
|
||||||
const users = await this.userRepository.findByOrganization(
|
const users = await this.userRepository.findByOrganization(currentUser.organizationId);
|
||||||
currentUser.organizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Filter by role if provided
|
// Filter by role if provided
|
||||||
const filteredUsers = role
|
const filteredUsers = role ? users.filter(u => u.role === role) : users;
|
||||||
? users.filter(u => u.role === role)
|
|
||||||
: users;
|
|
||||||
|
|
||||||
// Paginate
|
// Paginate
|
||||||
const startIndex = (page - 1) * pageSize;
|
const startIndex = (page - 1) * pageSize;
|
||||||
@ -418,7 +399,7 @@ export class UsersController {
|
|||||||
})
|
})
|
||||||
async updatePassword(
|
async updatePassword(
|
||||||
@Body() dto: UpdatePasswordDto,
|
@Body() dto: UpdatePasswordDto,
|
||||||
@CurrentUser() currentUser: UserPayload,
|
@CurrentUser() currentUser: UserPayload
|
||||||
): Promise<{ message: string }> {
|
): Promise<{ message: string }> {
|
||||||
this.logger.log(`[User: ${currentUser.email}] Updating password`);
|
this.logger.log(`[User: ${currentUser.email}] Updating password`);
|
||||||
|
|
||||||
@ -428,10 +409,7 @@ export class UsersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify current password
|
// Verify current password
|
||||||
const isPasswordValid = await argon2.verify(
|
const isPasswordValid = await argon2.verify(user.passwordHash, dto.currentPassword);
|
||||||
user.passwordHash,
|
|
||||||
dto.currentPassword,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
throw new ForbiddenException('Current password is incorrect');
|
throw new ForbiddenException('Current password is incorrect');
|
||||||
@ -459,8 +437,7 @@ export class UsersController {
|
|||||||
*/
|
*/
|
||||||
private generateTemporaryPassword(): string {
|
private generateTemporaryPassword(): string {
|
||||||
const length = 16;
|
const length = 16;
|
||||||
const charset =
|
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
|
||||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
|
|
||||||
let password = '';
|
let password = '';
|
||||||
|
|
||||||
const randomBytes = crypto.randomBytes(length);
|
const randomBytes = crypto.randomBytes(length);
|
||||||
|
|||||||
@ -1,20 +1,16 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { OrganizationsController } from '../controllers/organizations.controller';
|
import { OrganizationsController } from '../controllers/organizations.controller';
|
||||||
|
|
||||||
// Import domain ports
|
// Import domain ports
|
||||||
import { ORGANIZATION_REPOSITORY } from '../../domain/ports/out/organization.repository';
|
import { ORGANIZATION_REPOSITORY } from '../../domain/ports/out/organization.repository';
|
||||||
import { TypeOrmOrganizationRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-organization.repository';
|
import { TypeOrmOrganizationRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-organization.repository';
|
||||||
|
import { OrganizationOrmEntity } from '../../infrastructure/persistence/typeorm/entities/organization.orm-entity';
|
||||||
|
|
||||||
/**
|
|
||||||
* Organizations Module
|
|
||||||
*
|
|
||||||
* Handles organization management functionality:
|
|
||||||
* - Create organizations (admin only)
|
|
||||||
* - View organization details
|
|
||||||
* - Update organization (admin/manager)
|
|
||||||
* - List organizations
|
|
||||||
*/
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([OrganizationOrmEntity]), // 👈 This line registers the repository provider
|
||||||
|
],
|
||||||
controllers: [OrganizationsController],
|
controllers: [OrganizationsController],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
@ -22,6 +18,8 @@ import { TypeOrmOrganizationRepository } from '../../infrastructure/persistence/
|
|||||||
useClass: TypeOrmOrganizationRepository,
|
useClass: TypeOrmOrganizationRepository,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [
|
||||||
|
ORGANIZATION_REPOSITORY, // optional, if other modules need it
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class OrganizationsModule {}
|
export class OrganizationsModule {}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { RatesController } from '../controllers/rates.controller';
|
import { RatesController } from '../controllers/rates.controller';
|
||||||
import { CacheModule } from '../../infrastructure/cache/cache.module';
|
import { CacheModule } from '../../infrastructure/cache/cache.module';
|
||||||
import { CarrierModule } from '../../infrastructure/carriers/carrier.module';
|
import { CarrierModule } from '../../infrastructure/carriers/carrier.module';
|
||||||
@ -6,18 +7,14 @@ import { CarrierModule } from '../../infrastructure/carriers/carrier.module';
|
|||||||
// Import domain ports
|
// Import domain ports
|
||||||
import { RATE_QUOTE_REPOSITORY } from '../../domain/ports/out/rate-quote.repository';
|
import { RATE_QUOTE_REPOSITORY } from '../../domain/ports/out/rate-quote.repository';
|
||||||
import { TypeOrmRateQuoteRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-rate-quote.repository';
|
import { TypeOrmRateQuoteRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-rate-quote.repository';
|
||||||
|
import { RateQuoteOrmEntity } from '../../infrastructure/persistence/typeorm/entities/rate-quote.orm-entity';
|
||||||
|
|
||||||
/**
|
|
||||||
* Rates Module
|
|
||||||
*
|
|
||||||
* Handles rate search functionality:
|
|
||||||
* - Rate search API endpoint
|
|
||||||
* - Integration with carrier APIs
|
|
||||||
* - Redis caching for rate quotes
|
|
||||||
* - Rate quote persistence
|
|
||||||
*/
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [CacheModule, CarrierModule],
|
imports: [
|
||||||
|
CacheModule,
|
||||||
|
CarrierModule,
|
||||||
|
TypeOrmModule.forFeature([RateQuoteOrmEntity]), // 👈 Add this
|
||||||
|
],
|
||||||
controllers: [RatesController],
|
controllers: [RatesController],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
@ -25,6 +22,8 @@ import { TypeOrmRateQuoteRepository } from '../../infrastructure/persistence/typ
|
|||||||
useClass: TypeOrmRateQuoteRepository,
|
useClass: TypeOrmRateQuoteRepository,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [
|
||||||
|
RATE_QUOTE_REPOSITORY, // optional, if used in other modules
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class RatesModule {}
|
export class RatesModule {}
|
||||||
|
|||||||
@ -1,22 +1,16 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { UsersController } from '../controllers/users.controller';
|
import { UsersController } from '../controllers/users.controller';
|
||||||
|
|
||||||
// Import domain ports
|
// Import domain ports
|
||||||
import { USER_REPOSITORY } from '../../domain/ports/out/user.repository';
|
import { USER_REPOSITORY } from '../../domain/ports/out/user.repository';
|
||||||
import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
|
import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
|
||||||
|
import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities/user.orm-entity';
|
||||||
|
|
||||||
/**
|
|
||||||
* Users Module
|
|
||||||
*
|
|
||||||
* Handles user management functionality:
|
|
||||||
* - Create/invite users (admin/manager)
|
|
||||||
* - View user details
|
|
||||||
* - Update user (admin/manager)
|
|
||||||
* - Deactivate user (admin)
|
|
||||||
* - List users in organization
|
|
||||||
* - Update own password
|
|
||||||
*/
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([UserOrmEntity]), // 👈 Add this line
|
||||||
|
],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
@ -24,6 +18,8 @@ import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/
|
|||||||
useClass: TypeOrmUserRepository,
|
useClass: TypeOrmUserRepository,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [
|
||||||
|
USER_REPOSITORY, // optional, export if other modules need it
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
|||||||
@ -1,15 +1,9 @@
|
|||||||
/**
|
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
||||||
* BookingService (Domain Service)
|
import { Booking } from '../entities/booking.entity';
|
||||||
*
|
|
||||||
* Business logic for booking management
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Booking, BookingContainer } from '../entities/booking.entity';
|
|
||||||
import { BookingNumber } from '../value-objects/booking-number.vo';
|
|
||||||
import { BookingStatus } from '../value-objects/booking-status.vo';
|
|
||||||
import { BookingRepository } from '../ports/out/booking.repository';
|
import { BookingRepository } from '../ports/out/booking.repository';
|
||||||
import { RateQuoteRepository } from '../ports/out/rate-quote.repository';
|
import { RateQuoteRepository } from '../ports/out/rate-quote.repository';
|
||||||
|
import { BOOKING_REPOSITORY } from '../ports/out/booking.repository';
|
||||||
|
import { RATE_QUOTE_REPOSITORY } from '../ports/out/rate-quote.repository';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export interface CreateBookingInput {
|
export interface CreateBookingInput {
|
||||||
@ -24,7 +18,10 @@ export interface CreateBookingInput {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class BookingService {
|
export class BookingService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(BOOKING_REPOSITORY)
|
||||||
private readonly bookingRepository: BookingRepository,
|
private readonly bookingRepository: BookingRepository,
|
||||||
|
|
||||||
|
@Inject(RATE_QUOTE_REPOSITORY)
|
||||||
private readonly rateQuoteRepository: RateQuoteRepository
|
private readonly rateQuoteRepository: RateQuoteRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -35,7 +32,7 @@ export class BookingService {
|
|||||||
// Validate rate quote exists
|
// Validate rate quote exists
|
||||||
const rateQuote = await this.rateQuoteRepository.findById(input.rateQuoteId);
|
const rateQuote = await this.rateQuoteRepository.findById(input.rateQuoteId);
|
||||||
if (!rateQuote) {
|
if (!rateQuote) {
|
||||||
throw new Error(`Rate quote ${input.rateQuoteId} not found`);
|
throw new NotFoundException(`Rate quote ${input.rateQuoteId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Get userId and organizationId from context
|
// TODO: Get userId and organizationId from context
|
||||||
@ -51,7 +48,7 @@ export class BookingService {
|
|||||||
shipper: input.shipper,
|
shipper: input.shipper,
|
||||||
consignee: input.consignee,
|
consignee: input.consignee,
|
||||||
cargoDescription: input.cargoDescription,
|
cargoDescription: input.cargoDescription,
|
||||||
containers: input.containers.map((c) => ({
|
containers: input.containers.map(c => ({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
type: c.type,
|
type: c.type,
|
||||||
containerNumber: c.containerNumber,
|
containerNumber: c.containerNumber,
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
import CircuitBreaker from 'opossum';
|
import * as CircuitBreaker from 'opossum'; // ✅ Correction ici
|
||||||
import {
|
import {
|
||||||
CarrierConnectorPort,
|
CarrierConnectorPort,
|
||||||
CarrierRateSearchInput,
|
CarrierRateSearchInput,
|
||||||
@ -45,28 +45,28 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add request interceptor for logging
|
// Request interceptor
|
||||||
this.httpClient.interceptors.request.use(
|
this.httpClient.interceptors.request.use(
|
||||||
(request: any) => {
|
request => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Request: ${request.method?.toUpperCase()} ${request.url}`,
|
`Request: ${request.method?.toUpperCase()} ${request.url}`,
|
||||||
request.data ? JSON.stringify(request.data).substring(0, 200) : ''
|
request.data ? JSON.stringify(request.data).substring(0, 200) : ''
|
||||||
);
|
);
|
||||||
return request;
|
return request;
|
||||||
},
|
},
|
||||||
(error: any) => {
|
error => {
|
||||||
this.logger.error(`Request error: ${error?.message || 'Unknown error'}`);
|
this.logger.error(`Request error: ${error?.message || 'Unknown error'}`);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add response interceptor for logging
|
// Response interceptor
|
||||||
this.httpClient.interceptors.response.use(
|
this.httpClient.interceptors.response.use(
|
||||||
(response: any) => {
|
response => {
|
||||||
this.logger.debug(`Response: ${response.status} ${response.statusText}`);
|
this.logger.debug(`Response: ${response.status} ${response.statusText}`);
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error: any) => {
|
error => {
|
||||||
if (error?.code === 'ECONNABORTED') {
|
if (error?.code === 'ECONNABORTED') {
|
||||||
this.logger.warn(`Request timeout after ${config.timeout}ms`);
|
this.logger.warn(`Request timeout after ${config.timeout}ms`);
|
||||||
throw new CarrierTimeoutException(config.name, config.timeout);
|
throw new CarrierTimeoutException(config.name, config.timeout);
|
||||||
@ -76,7 +76,7 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create circuit breaker
|
// Circuit breaker
|
||||||
this.circuitBreaker = new CircuitBreaker(this.makeRequest.bind(this), {
|
this.circuitBreaker = new CircuitBreaker(this.makeRequest.bind(this), {
|
||||||
timeout: config.timeout,
|
timeout: config.timeout,
|
||||||
errorThresholdPercentage: config.circuitBreakerThreshold,
|
errorThresholdPercentage: config.circuitBreakerThreshold,
|
||||||
@ -84,18 +84,15 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
|||||||
name: `${config.name}-circuit-breaker`,
|
name: `${config.name}-circuit-breaker`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Circuit breaker event handlers
|
this.circuitBreaker.on('open', () =>
|
||||||
this.circuitBreaker.on('open', () => {
|
this.logger.warn('Circuit breaker opened - carrier unavailable')
|
||||||
this.logger.warn('Circuit breaker opened - carrier unavailable');
|
);
|
||||||
});
|
this.circuitBreaker.on('halfOpen', () =>
|
||||||
|
this.logger.log('Circuit breaker half-open - testing carrier availability')
|
||||||
this.circuitBreaker.on('halfOpen', () => {
|
);
|
||||||
this.logger.log('Circuit breaker half-open - testing carrier availability');
|
this.circuitBreaker.on('close', () =>
|
||||||
});
|
this.logger.log('Circuit breaker closed - carrier available')
|
||||||
|
);
|
||||||
this.circuitBreaker.on('close', () => {
|
|
||||||
this.logger.log('Circuit breaker closed - carrier available');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCarrierName(): string {
|
getCarrierName(): string {
|
||||||
@ -106,9 +103,6 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
|||||||
return this.config.code;
|
return this.config.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Make HTTP request with retry logic
|
|
||||||
*/
|
|
||||||
protected async makeRequest<T>(
|
protected async makeRequest<T>(
|
||||||
config: AxiosRequestConfig,
|
config: AxiosRequestConfig,
|
||||||
retries = this.config.maxRetries
|
retries = this.config.maxRetries
|
||||||
@ -126,41 +120,27 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if error is retryable
|
|
||||||
*/
|
|
||||||
protected isRetryableError(error: any): boolean {
|
protected isRetryableError(error: any): boolean {
|
||||||
// Retry on network errors, timeouts, and 5xx server errors
|
if (error.code === 'ECONNABORTED') return false;
|
||||||
if (error.code === 'ECONNABORTED') return false; // Don't retry timeouts
|
if (error.code === 'ENOTFOUND') return false;
|
||||||
if (error.code === 'ENOTFOUND') return false; // Don't retry DNS errors
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const status = error.response.status;
|
const status = error.response.status;
|
||||||
return status >= 500 && status < 600;
|
return status >= 500 && status < 600;
|
||||||
}
|
}
|
||||||
return true; // Retry network errors
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate retry delay with exponential backoff
|
|
||||||
*/
|
|
||||||
protected calculateRetryDelay(attempt: number): number {
|
protected calculateRetryDelay(attempt: number): number {
|
||||||
const baseDelay = 1000; // 1 second
|
const baseDelay = 1000;
|
||||||
const maxDelay = 5000; // 5 seconds
|
const maxDelay = 5000;
|
||||||
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
||||||
// Add jitter to prevent thundering herd
|
return delay + Math.random() * 1000; // jitter
|
||||||
return delay + Math.random() * 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sleep utility
|
|
||||||
*/
|
|
||||||
protected sleep(ms: number): Promise<void> {
|
protected sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Make request with circuit breaker protection
|
|
||||||
*/
|
|
||||||
protected async requestWithCircuitBreaker<T>(
|
protected async requestWithCircuitBreaker<T>(
|
||||||
config: AxiosRequestConfig
|
config: AxiosRequestConfig
|
||||||
): Promise<AxiosResponse<T>> {
|
): Promise<AxiosResponse<T>> {
|
||||||
@ -174,16 +154,9 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Health check implementation
|
|
||||||
*/
|
|
||||||
async healthCheck(): Promise<boolean> {
|
async healthCheck(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await this.requestWithCircuitBreaker({
|
await this.requestWithCircuitBreaker({ method: 'GET', url: '/health', timeout: 5000 });
|
||||||
method: 'GET',
|
|
||||||
url: '/health',
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.warn(`Health check failed: ${error?.message || 'Unknown error'}`);
|
this.logger.warn(`Health check failed: ${error?.message || 'Unknown error'}`);
|
||||||
@ -191,9 +164,6 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract methods to be implemented by specific carriers
|
|
||||||
*/
|
|
||||||
abstract searchRates(input: CarrierRateSearchInput): Promise<RateQuote[]>;
|
abstract searchRates(input: CarrierRateSearchInput): Promise<RateQuote[]>;
|
||||||
abstract checkAvailability(input: CarrierAvailabilityInput): Promise<number>;
|
abstract checkAvailability(input: CarrierAvailabilityInput): Promise<number>;
|
||||||
}
|
}
|
||||||
|
|||||||
99
apps/frontend/.dockerignore
Normal file
99
apps/frontend/.dockerignore
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
**/__tests__
|
||||||
|
**/__mocks__
|
||||||
|
*.spec.ts
|
||||||
|
*.test.ts
|
||||||
|
*.spec.tsx
|
||||||
|
*.test.tsx
|
||||||
|
e2e
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.swn
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
.github
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
README.md
|
||||||
|
docs
|
||||||
|
documentation
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp
|
||||||
|
temp
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.cache
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose*.yml
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.travis.yml
|
||||||
|
Jenkinsfile
|
||||||
|
azure-pipelines.yml
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# Other
|
||||||
|
.prettierrc
|
||||||
|
.prettierignore
|
||||||
|
.eslintrc.json
|
||||||
|
.eslintignore
|
||||||
|
postcss.config.js
|
||||||
|
tailwind.config.js
|
||||||
|
next-env.d.ts
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
# Storybook
|
||||||
|
storybook-static
|
||||||
|
.storybook
|
||||||
87
apps/frontend/Dockerfile
Normal file
87
apps/frontend/Dockerfile
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# ===============================================
|
||||||
|
# Stage 1: Dependencies Installation
|
||||||
|
# ===============================================
|
||||||
|
FROM node:20-alpine AS dependencies
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install all dependencies (including dev for build)
|
||||||
|
RUN npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
# ===============================================
|
||||||
|
# Stage 2: Build Application
|
||||||
|
# ===============================================
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependencies from previous stage
|
||||||
|
COPY --from=dependencies /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Set build-time environment variables
|
||||||
|
ARG NEXT_PUBLIC_API_URL
|
||||||
|
ARG NEXT_PUBLIC_APP_URL
|
||||||
|
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||||
|
ARG NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
||||||
|
ARG NEXT_PUBLIC_GA_MEASUREMENT_ID
|
||||||
|
|
||||||
|
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \
|
||||||
|
NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL \
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN \
|
||||||
|
NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT \
|
||||||
|
NEXT_PUBLIC_GA_MEASUREMENT_ID=$NEXT_PUBLIC_GA_MEASUREMENT_ID \
|
||||||
|
NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Build the Next.js application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ===============================================
|
||||||
|
# Stage 3: Production Image
|
||||||
|
# ===============================================
|
||||||
|
FROM node:20-alpine AS production
|
||||||
|
|
||||||
|
# Install dumb-init for proper signal handling
|
||||||
|
RUN apk add --no-cache dumb-init curl
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S nextjs -u 1001
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy built application from builder
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:3000/api/health || exit 1
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
PORT=3000 \
|
||||||
|
HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
# Use dumb-init to handle signals properly
|
||||||
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
|
|
||||||
|
# Start the Next.js application
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@ -2,6 +2,10 @@
|
|||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
swcMinify: true,
|
swcMinify: true,
|
||||||
|
|
||||||
|
// Standalone output for Docker (creates optimized server.js)
|
||||||
|
output: 'standalone',
|
||||||
|
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: '2mb',
|
bodySizeLimit: '2mb',
|
||||||
@ -11,7 +15,14 @@ const nextConfig = {
|
|||||||
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',
|
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
domains: ['localhost'],
|
domains: ['localhost', 'xpeditis.com', 'staging.xpeditis.com'],
|
||||||
|
// Allow S3 images in production
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: '**.amazonaws.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
97
docker/.env.production.example
Normal file
97
docker/.env.production.example
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# Xpeditis - Production Environment Variables
|
||||||
|
# Copy this file to .env.production and fill in the values
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# DOCKER REGISTRY
|
||||||
|
# ===================================
|
||||||
|
DOCKER_REGISTRY=docker.io
|
||||||
|
BACKEND_IMAGE=xpeditis/backend
|
||||||
|
BACKEND_TAG=latest
|
||||||
|
FRONTEND_IMAGE=xpeditis/frontend
|
||||||
|
FRONTEND_TAG=latest
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# DATABASE (PostgreSQL)
|
||||||
|
# ===================================
|
||||||
|
POSTGRES_DB=xpeditis_prod
|
||||||
|
POSTGRES_USER=xpeditis
|
||||||
|
POSTGRES_PASSWORD=CHANGE_ME_SECURE_PASSWORD_64_CHARS_MINIMUM
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# REDIS CACHE
|
||||||
|
# ===================================
|
||||||
|
REDIS_PASSWORD=CHANGE_ME_REDIS_PASSWORD_64_CHARS_MINIMUM
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# JWT AUTHENTICATION
|
||||||
|
# ===================================
|
||||||
|
JWT_SECRET=CHANGE_ME_JWT_SECRET_512_BITS_MINIMUM
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# AWS CONFIGURATION
|
||||||
|
# ===================================
|
||||||
|
AWS_REGION=eu-west-3
|
||||||
|
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
||||||
|
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||||
|
AWS_SES_REGION=eu-west-1
|
||||||
|
|
||||||
|
# S3 Buckets
|
||||||
|
S3_BUCKET_DOCUMENTS=xpeditis-prod-documents
|
||||||
|
S3_BUCKET_UPLOADS=xpeditis-prod-uploads
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# EMAIL CONFIGURATION
|
||||||
|
# ===================================
|
||||||
|
EMAIL_SERVICE=ses
|
||||||
|
EMAIL_FROM=noreply@xpeditis.com
|
||||||
|
EMAIL_FROM_NAME=Xpeditis
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# MONITORING (Sentry) - REQUIRED
|
||||||
|
# ===================================
|
||||||
|
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# ANALYTICS (Google Analytics) - REQUIRED
|
||||||
|
# ===================================
|
||||||
|
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# CARRIER APIs (Production) - REQUIRED
|
||||||
|
# ===================================
|
||||||
|
# Maersk Production
|
||||||
|
MAERSK_API_URL=https://api.maersk.com
|
||||||
|
MAERSK_API_KEY=your-maersk-production-api-key
|
||||||
|
|
||||||
|
# MSC Production
|
||||||
|
MSC_API_URL=https://api.msc.com
|
||||||
|
MSC_API_KEY=your-msc-production-api-key
|
||||||
|
|
||||||
|
# CMA CGM Production
|
||||||
|
CMA_CGM_API_URL=https://api.cma-cgm.com
|
||||||
|
CMA_CGM_API_KEY=your-cma-cgm-production-api-key
|
||||||
|
|
||||||
|
# Hapag-Lloyd Production
|
||||||
|
HAPAG_LLOYD_API_URL=https://api.hapag-lloyd.com
|
||||||
|
HAPAG_LLOYD_API_KEY=your-hapag-lloyd-api-key
|
||||||
|
|
||||||
|
# ONE (Ocean Network Express)
|
||||||
|
ONE_API_URL=https://api.one-line.com
|
||||||
|
ONE_API_KEY=your-one-api-key
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# SECURITY BEST PRACTICES
|
||||||
|
# ===================================
|
||||||
|
# ✅ Use AWS Secrets Manager for production secrets
|
||||||
|
# ✅ Rotate credentials every 90 days
|
||||||
|
# ✅ Enable AWS CloudTrail for audit logs
|
||||||
|
# ✅ Use IAM roles with least privilege
|
||||||
|
# ✅ Enable MFA on all AWS accounts
|
||||||
|
# ✅ Use strong passwords (min 64 characters, random)
|
||||||
|
# ✅ Never commit this file with real credentials
|
||||||
|
# ✅ Restrict database access to VPC only
|
||||||
|
# ✅ Enable SSL/TLS for all connections
|
||||||
|
# ✅ Monitor failed login attempts (Sentry)
|
||||||
|
# ✅ Setup automated backups (daily, 30-day retention)
|
||||||
|
# ✅ Test disaster recovery procedures monthly
|
||||||
82
docker/.env.staging.example
Normal file
82
docker/.env.staging.example
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# Xpeditis - Staging Environment Variables
|
||||||
|
# Copy this file to .env.staging and fill in the values
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# DOCKER REGISTRY
|
||||||
|
# ===================================
|
||||||
|
DOCKER_REGISTRY=docker.io
|
||||||
|
BACKEND_IMAGE=xpeditis/backend
|
||||||
|
BACKEND_TAG=staging-latest
|
||||||
|
FRONTEND_IMAGE=xpeditis/frontend
|
||||||
|
FRONTEND_TAG=staging-latest
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# DATABASE (PostgreSQL)
|
||||||
|
# ===================================
|
||||||
|
POSTGRES_DB=xpeditis_staging
|
||||||
|
POSTGRES_USER=xpeditis
|
||||||
|
POSTGRES_PASSWORD=CHANGE_ME_SECURE_PASSWORD_HERE
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# REDIS CACHE
|
||||||
|
# ===================================
|
||||||
|
REDIS_PASSWORD=CHANGE_ME_REDIS_PASSWORD_HERE
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# JWT AUTHENTICATION
|
||||||
|
# ===================================
|
||||||
|
JWT_SECRET=CHANGE_ME_JWT_SECRET_256_BITS_MINIMUM
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# AWS CONFIGURATION
|
||||||
|
# ===================================
|
||||||
|
AWS_REGION=eu-west-3
|
||||||
|
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
||||||
|
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||||
|
AWS_SES_REGION=eu-west-1
|
||||||
|
|
||||||
|
# S3 Buckets
|
||||||
|
S3_BUCKET_DOCUMENTS=xpeditis-staging-documents
|
||||||
|
S3_BUCKET_UPLOADS=xpeditis-staging-uploads
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# EMAIL CONFIGURATION
|
||||||
|
# ===================================
|
||||||
|
EMAIL_SERVICE=ses
|
||||||
|
EMAIL_FROM=noreply@staging.xpeditis.com
|
||||||
|
EMAIL_FROM_NAME=Xpeditis Staging
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# MONITORING (Sentry)
|
||||||
|
# ===================================
|
||||||
|
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# ANALYTICS (Google Analytics)
|
||||||
|
# ===================================
|
||||||
|
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# CARRIER APIs (Sandbox)
|
||||||
|
# ===================================
|
||||||
|
# Maersk Sandbox
|
||||||
|
MAERSK_API_URL_SANDBOX=https://sandbox.api.maersk.com
|
||||||
|
MAERSK_API_KEY_SANDBOX=your-maersk-sandbox-api-key
|
||||||
|
|
||||||
|
# MSC Sandbox
|
||||||
|
MSC_API_URL_SANDBOX=https://sandbox.msc.com/api
|
||||||
|
MSC_API_KEY_SANDBOX=your-msc-sandbox-api-key
|
||||||
|
|
||||||
|
# CMA CGM Sandbox
|
||||||
|
CMA_CGM_API_URL_SANDBOX=https://sandbox.cma-cgm.com/api
|
||||||
|
CMA_CGM_API_KEY_SANDBOX=your-cma-cgm-sandbox-api-key
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# NOTES
|
||||||
|
# ===================================
|
||||||
|
# 1. Never commit this file with real credentials
|
||||||
|
# 2. Use strong passwords (min 32 characters, random)
|
||||||
|
# 3. Rotate secrets regularly (every 90 days)
|
||||||
|
# 4. Use AWS Secrets Manager or similar for production
|
||||||
|
# 5. Enable MFA on all AWS accounts
|
||||||
444
docker/DOCKER_BUILD_GUIDE.md
Normal file
444
docker/DOCKER_BUILD_GUIDE.md
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
# Guide de Construction des Images Docker - Xpeditis
|
||||||
|
|
||||||
|
Ce guide explique comment construire les images Docker pour backend et frontend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Prérequis
|
||||||
|
|
||||||
|
### 1. Docker Installé
|
||||||
|
```bash
|
||||||
|
docker --version
|
||||||
|
# Docker version 24.0.0 ou supérieur
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Docker Registry Access
|
||||||
|
- **Docker Hub**: Créer un compte sur https://hub.docker.com
|
||||||
|
- **Ou** GitHub Container Registry (GHCR)
|
||||||
|
- **Ou** Registry privé
|
||||||
|
|
||||||
|
### 3. Login au Registry
|
||||||
|
```bash
|
||||||
|
# Docker Hub
|
||||||
|
docker login
|
||||||
|
|
||||||
|
# GitHub Container Registry
|
||||||
|
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
|
||||||
|
|
||||||
|
# Registry privé
|
||||||
|
docker login registry.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Méthode 1: Script Automatique (Recommandé)
|
||||||
|
|
||||||
|
### Build Staging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build seulement (pas de push)
|
||||||
|
./docker/build-images.sh staging
|
||||||
|
|
||||||
|
# Build ET push vers Docker Hub
|
||||||
|
./docker/build-images.sh staging --push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build seulement
|
||||||
|
./docker/build-images.sh production
|
||||||
|
|
||||||
|
# Build ET push
|
||||||
|
./docker/build-images.sh production --push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration du Registry
|
||||||
|
|
||||||
|
Par défaut, le script utilise `docker.io/xpeditis` comme registry.
|
||||||
|
|
||||||
|
Pour changer:
|
||||||
|
```bash
|
||||||
|
export DOCKER_REGISTRY=ghcr.io
|
||||||
|
export DOCKER_REPO=your-org
|
||||||
|
./docker/build-images.sh staging --push
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Méthode 2: Build Manuel
|
||||||
|
|
||||||
|
### Backend Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/backend
|
||||||
|
|
||||||
|
# Staging
|
||||||
|
docker build \
|
||||||
|
--file Dockerfile \
|
||||||
|
--tag xpeditis/backend:staging-latest \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
.
|
||||||
|
|
||||||
|
# Production
|
||||||
|
docker build \
|
||||||
|
--file Dockerfile \
|
||||||
|
--tag xpeditis/backend:latest \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/frontend
|
||||||
|
|
||||||
|
# Staging
|
||||||
|
docker build \
|
||||||
|
--file Dockerfile \
|
||||||
|
--tag xpeditis/frontend:staging-latest \
|
||||||
|
--build-arg NEXT_PUBLIC_API_URL=https://api-staging.xpeditis.com \
|
||||||
|
--build-arg NEXT_PUBLIC_APP_URL=https://staging.xpeditis.com \
|
||||||
|
--build-arg NEXT_PUBLIC_SENTRY_ENVIRONMENT=staging \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
.
|
||||||
|
|
||||||
|
# Production
|
||||||
|
docker build \
|
||||||
|
--file Dockerfile \
|
||||||
|
--tag xpeditis/frontend:latest \
|
||||||
|
--build-arg NEXT_PUBLIC_API_URL=https://api.xpeditis.com \
|
||||||
|
--build-arg NEXT_PUBLIC_APP_URL=https://xpeditis.com \
|
||||||
|
--build-arg NEXT_PUBLIC_SENTRY_ENVIRONMENT=production \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Push Images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
docker push xpeditis/backend:staging-latest
|
||||||
|
docker push xpeditis/backend:latest
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
docker push xpeditis/frontend:staging-latest
|
||||||
|
docker push xpeditis/frontend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tester les Images Localement
|
||||||
|
|
||||||
|
### 1. Créer un network Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create xpeditis-test
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Lancer PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name postgres-test \
|
||||||
|
--network xpeditis-test \
|
||||||
|
-e POSTGRES_DB=xpeditis_test \
|
||||||
|
-e POSTGRES_USER=xpeditis \
|
||||||
|
-e POSTGRES_PASSWORD=test123 \
|
||||||
|
-p 5432:5432 \
|
||||||
|
postgres:15-alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Lancer Redis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name redis-test \
|
||||||
|
--network xpeditis-test \
|
||||||
|
-p 6379:6379 \
|
||||||
|
redis:7-alpine \
|
||||||
|
redis-server --requirepass test123
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Lancer Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name backend-test \
|
||||||
|
--network xpeditis-test \
|
||||||
|
-e NODE_ENV=development \
|
||||||
|
-e PORT=4000 \
|
||||||
|
-e DATABASE_HOST=postgres-test \
|
||||||
|
-e DATABASE_PORT=5432 \
|
||||||
|
-e DATABASE_NAME=xpeditis_test \
|
||||||
|
-e DATABASE_USER=xpeditis \
|
||||||
|
-e DATABASE_PASSWORD=test123 \
|
||||||
|
-e REDIS_HOST=redis-test \
|
||||||
|
-e REDIS_PORT=6379 \
|
||||||
|
-e REDIS_PASSWORD=test123 \
|
||||||
|
-e JWT_SECRET=test-secret-key-256-bits-minimum-length-required \
|
||||||
|
-e CORS_ORIGIN=http://localhost:3000 \
|
||||||
|
-p 4000:4000 \
|
||||||
|
xpeditis/backend:staging-latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Lancer Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name frontend-test \
|
||||||
|
--network xpeditis-test \
|
||||||
|
-e NODE_ENV=development \
|
||||||
|
-e NEXT_PUBLIC_API_URL=http://localhost:4000 \
|
||||||
|
-e NEXT_PUBLIC_APP_URL=http://localhost:3000 \
|
||||||
|
-e API_URL=http://backend-test:4000 \
|
||||||
|
-p 3000:3000 \
|
||||||
|
xpeditis/frontend:staging-latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Vérifier
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend health check
|
||||||
|
curl http://localhost:4000/health
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
curl http://localhost:3000/api/health
|
||||||
|
|
||||||
|
# Ouvrir dans navigateur
|
||||||
|
open http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Voir les logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f backend-test
|
||||||
|
docker logs -f frontend-test
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Nettoyer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stop backend-test frontend-test postgres-test redis-test
|
||||||
|
docker rm backend-test frontend-test postgres-test redis-test
|
||||||
|
docker network rm xpeditis-test
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Optimisation des Images
|
||||||
|
|
||||||
|
### Tailles d'Images Typiques
|
||||||
|
|
||||||
|
- **Backend**: ~150-200 MB (après compression)
|
||||||
|
- **Frontend**: ~120-150 MB (après compression)
|
||||||
|
- **Total**: ~300 MB (pour les 2 images)
|
||||||
|
|
||||||
|
### Multi-Stage Build
|
||||||
|
|
||||||
|
Les Dockerfiles utilisent des builds multi-stage:
|
||||||
|
|
||||||
|
1. **Stage Dependencies**: Installation des dépendances
|
||||||
|
2. **Stage Builder**: Compilation TypeScript/Next.js
|
||||||
|
3. **Stage Production**: Image finale (seulement le nécessaire)
|
||||||
|
|
||||||
|
Avantages:
|
||||||
|
- ✅ Images légères (pas de dev dependencies)
|
||||||
|
- ✅ Build rapide (cache des layers)
|
||||||
|
- ✅ Sécurisé (pas de code source dans prod)
|
||||||
|
|
||||||
|
### Build Cache
|
||||||
|
|
||||||
|
Pour accélérer les builds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build avec cache
|
||||||
|
docker build --cache-from xpeditis/backend:staging-latest -t xpeditis/backend:staging-latest .
|
||||||
|
|
||||||
|
# Ou avec BuildKit (plus rapide)
|
||||||
|
DOCKER_BUILDKIT=1 docker build -t xpeditis/backend:staging-latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scan de Vulnérabilités
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Scan avec Docker Scout (gratuit)
|
||||||
|
docker scout cves xpeditis/backend:staging-latest
|
||||||
|
|
||||||
|
# Scan avec Trivy
|
||||||
|
trivy image xpeditis/backend:staging-latest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions Example
|
||||||
|
|
||||||
|
Voir `.github/workflows/docker-build.yml` (à créer):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Build and Push Docker Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and Push
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||||
|
./docker/build-images.sh production --push
|
||||||
|
else
|
||||||
|
./docker/build-images.sh staging --push
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Dépannage
|
||||||
|
|
||||||
|
### Problème 1: Build échoue avec erreur "npm ci"
|
||||||
|
|
||||||
|
**Symptôme**: `npm ci` failed with exit code 1
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Nettoyer le cache Docker
|
||||||
|
docker builder prune -a
|
||||||
|
|
||||||
|
# Rebuild sans cache
|
||||||
|
docker build --no-cache -t xpeditis/backend:staging-latest apps/backend/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème 2: Image trop grosse (>500 MB)
|
||||||
|
|
||||||
|
**Symptôme**: Image très volumineuse
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Vérifier que `.dockerignore` est présent
|
||||||
|
- Vérifier que `node_modules` n'est pas copié
|
||||||
|
- Utiliser `npm ci` au lieu de `npm install`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analyser les layers
|
||||||
|
docker history xpeditis/backend:staging-latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème 3: Next.js standalone build échoue
|
||||||
|
|
||||||
|
**Symptôme**: `Error: Cannot find module './standalone/server.js'`
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Vérifier que `next.config.js` a `output: 'standalone'`
|
||||||
|
- Rebuild frontend:
|
||||||
|
```bash
|
||||||
|
cd apps/frontend
|
||||||
|
npm run build
|
||||||
|
# Vérifier que .next/standalone existe
|
||||||
|
ls -la .next/standalone
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème 4: CORS errors en production
|
||||||
|
|
||||||
|
**Symptôme**: Frontend ne peut pas appeler le backend
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Vérifier `CORS_ORIGIN` dans backend
|
||||||
|
- Vérifier `NEXT_PUBLIC_API_URL` dans frontend
|
||||||
|
- Tester avec curl:
|
||||||
|
```bash
|
||||||
|
curl -H "Origin: https://staging.xpeditis.com" \
|
||||||
|
-H "Access-Control-Request-Method: GET" \
|
||||||
|
-X OPTIONS \
|
||||||
|
https://api-staging.xpeditis.com/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème 5: Health check fails
|
||||||
|
|
||||||
|
**Symptôme**: Container restart en boucle
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Voir les logs
|
||||||
|
docker logs backend-test
|
||||||
|
|
||||||
|
# Tester health check manuellement
|
||||||
|
docker exec backend-test curl -f http://localhost:4000/health
|
||||||
|
|
||||||
|
# Si curl manque, installer:
|
||||||
|
docker exec backend-test apk add curl
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Ressources
|
||||||
|
|
||||||
|
- **Dockerfile Best Practices**: https://docs.docker.com/develop/dev-best-practices/
|
||||||
|
- **Next.js Docker**: https://nextjs.org/docs/deployment#docker-image
|
||||||
|
- **NestJS Docker**: https://docs.nestjs.com/recipes/docker
|
||||||
|
- **Docker Build Reference**: https://docs.docker.com/engine/reference/commandline/build/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Sécurité
|
||||||
|
|
||||||
|
### Ne PAS Inclure dans les Images
|
||||||
|
|
||||||
|
❌ Secrets (JWT_SECRET, API keys)
|
||||||
|
❌ Fichiers `.env`
|
||||||
|
❌ Code source TypeScript (seulement JS compilé)
|
||||||
|
❌ node_modules de dev
|
||||||
|
❌ Tests et mocks
|
||||||
|
❌ Documentation
|
||||||
|
|
||||||
|
### Utiliser
|
||||||
|
|
||||||
|
✅ Variables d'environnement au runtime
|
||||||
|
✅ Docker secrets (si Swarm)
|
||||||
|
✅ Kubernetes secrets (si K8s)
|
||||||
|
✅ AWS Secrets Manager / Vault
|
||||||
|
✅ Non-root user dans container
|
||||||
|
✅ Health checks
|
||||||
|
✅ Resource limits
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Métriques de Build
|
||||||
|
|
||||||
|
Après chaque build, vérifier:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Taille des images
|
||||||
|
docker images | grep xpeditis
|
||||||
|
|
||||||
|
# Layers count
|
||||||
|
docker history xpeditis/backend:staging-latest | wc -l
|
||||||
|
|
||||||
|
# Scan vulnérabilités
|
||||||
|
docker scout cves xpeditis/backend:staging-latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Objectifs**:
|
||||||
|
- ✅ Backend < 200 MB
|
||||||
|
- ✅ Frontend < 150 MB
|
||||||
|
- ✅ Build time < 5 min
|
||||||
|
- ✅ Zéro vulnérabilité critique
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière mise à jour*: 2025-10-14
|
||||||
|
*Version*: 1.0.0
|
||||||
419
docker/PORTAINER_DEPLOYMENT_GUIDE.md
Normal file
419
docker/PORTAINER_DEPLOYMENT_GUIDE.md
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
# Guide de Déploiement Portainer - Xpeditis
|
||||||
|
|
||||||
|
Ce guide explique comment déployer les stacks Xpeditis (staging et production) sur Portainer avec Traefik.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Prérequis
|
||||||
|
|
||||||
|
### 1. Infrastructure Serveur
|
||||||
|
- **Serveur VPS/Dédié** avec Docker installé
|
||||||
|
- **Minimum**: 4 vCPU, 8 GB RAM, 100 GB SSD
|
||||||
|
- **Recommandé Production**: 8 vCPU, 16 GB RAM, 200 GB SSD
|
||||||
|
- **OS**: Ubuntu 22.04 LTS ou Debian 11+
|
||||||
|
|
||||||
|
### 2. Traefik déjà déployé
|
||||||
|
- Network `traefik_network` doit exister
|
||||||
|
- Let's Encrypt configuré (`letsencrypt` resolver)
|
||||||
|
- Ports 80 et 443 ouverts
|
||||||
|
|
||||||
|
### 3. DNS Configuré
|
||||||
|
**Staging**:
|
||||||
|
- `staging.xpeditis.com` → IP du serveur
|
||||||
|
- `api-staging.xpeditis.com` → IP du serveur
|
||||||
|
|
||||||
|
**Production**:
|
||||||
|
- `xpeditis.com` → IP du serveur
|
||||||
|
- `www.xpeditis.com` → IP du serveur
|
||||||
|
- `api.xpeditis.com` → IP du serveur
|
||||||
|
|
||||||
|
### 4. Images Docker
|
||||||
|
Les images Docker doivent être buildées et pushées sur un registry (Docker Hub, GitHub Container Registry, ou privé):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build backend
|
||||||
|
cd apps/backend
|
||||||
|
docker build -t xpeditis/backend:staging-latest .
|
||||||
|
docker push xpeditis/backend:staging-latest
|
||||||
|
|
||||||
|
# Build frontend
|
||||||
|
cd apps/frontend
|
||||||
|
docker build -t xpeditis/frontend:staging-latest .
|
||||||
|
docker push xpeditis/frontend:staging-latest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Déploiement sur Portainer
|
||||||
|
|
||||||
|
### Étape 1: Créer le network Traefik (si pas déjà fait)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create traefik_network
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étape 2: Préparer les variables d'environnement
|
||||||
|
|
||||||
|
#### Pour Staging:
|
||||||
|
1. Copier `.env.staging.example` vers `.env.staging`
|
||||||
|
2. Remplir toutes les valeurs (voir section Variables d'environnement ci-dessous)
|
||||||
|
3. **IMPORTANT**: Utiliser des mots de passe forts (min 32 caractères)
|
||||||
|
|
||||||
|
#### Pour Production:
|
||||||
|
1. Copier `.env.production.example` vers `.env.production`
|
||||||
|
2. Remplir toutes les valeurs avec les credentials de production
|
||||||
|
3. **IMPORTANT**: Utiliser des mots de passe ultra-forts (min 64 caractères)
|
||||||
|
|
||||||
|
### Étape 3: Déployer via Portainer UI
|
||||||
|
|
||||||
|
#### A. Accéder à Portainer
|
||||||
|
- URL: `https://portainer.votre-domaine.com` (ou `http://IP:9000`)
|
||||||
|
- Login avec vos credentials admin
|
||||||
|
|
||||||
|
#### B. Créer la Stack Staging
|
||||||
|
|
||||||
|
1. **Aller dans**: Stacks → Add Stack
|
||||||
|
2. **Name**: `xpeditis-staging`
|
||||||
|
3. **Build method**: Web editor
|
||||||
|
4. **Copier le contenu** de `portainer-stack-staging.yml`
|
||||||
|
5. **Onglet "Environment variables"**:
|
||||||
|
- Cliquer sur "Load variables from .env file"
|
||||||
|
- Copier-coller le contenu de `.env.staging`
|
||||||
|
- OU ajouter manuellement chaque variable
|
||||||
|
6. **Cliquer**: Deploy the stack
|
||||||
|
7. **Vérifier**: Les 4 services doivent démarrer (postgres, redis, backend, frontend)
|
||||||
|
|
||||||
|
#### C. Créer la Stack Production
|
||||||
|
|
||||||
|
1. **Aller dans**: Stacks → Add Stack
|
||||||
|
2. **Name**: `xpeditis-production`
|
||||||
|
3. **Build method**: Web editor
|
||||||
|
4. **Copier le contenu** de `portainer-stack-production.yml`
|
||||||
|
5. **Onglet "Environment variables"**:
|
||||||
|
- Cliquer sur "Load variables from .env file"
|
||||||
|
- Copier-coller le contenu de `.env.production`
|
||||||
|
- OU ajouter manuellement chaque variable
|
||||||
|
6. **Cliquer**: Deploy the stack
|
||||||
|
7. **Vérifier**: Les 6 services doivent démarrer (postgres, redis, backend x2, frontend x2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Variables d'environnement Critiques
|
||||||
|
|
||||||
|
### Variables Obligatoires (staging & production)
|
||||||
|
|
||||||
|
| Variable | Description | Exemple |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `POSTGRES_PASSWORD` | Mot de passe PostgreSQL | `XpEd1t1s_pG_S3cur3_2024!` |
|
||||||
|
| `REDIS_PASSWORD` | Mot de passe Redis | `R3d1s_C4ch3_P4ssw0rd!` |
|
||||||
|
| `JWT_SECRET` | Secret pour JWT tokens | `openssl rand -base64 64` |
|
||||||
|
| `AWS_ACCESS_KEY_ID` | AWS Access Key | `AKIAIOSFODNN7EXAMPLE` |
|
||||||
|
| `AWS_SECRET_ACCESS_KEY` | AWS Secret Key | `wJalrXUtnFEMI/K7MDENG/...` |
|
||||||
|
| `SENTRY_DSN` | Sentry monitoring URL | `https://xxx@sentry.io/123` |
|
||||||
|
| `MAERSK_API_KEY` | Clé API Maersk | Voir portail Maersk |
|
||||||
|
|
||||||
|
### Générer des Secrets Sécurisés
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PostgreSQL password (64 chars)
|
||||||
|
openssl rand -base64 48
|
||||||
|
|
||||||
|
# Redis password (64 chars)
|
||||||
|
openssl rand -base64 48
|
||||||
|
|
||||||
|
# JWT Secret (512 bits)
|
||||||
|
openssl rand -base64 64
|
||||||
|
|
||||||
|
# Generic secure password
|
||||||
|
pwgen -s 64 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Vérification du Déploiement
|
||||||
|
|
||||||
|
### 1. Vérifier l'état des conteneurs
|
||||||
|
|
||||||
|
Dans Portainer:
|
||||||
|
- **Stacks** → `xpeditis-staging` (ou production)
|
||||||
|
- Tous les services doivent être en status **running** (vert)
|
||||||
|
|
||||||
|
### 2. Vérifier les logs
|
||||||
|
|
||||||
|
Cliquer sur chaque service → **Logs** → Vérifier qu'il n'y a pas d'erreurs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ou via CLI
|
||||||
|
docker logs xpeditis-backend-staging -f
|
||||||
|
docker logs xpeditis-frontend-staging -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Vérifier les health checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend health check
|
||||||
|
curl https://api-staging.xpeditis.com/health
|
||||||
|
# Réponse attendue: {"status":"ok","timestamp":"..."}
|
||||||
|
|
||||||
|
# Frontend health check
|
||||||
|
curl https://staging.xpeditis.com/api/health
|
||||||
|
# Réponse attendue: {"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Vérifier Traefik
|
||||||
|
|
||||||
|
Dans Traefik dashboard:
|
||||||
|
- Routers: Doit afficher `xpeditis-backend-staging` et `xpeditis-frontend-staging`
|
||||||
|
- Services: Doit afficher les load balancers avec health checks verts
|
||||||
|
- Certificats: Let's Encrypt doit être vert
|
||||||
|
|
||||||
|
### 5. Vérifier SSL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier certificat SSL
|
||||||
|
curl -I https://staging.xpeditis.com
|
||||||
|
# Header "Strict-Transport-Security" doit être présent
|
||||||
|
|
||||||
|
# Test SSL avec SSLLabs
|
||||||
|
# https://www.ssllabs.com/ssltest/analyze.html?d=staging.xpeditis.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Test Complet
|
||||||
|
|
||||||
|
1. **Frontend**: Ouvrir `https://staging.xpeditis.com` dans un navigateur
|
||||||
|
2. **Backend**: Tester un endpoint: `https://api-staging.xpeditis.com/health`
|
||||||
|
3. **Login**: Créer un compte et se connecter
|
||||||
|
4. **Recherche de taux**: Tester une recherche Rotterdam → Shanghai
|
||||||
|
5. **Booking**: Créer un booking de test
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Dépannage
|
||||||
|
|
||||||
|
### Problème 1: Service ne démarre pas
|
||||||
|
|
||||||
|
**Symptôme**: Conteneur en status "Exited" ou "Restarting"
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Vérifier les logs: Portainer → Service → Logs
|
||||||
|
2. Erreurs communes:
|
||||||
|
- `POSTGRES_PASSWORD` manquant → Ajouter la variable
|
||||||
|
- `Cannot connect to postgres` → Vérifier que postgres est en running
|
||||||
|
- `Redis connection refused` → Vérifier que redis est en running
|
||||||
|
- `Port already in use` → Un autre service utilise le port
|
||||||
|
|
||||||
|
### Problème 2: Traefik ne route pas vers le service
|
||||||
|
|
||||||
|
**Symptôme**: 404 Not Found ou Gateway Timeout
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Vérifier que le network `traefik_network` existe:
|
||||||
|
```bash
|
||||||
|
docker network ls | grep traefik
|
||||||
|
```
|
||||||
|
2. Vérifier que les services sont connectés au network:
|
||||||
|
```bash
|
||||||
|
docker inspect xpeditis-backend-staging | grep traefik_network
|
||||||
|
```
|
||||||
|
3. Vérifier les labels Traefik dans Portainer → Service → Labels
|
||||||
|
4. Restart Traefik:
|
||||||
|
```bash
|
||||||
|
docker restart traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème 3: SSL Certificate Failed
|
||||||
|
|
||||||
|
**Symptôme**: "Your connection is not private" ou certificat invalide
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Vérifier que DNS pointe vers le serveur:
|
||||||
|
```bash
|
||||||
|
nslookup staging.xpeditis.com
|
||||||
|
```
|
||||||
|
2. Vérifier les logs Traefik:
|
||||||
|
```bash
|
||||||
|
docker logs traefik | grep -i letsencrypt
|
||||||
|
```
|
||||||
|
3. Vérifier que ports 80 et 443 sont ouverts:
|
||||||
|
```bash
|
||||||
|
sudo ufw status
|
||||||
|
sudo netstat -tlnp | grep -E '80|443'
|
||||||
|
```
|
||||||
|
4. Si nécessaire, supprimer le certificat et re-déployer:
|
||||||
|
```bash
|
||||||
|
docker exec traefik rm /letsencrypt/acme.json
|
||||||
|
docker restart traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème 4: Database connection failed
|
||||||
|
|
||||||
|
**Symptôme**: Backend logs montrent "Cannot connect to database"
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Vérifier que PostgreSQL est en running
|
||||||
|
2. Vérifier les credentials:
|
||||||
|
```bash
|
||||||
|
docker exec -it xpeditis-postgres-staging psql -U xpeditis -d xpeditis_staging
|
||||||
|
```
|
||||||
|
3. Vérifier le network interne:
|
||||||
|
```bash
|
||||||
|
docker exec -it xpeditis-backend-staging ping postgres-staging
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème 5: High memory usage
|
||||||
|
|
||||||
|
**Symptôme**: Serveur lent, OOM killer
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Vérifier l'utilisation mémoire:
|
||||||
|
```bash
|
||||||
|
docker stats
|
||||||
|
```
|
||||||
|
2. Réduire les limites dans docker-compose (section `deploy.resources`)
|
||||||
|
3. Augmenter la RAM du serveur
|
||||||
|
4. Optimiser les queries PostgreSQL (indexes, explain analyze)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Mise à Jour des Stacks
|
||||||
|
|
||||||
|
### Update Rolling (Zero Downtime)
|
||||||
|
|
||||||
|
#### Staging:
|
||||||
|
1. Build et push nouvelle image:
|
||||||
|
```bash
|
||||||
|
docker build -t xpeditis/backend:staging-v1.2.0 .
|
||||||
|
docker push xpeditis/backend:staging-v1.2.0
|
||||||
|
```
|
||||||
|
2. Dans Portainer → Stacks → `xpeditis-staging` → Editor
|
||||||
|
3. Changer `BACKEND_TAG=staging-v1.2.0`
|
||||||
|
4. Cliquer "Update the stack"
|
||||||
|
5. Portainer va pull la nouvelle image et redémarrer les services
|
||||||
|
|
||||||
|
#### Production (avec High Availability):
|
||||||
|
La stack production a 2 instances de chaque service (backend-prod-1, backend-prod-2). Traefik va load balancer entre les deux.
|
||||||
|
|
||||||
|
**Mise à jour sans downtime**:
|
||||||
|
1. Stopper `backend-prod-2` dans Portainer
|
||||||
|
2. Update l'image de `backend-prod-2`
|
||||||
|
3. Redémarrer `backend-prod-2`
|
||||||
|
4. Vérifier health check OK
|
||||||
|
5. Stopper `backend-prod-1`
|
||||||
|
6. Update l'image de `backend-prod-1`
|
||||||
|
7. Redémarrer `backend-prod-1`
|
||||||
|
8. Vérifier health check OK
|
||||||
|
|
||||||
|
**OU via Portainer** (plus simple):
|
||||||
|
1. Portainer → Stacks → `xpeditis-production` → Editor
|
||||||
|
2. Changer `BACKEND_TAG=v1.2.0`
|
||||||
|
3. Cliquer "Update the stack"
|
||||||
|
4. Portainer va mettre à jour les services un par un (rolling update automatique)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
### 1. Portainer Built-in Monitoring
|
||||||
|
|
||||||
|
Portainer → Containers → Sélectionner service → **Stats**
|
||||||
|
- CPU usage
|
||||||
|
- Memory usage
|
||||||
|
- Network I/O
|
||||||
|
- Block I/O
|
||||||
|
|
||||||
|
### 2. Sentry (Error Tracking)
|
||||||
|
|
||||||
|
Toutes les erreurs backend et frontend sont envoyées à Sentry (configuré via `SENTRY_DSN`)
|
||||||
|
|
||||||
|
URL: https://sentry.io/organizations/xpeditis/projects/
|
||||||
|
|
||||||
|
### 3. Logs Centralisés
|
||||||
|
|
||||||
|
**Voir tous les logs en temps réel**:
|
||||||
|
```bash
|
||||||
|
docker logs -f xpeditis-backend-staging
|
||||||
|
docker logs -f xpeditis-frontend-staging
|
||||||
|
docker logs -f xpeditis-postgres-staging
|
||||||
|
docker logs -f xpeditis-redis-staging
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rechercher dans les logs**:
|
||||||
|
```bash
|
||||||
|
docker logs xpeditis-backend-staging 2>&1 | grep "ERROR"
|
||||||
|
docker logs xpeditis-backend-staging 2>&1 | grep "booking"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Health Checks Dashboard
|
||||||
|
|
||||||
|
Créer un dashboard custom avec:
|
||||||
|
- Uptime Robot: https://uptimerobot.com (free tier: 50 monitors)
|
||||||
|
- Grafana + Prometheus (advanced)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Sécurité Best Practices
|
||||||
|
|
||||||
|
### 1. Mots de passe forts
|
||||||
|
✅ Min 64 caractères pour production
|
||||||
|
✅ Générés aléatoirement (openssl, pwgen)
|
||||||
|
✅ Stockés dans un gestionnaire de secrets (AWS Secrets Manager, Vault)
|
||||||
|
|
||||||
|
### 2. Rotation des credentials
|
||||||
|
✅ Tous les 90 jours
|
||||||
|
✅ Immédiatement si compromis
|
||||||
|
|
||||||
|
### 3. Backups automatiques
|
||||||
|
✅ PostgreSQL: Backup quotidien
|
||||||
|
✅ Retention: 30 jours staging, 90 jours production
|
||||||
|
✅ Test restore mensuel
|
||||||
|
|
||||||
|
### 4. Monitoring actif
|
||||||
|
✅ Sentry configuré
|
||||||
|
✅ Uptime monitoring actif
|
||||||
|
✅ Alertes email/Slack pour downtime
|
||||||
|
|
||||||
|
### 5. SSL/TLS
|
||||||
|
✅ HSTS activé (Strict-Transport-Security)
|
||||||
|
✅ TLS 1.2+ minimum
|
||||||
|
✅ Certificat Let's Encrypt auto-renew
|
||||||
|
|
||||||
|
### 6. Rate Limiting
|
||||||
|
✅ Traefik rate limiting configuré
|
||||||
|
✅ Application-level rate limiting (NestJS throttler)
|
||||||
|
✅ Brute-force protection active
|
||||||
|
|
||||||
|
### 7. Firewall
|
||||||
|
✅ Ports 80, 443 ouverts uniquement
|
||||||
|
✅ PostgreSQL/Redis accessibles uniquement depuis réseau interne Docker
|
||||||
|
✅ SSH avec clés uniquement (pas de mot de passe)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
### En cas de problème critique:
|
||||||
|
|
||||||
|
1. **Vérifier les logs** dans Portainer
|
||||||
|
2. **Vérifier Sentry** pour les erreurs récentes
|
||||||
|
3. **Restart du service** via Portainer (si safe)
|
||||||
|
4. **Rollback**: Portainer → Stacks → Redeploy previous version
|
||||||
|
|
||||||
|
### Contacts:
|
||||||
|
- **Tech Lead**: david-henri.arnaud@3ds.com
|
||||||
|
- **DevOps**: ops@xpeditis.com
|
||||||
|
- **Support**: support@xpeditis.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Ressources
|
||||||
|
|
||||||
|
- **Portainer Docs**: https://docs.portainer.io/
|
||||||
|
- **Traefik Docs**: https://doc.traefik.io/traefik/
|
||||||
|
- **Docker Docs**: https://docs.docker.com/
|
||||||
|
- **Let's Encrypt**: https://letsencrypt.org/docs/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière mise à jour*: 2025-10-14
|
||||||
|
*Version*: 1.0.0
|
||||||
|
*Auteur*: Xpeditis DevOps Team
|
||||||
154
docker/build-images.sh
Normal file
154
docker/build-images.sh
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Docker Image Build Script - Xpeditis
|
||||||
|
# ================================================================
|
||||||
|
# This script builds and optionally pushes Docker images for
|
||||||
|
# backend and frontend to a Docker registry.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./build-images.sh [staging|production] [--push]
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# ./build-images.sh staging # Build staging images only
|
||||||
|
# ./build-images.sh production --push # Build and push production images
|
||||||
|
# ================================================================
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
ENVIRONMENT=${1:-staging}
|
||||||
|
PUSH_IMAGES=${2:-}
|
||||||
|
REGISTRY=${DOCKER_REGISTRY:-docker.io}
|
||||||
|
REPO=${DOCKER_REPO:-xpeditis}
|
||||||
|
|
||||||
|
# Validate environment
|
||||||
|
if [[ "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "production" ]]; then
|
||||||
|
echo -e "${RED}Error: Environment must be 'staging' or 'production'${NC}"
|
||||||
|
echo "Usage: $0 [staging|production] [--push]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set tags based on environment
|
||||||
|
if [[ "$ENVIRONMENT" == "staging" ]]; then
|
||||||
|
BACKEND_TAG="staging-latest"
|
||||||
|
FRONTEND_TAG="staging-latest"
|
||||||
|
API_URL="https://api-staging.xpeditis.com"
|
||||||
|
APP_URL="https://staging.xpeditis.com"
|
||||||
|
SENTRY_ENV="staging"
|
||||||
|
else
|
||||||
|
BACKEND_TAG="latest"
|
||||||
|
FRONTEND_TAG="latest"
|
||||||
|
API_URL="https://api.xpeditis.com"
|
||||||
|
APP_URL="https://xpeditis.com"
|
||||||
|
SENTRY_ENV="production"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
echo -e "${BLUE} Building Xpeditis Docker Images${NC}"
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
echo -e "Environment: ${YELLOW}$ENVIRONMENT${NC}"
|
||||||
|
echo -e "Registry: ${YELLOW}$REGISTRY${NC}"
|
||||||
|
echo -e "Repository: ${YELLOW}$REPO${NC}"
|
||||||
|
echo -e "Backend Tag: ${YELLOW}$BACKEND_TAG${NC}"
|
||||||
|
echo -e "Frontend Tag: ${YELLOW}$FRONTEND_TAG${NC}"
|
||||||
|
echo -e "Push: ${YELLOW}${PUSH_IMAGES:-No}${NC}"
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Navigate to project root
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Build Backend Image
|
||||||
|
# ================================================================
|
||||||
|
echo -e "${GREEN}[1/2] Building Backend Image...${NC}"
|
||||||
|
echo "Image: $REGISTRY/$REPO/backend:$BACKEND_TAG"
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--file apps/backend/Dockerfile \
|
||||||
|
--tag $REGISTRY/$REPO/backend:$BACKEND_TAG \
|
||||||
|
--tag $REGISTRY/$REPO/backend:$(date +%Y%m%d-%H%M%S) \
|
||||||
|
--build-arg NODE_ENV=$ENVIRONMENT \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
apps/backend/
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Backend image built successfully${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Build Frontend Image
|
||||||
|
# ================================================================
|
||||||
|
echo -e "${GREEN}[2/2] Building Frontend Image...${NC}"
|
||||||
|
echo "Image: $REGISTRY/$REPO/frontend:$FRONTEND_TAG"
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--file apps/frontend/Dockerfile \
|
||||||
|
--tag $REGISTRY/$REPO/frontend:$FRONTEND_TAG \
|
||||||
|
--tag $REGISTRY/$REPO/frontend:$(date +%Y%m%d-%H%M%S) \
|
||||||
|
--build-arg NEXT_PUBLIC_API_URL=$API_URL \
|
||||||
|
--build-arg NEXT_PUBLIC_APP_URL=$APP_URL \
|
||||||
|
--build-arg NEXT_PUBLIC_SENTRY_ENVIRONMENT=$SENTRY_ENV \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
apps/frontend/
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Frontend image built successfully${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Push Images (if --push flag provided)
|
||||||
|
# ================================================================
|
||||||
|
if [[ "$PUSH_IMAGES" == "--push" ]]; then
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
echo -e "${BLUE} Pushing Images to Registry${NC}"
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Pushing backend image...${NC}"
|
||||||
|
docker push $REGISTRY/$REPO/backend:$BACKEND_TAG
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Pushing frontend image...${NC}"
|
||||||
|
docker push $REGISTRY/$REPO/frontend:$FRONTEND_TAG
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Images pushed successfully${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Summary
|
||||||
|
# ================================================================
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
echo -e "${BLUE} Build Complete!${NC}"
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "Images built:"
|
||||||
|
echo -e " • Backend: ${GREEN}$REGISTRY/$REPO/backend:$BACKEND_TAG${NC}"
|
||||||
|
echo -e " • Frontend: ${GREEN}$REGISTRY/$REPO/frontend:$FRONTEND_TAG${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$PUSH_IMAGES" != "--push" ]]; then
|
||||||
|
echo -e "${YELLOW}To push images to registry, run:${NC}"
|
||||||
|
echo -e " $0 $ENVIRONMENT --push"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "To test images locally:"
|
||||||
|
echo -e " docker run -p 4000:4000 $REGISTRY/$REPO/backend:$BACKEND_TAG"
|
||||||
|
echo -e " docker run -p 3000:3000 $REGISTRY/$REPO/frontend:$FRONTEND_TAG"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "To deploy with Portainer:"
|
||||||
|
echo -e " 1. Login to Portainer UI"
|
||||||
|
echo -e " 2. Go to Stacks → Add Stack"
|
||||||
|
echo -e " 3. Use ${YELLOW}docker/portainer-stack-$ENVIRONMENT.yml${NC}"
|
||||||
|
echo -e " 4. Fill environment variables from ${YELLOW}docker/.env.$ENVIRONMENT.example${NC}"
|
||||||
|
echo -e " 5. Deploy!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ All done!${NC}"
|
||||||
456
docker/portainer-stack-production.yml
Normal file
456
docker/portainer-stack-production.yml
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
# Xpeditis - Stack PRODUCTION
|
||||||
|
# Portainer Stack avec Traefik reverse proxy
|
||||||
|
# Domaines: xpeditis.com (frontend) | api.xpeditis.com (backend)
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL Database
|
||||||
|
postgres-prod:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: xpeditis-postgres-prod
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-xpeditis_prod}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-xpeditis}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?error}
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
volumes:
|
||||||
|
- postgres_data_prod:/var/lib/postgresql/data
|
||||||
|
- postgres_backups_prod:/backups
|
||||||
|
networks:
|
||||||
|
- xpeditis_internal_prod
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-xpeditis}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 4G
|
||||||
|
reservations:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 2G
|
||||||
|
|
||||||
|
# Redis Cache
|
||||||
|
redis-prod:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: xpeditis-redis-prod
|
||||||
|
restart: always
|
||||||
|
command: redis-server --requirepass ${REDIS_PASSWORD:?error} --maxmemory 1gb --maxmemory-policy allkeys-lru --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- redis_data_prod:/data
|
||||||
|
networks:
|
||||||
|
- xpeditis_internal_prod
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1.5G
|
||||||
|
reservations:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
# Backend API (NestJS) - Instance 1
|
||||||
|
backend-prod-1:
|
||||||
|
image: ${DOCKER_REGISTRY:-docker.io}/${BACKEND_IMAGE:-xpeditis/backend}:${BACKEND_TAG:-latest}
|
||||||
|
container_name: xpeditis-backend-prod-1
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
postgres-prod:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-prod:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
# Application
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 4000
|
||||||
|
INSTANCE_ID: backend-prod-1
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_HOST: postgres-prod
|
||||||
|
DATABASE_PORT: 5432
|
||||||
|
DATABASE_NAME: ${POSTGRES_DB:-xpeditis_prod}
|
||||||
|
DATABASE_USER: ${POSTGRES_USER:-xpeditis}
|
||||||
|
DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?error}
|
||||||
|
DATABASE_SYNC: "false"
|
||||||
|
DATABASE_LOGGING: "false"
|
||||||
|
DATABASE_POOL_MIN: 10
|
||||||
|
DATABASE_POOL_MAX: 50
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST: redis-prod
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?error}
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET: ${JWT_SECRET:?error}
|
||||||
|
JWT_ACCESS_EXPIRATION: 15m
|
||||||
|
JWT_REFRESH_EXPIRATION: 7d
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGIN: https://xpeditis.com,https://www.xpeditis.com
|
||||||
|
|
||||||
|
# Sentry (Monitoring)
|
||||||
|
SENTRY_DSN: ${SENTRY_DSN:?error}
|
||||||
|
SENTRY_ENVIRONMENT: production
|
||||||
|
SENTRY_TRACES_SAMPLE_RATE: 0.1
|
||||||
|
SENTRY_PROFILES_SAMPLE_RATE: 0.05
|
||||||
|
|
||||||
|
# AWS S3
|
||||||
|
AWS_REGION: ${AWS_REGION:-eu-west-3}
|
||||||
|
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:?error}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:?error}
|
||||||
|
S3_BUCKET_DOCUMENTS: ${S3_BUCKET_DOCUMENTS:-xpeditis-prod-documents}
|
||||||
|
S3_BUCKET_UPLOADS: ${S3_BUCKET_UPLOADS:-xpeditis-prod-uploads}
|
||||||
|
|
||||||
|
# Email (AWS SES)
|
||||||
|
EMAIL_SERVICE: ses
|
||||||
|
EMAIL_FROM: ${EMAIL_FROM:-noreply@xpeditis.com}
|
||||||
|
EMAIL_FROM_NAME: Xpeditis
|
||||||
|
AWS_SES_REGION: ${AWS_SES_REGION:-eu-west-1}
|
||||||
|
|
||||||
|
# Carrier APIs (Production)
|
||||||
|
MAERSK_API_URL: ${MAERSK_API_URL:-https://api.maersk.com}
|
||||||
|
MAERSK_API_KEY: ${MAERSK_API_KEY:?error}
|
||||||
|
MSC_API_URL: ${MSC_API_URL:-}
|
||||||
|
MSC_API_KEY: ${MSC_API_KEY:-}
|
||||||
|
CMA_CGM_API_URL: ${CMA_CGM_API_URL:-}
|
||||||
|
CMA_CGM_API_KEY: ${CMA_CGM_API_KEY:-}
|
||||||
|
|
||||||
|
# Security
|
||||||
|
RATE_LIMIT_GLOBAL: 100
|
||||||
|
RATE_LIMIT_AUTH: 5
|
||||||
|
RATE_LIMIT_SEARCH: 30
|
||||||
|
RATE_LIMIT_BOOKING: 20
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- backend_logs_prod:/app/logs
|
||||||
|
networks:
|
||||||
|
- xpeditis_internal_prod
|
||||||
|
- traefik_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=traefik_network"
|
||||||
|
|
||||||
|
# HTTPS Route
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod.rule=Host(`api.xpeditis.com`)"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod.tls=true"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod.priority=200"
|
||||||
|
- "traefik.http.services.xpeditis-backend-prod.loadbalancer.server.port=4000"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod.middlewares=xpeditis-backend-prod-headers,xpeditis-backend-prod-security,xpeditis-backend-prod-ratelimit"
|
||||||
|
|
||||||
|
# HTTP → HTTPS Redirect
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod-http.rule=Host(`api.xpeditis.com`)"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod-http.entrypoints=web"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod-http.priority=200"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod-http.middlewares=xpeditis-backend-prod-redirect"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod-http.service=xpeditis-backend-prod"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-redirect.redirectscheme.scheme=https"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-redirect.redirectscheme.permanent=true"
|
||||||
|
|
||||||
|
# Middleware Headers
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-headers.headers.customRequestHeaders.X-Forwarded-For="
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-headers.headers.customRequestHeaders.X-Real-IP="
|
||||||
|
|
||||||
|
# Security Headers (Strict Production)
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-security.headers.frameDeny=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-security.headers.contentTypeNosniff=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-security.headers.browserXssFilter=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-security.headers.stsSeconds=63072000"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-security.headers.stsIncludeSubdomains=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-security.headers.stsPreload=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-security.headers.forceSTSHeader=true"
|
||||||
|
|
||||||
|
# Rate Limiting (Stricter in Production)
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-ratelimit.ratelimit.average=50"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-ratelimit.ratelimit.burst=100"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-prod-ratelimit.ratelimit.period=1m"
|
||||||
|
|
||||||
|
# Health Check
|
||||||
|
- "traefik.http.services.xpeditis-backend-prod.loadbalancer.healthcheck.path=/health"
|
||||||
|
- "traefik.http.services.xpeditis-backend-prod.loadbalancer.healthcheck.interval=30s"
|
||||||
|
- "traefik.http.services.xpeditis-backend-prod.loadbalancer.healthcheck.timeout=5s"
|
||||||
|
|
||||||
|
# Load Balancing (Sticky Sessions)
|
||||||
|
- "traefik.http.services.xpeditis-backend-prod.loadbalancer.sticky.cookie=true"
|
||||||
|
- "traefik.http.services.xpeditis-backend-prod.loadbalancer.sticky.cookie.name=xpeditis_backend_route"
|
||||||
|
- "traefik.http.services.xpeditis-backend-prod.loadbalancer.sticky.cookie.secure=true"
|
||||||
|
- "traefik.http.services.xpeditis-backend-prod.loadbalancer.sticky.cookie.httpOnly=true"
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
# Backend API (NestJS) - Instance 2 (High Availability)
|
||||||
|
backend-prod-2:
|
||||||
|
image: ${DOCKER_REGISTRY:-docker.io}/${BACKEND_IMAGE:-xpeditis/backend}:${BACKEND_TAG:-latest}
|
||||||
|
container_name: xpeditis-backend-prod-2
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
postgres-prod:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-prod:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
# Application
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 4000
|
||||||
|
INSTANCE_ID: backend-prod-2
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_HOST: postgres-prod
|
||||||
|
DATABASE_PORT: 5432
|
||||||
|
DATABASE_NAME: ${POSTGRES_DB:-xpeditis_prod}
|
||||||
|
DATABASE_USER: ${POSTGRES_USER:-xpeditis}
|
||||||
|
DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?error}
|
||||||
|
DATABASE_SYNC: "false"
|
||||||
|
DATABASE_LOGGING: "false"
|
||||||
|
DATABASE_POOL_MIN: 10
|
||||||
|
DATABASE_POOL_MAX: 50
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST: redis-prod
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?error}
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET: ${JWT_SECRET:?error}
|
||||||
|
JWT_ACCESS_EXPIRATION: 15m
|
||||||
|
JWT_REFRESH_EXPIRATION: 7d
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGIN: https://xpeditis.com,https://www.xpeditis.com
|
||||||
|
|
||||||
|
# Sentry (Monitoring)
|
||||||
|
SENTRY_DSN: ${SENTRY_DSN:?error}
|
||||||
|
SENTRY_ENVIRONMENT: production
|
||||||
|
SENTRY_TRACES_SAMPLE_RATE: 0.1
|
||||||
|
SENTRY_PROFILES_SAMPLE_RATE: 0.05
|
||||||
|
|
||||||
|
# AWS S3
|
||||||
|
AWS_REGION: ${AWS_REGION:-eu-west-3}
|
||||||
|
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:?error}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:?error}
|
||||||
|
S3_BUCKET_DOCUMENTS: ${S3_BUCKET_DOCUMENTS:-xpeditis-prod-documents}
|
||||||
|
S3_BUCKET_UPLOADS: ${S3_BUCKET_UPLOADS:-xpeditis-prod-uploads}
|
||||||
|
|
||||||
|
# Email (AWS SES)
|
||||||
|
EMAIL_SERVICE: ses
|
||||||
|
EMAIL_FROM: ${EMAIL_FROM:-noreply@xpeditis.com}
|
||||||
|
EMAIL_FROM_NAME: Xpeditis
|
||||||
|
AWS_SES_REGION: ${AWS_SES_REGION:-eu-west-1}
|
||||||
|
|
||||||
|
# Carrier APIs (Production)
|
||||||
|
MAERSK_API_URL: ${MAERSK_API_URL:-https://api.maersk.com}
|
||||||
|
MAERSK_API_KEY: ${MAERSK_API_KEY:?error}
|
||||||
|
MSC_API_URL: ${MSC_API_URL:-}
|
||||||
|
MSC_API_KEY: ${MSC_API_KEY:-}
|
||||||
|
CMA_CGM_API_URL: ${CMA_CGM_API_URL:-}
|
||||||
|
CMA_CGM_API_KEY: ${CMA_CGM_API_KEY:-}
|
||||||
|
|
||||||
|
# Security
|
||||||
|
RATE_LIMIT_GLOBAL: 100
|
||||||
|
RATE_LIMIT_AUTH: 5
|
||||||
|
RATE_LIMIT_SEARCH: 30
|
||||||
|
RATE_LIMIT_BOOKING: 20
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- backend_logs_prod:/app/logs
|
||||||
|
networks:
|
||||||
|
- xpeditis_internal_prod
|
||||||
|
- traefik_network
|
||||||
|
labels:
|
||||||
|
# Same Traefik labels as backend-prod-1 (load balanced)
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=traefik_network"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-prod.rule=Host(`api.xpeditis.com`)"
|
||||||
|
- "traefik.http.services.xpeditis-backend-prod.loadbalancer.server.port=4000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
# Frontend (Next.js) - Instance 1
|
||||||
|
frontend-prod-1:
|
||||||
|
image: ${DOCKER_REGISTRY:-docker.io}/${FRONTEND_IMAGE:-xpeditis/frontend}:${FRONTEND_TAG:-latest}
|
||||||
|
container_name: xpeditis-frontend-prod-1
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- backend-prod-1
|
||||||
|
- backend-prod-2
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
NEXT_PUBLIC_API_URL: https://api.xpeditis.com
|
||||||
|
NEXT_PUBLIC_APP_URL: https://xpeditis.com
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN:?error}
|
||||||
|
NEXT_PUBLIC_SENTRY_ENVIRONMENT: production
|
||||||
|
NEXT_PUBLIC_GA_MEASUREMENT_ID: ${NEXT_PUBLIC_GA_MEASUREMENT_ID:?error}
|
||||||
|
|
||||||
|
# Backend API for SSR (internal load balanced)
|
||||||
|
API_URL: http://backend-prod-1:4000
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- xpeditis_internal_prod
|
||||||
|
- traefik_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=traefik_network"
|
||||||
|
|
||||||
|
# HTTPS Route
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod.rule=Host(`xpeditis.com`) || Host(`www.xpeditis.com`)"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod.tls=true"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod.priority=200"
|
||||||
|
- "traefik.http.services.xpeditis-frontend-prod.loadbalancer.server.port=3000"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod.middlewares=xpeditis-frontend-prod-headers,xpeditis-frontend-prod-security,xpeditis-frontend-prod-compress,xpeditis-frontend-prod-www-redirect"
|
||||||
|
|
||||||
|
# HTTP → HTTPS Redirect
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod-http.rule=Host(`xpeditis.com`) || Host(`www.xpeditis.com`)"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod-http.entrypoints=web"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod-http.priority=200"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod-http.middlewares=xpeditis-frontend-prod-redirect"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod-http.service=xpeditis-frontend-prod"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-redirect.redirectscheme.scheme=https"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-redirect.redirectscheme.permanent=true"
|
||||||
|
|
||||||
|
# WWW → non-WWW Redirect
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-www-redirect.redirectregex.regex=^https://www\\.(.+)"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-www-redirect.redirectregex.replacement=https://$${1}"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-www-redirect.redirectregex.permanent=true"
|
||||||
|
|
||||||
|
# Middleware Headers
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-headers.headers.customRequestHeaders.X-Forwarded-For="
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-headers.headers.customRequestHeaders.X-Real-IP="
|
||||||
|
|
||||||
|
# Security Headers (Strict Production)
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.frameDeny=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.contentTypeNosniff=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.browserXssFilter=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.stsSeconds=63072000"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.stsIncludeSubdomains=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.stsPreload=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.forceSTSHeader=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.contentSecurityPolicy=default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.xpeditis.com;"
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-prod-compress.compress=true"
|
||||||
|
|
||||||
|
# Health Check
|
||||||
|
- "traefik.http.services.xpeditis-frontend-prod.loadbalancer.healthcheck.path=/api/health"
|
||||||
|
- "traefik.http.services.xpeditis-frontend-prod.loadbalancer.healthcheck.interval=30s"
|
||||||
|
- "traefik.http.services.xpeditis-frontend-prod.loadbalancer.healthcheck.timeout=5s"
|
||||||
|
|
||||||
|
# Load Balancing (Sticky Sessions)
|
||||||
|
- "traefik.http.services.xpeditis-frontend-prod.loadbalancer.sticky.cookie=true"
|
||||||
|
- "traefik.http.services.xpeditis-frontend-prod.loadbalancer.sticky.cookie.name=xpeditis_frontend_route"
|
||||||
|
- "traefik.http.services.xpeditis-frontend-prod.loadbalancer.sticky.cookie.secure=true"
|
||||||
|
- "traefik.http.services.xpeditis-frontend-prod.loadbalancer.sticky.cookie.httpOnly=true"
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
# Frontend (Next.js) - Instance 2 (High Availability)
|
||||||
|
frontend-prod-2:
|
||||||
|
image: ${DOCKER_REGISTRY:-docker.io}/${FRONTEND_IMAGE:-xpeditis/frontend}:${FRONTEND_TAG:-latest}
|
||||||
|
container_name: xpeditis-frontend-prod-2
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- backend-prod-1
|
||||||
|
- backend-prod-2
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
NEXT_PUBLIC_API_URL: https://api.xpeditis.com
|
||||||
|
NEXT_PUBLIC_APP_URL: https://xpeditis.com
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN:?error}
|
||||||
|
NEXT_PUBLIC_SENTRY_ENVIRONMENT: production
|
||||||
|
NEXT_PUBLIC_GA_MEASUREMENT_ID: ${NEXT_PUBLIC_GA_MEASUREMENT_ID:?error}
|
||||||
|
|
||||||
|
# Backend API for SSR (internal load balanced)
|
||||||
|
API_URL: http://backend-prod-2:4000
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- xpeditis_internal_prod
|
||||||
|
- traefik_network
|
||||||
|
labels:
|
||||||
|
# Same Traefik labels as frontend-prod-1 (load balanced)
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=traefik_network"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-prod.rule=Host(`xpeditis.com`) || Host(`www.xpeditis.com`)"
|
||||||
|
- "traefik.http.services.xpeditis-frontend-prod.loadbalancer.server.port=3000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
networks:
|
||||||
|
xpeditis_internal_prod:
|
||||||
|
driver: bridge
|
||||||
|
name: xpeditis_internal_prod
|
||||||
|
traefik_network:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data_prod:
|
||||||
|
name: xpeditis_postgres_data_prod
|
||||||
|
postgres_backups_prod:
|
||||||
|
name: xpeditis_postgres_backups_prod
|
||||||
|
redis_data_prod:
|
||||||
|
name: xpeditis_redis_data_prod
|
||||||
|
backend_logs_prod:
|
||||||
|
name: xpeditis_backend_logs_prod
|
||||||
253
docker/portainer-stack-staging.yml
Normal file
253
docker/portainer-stack-staging.yml
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
# Xpeditis - Stack STAGING/PREPROD
|
||||||
|
# Portainer Stack avec Traefik reverse proxy
|
||||||
|
# Domaines: staging.xpeditis.com (frontend) | api-staging.xpeditis.com (backend)
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL Database
|
||||||
|
postgres-staging:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: xpeditis-postgres-staging
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-xpeditis_staging}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-xpeditis}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?error}
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
volumes:
|
||||||
|
- postgres_data_staging:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- xpeditis_internal_staging
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-xpeditis}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Redis Cache
|
||||||
|
redis-staging:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: xpeditis-redis-staging
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --requirepass ${REDIS_PASSWORD:?error} --maxmemory 512mb --maxmemory-policy allkeys-lru
|
||||||
|
volumes:
|
||||||
|
- redis_data_staging:/data
|
||||||
|
networks:
|
||||||
|
- xpeditis_internal_staging
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Backend API (NestJS)
|
||||||
|
backend-staging:
|
||||||
|
image: ${DOCKER_REGISTRY:-docker.io}/${BACKEND_IMAGE:-xpeditis/backend}:${BACKEND_TAG:-staging-latest}
|
||||||
|
container_name: xpeditis-backend-staging
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres-staging:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-staging:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
# Application
|
||||||
|
NODE_ENV: staging
|
||||||
|
PORT: 4000
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_HOST: postgres-staging
|
||||||
|
DATABASE_PORT: 5432
|
||||||
|
DATABASE_NAME: ${POSTGRES_DB:-xpeditis_staging}
|
||||||
|
DATABASE_USER: ${POSTGRES_USER:-xpeditis}
|
||||||
|
DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?error}
|
||||||
|
DATABASE_SYNC: "false"
|
||||||
|
DATABASE_LOGGING: "true"
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST: redis-staging
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?error}
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET: ${JWT_SECRET:?error}
|
||||||
|
JWT_ACCESS_EXPIRATION: 15m
|
||||||
|
JWT_REFRESH_EXPIRATION: 7d
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGIN: https://staging.xpeditis.com,http://localhost:3000
|
||||||
|
|
||||||
|
# Sentry (Monitoring)
|
||||||
|
SENTRY_DSN: ${SENTRY_DSN:-}
|
||||||
|
SENTRY_ENVIRONMENT: staging
|
||||||
|
SENTRY_TRACES_SAMPLE_RATE: 0.1
|
||||||
|
SENTRY_PROFILES_SAMPLE_RATE: 0.05
|
||||||
|
|
||||||
|
# AWS S3 (or MinIO)
|
||||||
|
AWS_REGION: ${AWS_REGION:-eu-west-3}
|
||||||
|
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:?error}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:?error}
|
||||||
|
S3_BUCKET_DOCUMENTS: ${S3_BUCKET_DOCUMENTS:-xpeditis-staging-documents}
|
||||||
|
S3_BUCKET_UPLOADS: ${S3_BUCKET_UPLOADS:-xpeditis-staging-uploads}
|
||||||
|
|
||||||
|
# Email (AWS SES or SMTP)
|
||||||
|
EMAIL_SERVICE: ${EMAIL_SERVICE:-ses}
|
||||||
|
EMAIL_FROM: ${EMAIL_FROM:-noreply@staging.xpeditis.com}
|
||||||
|
EMAIL_FROM_NAME: Xpeditis Staging
|
||||||
|
AWS_SES_REGION: ${AWS_SES_REGION:-eu-west-1}
|
||||||
|
|
||||||
|
# Carrier APIs (Sandbox)
|
||||||
|
MAERSK_API_URL: ${MAERSK_API_URL_SANDBOX:-https://sandbox.api.maersk.com}
|
||||||
|
MAERSK_API_KEY: ${MAERSK_API_KEY_SANDBOX:-}
|
||||||
|
MSC_API_URL: ${MSC_API_URL_SANDBOX:-}
|
||||||
|
MSC_API_KEY: ${MSC_API_KEY_SANDBOX:-}
|
||||||
|
|
||||||
|
# Security
|
||||||
|
RATE_LIMIT_GLOBAL: 200
|
||||||
|
RATE_LIMIT_AUTH: 10
|
||||||
|
RATE_LIMIT_SEARCH: 50
|
||||||
|
RATE_LIMIT_BOOKING: 30
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- backend_logs_staging:/app/logs
|
||||||
|
networks:
|
||||||
|
- xpeditis_internal_staging
|
||||||
|
- traefik_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=traefik_network"
|
||||||
|
|
||||||
|
# HTTPS Route
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging.rule=Host(`api-staging.xpeditis.com`)"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging.tls=true"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging.priority=100"
|
||||||
|
- "traefik.http.services.xpeditis-backend-staging.loadbalancer.server.port=4000"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging.middlewares=xpeditis-backend-staging-headers,xpeditis-backend-staging-security"
|
||||||
|
|
||||||
|
# HTTP → HTTPS Redirect
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging-http.rule=Host(`api-staging.xpeditis.com`)"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging-http.entrypoints=web"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging-http.priority=100"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging-http.middlewares=xpeditis-backend-staging-redirect"
|
||||||
|
- "traefik.http.routers.xpeditis-backend-staging-http.service=xpeditis-backend-staging"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-redirect.redirectscheme.scheme=https"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-redirect.redirectscheme.permanent=true"
|
||||||
|
|
||||||
|
# Middleware Headers
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-headers.headers.customRequestHeaders.X-Forwarded-For="
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-headers.headers.customRequestHeaders.X-Real-IP="
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-security.headers.frameDeny=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-security.headers.contentTypeNosniff=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-security.headers.browserXssFilter=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-security.headers.stsSeconds=31536000"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-security.headers.stsIncludeSubdomains=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-security.headers.stsPreload=true"
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-ratelimit.ratelimit.average=100"
|
||||||
|
- "traefik.http.middlewares.xpeditis-backend-staging-ratelimit.ratelimit.burst=200"
|
||||||
|
|
||||||
|
# Health Check
|
||||||
|
- "traefik.http.services.xpeditis-backend-staging.loadbalancer.healthcheck.path=/health"
|
||||||
|
- "traefik.http.services.xpeditis-backend-staging.loadbalancer.healthcheck.interval=30s"
|
||||||
|
- "traefik.http.services.xpeditis-backend-staging.loadbalancer.healthcheck.timeout=5s"
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Frontend (Next.js)
|
||||||
|
frontend-staging:
|
||||||
|
image: ${DOCKER_REGISTRY:-docker.io}/${FRONTEND_IMAGE:-xpeditis/frontend}:${FRONTEND_TAG:-staging-latest}
|
||||||
|
container_name: xpeditis-frontend-staging
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- backend-staging
|
||||||
|
environment:
|
||||||
|
NODE_ENV: staging
|
||||||
|
NEXT_PUBLIC_API_URL: https://api-staging.xpeditis.com
|
||||||
|
NEXT_PUBLIC_APP_URL: https://staging.xpeditis.com
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN:-}
|
||||||
|
NEXT_PUBLIC_SENTRY_ENVIRONMENT: staging
|
||||||
|
NEXT_PUBLIC_GA_MEASUREMENT_ID: ${NEXT_PUBLIC_GA_MEASUREMENT_ID:-}
|
||||||
|
|
||||||
|
# Backend API for SSR (internal)
|
||||||
|
API_URL: http://backend-staging:4000
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- xpeditis_internal_staging
|
||||||
|
- traefik_network
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=traefik_network"
|
||||||
|
|
||||||
|
# HTTPS Route
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging.rule=Host(`staging.xpeditis.com`)"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging.tls=true"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging.priority=100"
|
||||||
|
- "traefik.http.services.xpeditis-frontend-staging.loadbalancer.server.port=3000"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging.middlewares=xpeditis-frontend-staging-headers,xpeditis-frontend-staging-security,xpeditis-frontend-staging-compress"
|
||||||
|
|
||||||
|
# HTTP → HTTPS Redirect
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging-http.rule=Host(`staging.xpeditis.com`)"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging-http.entrypoints=web"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging-http.priority=100"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging-http.middlewares=xpeditis-frontend-staging-redirect"
|
||||||
|
- "traefik.http.routers.xpeditis-frontend-staging-http.service=xpeditis-frontend-staging"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-redirect.redirectscheme.scheme=https"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-redirect.redirectscheme.permanent=true"
|
||||||
|
|
||||||
|
# Middleware Headers
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-headers.headers.customRequestHeaders.X-Forwarded-For="
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-headers.headers.customRequestHeaders.X-Real-IP="
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.frameDeny=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.contentTypeNosniff=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.browserXssFilter=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.stsSeconds=31536000"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.stsIncludeSubdomains=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.stsPreload=true"
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.customResponseHeaders.X-Robots-Tag=noindex,nofollow"
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
- "traefik.http.middlewares.xpeditis-frontend-staging-compress.compress=true"
|
||||||
|
|
||||||
|
# Health Check
|
||||||
|
- "traefik.http.services.xpeditis-frontend-staging.loadbalancer.healthcheck.path=/api/health"
|
||||||
|
- "traefik.http.services.xpeditis-frontend-staging.loadbalancer.healthcheck.interval=30s"
|
||||||
|
- "traefik.http.services.xpeditis-frontend-staging.loadbalancer.healthcheck.timeout=5s"
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
xpeditis_internal_staging:
|
||||||
|
driver: bridge
|
||||||
|
name: xpeditis_internal_staging
|
||||||
|
traefik_network:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data_staging:
|
||||||
|
name: xpeditis_postgres_data_staging
|
||||||
|
redis_data_staging:
|
||||||
|
name: xpeditis_redis_data_staging
|
||||||
|
backend_logs_staging:
|
||||||
|
name: xpeditis_backend_logs_staging
|
||||||
Loading…
Reference in New Issue
Block a user