diff --git a/src/app/dashboard/osint/page.tsx b/src/app/dashboard/osint/page.tsx new file mode 100644 index 0000000..5937edc --- /dev/null +++ b/src/app/dashboard/osint/page.tsx @@ -0,0 +1,398 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface CountryResult { + country: string; + label: string; + count: number; + columns: string[]; + rows: unknown[][]; +} + +interface CompanyLookupResponse { + query: string; + results: CountryResult[]; + errors: string[]; + total_hits: number; +} + +interface OfacHit { + source: string; + hits: number; + columns: string[]; + rows: unknown[][]; + error: string | null; +} + +interface YenteHit { + id: string; + caption: string; + schema: string; + datasets: string[]; + score: number; +} + +interface SanctionsResponse { + verdict: 'HIT' | 'CLEAR'; + name: string; + ofac: OfacHit; + yente: { source: string; hits: number; results: YenteHit[]; error: string | null }; + checked_at: string; +} + +type ActiveTab = 'lookup' | 'sanctions'; +type Country = 'all' | 'ro' | 'de' | 'uk' | 'us'; + +const COUNTRY_LABELS: Record = { + all: 'Toate', + ro: '🇷🇴 România', + de: '🇩🇪 Germania', + uk: '🇬🇧 UK', + us: '🇺🇸 SUA', +}; + +const FLAG: Record = { ro: '🇷🇴', de: '🇩🇪', uk: '🇬🇧', us: '🇺🇸' }; + +// ─── Component ──────────────────────────────────────────────────────────────── + +export default function OsintPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const [tab, setTab] = useState('lookup'); + + // Company lookup state + const [lookupName, setLookupName] = useState(''); + const [country, setCountry] = useState('all'); + const [lookupResult, setLookupResult] = useState(null); + + // Sanctions state + const [sanctionsName, setSanctionsName] = useState(''); + const [sanctionsResult, setSanctionsResult] = useState(null); + + const lookupMutation = useMutation({ + mutationFn: (payload: { name: string; country: Country }) => + apiFetch('/v1/dataquery/company/lookup', { + tenantId, + method: 'POST', + body: { name: payload.name, country: payload.country === 'all' ? undefined : payload.country }, + }), + onSuccess: (data) => setLookupResult(data), + }); + + const sanctionsMutation = useMutation({ + mutationFn: (name: string) => + apiFetch('/v1/dataquery/sanctions', { + tenantId, + method: 'POST', + body: { name }, + }), + onSuccess: (data) => setSanctionsResult(data), + }); + + return ( +
+ {/* Header */} +
+

OSINT / Intelligence

+

+ Căutare în registrele naționale RO · DE · UK · US și screening sancțiuni OFAC + OpenSanctions. +

+
+ + {/* Tabs */} +
+ {(['lookup', 'sanctions'] as const).map((t) => ( + + ))} +
+ + {/* ── Company Lookup Tab ─────────────────────────────────────────────── */} + {tab === 'lookup' && ( +
+
{ + e.preventDefault(); + if (lookupName.trim().length >= 2) { + lookupMutation.mutate({ name: lookupName.trim(), country }); + } + }} + className="card mb-6 flex flex-col gap-3 p-4 sm:flex-row sm:items-end" + > +
+ + setLookupName(e.target.value)} + minLength={2} + required + /> +
+
+ + +
+ +
+ + {lookupMutation.isError && ( +

+ {lookupMutation.error instanceof Error + ? lookupMutation.error.message + : 'Eroare la căutare'} +

+ )} + + {lookupResult && ( +
+
+ + {lookupResult.total_hits} rezultate + + pentru “{lookupResult.query}” +
+ + {lookupResult.errors.length > 0 && ( +
+ ⚠ Unele surse au eșuat: {lookupResult.errors.join('; ')} +
+ )} + +
+ {lookupResult.results.map((r) => ( +
+
+ {FLAG[r.country] ?? '🌐'} + {r.label} + {r.count} înregistrări +
+ {r.count === 0 ? ( +

Niciun rezultat.

+ ) : ( +
+ + + + {r.columns.map((col) => ( + + ))} + + + + {r.rows.map((row, i) => ( + + {(row as unknown[]).map((cell, j) => ( + + ))} + + ))} + +
+ {col} +
+ {cell == null ? '—' : String(cell)} +
+
+ )} +
+ ))} +
+
+ )} +
+ )} + + {/* ── Sanctions Tab ─────────────────────────────────────────────────── */} + {tab === 'sanctions' && ( +
+
{ + e.preventDefault(); + if (sanctionsName.trim().length >= 2) { + sanctionsMutation.mutate(sanctionsName.trim()); + } + }} + className="card mb-6 flex gap-3 p-4" + > + setSanctionsName(e.target.value)} + minLength={2} + required + /> + +
+ + {sanctionsMutation.isError && ( +

+ {sanctionsMutation.error instanceof Error + ? sanctionsMutation.error.message + : 'Eroare la verificare'} +

+ )} + + {sanctionsResult && ( +
+ {/* Verdict banner */} +
+ + {sanctionsResult.verdict === 'HIT' ? '🚨' : '✅'} + +
+

+ {sanctionsResult.verdict === 'HIT' ? 'POTRIVIRE DETECTATĂ' : 'CLAR — fără potriviri'} +

+

+ Verificat la {new Date(sanctionsResult.checked_at).toLocaleString('ro-RO')} ·{' '} + {sanctionsResult.name} +

+
+
+ + {/* OFAC */} +
+
+ 🇺🇸 OFAC SDN List + 0 ? 'text-signal-danger' : 'text-signal-ok' + }`} + > + {sanctionsResult.ofac.hits} potriviri + +
+ {sanctionsResult.ofac.error ? ( +

+ ⚠ {sanctionsResult.ofac.error} +

+ ) : sanctionsResult.ofac.hits === 0 ? ( +

Nicio înregistrare în lista OFAC SDN.

+ ) : ( +
+ + + + {sanctionsResult.ofac.columns.map((col) => ( + + ))} + + + + {sanctionsResult.ofac.rows.map((row, i) => ( + + {(row as unknown[]).map((cell, j) => ( + + ))} + + ))} + +
+ {col} +
+ {cell == null ? '—' : String(cell)} +
+
+ )} +
+ + {/* OpenSanctions */} +
+
+ 🌐 OpenSanctions (Yente) + 0 ? 'text-signal-danger' : 'text-signal-ok' + }`} + > + {sanctionsResult.yente.hits} potriviri + +
+ {sanctionsResult.yente.error ? ( +

+ ⚠ {sanctionsResult.yente.error} +

+ ) : sanctionsResult.yente.hits === 0 ? ( +

+ Nicio înregistrare în OpenSanctions. +

+ ) : ( +
+ {sanctionsResult.yente.results.map((hit) => ( +
+
+

{hit.caption}

+

+ {hit.schema} · Seturi:{' '} + {Array.isArray(hit.datasets) ? hit.datasets.join(', ') : '—'} +

+
+ + {typeof hit.score === 'number' ? `${Math.round(hit.score * 100)}%` : '—'} + +
+ ))} +
+ )} +
+
+ )} +
+ )} +
+ ); +}