diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..df0b572 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,134 @@ +version: 2 + +updates: + # ── Node.js / pnpm dependencies ────────────────────────────────── + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "Asia/Ho_Chi_Minh" + open-pull-requests-limit: 10 + reviewers: + - "goodgo/platform-team" + labels: + - "dependencies" + - "security" + # Group minor/patch updates to reduce PR noise + groups: + dev-dependencies: + patterns: + - "@types/*" + - "eslint*" + - "prettier*" + - "typescript*" + - "vitest*" + - "@playwright/*" + - "husky" + - "lint-staged" + - "tsx" + - "turbo" + update-types: + - "minor" + - "patch" + nestjs: + patterns: + - "@nestjs/*" + update-types: + - "minor" + - "patch" + prisma: + patterns: + - "prisma" + - "@prisma/*" + update-types: + - "minor" + - "patch" + # Security updates always get individual PRs (not grouped) + commit-message: + prefix: "deps" + include: "scope" + + # ── Python dependencies (AI services) ──────────────────────────── + - package-ecosystem: "pip" + directory: "/libs/ai-services" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "Asia/Ho_Chi_Minh" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "security" + - "ai-services" + commit-message: + prefix: "deps(ai)" + include: "scope" + + # ── GitHub Actions ─────────────────────────────────────────────── + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "Asia/Ho_Chi_Minh" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "ci" + groups: + github-actions: + patterns: + - "*" + update-types: + - "minor" + - "patch" + commit-message: + prefix: "ci" + include: "scope" + + # ── Docker base images ────────────────────────────────────────── + - package-ecosystem: "docker" + directory: "/apps/api" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "Asia/Ho_Chi_Minh" + open-pull-requests-limit: 3 + labels: + - "dependencies" + - "docker" + commit-message: + prefix: "docker(api)" + + - package-ecosystem: "docker" + directory: "/apps/web" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "Asia/Ho_Chi_Minh" + open-pull-requests-limit: 3 + labels: + - "dependencies" + - "docker" + commit-message: + prefix: "docker(web)" + + - package-ecosystem: "docker" + directory: "/libs/ai-services" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "Asia/Ho_Chi_Minh" + open-pull-requests-limit: 3 + labels: + - "dependencies" + - "docker" + commit-message: + prefix: "docker(ai)" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..3461871 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,61 @@ +name: CodeQL Analysis + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Run weekly on Monday at 06:17 UTC — off-peak to avoid :00/:30 congestion + - cron: "17 6 * * 1" + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: CodeQL (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + language: [javascript-typescript] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # Use extended security queries for deeper analysis + queries: security-extended,security-and-quality + config: | + paths: + - apps/ + - libs/ + paths-ignore: + - node_modules/ + - "**/dist/" + - "**/*.spec.ts" + - "**/*.test.ts" + - "**/__tests__/" + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" + # SARIF results are automatically uploaded to GitHub Security tab + upload: always diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..9dea7d7 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,312 @@ +name: Security Scanning + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Run daily at 05:43 UTC — catch new CVEs early + - cron: "43 5 * * *" + +concurrency: + group: security-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + security-events: write + +jobs: + # ── Dependency Audit ───────────────────────────────────────────── + dependency-audit: + name: Dependency Audit (pnpm) + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run pnpm audit + run: | + echo "## Dependency Audit Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Run audit, capture output and exit code + set +e + AUDIT_OUTPUT=$(pnpm audit --json 2>&1) + AUDIT_EXIT=$? + set -e + + # Parse and display summary + echo "$AUDIT_OUTPUT" | jq -r ' + if .metadata then + "| Severity | Count |\n|----------|-------|\n" + + "| Critical | \(.metadata.vulnerabilities.critical // 0) |\n" + + "| High | \(.metadata.vulnerabilities.high // 0) |\n" + + "| Moderate | \(.metadata.vulnerabilities.moderate // 0) |\n" + + "| Low | \(.metadata.vulnerabilities.low // 0) |\n" + + "| Info | \(.metadata.vulnerabilities.info // 0) |" + else + "No vulnerabilities found ✅" + end + ' >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "Audit completed" >> $GITHUB_STEP_SUMMARY + + # Also run human-readable output + pnpm audit 2>&1 || true + + # Fail on critical or high vulnerabilities only + pnpm audit --audit-level=critical + continue-on-error: false + + # ── Container Scanning (Trivy) ─────────────────────────────────── + trivy-api: + name: Trivy Scan — API Image + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build API image for scanning + uses: docker/build-push-action@v6 + with: + context: . + file: apps/api/Dockerfile + push: false + load: true + tags: goodgo-api:scan + cache-from: type=gha,scope=api-scan + cache-to: type=gha,mode=max,scope=api-scan + + - name: Run Trivy vulnerability scanner (API) + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: "goodgo-api:scan" + format: "sarif" + output: "trivy-api-results.sarif" + severity: "CRITICAL,HIGH" + # Ignore unfixed vulns to reduce noise + ignore-unfixed: true + + - name: Upload Trivy SARIF (API) + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: "trivy-api-results.sarif" + category: "trivy-api" + + - name: Trivy table output (API) + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: "goodgo-api:scan" + format: "table" + severity: "CRITICAL,HIGH,MEDIUM" + ignore-unfixed: true + + trivy-web: + name: Trivy Scan — Web Image + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Web image for scanning + uses: docker/build-push-action@v6 + with: + context: . + file: apps/web/Dockerfile + push: false + load: true + tags: goodgo-web:scan + cache-from: type=gha,scope=web-scan + cache-to: type=gha,mode=max,scope=web-scan + + - name: Run Trivy vulnerability scanner (Web) + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: "goodgo-web:scan" + format: "sarif" + output: "trivy-web-results.sarif" + severity: "CRITICAL,HIGH" + ignore-unfixed: true + + - name: Upload Trivy SARIF (Web) + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: "trivy-web-results.sarif" + category: "trivy-web" + + - name: Trivy table output (Web) + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: "goodgo-web:scan" + format: "table" + severity: "CRITICAL,HIGH,MEDIUM" + ignore-unfixed: true + + trivy-ai: + name: Trivy Scan — AI Services Image + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build AI Services image for scanning + uses: docker/build-push-action@v6 + with: + context: ./libs/ai-services + file: libs/ai-services/Dockerfile + push: false + load: true + tags: goodgo-ai:scan + cache-from: type=gha,scope=ai-scan + cache-to: type=gha,mode=max,scope=ai-scan + + - name: Run Trivy vulnerability scanner (AI) + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: "goodgo-ai:scan" + format: "sarif" + output: "trivy-ai-results.sarif" + severity: "CRITICAL,HIGH" + ignore-unfixed: true + + - name: Upload Trivy SARIF (AI) + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: "trivy-ai-results.sarif" + category: "trivy-ai" + + - name: Trivy table output (AI) + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: "goodgo-ai:scan" + format: "table" + severity: "CRITICAL,HIGH,MEDIUM" + ignore-unfixed: true + + # ── Filesystem / IaC Scanning ──────────────────────────────────── + trivy-fs: + name: Trivy Filesystem Scan + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Trivy filesystem scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: "fs" + scan-ref: "." + format: "sarif" + output: "trivy-fs-results.sarif" + severity: "CRITICAL,HIGH" + ignore-unfixed: true + scanners: "vuln,secret,misconfig" + + - name: Upload Trivy SARIF (filesystem) + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: "trivy-fs-results.sarif" + category: "trivy-filesystem" + + - name: Trivy filesystem table output + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: "fs" + scan-ref: "." + format: "table" + severity: "CRITICAL,HIGH,MEDIUM" + scanners: "vuln,secret,misconfig" + + # ── Summary Gate ───────────────────────────────────────────────── + security-gate: + name: Security Gate + runs-on: ubuntu-latest + needs: [dependency-audit, trivy-api, trivy-web, trivy-ai, trivy-fs] + if: always() + + steps: + - name: Check security scan results + run: | + echo "## Security Scan Summary" >> $GITHUB_STEP_SUMMARY + + # Check each job result + FAILED=false + + if [ "${{ needs.dependency-audit.result }}" != "success" ]; then + echo "❌ Dependency audit: ${{ needs.dependency-audit.result }}" >> $GITHUB_STEP_SUMMARY + FAILED=true + else + echo "✅ Dependency audit: passed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.trivy-api.result }}" != "success" ]; then + echo "❌ Trivy API scan: ${{ needs.trivy-api.result }}" >> $GITHUB_STEP_SUMMARY + FAILED=true + else + echo "✅ Trivy API scan: passed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.trivy-web.result }}" != "success" ]; then + echo "❌ Trivy Web scan: ${{ needs.trivy-web.result }}" >> $GITHUB_STEP_SUMMARY + FAILED=true + else + echo "✅ Trivy Web scan: passed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.trivy-ai.result }}" != "success" ]; then + echo "❌ Trivy AI scan: ${{ needs.trivy-ai.result }}" >> $GITHUB_STEP_SUMMARY + FAILED=true + else + echo "✅ Trivy AI scan: passed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.trivy-fs.result }}" != "success" ]; then + echo "❌ Trivy filesystem scan: ${{ needs.trivy-fs.result }}" >> $GITHUB_STEP_SUMMARY + FAILED=true + else + echo "✅ Trivy filesystem scan: passed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$FAILED" = true ]; then + echo "" + echo "⚠️ One or more security scans failed. Review the Security tab for details." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "" + echo "🎉 All security scans passed!" >> $GITHUB_STEP_SUMMARY diff --git a/apps/web/i18n/config.ts b/apps/web/i18n/config.ts new file mode 100644 index 0000000..b5be55c --- /dev/null +++ b/apps/web/i18n/config.ts @@ -0,0 +1,3 @@ +export const locales = ['vi', 'en'] as const; +export type Locale = (typeof locales)[number]; +export const defaultLocale: Locale = 'vi'; diff --git a/apps/web/i18n/navigation.ts b/apps/web/i18n/navigation.ts new file mode 100644 index 0000000..a10a415 --- /dev/null +++ b/apps/web/i18n/navigation.ts @@ -0,0 +1,4 @@ +import { createNavigation } from 'next-intl/navigation'; +import { routing } from './routing'; + +export const { Link, redirect, usePathname, useRouter } = createNavigation(routing); diff --git a/apps/web/i18n/request.ts b/apps/web/i18n/request.ts new file mode 100644 index 0000000..d14d7cd --- /dev/null +++ b/apps/web/i18n/request.ts @@ -0,0 +1,16 @@ +import { getRequestConfig } from 'next-intl/server'; +import { routing } from './routing'; + +export default getRequestConfig(async ({ requestLocale }) => { + let locale = await requestLocale; + + // Ensure a valid locale is used + if (!locale || !routing.locales.includes(locale as (typeof routing.locales)[number])) { + locale = routing.defaultLocale; + } + + return { + locale, + messages: (await import(`../messages/${locale}.json`)).default, + }; +}); diff --git a/apps/web/i18n/routing.ts b/apps/web/i18n/routing.ts new file mode 100644 index 0000000..8cb0905 --- /dev/null +++ b/apps/web/i18n/routing.ts @@ -0,0 +1,8 @@ +import { defineRouting } from 'next-intl/routing'; +import { locales, defaultLocale } from './config'; + +export const routing = defineRouting({ + locales, + defaultLocale, + localePrefix: 'as-needed', +}); diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json new file mode 100644 index 0000000..1bfb1b3 --- /dev/null +++ b/apps/web/messages/en.json @@ -0,0 +1,110 @@ +{ + "metadata": { + "title": "GoodGo — Vietnam Real Estate Platform", + "description": "GoodGo — smart real estate platform in Vietnam. Buy, sell, and rent properties easily with over 10,000+ listings nationwide.", + "ogTitle": "GoodGo — Vietnam Real Estate Platform", + "ogDescription": "Buy, sell, and rent properties easily with GoodGo — Vietnam's leading smart real estate platform." + }, + "common": { + "goodgo": "GoodGo", + "loading": "Loading...", + "retry": "Retry", + "retrying": "Retrying...", + "goHome": "Go to homepage", + "search": "Search", + "login": "Login", + "register": "Register", + "logout": "Logout", + "admin": "Admin", + "dashboard": "Dashboard", + "errorCode": "Error code: {code}", + "retriedCount": "Retried {count} times", + "allRightsReserved": "© 2026 GoodGo. All rights reserved.", + "skipToContent": "Skip to main content" + }, + "nav": { + "home": "Home", + "search": "Search", + "mainNav": "Main navigation", + "dashboardNav": "Dashboard", + "adminNav": "Administration" + }, + "dashboard": { + "title": "Dashboard", + "listings": "Listings", + "createListing": "Create listing", + "analytics": "Analytics", + "aiValuation": "AI Valuation", + "profile": "Profile", + "subscription": "Subscription", + "payments": "Payments", + "darkMode": "Switch to dark mode", + "lightMode": "Switch to light mode" + }, + "adminNav": { + "dashboard": "Dashboard", + "users": "User management", + "moderation": "Content moderation", + "kyc": "KYC verification", + "closeMenu": "Close menu", + "openMenu": "Open menu" + }, + "landing": { + "heroTitle": "Find your perfect", + "heroTitleHighlight": "property", + "heroSubtitle": "Smart real estate platform in Vietnam — buy, sell, and rent properties with ease", + "searchPlaceholder": "Enter area, project, or keyword...", + "transactionTypeLabel": "Type", + "featuredTitle": "Featured listings", + "featuredSubtitle": "Explore the most popular properties", + "viewAll": "View all", + "loadError": "Unable to load listings. Please try again.", + "noFeatured": "No featured listings yet", + "districtsTitle": "Popular areas", + "districtsSubtitle": "Search by popular districts", + "statsTitle": "GoodGo in numbers", + "statsSubtitle": "Vietnam's trusted real estate platform", + "ctaTitle": "Have a property to list?", + "ctaSubtitle": "List for free today and reach thousands of potential buyers", + "registerFree": "Register for free", + "searchNow": "Search now" + }, + "stats": { + "listings": "Listings", + "users": "Users", + "transactions": "Successful transactions", + "provinces": "Provinces" + }, + "footer": { + "description": "Smart real estate platform in Vietnam", + "propertyTypes": "Property types", + "areas": "Areas", + "support": "Support" + }, + "propertyTypes": { + "APARTMENT": "Apartment", + "HOUSE": "House", + "VILLA": "Villa", + "LAND": "Land", + "OFFICE": "Office", + "SHOPHOUSE": "Shophouse" + }, + "transactionTypes": { + "SALE": "Sale", + "RENT": "Rent" + }, + "notFound": { + "title": "Page not found", + "description": "The page you are looking for does not exist or has been moved." + }, + "error": { + "title": "An error occurred", + "description": "Sorry, something went wrong. Please try again.", + "autoRetrying": "Automatically retrying..." + }, + "language": { + "label": "Language", + "vi": "Tiếng Việt", + "en": "English" + } +} diff --git a/apps/web/messages/vi.json b/apps/web/messages/vi.json new file mode 100644 index 0000000..6e1e77a --- /dev/null +++ b/apps/web/messages/vi.json @@ -0,0 +1,110 @@ +{ + "metadata": { + "title": "GoodGo — Nền tảng Bất động sản Việt Nam", + "description": "GoodGo — nền tảng bất động sản thông minh tại Việt Nam. Mua bán, cho thuê nhà đất dễ dàng với hơn 10,000+ tin đăng trên toàn quốc.", + "ogTitle": "GoodGo — Nền tảng Bất động sản Việt Nam", + "ogDescription": "Mua bán, cho thuê bất động sản dễ dàng với GoodGo — nền tảng thông minh, uy tín hàng đầu Việt Nam." + }, + "common": { + "goodgo": "GoodGo", + "loading": "Đang tải...", + "retry": "Thử lại", + "retrying": "Đang thử lại...", + "goHome": "Về trang chủ", + "search": "Tìm kiếm", + "login": "Đăng nhập", + "register": "Đăng ký", + "logout": "Đăng xuất", + "admin": "Admin", + "dashboard": "Bảng điều khiển", + "errorCode": "Mã lỗi: {code}", + "retriedCount": "Đã thử lại {count} lần", + "allRightsReserved": "© 2026 GoodGo. Tất cả quyền được bảo lưu.", + "skipToContent": "Chuyển đến nội dung chính" + }, + "nav": { + "home": "Trang chủ", + "search": "Tìm kiếm", + "mainNav": "Điều hướng chính", + "dashboardNav": "Bảng điều khiển", + "adminNav": "Quản trị" + }, + "dashboard": { + "title": "Bảng điều khiển", + "listings": "Tin đăng", + "createListing": "Đăng tin", + "analytics": "Phân tích", + "aiValuation": "Định giá AI", + "profile": "Hồ sơ", + "subscription": "Gói dịch vụ", + "payments": "Thanh toán", + "darkMode": "Chuyển sang chế độ tối", + "lightMode": "Chuyển sang chế độ sáng" + }, + "adminNav": { + "dashboard": "Dashboard", + "users": "Quản lý người dùng", + "moderation": "Kiểm duyệt tin", + "kyc": "Duyệt KYC", + "closeMenu": "Đóng menu", + "openMenu": "Mở menu" + }, + "landing": { + "heroTitle": "Tìm kiếm bất động sản", + "heroTitleHighlight": "hoàn hảo", + "heroSubtitle": "Nền tảng bất động sản thông minh tại Việt Nam — mua bán, cho thuê nhà đất dễ dàng", + "searchPlaceholder": "Nhập khu vực, dự án, hoặc từ khóa...", + "transactionTypeLabel": "Loại GD", + "featuredTitle": "Tin đăng nổi bật", + "featuredSubtitle": "Khám phá các bất động sản được quan tâm nhất", + "viewAll": "Xem tất cả", + "loadError": "Không thể tải tin đăng. Vui lòng thử lại.", + "noFeatured": "Chưa có tin đăng nổi bật", + "districtsTitle": "Khu vực nổi bật", + "districtsSubtitle": "Tìm kiếm theo quận huyện phổ biến", + "statsTitle": "GoodGo trong số liệu", + "statsSubtitle": "Nền tảng bất động sản đáng tin cậy tại Việt Nam", + "ctaTitle": "Bạn có bất động sản muốn đăng?", + "ctaSubtitle": "Đăng tin miễn phí ngay hôm nay, tiếp cận hàng ngàn người mua tiềm năng", + "registerFree": "Đăng ký miễn phí", + "searchNow": "Tìm kiếm ngay" + }, + "stats": { + "listings": "Tin đăng", + "users": "Người dùng", + "transactions": "Giao dịch thành công", + "provinces": "Tỉnh thành" + }, + "footer": { + "description": "Nền tảng bất động sản thông minh tại Việt Nam", + "propertyTypes": "Loại BĐS", + "areas": "Khu vực", + "support": "Hỗ trợ" + }, + "propertyTypes": { + "APARTMENT": "Căn hộ", + "HOUSE": "Nhà riêng", + "VILLA": "Biệt thự", + "LAND": "Đất nền", + "OFFICE": "Văn phòng", + "SHOPHOUSE": "Shophouse" + }, + "transactionTypes": { + "SALE": "Bán", + "RENT": "Cho thuê" + }, + "notFound": { + "title": "Không tìm thấy trang", + "description": "Trang bạn đang tìm không tồn tại hoặc đã được di chuyển." + }, + "error": { + "title": "Đã xảy ra lỗi", + "description": "Rất tiếc, đã có lỗi xảy ra. Vui lòng thử lại.", + "autoRetrying": "Đang tự động thử lại..." + }, + "language": { + "label": "Ngôn ngữ", + "vi": "Tiếng Việt", + "en": "English" + } +}