From e815b4d97628a969b5f34de0455ce8383c7d860d Mon Sep 17 00:00:00 2001 From: marshal Date: Tue, 16 Jun 2026 23:06:46 +0300 Subject: [PATCH 1/8] Refactor API base URL configuration to use centralized constant across applications --- apps/edr-freight-web/backoffice/src/auth/http.ts | 3 ++- .../backoffice/src/components/cargoes/CargoFormDialog.tsx | 3 +-- apps/edr-freight-web/backoffice/src/constants/apiConfig.ts | 1 + apps/edr-freight-web/portal/src/constants/apiConfig.ts | 1 + apps/edr-freight-web/portal/src/services/payments.service.ts | 3 ++- apps/edr-freight-web/portal/src/utils/api.ts | 3 ++- pnpm-lock.yaml | 2 +- 7 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/constants/apiConfig.ts create mode 100644 apps/edr-freight-web/portal/src/constants/apiConfig.ts diff --git a/apps/edr-freight-web/backoffice/src/auth/http.ts b/apps/edr-freight-web/backoffice/src/auth/http.ts index 115a0d9d1..5aa78e2d3 100644 --- a/apps/edr-freight-web/backoffice/src/auth/http.ts +++ b/apps/edr-freight-web/backoffice/src/auth/http.ts @@ -1,5 +1,6 @@ import axios from "axios"; +import { API_BASE_URL } from "@/constants/apiConfig"; import { AUTH_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, @@ -16,7 +17,7 @@ type RetriableRequest = { }; const api = axios.create({ - baseURL: `${import.meta.env.VITE_BASE_API_URL}/api`, + baseURL: `${API_BASE_URL}/api`, withCredentials: true, }); diff --git a/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx index 6d1cfb545..625e32a3a 100644 --- a/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/cargoes/CargoFormDialog.tsx @@ -14,6 +14,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Loader2 } from 'lucide-react'; +import { API_BASE_URL } from '@/constants/apiConfig'; interface Cargo { id: string; @@ -32,8 +33,6 @@ interface CargoFormDialogProps { onSuccess?: () => void; } -const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'; - export default function CargoFormDialog({ open, onOpenChange, diff --git a/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts b/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts new file mode 100644 index 000000000..02bf46ac9 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/constants/apiConfig.ts @@ -0,0 +1 @@ +export const API_BASE_URL = 'https://edrfreightapi.triaplc.com'; diff --git a/apps/edr-freight-web/portal/src/constants/apiConfig.ts b/apps/edr-freight-web/portal/src/constants/apiConfig.ts new file mode 100644 index 000000000..02bf46ac9 --- /dev/null +++ b/apps/edr-freight-web/portal/src/constants/apiConfig.ts @@ -0,0 +1 @@ +export const API_BASE_URL = 'https://edrfreightapi.triaplc.com'; diff --git a/apps/edr-freight-web/portal/src/services/payments.service.ts b/apps/edr-freight-web/portal/src/services/payments.service.ts index 3c345f648..119aea4ed 100644 --- a/apps/edr-freight-web/portal/src/services/payments.service.ts +++ b/apps/edr-freight-web/portal/src/services/payments.service.ts @@ -1,4 +1,5 @@ import { URL_CONSTANTS } from "@/constants/URLS"; +import { API_BASE_URL } from "@/constants/apiConfig"; import { client } from "../utils/api"; const P = URL_CONSTANTS.PAYMENTS; @@ -57,7 +58,7 @@ function buildCheckoutUrl(payload: { method: PaymentMethod; platform?: PaymentPlatform; }): string { - const base = (import.meta.env.VITE_API_URL ?? "").replace(/\/$/, ""); + const base = API_BASE_URL.replace(/\/$/, ""); const params = new URLSearchParams({ bookingId: payload.bookingId, method: payload.method, diff --git a/apps/edr-freight-web/portal/src/utils/api.ts b/apps/edr-freight-web/portal/src/utils/api.ts index 446eec591..289709906 100644 --- a/apps/edr-freight-web/portal/src/utils/api.ts +++ b/apps/edr-freight-web/portal/src/utils/api.ts @@ -1,9 +1,10 @@ import { UseQueryOptions, QueryObserverOptions } from "@tanstack/react-query"; import axios, { AxiosError, InternalAxiosRequestConfig } from "axios"; import { URL_CONSTANTS } from "@/constants/URLS"; +import { API_BASE_URL } from "@/constants/apiConfig"; const client = axios.create({ - baseURL: import.meta.env.VITE_API_URL, + baseURL: API_BASE_URL, }); function getCookie(name: string): string | undefined { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 750c9ec01..c1b29edac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -436,7 +436,7 @@ importers: version: 10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/microservices': specifier: ^11.1.24 - version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/passport': specifier: ^10.0.3 version: 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) From 9329e6ca5798a4591a5fecd9d26f90e19e00e05b Mon Sep 17 00:00:00 2001 From: marshal Date: Tue, 16 Jun 2026 23:08:27 +0300 Subject: [PATCH 2/8] Refactor payment type exports to separate types from enums in index --- packages/types/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 397d240bf..b4fd73d81 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -2,5 +2,5 @@ export * from "./common/index"; export * from "./freight/index"; export * as Freight from "./freight/index"; export * as Passenger from "./passenger/index"; -export type { PaymentEvent, PaymentEventType, PaymentFailedEvent, PaymentSucceededEvent } from "./common/payments"; -export { PaymentIntentSnapshot, InitiatePaymentRequest, PaymentReferenceType, PaymentService } from "./common/payments"; +export type { PaymentEvent, PaymentEventType, PaymentFailedEvent, PaymentSucceededEvent, PaymentIntentSnapshot, InitiatePaymentRequest } from "./common/payments"; +export { PaymentReferenceType, PaymentService } from "./common/payments"; From fd49d904a89aa3aebe79cd0b23e6b286738af16e Mon Sep 17 00:00:00 2001 From: Sennay Date: Tue, 16 Jun 2026 23:25:21 +0300 Subject: [PATCH 3/8] remove PolinRider malware scan workflow This workflow scans for PolinRider malware signatures, checks git history for suspicious patterns, and enforces a clean-scan gate to block deployments if malware is detected. --- .github/workflows/polinrider-scan.yml | 242 -------------------------- 1 file changed, 242 deletions(-) diff --git a/.github/workflows/polinrider-scan.yml b/.github/workflows/polinrider-scan.yml index 594f181d1..8b1378917 100644 --- a/.github/workflows/polinrider-scan.yml +++ b/.github/workflows/polinrider-scan.yml @@ -1,243 +1 @@ -name: PolinRider Malware Scan -# ── Triggers ────────────────────────────────────────────────────────────────── -# Runs on every push and every PR targeting main/master/develop. -# Also available as a manual trigger (workflow_dispatch) and on a nightly -# schedule so dormant infections in older branches are caught too. -on: - push: - branches: ["**"] - pull_request: - branches: ["**"] - schedule: - # Nightly full-repo scan at 02:00 UTC - - cron: "0 2 * * *" - workflow_dispatch: - -# ── Permissions ─────────────────────────────────────────────────────────────── -permissions: - contents: read # checkout - security-events: write # upload SARIF to GitHub Security tab - actions: read - checks: write # annotate PRs with scan findings - -# ── Deployment gate ─────────────────────────────────────────────────────────── -# All other jobs (build, test, deploy) should list this job under `needs:`. -# If this job fails (exit code 1 from the scanner), the whole workflow stops. -jobs: - polinrider-scan: - name: "PolinRider / Famous Chollima Scan" - runs-on: ubuntu-latest - # Prevent CI from being disabled by any workflow override - if: always() - - steps: - # ── 1. Checkout full history ───────────────────────────────────────────── - # Full depth so we can inspect recent commits for temp_auto_push.bat traces - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # ── 2. Detect suspicious force-push patterns in git history ────────────── - - name: Check git history for force-push and timestamp manipulation - id: git-check - shell: bash - run: | - echo "=== Checking for suspicious git history patterns ===" - - # Check for .gitignore entries hiding known malware artifacts - GITIGNORE_HITS=0 - if [ -f .gitignore ]; then - for pattern in "branch_structure.json" "temp_auto_push.bat" "temp_interactive_push.bat"; do - if grep -qF "$pattern" .gitignore 2>/dev/null; then - echo "::warning file=.gitignore::SUSPICIOUS: .gitignore hides known PolinRider artifact: $pattern" - GITIGNORE_HITS=$((GITIGNORE_HITS + 1)) - fi - done - fi - - # Check if malware persistence artifacts exist anywhere in the tree - ARTIFACTS_FOUND=0 - for artifact in "temp_auto_push.bat" "temp_interactive_push.bat" "branch_structure.json"; do - FOUND=$(find . -name "$artifact" -not -path "./.git/*" 2>/dev/null) - if [ -n "$FOUND" ]; then - echo "::error ::CRITICAL: PolinRider persistence artifact found: $artifact" - echo "$FOUND" - ARTIFACTS_FOUND=$((ARTIFACTS_FOUND + 1)) - fi - done - - # Scan recent commit messages for --no-verify (used by temp_auto_push.bat) - NO_VERIFY_COMMITS=$(git log --oneline -50 --format="%H %s" 2>/dev/null | grep -i "no.verify\|force.*push\|amend" || true) - if [ -n "$NO_VERIFY_COMMITS" ]; then - echo "::warning ::Recent commits with suspicious metadata (--no-verify / force amend patterns):" - echo "$NO_VERIFY_COMMITS" - fi - - # Check for .woff2 files with unusually large sizes (>50KB is suspicious) - find . -name "*.woff2" -not -path "./.git/*" -size +50k 2>/dev/null | while read f; do - SIZE=$(stat -c%s "$f" 2>/dev/null || echo 0) - echo "::warning file=$f::Oversized .woff2 font file ($SIZE bytes) — may contain embedded payload" - done - - echo "GITIGNORE_HITS=$GITIGNORE_HITS" >> "$GITHUB_OUTPUT" - echo "ARTIFACTS_FOUND=$ARTIFACTS_FOUND" >> "$GITHUB_OUTPUT" - - # ── 3. Run the JavaScript malware scanner ──────────────────────────────── - - name: Run PolinRider malware scanner - id: scanner - shell: bash - run: | - echo "=== Running PolinRider IOC scanner ===" - - # The scanner is zero-dependency — just needs Node.js (always present on ubuntu-latest) - node .github/scripts/scan.js \ - --json \ - --output scan-report.json \ - . - - SCANNER_EXIT=$? - echo "SCANNER_EXIT=$SCANNER_EXIT" >> "$GITHUB_OUTPUT" - - # Also emit a human-readable summary to the Actions log - node .github/scripts/scan.js . || true - - exit $SCANNER_EXIT - - # ── 4. Upload scan report as artifact ──────────────────────────────────── - # - name: Upload scan report - # if: always() - # uses: actions/upload-artifact@v4 - # with: - # name: polinrider-scan-report - # path: scan-report.json - # retention-days: 90 - - # # ── 5. Convert to SARIF and upload to GitHub Security tab ───────────── - # - name: Convert scan results to SARIF - # if: always() - # shell: bash - # run: | - # node - << 'SCRIPT' - # const fs = require('fs'); - - # let report; - # try { - # report = JSON.parse(fs.readFileSync('scan-report.json', 'utf8')); - # } catch { - # // No report = no findings, write empty SARIF - # report = { results: [] }; - # } - - # const severityMap = { - # CRITICAL: 'error', - # HIGH: 'warning', - # MEDIUM: 'note', - # }; - - # const sarif = { - # version: '2.1.0', - # $schema: 'https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json', - # runs: [{ - # tool: { - # driver: { - # name: 'PolinRider Malware Scanner', - # version: '1.0.0', - # informationUri: 'https://github.com/your-org/your-repo', - # rules: [ - # { id: 'POLINRIDER-001', name: 'StringShufflerVariable', - # shortDescription: { text: 'PolinRider _$_1e42 shuffler variable' }, - # helpUri: 'https://safedep.io/astro-config-blockchain-c2-supply-chain/' }, - # { id: 'POLINRIDER-002', name: 'CampaignMarkerAssignment', - # shortDescription: { text: "global['!'] campaign marker" } }, - # { id: 'POLINRIDER-003', name: 'ShufflerSeedString', - # shortDescription: { text: 'rmcej%otb% seed string' } }, - # { id: 'POLINRIDER-004', name: 'KnownC2IP', - # shortDescription: { text: 'Known PolinRider C2 IP address' } }, - # { id: 'POLINRIDER-005', name: 'TRONWallet', - # shortDescription: { text: 'Known TRON dead-drop wallet' } }, - # { id: 'POLINRIDER-006', name: 'AptosAddress', - # shortDescription: { text: 'Known Aptos dead-drop address' } }, - # { id: 'POLINRIDER-007', name: 'XORKey', - # shortDescription: { text: 'Known XOR decryption key' } }, - # { id: 'POLINRIDER-008', name: 'KnownMalwareHash', - # shortDescription: { text: 'SHA-256 matches known malware sample' } }, - # { id: 'POLINRIDER-009', name: 'BlockchainC2Contact', - # shortDescription: { text: 'Blockchain RPC dead-drop infrastructure' } }, - # { id: 'POLINRIDER-010', name: 'HiddenProcessSpawn', - # shortDescription: { text: 'windowsHide:true hidden process spawn' } }, - # { id: 'POLINRIDER-011', name: 'DuplicateCreateRequire', - # shortDescription: { text: 'Duplicate createRequire injection' } }, - # { id: 'POLINRIDER-012', name: 'HorizontalWhitespacePadding', - # shortDescription: { text: 'Hidden payload via horizontal whitespace' } }, - # { id: 'POLINRIDER-013', name: 'ConfigFileSizeAnomaly', - # shortDescription: { text: 'Config file size anomaly' } }, - # { id: 'POLINRIDER-014', name: 'PersistenceArtifact', - # shortDescription: { text: 'PolinRider persistence artifact present' } }, - # { id: 'POLINRIDER-015', name: 'CampaignMarkerPattern', - # shortDescription: { text: 'Numeric campaign marker pattern' } }, - # { id: 'POLINRIDER-016', name: 'SfLObfuscationFunction', - # shortDescription: { text: 'sfL obfuscation function' } }, - # { id: 'POLINRIDER-017', name: 'GlobalRequireInjection', - # shortDescription: { text: 'global require/module injection' } }, - # ], - # }, - # }, - # results: (report.results || []).flatMap(file => - # (file.findings || []).map(finding => ({ - # ruleId: finding.id, - # level: severityMap[finding.severity] || 'warning', - # message: { text: finding.description + ' — ' + finding.matches.join('; ') }, - # locations: [{ - # physicalLocation: { - # artifactLocation: { uri: file.filePath.replace(/^\.\//,''), uriBaseId: '%SRCROOT%' }, - # region: { startLine: 1 }, - # }, - # }], - # })) - # ), - # }], - # }; - - # fs.writeFileSync('scan-results.sarif', JSON.stringify(sarif, null, 2)); - # console.log('SARIF written.'); - # SCRIPT - - # - name: Upload SARIF to GitHub Security tab - # if: always() - # uses: github/codeql-action/upload-sarif@v3 - # with: - # sarif_file: scan-results.sarif - # category: polinrider-malware-scan - - # ── 6. Block deployment if infected ────────────────────────────────────── - - name: Enforce clean-scan gate - if: steps.scanner.outputs.SCANNER_EXIT == '1' || steps.git-check.outputs.ARTIFACTS_FOUND != '0' - shell: bash - run: | - echo "" - echo "╔══════════════════════════════════════════════════════════════════╗" - echo "║ DEPLOYMENT BLOCKED — PolinRider malware signatures detected ║" - echo "║ ║" - echo "║ This repository contains code signatures consistent with the ║" - echo "║ PolinRider supply-chain campaign (DPRK / Famous Chollima). ║" - echo "║ ║" - echo "║ DO NOT run npm install, build, or deploy until remediated. ║" - echo "║ ║" - echo "║ See scan-report.json artifact for full details. ║" - echo "╚══════════════════════════════════════════════════════════════════╝" - exit 1 - - # ── Dependent jobs — add `needs: polinrider-scan` to block on clean scan ───── - # Example: your existing build/deploy jobs should look like this: - # - # build: - # needs: polinrider-scan - # runs-on: ubuntu-latest - # steps: - # ... - # - # deploy: - # needs: [polinrider-scan, build] - # ... From 6d8eb65a3608a31517346414932b47f0dd567e02 Mon Sep 17 00:00:00 2001 From: marshal Date: Tue, 16 Jun 2026 23:26:03 +0300 Subject: [PATCH 4/8] Add TypeScript type annotations to Vite config and dedupe Mantine dependencies --- .../edr-freight-web/backoffice/vite.config.ts | 29 +++++++++++++++---- packages/types/src/common/payments.ts | 2 +- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/apps/edr-freight-web/backoffice/vite.config.ts b/apps/edr-freight-web/backoffice/vite.config.ts index 0772bea14..f5a3563a8 100644 --- a/apps/edr-freight-web/backoffice/vite.config.ts +++ b/apps/edr-freight-web/backoffice/vite.config.ts @@ -1,23 +1,35 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; +import type { IncomingMessage, ServerResponse } from "node:http"; -/// -import { defineConfig } from "vite"; +import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; +import type { ViteDevServer, PreviewServer } from "vite"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); + function userManagementSpaFallback() { - const rewrite = (req) => { - const url = req.url || ''; + const rewrite = (req: IncomingMessage) => { + const url = req.url ?? ''; if (!url.startsWith('/_um/') && url !== '/_um') return; if (/\.[a-zA-Z0-9]+$/.test(url.split('?')[0])) return; req.url = '/_um/index.html'; }; return { name: 'user-management-spa-fallback', - configureServer(s){ s.middlewares.use((req,_r,next)=>{rewrite(req);next();}); }, - configurePreviewServer(s){ s.middlewares.use((req,_r,next)=>{rewrite(req);next();}); }, + configureServer(s: ViteDevServer) { + s.middlewares.use((req: IncomingMessage, _r: ServerResponse, next: () => void) => { + rewrite(req); + next(); + }); + }, + configurePreviewServer(s: PreviewServer) { + s.middlewares.use((req: IncomingMessage, _r: ServerResponse, next: () => void) => { + rewrite(req); + next(); + }); + }, }; } @@ -29,6 +41,11 @@ export default defineConfig({ // Resolve from TS source so Vite gets ESM named exports (dist is CommonJS). "@edr/types": path.resolve(__dirname, "../../../packages/types/src/index.ts"), }, + // Force a single copy of these singletons so MantineProvider context is + // shared between the backoffice app and @edr/ui-common (which ships its + // own node_modules copy). Without this, two separate @mantine/core + // instances are bundled and the context lookup fails at runtime. + dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"], }, server: { port: 5183, diff --git a/packages/types/src/common/payments.ts b/packages/types/src/common/payments.ts index 6439395f4..ebc6e34dd 100644 --- a/packages/types/src/common/payments.ts +++ b/packages/types/src/common/payments.ts @@ -137,7 +137,7 @@ export interface ConfirmPaymentRequest { } /** Response of `POST /payments/initiate` and shape of intent lookups. */ -export interface PaymentIntentSnapshot { +export type PaymentIntentSnapshot ={ intentId: string; service: PaymentService; referenceType: PaymentReferenceType; From 9ad78bffc180af3d5abf5bee3c7a40b8a9fb7d39 Mon Sep 17 00:00:00 2001 From: SennayT Date: Tue, 16 Jun 2026 23:27:25 +0300 Subject: [PATCH 5/8] remove virus scanner --- .github/workflows/polinrider-scan.yml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/workflows/polinrider-scan.yml diff --git a/.github/workflows/polinrider-scan.yml b/.github/workflows/polinrider-scan.yml deleted file mode 100644 index 8b1378917..000000000 --- a/.github/workflows/polinrider-scan.yml +++ /dev/null @@ -1 +0,0 @@ - From b191fe2335d26ef82d916828cd9736366aea494d Mon Sep 17 00:00:00 2001 From: marshal Date: Wed, 17 Jun 2026 00:44:39 +0300 Subject: [PATCH 6/8] Update Vite configuration to use Vitest for testing, add TypeScript source resolution for types, and deduplicate Mantine dependencies to ensure shared context. --- apps/edr-freight-web/portal/vite.config.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/edr-freight-web/portal/vite.config.ts b/apps/edr-freight-web/portal/vite.config.ts index 8d01990aa..3d8023494 100644 --- a/apps/edr-freight-web/portal/vite.config.ts +++ b/apps/edr-freight-web/portal/vite.config.ts @@ -1,7 +1,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; -import { defineConfig } from "vite"; +import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; @@ -12,10 +12,20 @@ export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), + // Resolve from TS source so Vite gets ESM named exports (dist is CommonJS). + "@edr/types": path.resolve(__dirname, "../../../packages/types/src/index.ts"), }, + // Force a single copy of these singletons so MantineProvider context is + // shared between the portal app and @edr/ui-common (which ships its + // own node_modules copy). Without this, two separate @mantine/core + // instances are bundled and the context lookup fails at runtime. + dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"], }, server: { port: 5173, host: "0.0.0.0", }, + test: { + environment: "node", + }, }); From dbe4983b75d3dd464dea92c88d65d873a1587cbb Mon Sep 17 00:00:00 2001 From: marshal Date: Wed, 17 Jun 2026 01:29:48 +0300 Subject: [PATCH 7/8] Pin Mantine dependencies to portal's node_modules and optimize dependency pre-bundling to prevent React version conflicts --- apps/edr-freight-web/backoffice/vite.config.ts | 1 + apps/edr-freight-web/portal/vite.config.ts | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/edr-freight-web/backoffice/vite.config.ts b/apps/edr-freight-web/backoffice/vite.config.ts index f5a3563a8..b6ec390a6 100644 --- a/apps/edr-freight-web/backoffice/vite.config.ts +++ b/apps/edr-freight-web/backoffice/vite.config.ts @@ -23,6 +23,7 @@ function userManagementSpaFallback() { rewrite(req); next(); }); + }, configurePreviewServer(s: PreviewServer) { s.middlewares.use((req: IncomingMessage, _r: ServerResponse, next: () => void) => { diff --git a/apps/edr-freight-web/portal/vite.config.ts b/apps/edr-freight-web/portal/vite.config.ts index 3d8023494..99e2a41d5 100644 --- a/apps/edr-freight-web/portal/vite.config.ts +++ b/apps/edr-freight-web/portal/vite.config.ts @@ -7,6 +7,12 @@ import tailwindcss from "@tailwindcss/vite"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// Pin Mantine to this app's copy. pnpm can install a second @mantine/core under +// @edr/ui-common (linked to react@18) while the portal uses react@19 — dedupe +// alone does not merge those into one module in production builds. +const mantineCore = path.resolve(__dirname, "node_modules/@mantine/core"); +const mantineHooks = path.resolve(__dirname, "node_modules/@mantine/hooks"); + export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { @@ -14,13 +20,14 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), // Resolve from TS source so Vite gets ESM named exports (dist is CommonJS). "@edr/types": path.resolve(__dirname, "../../../packages/types/src/index.ts"), + "@mantine/core": mantineCore, + "@mantine/hooks": mantineHooks, }, - // Force a single copy of these singletons so MantineProvider context is - // shared between the portal app and @edr/ui-common (which ships its - // own node_modules copy). Without this, two separate @mantine/core - // instances are bundled and the context lookup fails at runtime. dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"], }, + optimizeDeps: { + include: ["@mantine/core", "@mantine/hooks", "@edr/ui-common"], + }, server: { port: 5173, host: "0.0.0.0", From 7106a549ab2c75a3bc63b4592248d47bbf07efad Mon Sep 17 00:00:00 2001 From: Marshal Date: Tue, 16 Jun 2026 23:06:41 +0000 Subject: [PATCH 8/8] Add signature management features for booking contracts - Implement MySignatureCard component for viewing and updating saved signatures. - Enhance BookingContractPage to utilize saved signatures for contract signing. - Introduce hooks for fetching and saving user signatures. - Update ContractView interface to include saved signature details. - Create signatures service for API interactions related to user signatures. --- .../components/profile/MySignatureCard.tsx | 141 ++++++++++++++++++ .../portal/src/hooks/useSavedSignature.ts | 30 ++++ .../portal/src/pages/ProfilePage.tsx | 3 + .../pages/bookings/BookingContractPage.tsx | 77 ++++++++-- .../portal/src/services/bookings.service.ts | 5 + .../portal/src/services/signatures.service.ts | 28 ++++ 6 files changed, 269 insertions(+), 15 deletions(-) create mode 100644 apps/edr-freight-web/portal/src/components/profile/MySignatureCard.tsx create mode 100644 apps/edr-freight-web/portal/src/hooks/useSavedSignature.ts create mode 100644 apps/edr-freight-web/portal/src/services/signatures.service.ts diff --git a/apps/edr-freight-web/portal/src/components/profile/MySignatureCard.tsx b/apps/edr-freight-web/portal/src/components/profile/MySignatureCard.tsx new file mode 100644 index 000000000..8eb0b8e4e --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/profile/MySignatureCard.tsx @@ -0,0 +1,141 @@ +import { useState } from "react"; +import { FileSignature, Loader2 } from "lucide-react"; + +import { ContractSignaturePad } from "@/components/bookings/ContractSignaturePad"; +import useAuth from "@/hooks/useAuth"; +import { + useMySignature, + useSaveSignature, +} from "@/hooks/useSavedSignature"; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Label, +} from "@edr/ui-common"; + +/** + * Lets the signed-in customer view and update the reusable signature stored on + * their profile. The same signature is offered for approval when signing a + * booking contract. + */ +export function MySignatureCard() { + const { user } = useAuth(); + const { data: saved, isPending } = useMySignature(); + const saveMutation = useSaveSignature(); + + const [open, setOpen] = useState(false); + const [signerName, setSignerName] = useState(""); + const [signatureData, setSignatureData] = useState(null); + + const defaultName = user?.name?.en || user?.username || user?.email || ""; + + const openDialog = () => { + setSignerName(saved?.signerDisplayName ?? defaultName); + setSignatureData(null); + setOpen(true); + }; + + const save = () => { + if (!signatureData || !signerName.trim()) return; + saveMutation.mutate( + { + signerDisplayName: signerName.trim(), + signatureImageBase64: signatureData, + }, + { onSuccess: () => setOpen(false) }, + ); + }; + + return ( + + + + + My signature + + + Reused to approve and sign booking contracts. + + + + {isPending ? ( +
+ +
+ ) : saved?.signatureImageUrl ? ( +
+
+ My saved signature +
+

+ Saved as {saved.signerDisplayName} +

+
+ ) : ( +

+ You have not saved a signature yet. +

+ )} + +
+ + + + + Save your signature + + Draw your signature below. It will be stored on your profile for + future contracts. + + +
+
+ + setSignerName(e.target.value)} + placeholder="As shown on contracts" + /> +
+ +
+ + + + +
+
+
+ ); +} diff --git a/apps/edr-freight-web/portal/src/hooks/useSavedSignature.ts b/apps/edr-freight-web/portal/src/hooks/useSavedSignature.ts new file mode 100644 index 000000000..b8c9480a7 --- /dev/null +++ b/apps/edr-freight-web/portal/src/hooks/useSavedSignature.ts @@ -0,0 +1,30 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; + +import { + signaturesService, + type SaveSignaturePayload, +} from "@/services/signatures.service"; + +const SAVED_SIGNATURE_KEY = ["me", "signature"] as const; + +export function useMySignature() { + return useQuery({ + queryKey: SAVED_SIGNATURE_KEY, + queryFn: () => signaturesService.getMySignature(), + staleTime: 60_000, + }); +} + +export function useSaveSignature() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (payload: SaveSignaturePayload) => + signaturesService.saveMySignature(payload), + onSuccess: () => { + toast.success("Signature saved"); + void qc.invalidateQueries({ queryKey: SAVED_SIGNATURE_KEY }); + }, + onError: () => toast.error("Failed to save signature"), + }); +} diff --git a/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx b/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx index 9c4354723..6e55e0407 100644 --- a/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx +++ b/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx @@ -1,6 +1,7 @@ import { User, Building2, Phone, Mail, MapPin, ShieldCheck, Briefcase, UserCheck, Fingerprint, FileCheck, Globe, Building } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { api } from "@/services/api"; +import { MySignatureCard } from "@/components/profile/MySignatureCard"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, Badge, Separator } from "@edr/ui-common"; function InfoItem({ icon, label, value }: { icon?: React.ReactNode; label: string; value?: string | null }) { @@ -175,6 +176,8 @@ export default function ProfilePage() { + + diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingContractPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingContractPage.tsx index dd1916093..062a8a766 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingContractPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingContractPage.tsx @@ -25,6 +25,9 @@ export default function BookingContractPage() { const [signOpen, setSignOpen] = useState(false); const [signerName, setSignerName] = useState(""); const [signatureData, setSignatureData] = useState(null); + // When a saved signature exists we offer it for approval first; the customer + // can switch to drawing a fresh one. + const [drawNew, setDrawNew] = useState(false); const { data, isLoading, isError, refetch } = useQuery({ queryKey: ["booking-contract-view", id], @@ -32,6 +35,31 @@ export default function BookingContractPage() { enabled: Boolean(id), }); + const savedSignature = data?.savedSignature ?? null; + const savedSignatureImage = savedSignature?.signatureImageUrl ?? null; + const usingSaved = Boolean(savedSignatureImage) && !drawNew; + + const openSign = () => { + // Prefill from the saved signature so the customer only has to approve it. + setSignerName(savedSignature?.signerDisplayName ?? ""); + setSignatureData(null); + setDrawNew(false); + setSignOpen(true); + }; + + const confirmSign = () => { + if (!signerName.trim()) return; + // Approve the saved signature, or submit the freshly drawn one. + const image = usingSaved ? savedSignatureImage : signatureData; + if (!image) return; + signMutation.mutate({ + role: "CUSTOMER", + signatureImageBase64: image, + signerDisplayName: signerName.trim(), + consentText: "I agree to the terms of this contract.", + }); + }; + const signMutation = useMutation({ mutationFn: (payload: SignContractPayload) => bookingsService.signContract(id!, payload), @@ -102,9 +130,9 @@ export default function BookingContractPage() { PDF {data.canSignCustomer && ( - )} @@ -122,9 +150,13 @@ export default function BookingContractPage() { {signOpen && (
-

Sign contract

+

+ {usingSaved ? "Approve signature" : "Sign contract"} +

- {data.reference} — your signature will be stored securely. + {usingSaved + ? `${data.reference} — review your saved signature and approve it.` + : `${data.reference} — your signature will be stored securely.`}

diff --git a/apps/edr-freight-web/portal/src/services/bookings.service.ts b/apps/edr-freight-web/portal/src/services/bookings.service.ts index ac6411505..9933fa227 100644 --- a/apps/edr-freight-web/portal/src/services/bookings.service.ts +++ b/apps/edr-freight-web/portal/src/services/bookings.service.ts @@ -23,6 +23,11 @@ export interface ContractView { signedAt: string; signatureImageUrl?: string | null; }>; + /** Current viewer's reusable saved signature, if they have one. */ + savedSignature?: { + signerDisplayName: string; + signatureImageUrl?: string | null; + } | null; } export interface PriceLineItem { diff --git a/apps/edr-freight-web/portal/src/services/signatures.service.ts b/apps/edr-freight-web/portal/src/services/signatures.service.ts new file mode 100644 index 000000000..05ae61ea1 --- /dev/null +++ b/apps/edr-freight-web/portal/src/services/signatures.service.ts @@ -0,0 +1,28 @@ +import { client } from "../utils/api"; + +const SIGNATURE_URL = "/api/me/signature"; + +export interface SavedSignature { + signerDisplayName: string; + signatureImageUrl?: string | null; +} + +export interface SaveSignaturePayload { + signerDisplayName: string; + signatureImageBase64: string; +} + +export const signaturesService = { + /** The current user's reusable saved signature, or null if none. */ + getMySignature: async (): Promise => { + const { data } = await client.get(SIGNATURE_URL); + return (data.data ?? data) ?? null; + }, + + saveMySignature: async ( + payload: SaveSignaturePayload, + ): Promise => { + const { data } = await client.put(SIGNATURE_URL, payload); + return (data.data ?? data) ?? null; + }, +};