diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23464ae34..2c546367e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,13 +23,13 @@ jobs: steps: - uses: actions/checkout@v7 with: - fetch-depth: 0 # Full history needed for Gitleaks commit scanning + fetch-depth: 0 # Full history needed for Gitleaks commit scanning - name: Run Gitleaks secret scan uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # Optional: for org-level scanning + GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # Optional: for org-level scanning with: args: > --redact @@ -270,6 +270,20 @@ jobs: KEYCLOAK_CLIENT_ID: pos-shell-app PLATFORM_BASE_URL: https://platform.staging.54link.io PLATFORM_API_KEY: ci-smoke-platform-key + # Required by server env validation in NODE_ENV=production (CI test values). + CRON_SECRET: ci-smoke-cron-secret-32-characters-x + INTERNAL_API_KEY: ci-smoke-internal-api-key-32-chars- + TX_SIGNING_SECRET: ci-smoke-tx-signing-secret-32-chars + KEYCLOAK_CLIENT_SECRET: ci-smoke-keycloak-client-secret-32- + PLATFORM_SERVICE_TOKEN: ci-smoke-platform-service-token-32 + MINIO_SECRET_KEY: ci-smoke-minio-secret-key-32-chars- + MINIO_ACCESS_KEY: ci-smoke-minio-access-key-32-chars- + APISIX_ADMIN_KEY: ci-smoke-apisix-admin-key-32-chars- + TERMII_API_KEY: ci-smoke-termii-api-key-32-chars-xx + FLUVIO_API_KEY: ci-smoke-fluvio-api-key-32-chars-xx + MQTT_PASSWORD: ci-smoke-mqtt-password-32-chars-xxx + # Security sidecars are not deployed in CI — fail open. + SECURITY_FAIL_OPEN: "true" services: postgres: image: postgres:16-alpine @@ -306,6 +320,9 @@ jobs: run: pnpm db:push continue-on-error: true + - name: Seed smoke test data (agents/admin/supervisor fixtures) + run: node scripts/seed-e2e.mjs + - name: Start server in background run: | node dist/index.js & @@ -375,6 +392,26 @@ jobs: BASE_URL: http://localhost:3000 TEST_ADMIN_EMAIL: admin@54link.ng TEST_ADMIN_PASS: TestAdmin123! + # Required by server env validation in NODE_ENV=production (CI test values). + CRON_SECRET: ci-e2e-cron-secret-32-characters-xx + INTERNAL_API_KEY: ci-e2e-internal-api-key-32-chars-xx + TX_SIGNING_SECRET: ci-e2e-tx-signing-secret-32-chars-x + KEYCLOAK_CLIENT_SECRET: ci-e2e-keycloak-client-secret-32-ch + PLATFORM_API_KEY: ci-e2e-platform-api-key-32-chars-xx + PLATFORM_SERVICE_TOKEN: ci-e2e-platform-service-token-32-ch + MINIO_SECRET_KEY: ci-e2e-minio-secret-key-32-chars-xx + MINIO_ACCESS_KEY: ci-e2e-minio-access-key-32-chars-xx + APISIX_ADMIN_KEY: ci-e2e-apisix-admin-key-32-chars-xx + TERMII_API_KEY: ci-e2e-termii-api-key-32-chars-xxxx + FLUVIO_API_KEY: ci-e2e-fluvio-api-key-32-chars-xxxx + MQTT_PASSWORD: ci-e2e-mqtt-password-32-chars-xxxxx + # Security sidecars (Rust DDoS / Go PBAC / Python Fraud ML) are not + # deployed in CI — fail open so requests are not blocked by their absence. + SECURITY_FAIL_OPEN: "true" + # The app drives every request from a single IP under Playwright, which + # trips the in-process connection throttle and per-IP rate limiters + # (503/429). Disable them for the E2E run; production keeps them on. + DISABLE_RATE_LIMITS: "true" services: postgres: image: postgres:16-alpine @@ -414,6 +451,9 @@ jobs: run: pnpm db:push continue-on-error: true + - name: Seed E2E test data (agents/admin/supervisor fixtures) + run: node scripts/seed-e2e.mjs + - name: Start server in background run: | node dist/index.js & @@ -833,8 +873,8 @@ jobs: - name: Run OWASP ZAP Baseline Scan uses: zaproxy/action-baseline@v0.15.0 with: - target: 'http://localhost:3000' - rules_file_name: '.zap/rules.tsv' + target: "http://localhost:3000" + rules_file_name: ".zap/rules.tsv" cmd_options: > -a -j @@ -842,17 +882,17 @@ jobs: -I fail_action: true allow_issue_writing: true - artifact_name: 'zap-baseline-report' + artifact_name: "zap-baseline-report" continue-on-error: false - name: Run OWASP ZAP API Scan uses: zaproxy/action-api-scan@v0.10.0 with: - target: 'http://localhost:3000/api/trpc' + target: "http://localhost:3000/api/trpc" format: openapi - rules_file_name: '.zap/rules.tsv' - cmd_options: '-a -j -l WARN' - artifact_name: 'zap-api-report' + rules_file_name: ".zap/rules.tsv" + cmd_options: "-a -j -l WARN" + artifact_name: "zap-api-report" continue-on-error: true - name: Upload ZAP reports diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4113b0d06..85c296a5c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -170,13 +170,11 @@ jobs: with: python-version: "3.12" - - name: Install Python dependencies - run: | - pip install --upgrade pip - for req in services/python/*/requirements.txt; do - pip install -r "$req" 2>/dev/null || true - done - continue-on-error: true + # NOTE: CodeQL uses buildless extraction for Python, so installing + # dependencies is not required for analysis. Installing every service's + # requirements.txt into one environment exhausted the runner disk + # (~80 MB left) and intermittently crashed the analyze step, so it is + # omitted intentionally. - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/e2e-playwright.yml b/.github/workflows/e2e-playwright.yml index 31f49d186..9a694232d 100644 --- a/.github/workflows/e2e-playwright.yml +++ b/.github/workflows/e2e-playwright.yml @@ -67,6 +67,10 @@ jobs: VITE_FRONTEND_FORGE_API_KEY: e2e-frontend-forge-key VITE_FRONTEND_FORGE_API_URL: http://localhost:3001/api NODE_ENV: test + # The app drives every request from a single IP under Playwright, which + # trips the in-process connection throttle and per-IP rate limiters + # (503/429). Disable them for the E2E run; production keeps them on. + DISABLE_RATE_LIMITS: "true" steps: - name: Checkout repository @@ -97,6 +101,16 @@ jobs: env: POSTGRES_URL: ${{ env.POSTGRES_URL }} + # The specs authenticate as fixed fixtures (AGT001/1234, ADMIN1/0000, + # SUP001/9999). seed-comprehensive only generates randomized AGT#### rows + # with no PIN hash, so add the deterministic login fixtures the E2E flow + # logs in with. Idempotent upsert by agentCode. + - name: Seed E2E login fixtures (AGT001/ADMIN1/SUP001) + run: node scripts/seed-e2e.mjs + env: + POSTGRES_URL: ${{ env.POSTGRES_URL }} + DATABASE_URL: ${{ env.DATABASE_URL }} + - name: Build application run: pnpm build diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index bdc4a1052..96c5355a9 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -59,26 +59,10 @@ jobs: with: sarif_file: trivy-results.sarif - codeql: - name: CodeQL Analysis - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - matrix: - language: [javascript-typescript] - steps: - - uses: actions/checkout@v7 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + # CodeQL analysis is owned by the dedicated codeql.yml workflow (JS/TS, Go, + # Python). A duplicate job here uploaded a second SARIF for the same + # javascript-typescript category, causing a category collision that failed + # the "CodeQL Analysis" check. Removed to keep a single authoritative run. secret-scan: name: Secret Detection diff --git a/.gitignore b/.gitignore index e1dcbe709..45b05a229 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,7 @@ services/python/ml-pipeline/models/weights/*.json services/python/ml-pipeline/models/lakehouse/ services/python/ml-pipeline/models/registry/ /data/ + +# Playwright E2E artifacts +test-results/ +playwright-report/ diff --git a/client/src/pages/AgentLogin.tsx b/client/src/pages/AgentLogin.tsx index 3b1265962..f84860a95 100644 --- a/client/src/pages/AgentLogin.tsx +++ b/client/src/pages/AgentLogin.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useRef } from "react"; import { trpc } from "../lib/trpc"; import { usePosStore } from "../store/posStore"; import { toast } from "sonner"; @@ -17,6 +17,7 @@ type Screen = "code" | "pin" | "forgot_phone" | "forgot_newpin"; export default function AgentLogin() { const [agentCode, setAgentCode] = useState(""); + const agentCodeRef = useRef(""); const [pin, setPin] = useState(""); const [step, setStep] = useState("code"); const [loading, setLoading] = useState(false); @@ -45,7 +46,10 @@ export default function AgentLogin() { }); const handleCodeSubmit = () => { - if (agentCode.trim().length < 3) { + // Read from the ref so the latest keyed value is used even if the React + // state update from the input's onChange has not committed yet. + const code = (agentCodeRef.current || agentCode).trim(); + if (code.length < 3) { toast.error("Enter a valid agent code"); return; } @@ -63,7 +67,7 @@ export default function AgentLogin() { if (newPin.length === 4) { setLoading(true); loginMutation.mutate({ - agentCode: agentCode.trim().toUpperCase(), + agentCode: (agentCodeRef.current || agentCode).trim().toUpperCase(), pin: newPin, }); } @@ -200,7 +204,11 @@ export default function AgentLogin() { setAgentCode(e.target.value.toUpperCase())} + onChange={e => { + const v = e.target.value.toUpperCase(); + agentCodeRef.current = v; + setAgentCode(v); + }} onKeyDown={e => e.key === "Enter" && handleCodeSubmit()} placeholder="e.g. AGT001" className="w-full rounded-2xl px-4 py-4 text-white text-lg font-bold outline-none mb-4" @@ -316,6 +324,7 @@ export default function AgentLogin() { {PAD_KEYS.map((key, idx) => (