mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 12:58:13 +00:00
fix fayda
This commit is contained in:
@@ -93,6 +93,7 @@ import WarehouseInvoicesPage from "./pages/warehouses/WarehouseInvoicesPage";
|
||||
import WarehouseListPage from "./pages/warehouses/WarehouseListPage";
|
||||
import WarehouseRulesPage from "./pages/warehouses/WarehouseRulesPage";
|
||||
import { HealthCheck } from "./features/health/HealthCheck";
|
||||
import FaydaCallbackPage from "./pages/FaydaCallbackPage";
|
||||
|
||||
const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
{
|
||||
@@ -463,6 +464,7 @@ const App = () => {
|
||||
<Routes>
|
||||
<Route path="/auth" element={<LoginPage />} />
|
||||
<Route path="/um/*" element={<UserManagementHostPage />} />
|
||||
<Route path="/callback" element={<FaydaCallbackPage />} />
|
||||
<Route path="*" element={<Navigate to="/auth" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
@@ -472,6 +474,7 @@ const App = () => {
|
||||
<Routes>
|
||||
<Route path="/um/*" element={<UserManagementHostPage />} />
|
||||
<Route path="/health" element={<HealthCheck />} />
|
||||
<Route path="/callback" element={<FaydaCallbackPage />} />
|
||||
<Route path="/" element={<Navigate to="/dashboard/overview" replace />} />
|
||||
<Route path="/dashboard" element={<Navigate to="/dashboard/overview" replace />} />
|
||||
<Route path="/dashboard" element={<DashboardShell />}>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Loader2, Calendar } from "lucide-react";
|
||||
import { Loader2, Calendar, ShieldCheck } from "lucide-react";
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
@@ -20,6 +22,10 @@ import {
|
||||
type FleetFormFieldDef,
|
||||
} from "@/pages/fleet/config/resources";
|
||||
import type { FleetRecord } from "@/services/fleet/fleet.service";
|
||||
import {
|
||||
verifaydaService,
|
||||
type FaydaCallbackMessage,
|
||||
} from "@/services/verifayda.service";
|
||||
|
||||
export interface FleetFormDialogProps {
|
||||
open: boolean;
|
||||
@@ -31,6 +37,12 @@ export interface FleetFormDialogProps {
|
||||
isSubmitting: boolean;
|
||||
selectOptionsLoading?: boolean;
|
||||
onSubmit: (values: Record<string, unknown>) => void;
|
||||
/**
|
||||
* Show a "Verify with Fayda" step: opens the eSignet popup and prefills
|
||||
* firstName/lastName/email/phoneNumber/dateOfBirth from the verified
|
||||
* identity, stamping faydaVerified + faydaSub on the payload.
|
||||
*/
|
||||
verifyWithFayda?: boolean;
|
||||
}
|
||||
|
||||
const buildInitialValues = (
|
||||
@@ -72,9 +84,12 @@ const FleetFormDialog = ({
|
||||
isSubmitting,
|
||||
selectOptionsLoading,
|
||||
onSubmit,
|
||||
verifyWithFayda,
|
||||
}: FleetFormDialogProps) => {
|
||||
const [values, setValues] = useState<Record<string, unknown>>({});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [faydaLoading, setFaydaLoading] = useState(false);
|
||||
const [faydaError, setFaydaError] = useState<string | null>(null);
|
||||
|
||||
// Seed the form ONLY when the dialog opens or the edited record changes — NOT
|
||||
// when `fields`/`emptyValues` get new object refs (they're rebuilt whenever the
|
||||
@@ -87,10 +102,85 @@ const FleetFormDialog = ({
|
||||
if (open) {
|
||||
setValues(buildInitialValues(fields, emptyValues, initialRecord));
|
||||
setErrors({});
|
||||
setFaydaError(null);
|
||||
setFaydaLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, recordId]);
|
||||
|
||||
// Receive the ?code&state relayed by the /callback popup, exchange it for
|
||||
// the verified identity, and prefill the matching form fields.
|
||||
useEffect(() => {
|
||||
if (!open || !verifyWithFayda) return;
|
||||
const onMessage = async (event: MessageEvent<FaydaCallbackMessage>) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== "fayda-callback") return;
|
||||
|
||||
if (event.data.error) {
|
||||
setFaydaLoading(false);
|
||||
setFaydaError(event.data.errorDescription ?? event.data.error);
|
||||
return;
|
||||
}
|
||||
if (!event.data.code || !event.data.state) return;
|
||||
|
||||
try {
|
||||
const result = await verifaydaService.complete(event.data.code, event.data.state);
|
||||
if (!result.verified) {
|
||||
setFaydaError("Identity could not be verified");
|
||||
return;
|
||||
}
|
||||
const nameParts = (result.fullName ?? "").trim().split(/\s+/).filter(Boolean);
|
||||
const [firstName, ...rest] = nameParts;
|
||||
setValues((current) => ({
|
||||
...current,
|
||||
...(firstName ? { firstName } : {}),
|
||||
...(rest.length ? { lastName: rest.join(" ") } : {}),
|
||||
...(result.email ? { email: result.email } : {}),
|
||||
...(result.phoneNumber ? { phoneNumber: result.phoneNumber } : {}),
|
||||
...(result.birthdate ? { dateOfBirth: result.birthdate } : {}),
|
||||
faydaVerified: true,
|
||||
...(result.iamUserId ? { faydaSub: result.iamUserId } : {}),
|
||||
}));
|
||||
setFaydaError(null);
|
||||
} catch (err) {
|
||||
const message =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
||||
(err instanceof Error ? err.message : "Verification failed");
|
||||
setFaydaError(message);
|
||||
} finally {
|
||||
setFaydaLoading(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", onMessage);
|
||||
return () => window.removeEventListener("message", onMessage);
|
||||
}, [open, verifyWithFayda]);
|
||||
|
||||
const handleFaydaVerify = async () => {
|
||||
setFaydaError(null);
|
||||
setFaydaLoading(true);
|
||||
try {
|
||||
const { authorizationUrl } = await verifaydaService.start();
|
||||
const popup = window.open(
|
||||
authorizationUrl,
|
||||
"fayda-verify",
|
||||
"width=480,height=760,noopener=no",
|
||||
);
|
||||
if (!popup) {
|
||||
setFaydaLoading(false);
|
||||
setFaydaError("Pop-up blocked — allow pop-ups for this site and retry.");
|
||||
}
|
||||
// Loading stays on until the popup posts back; reopening the dialog resets it.
|
||||
} catch (err) {
|
||||
setFaydaLoading(false);
|
||||
const message =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
||||
(err instanceof Error ? err.message : "Could not start verification");
|
||||
setFaydaError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const faydaVerified = values.faydaVerified === true;
|
||||
|
||||
const shortFields = useMemo(
|
||||
() => fields.filter((f) => f.type !== "textarea"),
|
||||
[fields],
|
||||
@@ -333,6 +423,39 @@ const FleetFormDialog = ({
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
{verifyWithFayda && (
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
{faydaVerified ? (
|
||||
<Badge
|
||||
color="green"
|
||||
variant="light"
|
||||
size="lg"
|
||||
leftSection={<ShieldCheck size={14} />}
|
||||
>
|
||||
Identity verified with Fayda
|
||||
</Badge>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
Verify the driver's identity with Fayda to prefill their details.
|
||||
</Text>
|
||||
)}
|
||||
<Button
|
||||
variant={faydaVerified ? "default" : "light"}
|
||||
color="edr-green"
|
||||
size="xs"
|
||||
leftSection={<ShieldCheck size={14} />}
|
||||
loading={faydaLoading}
|
||||
onClick={handleFaydaVerify}
|
||||
>
|
||||
{faydaVerified ? "Re-verify" : "Verify with Fayda"}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
{verifyWithFayda && faydaError && (
|
||||
<Alert color="red" variant="light">
|
||||
{faydaError}
|
||||
</Alert>
|
||||
)}
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
{shortFields.map(renderField)}
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Badge, Text } from "@mantine/core";
|
||||
import type { ColumnFormat } from "@/pages/ruleEngine/config/resources";
|
||||
import { formatCell as formatRuleEngineCell } from "@/components/ruleEngine/ruleEngineFormat";
|
||||
|
||||
export type FleetColumnFormat = ColumnFormat | "statusBadge";
|
||||
export type FleetColumnFormat = ColumnFormat | "statusBadge" | "verifiedBadge";
|
||||
|
||||
const optionLabelMap = new Map<string, Map<string, string>>();
|
||||
|
||||
@@ -20,6 +20,16 @@ export const formatFleetCell = (
|
||||
format?: FleetColumnFormat,
|
||||
accessorKey?: string,
|
||||
): ReactNode => {
|
||||
if (format === "verifiedBadge") {
|
||||
return value === true ? (
|
||||
<Badge variant="light" color="green" size="sm" radius="md">
|
||||
Verified
|
||||
</Badge>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">—</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (format === "statusBadge") {
|
||||
const status = value == null || value === "" ? "—" : String(value);
|
||||
const getStatusColor = (st: string): string => {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Center, Loader, Stack, Text } from "@mantine/core";
|
||||
|
||||
import type { FaydaCallbackMessage } from "@/services/verifayda.service";
|
||||
|
||||
/**
|
||||
* Landing page for the eSignet redirect_uri (FAYDA_WEB_REDIRECT_URI →
|
||||
* http://localhost:5183/callback). Runs inside the verification popup:
|
||||
* relays ?code&state (or ?error) to the window that opened it via
|
||||
* postMessage, then closes itself. The opener performs the /complete call
|
||||
* so the single-use session is only consumed once, in one place.
|
||||
*/
|
||||
const FaydaCallbackPage = () => {
|
||||
const [standalone, setStandalone] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const message: FaydaCallbackMessage = {
|
||||
type: "fayda-callback",
|
||||
code: params.get("code") ?? undefined,
|
||||
state: params.get("state") ?? undefined,
|
||||
error: params.get("error") ?? undefined,
|
||||
errorDescription: params.get("error_description") ?? undefined,
|
||||
};
|
||||
|
||||
if (window.opener && window.opener !== window) {
|
||||
(window.opener as Window).postMessage(message, window.location.origin);
|
||||
window.close();
|
||||
} else {
|
||||
// Opened as a full-page redirect instead of a popup — nothing to relay to.
|
||||
setStandalone(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Center h="100vh">
|
||||
<Stack align="center" gap="sm">
|
||||
{standalone ? (
|
||||
<>
|
||||
<Text fw={600}>Verification window lost its parent page</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Close this tab and restart the verification from the form.
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Loader size="sm" />
|
||||
<Text size="sm" c="dimmed">Completing Fayda verification…</Text>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaydaCallbackPage;
|
||||
@@ -510,6 +510,7 @@ const FleetResourcePage = () => {
|
||||
isSubmitting={create.isPending || update.isPending}
|
||||
selectOptionsLoading={selectOptionsLoading}
|
||||
onSubmit={handleFormSubmit}
|
||||
verifyWithFayda={Boolean(config.faydaVerification)}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -29,6 +29,7 @@ export const driversConfig: FleetResourceConfig = {
|
||||
options: DRIVER_STATUS_OPTIONS,
|
||||
},
|
||||
],
|
||||
faydaVerification: true,
|
||||
searchKeys: ["firstName", "lastName", "email", "phoneNumber", "licenseNumber", "status"],
|
||||
columns: [
|
||||
{ id: "licenseNumber", header: "License Number", accessorKey: "licenseNumber", format: "code", size: 140 },
|
||||
@@ -38,6 +39,7 @@ export const driversConfig: FleetResourceConfig = {
|
||||
{ id: "phoneNumber", header: "Phone", accessorKey: "phoneNumber", format: "code", size: 120 },
|
||||
{ id: "licenseExpiryDate", header: "License Expiry", accessorKey: "licenseExpiryDate", format: "code", size: 130 },
|
||||
{ id: "status", header: "Status", accessorKey: "status", format: "statusBadge", size: 100 },
|
||||
{ id: "faydaVerified", header: "Fayda", accessorKey: "faydaVerified", format: "verifiedBadge", size: 90 },
|
||||
],
|
||||
formFields: [
|
||||
{ name: "licenseNumber", label: "License Number", type: "text", required: true },
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface FleetResourceColumn {
|
||||
id: string;
|
||||
header: string;
|
||||
accessorKey: string;
|
||||
format?: ColumnFormat | "statusBadge";
|
||||
format?: ColumnFormat | "statusBadge" | "verifiedBadge";
|
||||
size?: number;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ export interface FleetResourceConfig {
|
||||
cardCodeKey?: string;
|
||||
cardSubtitleKey?: string;
|
||||
searchKeys: string[];
|
||||
/** Offer Fayda identity verification in the add/edit form (drivers). */
|
||||
faydaVerification?: boolean;
|
||||
}
|
||||
|
||||
export const FLEET_BASE_PATH_BY_SLUG: Record<FleetResourceSlug, string> = {
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface Driver {
|
||||
address?: string | null;
|
||||
emergencyContact?: string | null;
|
||||
notes?: string | null;
|
||||
faydaVerified?: boolean;
|
||||
faydaSub?: string | null;
|
||||
totalTrips: number;
|
||||
rating: number;
|
||||
createdAt: string;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { api as apiClient } from '../auth/http';
|
||||
|
||||
export interface FaydaStartResponse {
|
||||
authorizationUrl: string;
|
||||
}
|
||||
|
||||
export interface FaydaCompleteResult {
|
||||
purpose: 'LOGIN' | 'VERIFY';
|
||||
verified: boolean;
|
||||
fullName?: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
/** ISO yyyy-MM-dd */
|
||||
birthdate?: string;
|
||||
gender?: string;
|
||||
iamUserId?: string;
|
||||
userDataSaved?: boolean;
|
||||
}
|
||||
|
||||
/** Message posted from the /callback popup back to the opener window. */
|
||||
export interface FaydaCallbackMessage {
|
||||
type: 'fayda-callback';
|
||||
code?: string;
|
||||
state?: string;
|
||||
error?: string;
|
||||
errorDescription?: string;
|
||||
}
|
||||
|
||||
export const verifaydaService = {
|
||||
/** Returns the eSignet authorize URL to open in a popup. */
|
||||
start: () =>
|
||||
apiClient
|
||||
.post<FaydaStartResponse>('/fayda/verification/start', {
|
||||
purpose: 'VERIFY',
|
||||
platform: 'WEB',
|
||||
})
|
||||
.then((r) => r.data),
|
||||
|
||||
/** Exchange the callback code+state for the verified identity attributes. */
|
||||
complete: (code: string, state: string) =>
|
||||
apiClient
|
||||
.get<FaydaCompleteResult>(
|
||||
`/fayda/verification/complete?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`,
|
||||
)
|
||||
.then((r) => r.data),
|
||||
};
|
||||
Reference in New Issue
Block a user