amliq-dashboard 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/.env.production +1 -0
- package/Dockerfile +26 -0
- package/QUICK_START.md +267 -0
- package/README.md +161 -0
- package/TESTING_GUIDE.md +322 -0
- package/dist/.well-known/ai-plugin.json +15 -0
- package/dist/_headers +8 -0
- package/dist/_redirects +1 -0
- package/dist/assets/APIKeys-BvwFauIs.js +6 -0
- package/dist/assets/APIKeys-DaMSBZQL.js +1 -0
- package/dist/assets/AdverseMedia-DckXfMzS.js +4 -0
- package/dist/assets/AlertDetailPage-Bc2_zwu_.js +1 -0
- package/dist/assets/AlertQueue-DFtBiRoM.js +8 -0
- package/dist/assets/Analytics-DtruB5lQ.js +1 -0
- package/dist/assets/AuditTrail-BK68ePu0.js +1 -0
- package/dist/assets/AuthDivider-gRHEt7gx.js +1 -0
- package/dist/assets/Badge-B9VFkofy.js +1 -0
- package/dist/assets/BatchJobs-Bz6KFnXD.js +6 -0
- package/dist/assets/BillingPage-R9nQ5nFb.js +11 -0
- package/dist/assets/Card-Cw-T_-oU.js +1 -0
- package/dist/assets/CaseDetail-DYs7Zg_y.js +7 -0
- package/dist/assets/CaseManagement-B1Q4Dp1Y.js +1 -0
- package/dist/assets/ComplianceReport-Dhssqc9T.js +1 -0
- package/dist/assets/Configuration-B92kl9T0.js +1 -0
- package/dist/assets/CryptoScreening-C7yFlskC.js +1 -0
- package/dist/assets/Dashboard-w4-Nhv9q.js +17 -0
- package/dist/assets/DataSources-u3-Ri_5_.js +3 -0
- package/dist/assets/EDDWorkflow-CFzlEO0e.js +1 -0
- package/dist/assets/EmptyState-Do0xJAT9.js +6 -0
- package/dist/assets/ForgotPassword-BPYyQFQp.js +1 -0
- package/dist/assets/LandingPage-CjaP0zw4.js +41 -0
- package/dist/assets/ListsMarketplace-CG6KkT9B.js +6 -0
- package/dist/assets/Login-677LEGP1.js +1 -0
- package/dist/assets/MFASetup-BxqCPvui.js +1 -0
- package/dist/assets/Monitoring-9TN9MK4y.js +1 -0
- package/dist/assets/Onboarding-Cf0Y83lX.js +1 -0
- package/dist/assets/Operations-CcXwc2xw.js +10 -0
- package/dist/assets/Overview-B_Ky0VWV.js +1 -0
- package/dist/assets/PEPScreening-pUQjTH9c.js +1 -0
- package/dist/assets/PageHeader-BObHFn0i.js +1 -0
- package/dist/assets/PrivacyPolicy-DZ2xrKir.js +1 -0
- package/dist/assets/ResetPassword-AtxtpIG1.js +1 -0
- package/dist/assets/RiskAssessment-Dzuh1Usv.js +1 -0
- package/dist/assets/SARForm-D8w2ZGe-.js +1 -0
- package/dist/assets/SanctionsLists-CPA3UH2v.js +1 -0
- package/dist/assets/ScheduledTasks-Dsjk-6UR.js +1 -0
- package/dist/assets/ScreenEntity-D1U0YL7v.js +3 -0
- package/dist/assets/ScreeningProgress-BW49PzoB.js +66 -0
- package/dist/assets/SearchField-CulFKdiI.js +1 -0
- package/dist/assets/Signup-C0FmFOo_.js +1 -0
- package/dist/assets/SystemHealth-B_8u4eva.js +1 -0
- package/dist/assets/TaskHistory-BCmEM89q.js +3 -0
- package/dist/assets/Team-CUo_FlU7.js +1 -0
- package/dist/assets/TenantDetail-CgZQHUY8.js +1 -0
- package/dist/assets/Tenants-RB9E9ick.js +2 -0
- package/dist/assets/TermsOfService-BfL-kndX.js +1 -0
- package/dist/assets/TransactionMonitoring-DGh_T22s.js +14 -0
- package/dist/assets/TxnScreening-B7cm2-eh.js +1 -0
- package/dist/assets/UBOChain-falXAsCo.js +1 -0
- package/dist/assets/Users-Doakpg9c.js +1 -0
- package/dist/assets/Webhooks-BZpAfaxv.js +1 -0
- package/dist/assets/alert-triangle-YCFVm7B_.js +6 -0
- package/dist/assets/alerts-COUnTwWY.js +1 -0
- package/dist/assets/arrow-left-9ApLqP95.js +6 -0
- package/dist/assets/check-VTcvVtIU.js +6 -0
- package/dist/assets/check-circle-2-Dn-lG5YQ.js +6 -0
- package/dist/assets/code-DIJRWFVv.js +6 -0
- package/dist/assets/fingerprint-C0MGVtax.js +6 -0
- package/dist/assets/flag-B8_Gwxh6.js +6 -0
- package/dist/assets/index-CfdYcqRM.js +289 -0
- package/dist/assets/index-HPU_Rhdu.css +1 -0
- package/dist/assets/layers-zHz2o4vo.js +6 -0
- package/dist/assets/loader-B9_dNihg.js +6 -0
- package/dist/assets/mail-BijL2qwp.js +6 -0
- package/dist/assets/plus-i0oBIIPS.js +6 -0
- package/dist/assets/refresh-cw-CSuAwutt.js +6 -0
- package/dist/assets/useAnalytics-Bj8IONcw.js +72 -0
- package/dist/assets/x-circle-DLGKvijE.js +6 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +51 -0
- package/dist/llms.txt +28 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +17 -0
- package/dist/manifest.json +44 -0
- package/dist/robots.txt +15 -0
- package/dist/schema.json +13 -0
- package/dist/sitemap.xml +28 -0
- package/dist/sw.js +67 -0
- package/e2e/auth-setup.ts +20 -0
- package/e2e/billing.spec.ts +50 -0
- package/e2e/cases.spec.ts +53 -0
- package/e2e/compliance.spec.ts +65 -0
- package/e2e/config.spec.ts +56 -0
- package/e2e/dashboard.spec.ts +47 -0
- package/e2e/fixtures.ts +33 -0
- package/e2e/lists.spec.ts +43 -0
- package/e2e/login.spec.ts +64 -0
- package/e2e/media.spec.ts +48 -0
- package/e2e/mocks.ts +51 -0
- package/e2e/monitoring.spec.ts +44 -0
- package/e2e/navigation.spec.ts +56 -0
- package/e2e/onboarding.spec.ts +49 -0
- package/e2e/responsive.spec.ts +40 -0
- package/e2e/risk.spec.ts +61 -0
- package/e2e/screening.spec.ts +76 -0
- package/e2e/team.spec.ts +53 -0
- package/index.html +50 -0
- package/package.json +47 -0
- package/playwright.config.ts +35 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/ai-plugin.json +15 -0
- package/public/_headers +8 -0
- package/public/_redirects +1 -0
- package/public/favicon.svg +15 -0
- package/public/llms.txt +28 -0
- package/public/logo.png +0 -0
- package/public/logo.svg +17 -0
- package/public/manifest.json +44 -0
- package/public/robots.txt +15 -0
- package/public/schema.json +13 -0
- package/public/sitemap.xml +28 -0
- package/public/sw.js +67 -0
- package/scripts/test-runner.sh +152 -0
- package/src/App.tsx +48 -0
- package/src/api/alerts.ts +19 -0
- package/src/api/analytics.ts +6 -0
- package/src/api/audit.ts +11 -0
- package/src/api/auth.ts +26 -0
- package/src/api/billing.ts +54 -0
- package/src/api/cases.ts +48 -0
- package/src/api/client.ts +50 -0
- package/src/api/config.ts +30 -0
- package/src/api/edd.ts +25 -0
- package/src/api/enforcement.ts +33 -0
- package/src/api/lists.ts +24 -0
- package/src/api/monitoring.ts +82 -0
- package/src/api/pep.ts +30 -0
- package/src/api/risk.ts +26 -0
- package/src/api/screening.ts +37 -0
- package/src/api/team.ts +27 -0
- package/src/api/transactions.ts +37 -0
- package/src/components/admin/TenantCards.tsx +51 -0
- package/src/components/alerts/AlertActions.test.tsx +80 -0
- package/src/components/alerts/AlertActions.tsx +68 -0
- package/src/components/alerts/AlertCard.test.tsx +86 -0
- package/src/components/alerts/AlertCard.tsx +60 -0
- package/src/components/alerts/AlertDetailSidebar.tsx +63 -0
- package/src/components/alerts/AlertFilters.test.tsx +73 -0
- package/src/components/alerts/AlertFilters.tsx +88 -0
- package/src/components/alerts/EntityDetailsCard.test.tsx +61 -0
- package/src/components/alerts/EntityDetailsCard.tsx +37 -0
- package/src/components/alerts/NotesCard.test.tsx +39 -0
- package/src/components/alerts/NotesCard.tsx +28 -0
- package/src/components/auth/AuthDivider.tsx +18 -0
- package/src/components/auth/LoginForm.tsx +62 -0
- package/src/components/auth/LoginLeftPanel.tsx +43 -0
- package/src/components/auth/PasswordStrength.tsx +26 -0
- package/src/components/auth/SignInButtons.tsx +46 -0
- package/src/components/auth/SignupLeftPanel.tsx +35 -0
- package/src/components/auth/countries.ts +17 -0
- package/src/components/charts/AreaChart.tsx +45 -0
- package/src/components/charts/BarChart.tsx +48 -0
- package/src/components/charts/DonutChart.tsx +47 -0
- package/src/components/compliance/CaseActions.tsx +74 -0
- package/src/components/compliance/CaseTimeline.tsx +68 -0
- package/src/components/compliance/MediaResultCard.tsx +87 -0
- package/src/components/compliance/PEPResultCard.tsx +78 -0
- package/src/components/config/MatchingModesCard.test.tsx +75 -0
- package/src/components/config/MatchingModesCard.tsx +40 -0
- package/src/components/config/ScreeningLayersCard.test.tsx +57 -0
- package/src/components/config/ScreeningLayersCard.tsx +36 -0
- package/src/components/config/ThresholdsCard.test.tsx +79 -0
- package/src/components/config/ThresholdsCard.tsx +51 -0
- package/src/components/dashboard/ActivityFeed.tsx +93 -0
- package/src/components/dashboard/DashboardGreeting.tsx +23 -0
- package/src/components/dashboard/DashboardSkeleton.tsx +35 -0
- package/src/components/dashboard/QuickActions.tsx +52 -0
- package/src/components/dashboard/TopEntitiesTable.tsx +63 -0
- package/src/components/data/ComplianceMetrics.test.tsx +32 -0
- package/src/components/data/ComplianceMetrics.tsx +40 -0
- package/src/components/data/ConfidenceScore.test.tsx +62 -0
- package/src/components/data/ConfidenceScore.tsx +27 -0
- package/src/components/data/StatCard.test.tsx +72 -0
- package/src/components/data/StatCard.tsx +63 -0
- package/src/components/data/StatusBadge.test.tsx +63 -0
- package/src/components/data/StatusBadge.tsx +39 -0
- package/src/components/layout/AppShell.tsx +48 -0
- package/src/components/layout/Breadcrumbs.tsx +48 -0
- package/src/components/layout/CommandPalette.tsx +81 -0
- package/src/components/layout/DashboardLayout.tsx +19 -0
- package/src/components/layout/MobileHeader.tsx +30 -0
- package/src/components/layout/NavGroup.test.tsx +63 -0
- package/src/components/layout/NavGroup.tsx +64 -0
- package/src/components/layout/NotificationBell.tsx +89 -0
- package/src/components/layout/PageHeader.test.tsx +61 -0
- package/src/components/layout/PageHeader.tsx +19 -0
- package/src/components/layout/ProtectedRoute.tsx +31 -0
- package/src/components/layout/PublicLayout.tsx +17 -0
- package/src/components/layout/Sidebar.test.tsx +67 -0
- package/src/components/layout/Sidebar.tsx +92 -0
- package/src/components/layout/Toolbar.test.tsx +47 -0
- package/src/components/layout/Toolbar.tsx +68 -0
- package/src/components/layout/navItems.ts +94 -0
- package/src/components/lists/ListCard.tsx +78 -0
- package/src/components/lists/ListMarketplaceCard.tsx +77 -0
- package/src/components/screening/CircularConfidence.tsx +38 -0
- package/src/components/screening/LimitReachedBanner.tsx +31 -0
- package/src/components/screening/ListSelector.tsx +64 -0
- package/src/components/screening/MatchDetailHeader.tsx +64 -0
- package/src/components/screening/MatchEntityInfo.tsx +56 -0
- package/src/components/screening/MatchEvidenceBars.tsx +65 -0
- package/src/components/screening/MatchMetadata.tsx +95 -0
- package/src/components/screening/ScreenResults.tsx +49 -0
- package/src/components/screening/ScreeningForm.test.tsx +83 -0
- package/src/components/screening/ScreeningForm.tsx +100 -0
- package/src/components/screening/ScreeningLayersList.test.tsx +33 -0
- package/src/components/screening/ScreeningLayersList.tsx +46 -0
- package/src/components/screening/ScreeningProgress.tsx +91 -0
- package/src/components/screening/ScreeningQuotaBanner.tsx +64 -0
- package/src/components/screening/ScreeningResultCard.tsx +47 -0
- package/src/components/screening/ScreeningResultRow.tsx +45 -0
- package/src/components/screening/ScreeningResults.test.tsx +37 -0
- package/src/components/screening/ScreeningResults.tsx +33 -0
- package/src/components/screening/ShareResults.tsx +103 -0
- package/src/components/screening/ThresholdSlider.tsx +23 -0
- package/src/components/transactions/WebhookCTA.tsx +63 -0
- package/src/components/ui/Avatar.test.tsx +47 -0
- package/src/components/ui/Avatar.tsx +35 -0
- package/src/components/ui/Badge.test.tsx +49 -0
- package/src/components/ui/Badge.tsx +33 -0
- package/src/components/ui/Button.test.tsx +56 -0
- package/src/components/ui/Button.tsx +46 -0
- package/src/components/ui/Card.test.tsx +61 -0
- package/src/components/ui/Card.tsx +29 -0
- package/src/components/ui/ConfirmModal.tsx +67 -0
- package/src/components/ui/Divider.test.tsx +24 -0
- package/src/components/ui/Divider.tsx +5 -0
- package/src/components/ui/EmptyState.test.tsx +49 -0
- package/src/components/ui/EmptyState.tsx +22 -0
- package/src/components/ui/ErrorBoundary.tsx +44 -0
- package/src/components/ui/ExportMenu.tsx +71 -0
- package/src/components/ui/LanguageSwitcher.tsx +37 -0
- package/src/components/ui/LoadingSpinner.test.tsx +41 -0
- package/src/components/ui/LoadingSpinner.tsx +21 -0
- package/src/components/ui/MetricCard.tsx +63 -0
- package/src/components/ui/ScoreRing.tsx +51 -0
- package/src/components/ui/SearchField.test.tsx +55 -0
- package/src/components/ui/SearchField.tsx +31 -0
- package/src/components/ui/SeverityBadge.tsx +57 -0
- package/src/components/ui/ThemeToggle.tsx +37 -0
- package/src/components/ui/Toast.test.tsx +79 -0
- package/src/components/ui/Toast.tsx +75 -0
- package/src/components/ui/Toggle.test.tsx +85 -0
- package/src/components/ui/Toggle.tsx +46 -0
- package/src/context/AuthContext.tsx +71 -0
- package/src/data/pepProfiles.ts +76 -0
- package/src/data/pepProfilesExtra.ts +58 -0
- package/src/hooks/useAlerts.ts +36 -0
- package/src/hooks/useAnalytics.ts +23 -0
- package/src/hooks/useApi.test.ts +79 -0
- package/src/hooks/useApi.ts +33 -0
- package/src/hooks/useAudit.ts +28 -0
- package/src/hooks/useBilling.ts +38 -0
- package/src/hooks/useConfig.ts +35 -0
- package/src/hooks/useDebounce.test.ts +84 -0
- package/src/hooks/useDebounce.ts +15 -0
- package/src/hooks/useDirection.ts +15 -0
- package/src/hooks/useLists.ts +34 -0
- package/src/hooks/useMediaQuery.test.ts +97 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/useScreening.ts +33 -0
- package/src/hooks/useSidebar.ts +18 -0
- package/src/hooks/useUsage.ts +27 -0
- package/src/i18n/config.ts +33 -0
- package/src/i18n/locales/ar/admin.json +19 -0
- package/src/i18n/locales/ar/alerts.json +52 -0
- package/src/i18n/locales/ar/analytics.json +9 -0
- package/src/i18n/locales/ar/audit.json +12 -0
- package/src/i18n/locales/ar/auth.json +60 -0
- package/src/i18n/locales/ar/batch.json +5 -0
- package/src/i18n/locales/ar/billing.json +41 -0
- package/src/i18n/locales/ar/common.json +65 -0
- package/src/i18n/locales/ar/compliance.json +83 -0
- package/src/i18n/locales/ar/config.json +13 -0
- package/src/i18n/locales/ar/dashboard.json +19 -0
- package/src/i18n/locales/ar/errors.json +9 -0
- package/src/i18n/locales/ar/index.ts +29 -0
- package/src/i18n/locales/ar/legal.json +10 -0
- package/src/i18n/locales/ar/lists.json +6 -0
- package/src/i18n/locales/ar/marketing.json +110 -0
- package/src/i18n/locales/ar/monitoring.json +15 -0
- package/src/i18n/locales/ar/nav.json +35 -0
- package/src/i18n/locales/ar/onboarding.json +25 -0
- package/src/i18n/locales/ar/platform.json +23 -0
- package/src/i18n/locales/ar/screening.json +26 -0
- package/src/i18n/locales/ar/team.json +11 -0
- package/src/i18n/locales/en/admin.json +21 -0
- package/src/i18n/locales/en/alerts.json +52 -0
- package/src/i18n/locales/en/analytics.json +9 -0
- package/src/i18n/locales/en/audit.json +12 -0
- package/src/i18n/locales/en/auth.json +60 -0
- package/src/i18n/locales/en/batch.json +5 -0
- package/src/i18n/locales/en/billing.json +87 -0
- package/src/i18n/locales/en/common.json +65 -0
- package/src/i18n/locales/en/compliance.json +83 -0
- package/src/i18n/locales/en/config.json +29 -0
- package/src/i18n/locales/en/dashboard.json +23 -0
- package/src/i18n/locales/en/errors.json +9 -0
- package/src/i18n/locales/en/index.ts +29 -0
- package/src/i18n/locales/en/legal.json +114 -0
- package/src/i18n/locales/en/lists.json +6 -0
- package/src/i18n/locales/en/marketing.json +174 -0
- package/src/i18n/locales/en/monitoring.json +15 -0
- package/src/i18n/locales/en/nav.json +35 -0
- package/src/i18n/locales/en/onboarding.json +31 -0
- package/src/i18n/locales/en/platform.json +25 -0
- package/src/i18n/locales/en/screening.json +26 -0
- package/src/i18n/locales/en/team.json +12 -0
- package/src/i18n/locales/he/admin.json +19 -0
- package/src/i18n/locales/he/alerts.json +52 -0
- package/src/i18n/locales/he/analytics.json +9 -0
- package/src/i18n/locales/he/audit.json +12 -0
- package/src/i18n/locales/he/auth.json +60 -0
- package/src/i18n/locales/he/batch.json +5 -0
- package/src/i18n/locales/he/billing.json +41 -0
- package/src/i18n/locales/he/common.json +65 -0
- package/src/i18n/locales/he/compliance.json +83 -0
- package/src/i18n/locales/he/config.json +13 -0
- package/src/i18n/locales/he/dashboard.json +19 -0
- package/src/i18n/locales/he/errors.json +9 -0
- package/src/i18n/locales/he/index.ts +29 -0
- package/src/i18n/locales/he/legal.json +10 -0
- package/src/i18n/locales/he/lists.json +6 -0
- package/src/i18n/locales/he/marketing.json +110 -0
- package/src/i18n/locales/he/monitoring.json +15 -0
- package/src/i18n/locales/he/nav.json +35 -0
- package/src/i18n/locales/he/onboarding.json +25 -0
- package/src/i18n/locales/he/platform.json +23 -0
- package/src/i18n/locales/he/screening.json +26 -0
- package/src/i18n/locales/he/team.json +11 -0
- package/src/index.css +112 -0
- package/src/main.tsx +15 -0
- package/src/pages/APIKeys.tsx +120 -0
- package/src/pages/AddMonitorModal.tsx +30 -0
- package/src/pages/AdverseMedia.test.tsx +18 -0
- package/src/pages/AdverseMedia.tsx +89 -0
- package/src/pages/AlertDetailPage.tsx +64 -0
- package/src/pages/AlertQueue.test.tsx +48 -0
- package/src/pages/AlertQueue.tsx +63 -0
- package/src/pages/Analytics.tsx +50 -0
- package/src/pages/AuditTrail.tsx +64 -0
- package/src/pages/BatchJobs.tsx +79 -0
- package/src/pages/CaseDetail.tsx +72 -0
- package/src/pages/CaseManagement.test.tsx +31 -0
- package/src/pages/CaseManagement.tsx +92 -0
- package/src/pages/Configuration.test.tsx +96 -0
- package/src/pages/Configuration.tsx +123 -0
- package/src/pages/CryptoScreening.tsx +109 -0
- package/src/pages/Dashboard.test.tsx +51 -0
- package/src/pages/Dashboard.tsx +66 -0
- package/src/pages/EDDWorkflow.test.tsx +29 -0
- package/src/pages/EDDWorkflow.tsx +73 -0
- package/src/pages/ForgotPassword.test.tsx +49 -0
- package/src/pages/ForgotPassword.tsx +67 -0
- package/src/pages/ListsMarketplace.tsx +102 -0
- package/src/pages/Login.test.tsx +100 -0
- package/src/pages/Login.tsx +57 -0
- package/src/pages/MFASetup.tsx +114 -0
- package/src/pages/MonitorProfileCard.tsx +27 -0
- package/src/pages/Monitoring.tsx +68 -0
- package/src/pages/Onboarding.test.tsx +36 -0
- package/src/pages/Onboarding.tsx +60 -0
- package/src/pages/PEPScreening.test.tsx +15 -0
- package/src/pages/PEPScreening.tsx +100 -0
- package/src/pages/ResetPassword.tsx +81 -0
- package/src/pages/RiskAssessment.test.tsx +15 -0
- package/src/pages/RiskAssessment.tsx +108 -0
- package/src/pages/SanctionsLists.tsx +74 -0
- package/src/pages/ScreenEntity.test.tsx +82 -0
- package/src/pages/ScreenEntity.tsx +76 -0
- package/src/pages/Signup.test.tsx +98 -0
- package/src/pages/Signup.tsx +92 -0
- package/src/pages/TaskHistory.tsx +183 -0
- package/src/pages/Team.test.tsx +15 -0
- package/src/pages/Team.tsx +140 -0
- package/src/pages/TransactionMonitoring.test.tsx +18 -0
- package/src/pages/TransactionMonitoring.tsx +118 -0
- package/src/pages/TxnScreening.tsx +125 -0
- package/src/pages/UBOChain.test.tsx +35 -0
- package/src/pages/UBOChain.tsx +65 -0
- package/src/pages/Webhooks.tsx +137 -0
- package/src/pages/admin/DataSources.tsx +230 -0
- package/src/pages/admin/Operations.tsx +103 -0
- package/src/pages/admin/ScheduledTasks.tsx +155 -0
- package/src/pages/admin/SystemHealth.tsx +58 -0
- package/src/pages/admin/TenantDetail.tsx +62 -0
- package/src/pages/admin/Tenants.test.tsx +20 -0
- package/src/pages/admin/Tenants.tsx +63 -0
- package/src/pages/admin/opsRunners.ts +81 -0
- package/src/pages/admin/opsTerminal.tsx +63 -0
- package/src/pages/billing/ActiveSubscriptions.tsx +41 -0
- package/src/pages/billing/AddProductModal.tsx +99 -0
- package/src/pages/billing/BillingPage.test.tsx +30 -0
- package/src/pages/billing/BillingPage.tsx +67 -0
- package/src/pages/billing/CurrentPlan.tsx +50 -0
- package/src/pages/billing/InvoiceList.tsx +104 -0
- package/src/pages/billing/InvoiceRow.tsx +37 -0
- package/src/pages/billing/LemonSqueezySetup.tsx +51 -0
- package/src/pages/billing/PaymentAlert.tsx +33 -0
- package/src/pages/billing/PlanComparison.tsx +53 -0
- package/src/pages/billing/ProductUsage.tsx +43 -0
- package/src/pages/billing/PromoCodeInput.tsx +58 -0
- package/src/pages/billing/SeatManager.tsx +80 -0
- package/src/pages/billing/SeatRow.tsx +32 -0
- package/src/pages/billing/SubscriptionCard.tsx +73 -0
- package/src/pages/billing/UpgradeModal.tsx +53 -0
- package/src/pages/billing/UsageHistory.tsx +37 -0
- package/src/pages/billing/UsageMeter.tsx +26 -0
- package/src/pages/billing/UsageOverview.tsx +38 -0
- package/src/pages/legal/PrivacyPolicy.tsx +25 -0
- package/src/pages/legal/TermsOfService.tsx +25 -0
- package/src/pages/marketing/BundleCallout.tsx +24 -0
- package/src/pages/marketing/CTASection.tsx +48 -0
- package/src/pages/marketing/CaseStudy.tsx +37 -0
- package/src/pages/marketing/ComparisonTable.tsx +66 -0
- package/src/pages/marketing/CompetitiveEdge.tsx +55 -0
- package/src/pages/marketing/DataCoverage.tsx +54 -0
- package/src/pages/marketing/DataRain.tsx +30 -0
- package/src/pages/marketing/EnterpriseCTA.tsx +26 -0
- package/src/pages/marketing/FAQItem.tsx +27 -0
- package/src/pages/marketing/FAQSchema.tsx +43 -0
- package/src/pages/marketing/FAQSection.tsx +19 -0
- package/src/pages/marketing/FeatureDetail.tsx +19 -0
- package/src/pages/marketing/FeaturesGrid.tsx +60 -0
- package/src/pages/marketing/FeaturesSpotlight.tsx +68 -0
- package/src/pages/marketing/FooterSection.tsx +92 -0
- package/src/pages/marketing/GradientOrbs.tsx +26 -0
- package/src/pages/marketing/HeroSearch.tsx +113 -0
- package/src/pages/marketing/HeroSection.tsx +72 -0
- package/src/pages/marketing/LandingPage.tsx +45 -0
- package/src/pages/marketing/LogoCloud.tsx +31 -0
- package/src/pages/marketing/LogoMarquee.tsx +45 -0
- package/src/pages/marketing/MarketingNav.tsx +80 -0
- package/src/pages/marketing/MatchingLayers.tsx +78 -0
- package/src/pages/marketing/MobileMenu.tsx +45 -0
- package/src/pages/marketing/PricingCard.tsx +57 -0
- package/src/pages/marketing/PricingFeatureRow.tsx +16 -0
- package/src/pages/marketing/PricingSection.tsx +103 -0
- package/src/pages/marketing/PricingToggle.tsx +40 -0
- package/src/pages/marketing/ProductPricingCards.tsx +23 -0
- package/src/pages/marketing/ProductShowcase.tsx +81 -0
- package/src/pages/marketing/ProductTabs.tsx +45 -0
- package/src/pages/marketing/QuoteRotator.tsx +56 -0
- package/src/pages/marketing/StatsBar.tsx +81 -0
- package/src/pages/marketing/StatsSection.tsx +44 -0
- package/src/pages/marketing/TestimonialCard.tsx +38 -0
- package/src/pages/marketing/TestimonialsSection.tsx +32 -0
- package/src/pages/marketing/animations.tsx +56 -0
- package/src/pages/onboarding/CountryStep.tsx +38 -0
- package/src/pages/onboarding/ListsStep.tsx +44 -0
- package/src/pages/onboarding/StepIndicator.tsx +34 -0
- package/src/pages/onboarding/ThresholdStep.tsx +49 -0
- package/src/pages/platform/APIKeys.tsx +102 -0
- package/src/pages/platform/Overview.tsx +58 -0
- package/src/pages/platform/Users.tsx +110 -0
- package/src/pages/reporting/ComplianceReport.tsx +99 -0
- package/src/pages/reporting/SARForm.tsx +99 -0
- package/src/routes/appRoutes.tsx +60 -0
- package/src/routes/compliance.tsx +28 -0
- package/src/routes/lazyCompliance.ts +34 -0
- package/src/routes/lazyPages.ts +35 -0
- package/src/routes/lazyPlatform.ts +5 -0
- package/src/routes/platform.tsx +15 -0
- package/src/styles/effects.css +76 -0
- package/src/test/setup.ts +25 -0
- package/src/test/utils.tsx +49 -0
- package/src/types/alert.ts +31 -0
- package/src/types/analytics.ts +16 -0
- package/src/types/audit.ts +11 -0
- package/src/types/billing.ts +60 -0
- package/src/types/common.ts +15 -0
- package/src/types/config.ts +19 -0
- package/src/types/entity.ts +34 -0
- package/src/types/index.ts +8 -0
- package/src/types/list.ts +15 -0
- package/src/types/screening.ts +32 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +65 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +11 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useParams } from 'react-router-dom'
|
|
3
|
+
import { useTranslation } from 'react-i18next'
|
|
4
|
+
import { api } from '../api/client'
|
|
5
|
+
|
|
6
|
+
interface EDDReport {
|
|
7
|
+
id: string
|
|
8
|
+
entity_name: string
|
|
9
|
+
status: string
|
|
10
|
+
checklist: Record<string, boolean>
|
|
11
|
+
risk_level: string
|
|
12
|
+
notes: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const checkKeys: Record<string, string> = {
|
|
16
|
+
identity_verified: 'edd.identity',
|
|
17
|
+
source_of_funds: 'edd.source_of_funds',
|
|
18
|
+
source_of_wealth: 'edd.source_of_wealth',
|
|
19
|
+
pep_screening: 'edd.pep_screening',
|
|
20
|
+
adverse_media_check: 'edd.adverse_media',
|
|
21
|
+
sanctions_screening: 'edd.sanctions',
|
|
22
|
+
ubo_verification: 'edd.ubo_verification',
|
|
23
|
+
country_risk_assessment: 'edd.country_risk',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function EDDWorkflow() {
|
|
27
|
+
const { t } = useTranslation('compliance')
|
|
28
|
+
const { id } = useParams<{ id: string }>()
|
|
29
|
+
const [report, setReport] = useState<EDDReport | null>(null)
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!id) return
|
|
33
|
+
api.get<EDDReport>(`/edd/${id}`)
|
|
34
|
+
.then(d => setReport(d ?? null))
|
|
35
|
+
.catch(() => setReport(null))
|
|
36
|
+
}, [id])
|
|
37
|
+
|
|
38
|
+
if (!report) return <div className="p-lg" role="status" aria-live="polite">Loading...</div>
|
|
39
|
+
|
|
40
|
+
const completed = Object.values(report.checklist).filter(Boolean).length
|
|
41
|
+
const total = Object.keys(report.checklist).length
|
|
42
|
+
const progress = total > 0 ? (completed / total) * 100 : 0
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="px-md py-lg sm:p-8 max-w-4xl mx-auto">
|
|
46
|
+
<h1 className="text-xl sm:text-2xl font-semibold mb-2">{t('edd.title')}</h1>
|
|
47
|
+
<p className="text-apple-label-secondary mb-6">{report.entity_name}</p>
|
|
48
|
+
<div className="card-vibrancy p-6">
|
|
49
|
+
<div className="flex justify-between mb-2 text-sm">
|
|
50
|
+
<span>{t('edd.progress')}</span>
|
|
51
|
+
<span>{completed}/{total} {t('edd.checks')}</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="w-full bg-apple-bg-tertiary rounded-full h-2 mb-6">
|
|
54
|
+
<div className="bg-apple-green h-2 rounded-full"
|
|
55
|
+
style={{ width: `${progress}%` }} />
|
|
56
|
+
</div>
|
|
57
|
+
<div className="space-y-3">
|
|
58
|
+
{Object.entries(report.checklist).map(([key, done]) => (
|
|
59
|
+
<div key={key} className="flex items-center gap-3">
|
|
60
|
+
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
|
61
|
+
done ? 'bg-apple-green border-apple-green' : 'border-apple-label-tertiary'}`}>
|
|
62
|
+
{done && <span className="text-white text-xs">✓</span>}
|
|
63
|
+
</div>
|
|
64
|
+
<span className={done ? 'text-apple-label-tertiary line-through' : ''}>
|
|
65
|
+
{checkKeys[key] ? t(checkKeys[key]) : key}
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
4
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
5
|
+
import ForgotPassword from './ForgotPassword'
|
|
6
|
+
|
|
7
|
+
const renderForgot = () =>
|
|
8
|
+
render(
|
|
9
|
+
<MemoryRouter>
|
|
10
|
+
<ForgotPassword />
|
|
11
|
+
</MemoryRouter>
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
describe('ForgotPassword', () => {
|
|
15
|
+
it('renders title and description', () => {
|
|
16
|
+
renderForgot()
|
|
17
|
+
expect(screen.getByText(/reset your password/i)).toBeInTheDocument()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('renders email input', () => {
|
|
21
|
+
renderForgot()
|
|
22
|
+
expect(screen.getByLabelText(/email address/i)).toBeInTheDocument()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('renders submit button', () => {
|
|
26
|
+
renderForgot()
|
|
27
|
+
expect(screen.getByRole('button', { name: /send reset link/i })).toBeInTheDocument()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('renders back to login link', () => {
|
|
31
|
+
renderForgot()
|
|
32
|
+
expect(screen.getByText(/back to login/i)).toBeInTheDocument()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('shows success message after submission', async () => {
|
|
36
|
+
renderForgot()
|
|
37
|
+
await userEvent.type(screen.getByLabelText(/email address/i), 'user@test.com')
|
|
38
|
+
await userEvent.click(screen.getByRole('button', { name: /send reset link/i }))
|
|
39
|
+
expect(screen.getByText(/check your inbox/i)).toBeInTheDocument()
|
|
40
|
+
expect(screen.queryByRole('button', { name: /send reset link/i })).not.toBeInTheDocument()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('hides form after submission', async () => {
|
|
44
|
+
renderForgot()
|
|
45
|
+
await userEvent.type(screen.getByLabelText(/email address/i), 'user@test.com')
|
|
46
|
+
await userEvent.click(screen.getByRole('button', { name: /send reset link/i }))
|
|
47
|
+
expect(screen.queryByLabelText(/email address/i)).not.toBeInTheDocument()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { Link } from 'react-router-dom'
|
|
3
|
+
import { useTranslation } from 'react-i18next'
|
|
4
|
+
import { ArrowLeft, Mail } from 'lucide-react'
|
|
5
|
+
import { api } from '../api/client'
|
|
6
|
+
|
|
7
|
+
export function ForgotPassword() {
|
|
8
|
+
const { t } = useTranslation('auth')
|
|
9
|
+
const [email, setEmail] = useState('')
|
|
10
|
+
const [submitted, setSubmitted] = useState(false)
|
|
11
|
+
const [loading, setLoading] = useState(false)
|
|
12
|
+
const [error, setError] = useState('')
|
|
13
|
+
|
|
14
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
15
|
+
e.preventDefault()
|
|
16
|
+
setLoading(true)
|
|
17
|
+
setError('')
|
|
18
|
+
try {
|
|
19
|
+
await api.post('/auth/forgot-password', { email })
|
|
20
|
+
setSubmitted(true)
|
|
21
|
+
} catch (err) {
|
|
22
|
+
setError(err instanceof Error ? err.message : 'Request failed')
|
|
23
|
+
} finally {
|
|
24
|
+
setLoading(false)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="min-h-screen bg-apple-bg-primary flex items-center justify-center px-4">
|
|
30
|
+
<div className="w-full max-w-sm">
|
|
31
|
+
<Link to="/login"
|
|
32
|
+
className="inline-flex items-center gap-1.5 text-sm text-apple-label-secondary hover:text-white mb-8">
|
|
33
|
+
<ArrowLeft size={16} />
|
|
34
|
+
{t('forgot.back_to_login')}
|
|
35
|
+
</Link>
|
|
36
|
+
|
|
37
|
+
<h1 className="text-2xl font-bold text-white mb-2">{t('forgot.title')}</h1>
|
|
38
|
+
<p className="text-sm text-apple-label-secondary mb-8">{t('forgot.description')}</p>
|
|
39
|
+
|
|
40
|
+
{submitted ? (
|
|
41
|
+
<div className="rounded-apple-lg bg-green-500/10 border border-green-500/30 p-6 text-center">
|
|
42
|
+
<Mail size={32} className="text-green-400 mx-auto mb-3" />
|
|
43
|
+
<p className="text-white font-medium mb-1">{t('forgot.success_title')}</p>
|
|
44
|
+
<p className="text-sm text-apple-label-secondary">
|
|
45
|
+
{t('forgot.success_message', { email })}
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
) : (
|
|
49
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
50
|
+
<input type="email" required value={email}
|
|
51
|
+
onChange={e => setEmail(e.target.value)}
|
|
52
|
+
aria-label={t('forgot.email_placeholder')}
|
|
53
|
+
placeholder={t('forgot.email_placeholder')}
|
|
54
|
+
className="input-field w-full" autoComplete="email" autoFocus />
|
|
55
|
+
{error && <p role="alert" className="text-apple-red text-sm">{error}</p>}
|
|
56
|
+
<button type="submit" disabled={loading}
|
|
57
|
+
className="w-full py-2.5 rounded-apple-md bg-apple-blue text-white font-medium hover:bg-apple-blue/90 transition-colors cursor-pointer disabled:opacity-50">
|
|
58
|
+
{loading ? 'Sending...' : t('forgot.submit')}
|
|
59
|
+
</button>
|
|
60
|
+
</form>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default ForgotPassword
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Globe } from 'lucide-react';
|
|
3
|
+
import { SearchField } from '../components/ui/SearchField';
|
|
4
|
+
import { LoadingSpinner } from '../components/ui/LoadingSpinner';
|
|
5
|
+
import { ListMarketplaceCard } from '../components/lists/ListMarketplaceCard';
|
|
6
|
+
import { api } from '../api/client';
|
|
7
|
+
|
|
8
|
+
interface MarketplaceList {
|
|
9
|
+
id: string; name: string; description: string; region: string;
|
|
10
|
+
category: string; source_url: string; entity_count: number;
|
|
11
|
+
update_frequency: string; last_synced: string; enabled: boolean; tier: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const REGIONS = ['All', 'Global', 'Americas', 'Europe', 'Middle East', 'Asia-Pacific', 'Africa'];
|
|
15
|
+
const CATEGORIES = ['All', 'sanctions', 'pep', 'law_enforcement', 'regulatory'];
|
|
16
|
+
|
|
17
|
+
const categoryLabel: Record<string, string> = {
|
|
18
|
+
All: 'All', sanctions: 'Sanctions', pep: 'PEP',
|
|
19
|
+
law_enforcement: 'Law Enforcement', regulatory: 'Regulatory',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function ListsMarketplace() {
|
|
23
|
+
const [lists, setLists] = useState<MarketplaceList[]>([]);
|
|
24
|
+
const [loading, setLoading] = useState(true);
|
|
25
|
+
const [search, setSearch] = useState('');
|
|
26
|
+
const [region, setRegion] = useState('All');
|
|
27
|
+
const [category, setCategory] = useState('All');
|
|
28
|
+
|
|
29
|
+
const fetchLists = useCallback(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const data = await api.get<{ lists: MarketplaceList[] }>('/lists/marketplace');
|
|
32
|
+
setLists(data.lists);
|
|
33
|
+
} finally { setLoading(false); }
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => { fetchLists(); }, [fetchLists]);
|
|
37
|
+
|
|
38
|
+
const toggle = async (id: string, enabled: boolean) => {
|
|
39
|
+
const action = enabled ? 'disable' : 'enable';
|
|
40
|
+
await api.post(`/lists/marketplace/${id}/${action}`, {});
|
|
41
|
+
setLists(prev => prev.map(l => l.id === id ? { ...l, enabled: !enabled } : l));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const filtered = lists.filter(l => {
|
|
45
|
+
const q = search.toLowerCase();
|
|
46
|
+
const matchSearch = (l.name ?? '').toLowerCase().includes(q) || (l.description ?? '').toLowerCase().includes(q);
|
|
47
|
+
const matchRegion = region === 'All' || l.region === region;
|
|
48
|
+
const matchCategory = category === 'All' || l.category === category;
|
|
49
|
+
return matchSearch && matchRegion && matchCategory;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (loading) return <div className="flex items-center justify-center h-96"><LoadingSpinner /></div>;
|
|
53
|
+
|
|
54
|
+
const totalEntities = lists.reduce((sum, l) => sum + l.entity_count, 0);
|
|
55
|
+
const enabledCount = lists.filter(l => l.enabled).length;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
<MarketplaceHero total={lists.length} enabled={enabledCount} entities={totalEntities} />
|
|
60
|
+
<FilterBar items={REGIONS} active={region} onSelect={setRegion} label="Region" />
|
|
61
|
+
<FilterBar items={CATEGORIES} active={category} onSelect={setCategory} label="Category" labelMap={categoryLabel} />
|
|
62
|
+
<div className="mb-lg"><SearchField placeholder="Search lists..." value={search} onChange={setSearch} /></div>
|
|
63
|
+
<p className="sf-caption mb-md">{filtered.length} lists available</p>
|
|
64
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-lg">
|
|
65
|
+
{filtered.map(l => (
|
|
66
|
+
<ListMarketplaceCard key={l.id} list={l} onToggle={() => toggle(l.id, l.enabled)} />
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function MarketplaceHero({ total, enabled, entities }: { total: number; enabled: number; entities: number }) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="card-vibrancy p-xxl mb-xxl text-center bg-gradient-to-br from-apple-blue/10 to-transparent">
|
|
76
|
+
<Globe className="w-10 h-10 text-apple-blue mx-auto mb-md" />
|
|
77
|
+
<h1 className="text-3xl font-bold tracking-tight text-white mb-sm">
|
|
78
|
+
{total} Global Sanctions Lists
|
|
79
|
+
</h1>
|
|
80
|
+
<p className="sf-caption max-w-lg mx-auto">
|
|
81
|
+
{entities.toLocaleString()} entities across sanctions, PEP, law enforcement and regulatory databases. {enabled} currently active.
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function FilterBar({ items, active, onSelect, label, labelMap }: {
|
|
88
|
+
items: string[]; active: string; onSelect: (v: string) => void; label: string;
|
|
89
|
+
labelMap?: Record<string, string>;
|
|
90
|
+
}) {
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex flex-wrap gap-sm mb-md" role="tablist" aria-label={`${label} filter`}>
|
|
93
|
+
{items.map(item => (
|
|
94
|
+
<button key={item} role="tab" aria-selected={active === item} onClick={() => onSelect(item)}
|
|
95
|
+
className={`rounded-full px-lg py-sm text-[13px] font-semibold transition-all cursor-pointer ${
|
|
96
|
+
active === item ? 'bg-apple-blue text-white shadow-[0_0_12px_rgba(10,132,255,0.2)]'
|
|
97
|
+
: 'bg-white/5 text-apple-label-secondary hover:bg-white/10 hover:text-white'
|
|
98
|
+
}`}>{labelMap?.[item] ?? item}</button>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
4
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
5
|
+
|
|
6
|
+
const mockLogin = vi.fn()
|
|
7
|
+
const mockNavigate = vi.fn()
|
|
8
|
+
|
|
9
|
+
vi.mock('react-router-dom', async () => ({
|
|
10
|
+
...(await vi.importActual('react-router-dom')),
|
|
11
|
+
useNavigate: () => mockNavigate,
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
vi.mock('../context/AuthContext', () => ({
|
|
15
|
+
useAuth: () => ({ login: mockLogin }),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
vi.mock('../components/auth/SignInButtons', () => ({
|
|
19
|
+
default: () => <div data-testid="sign-in-buttons" />,
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
vi.mock('../components/auth/AuthDivider', () => ({
|
|
23
|
+
default: () => <div data-testid="auth-divider" />,
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
import { Login } from './Login'
|
|
27
|
+
|
|
28
|
+
const renderLogin = (initialEntries = ['/login']) =>
|
|
29
|
+
render(
|
|
30
|
+
<MemoryRouter initialEntries={initialEntries}>
|
|
31
|
+
<Login />
|
|
32
|
+
</MemoryRouter>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
describe('Login', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('renders email and password fields', () => {
|
|
41
|
+
renderLogin()
|
|
42
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument()
|
|
43
|
+
expect(screen.getByLabelText('Password')).toBeInTheDocument()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('renders submit button', () => {
|
|
47
|
+
renderLogin()
|
|
48
|
+
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('submits form with email and password', async () => {
|
|
52
|
+
mockLogin.mockResolvedValue(undefined)
|
|
53
|
+
renderLogin()
|
|
54
|
+
await userEvent.type(screen.getByLabelText('Email'), 'a@b.com')
|
|
55
|
+
await userEvent.type(screen.getByLabelText('Password'), 'pass1234')
|
|
56
|
+
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
|
|
57
|
+
expect(mockLogin).toHaveBeenCalledWith('a@b.com', 'pass1234')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('navigates to dashboard on success', async () => {
|
|
61
|
+
mockLogin.mockResolvedValue(undefined)
|
|
62
|
+
renderLogin()
|
|
63
|
+
await userEvent.type(screen.getByLabelText('Email'), 'a@b.com')
|
|
64
|
+
await userEvent.type(screen.getByLabelText('Password'), 'pass1234')
|
|
65
|
+
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
|
|
66
|
+
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/dashboard'))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('displays error on login failure', async () => {
|
|
70
|
+
mockLogin.mockRejectedValue(new Error('Invalid credentials'))
|
|
71
|
+
renderLogin()
|
|
72
|
+
await userEvent.type(screen.getByLabelText('Email'), 'a@b.com')
|
|
73
|
+
await userEvent.type(screen.getByLabelText('Password'), 'wrong')
|
|
74
|
+
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
|
|
75
|
+
await waitFor(() => {
|
|
76
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Invalid credentials')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('shows loading state during submission', async () => {
|
|
81
|
+
mockLogin.mockImplementation(() => new Promise(() => {}))
|
|
82
|
+
renderLogin()
|
|
83
|
+
await userEvent.type(screen.getByLabelText('Email'), 'a@b.com')
|
|
84
|
+
await userEvent.type(screen.getByLabelText('Password'), 'pass')
|
|
85
|
+
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
|
|
86
|
+
expect(screen.getByRole('button', { name: /signing in/i })).toBeDisabled()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('renders forgot password and signup links', () => {
|
|
90
|
+
renderLogin()
|
|
91
|
+
expect(screen.getByText(/forgot password/i)).toBeInTheDocument()
|
|
92
|
+
expect(screen.getByText(/start free trial/i)).toBeInTheDocument()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('renders OAuth buttons and divider', () => {
|
|
96
|
+
renderLogin()
|
|
97
|
+
expect(screen.getByTestId('sign-in-buttons')).toBeInTheDocument()
|
|
98
|
+
expect(screen.getByTestId('auth-divider')).toBeInTheDocument()
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useNavigate, Link, useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { useAuth } from '../context/AuthContext';
|
|
5
|
+
import { LoginLeftPanel } from '../components/auth/LoginLeftPanel';
|
|
6
|
+
import { LoginForm } from '../components/auth/LoginForm';
|
|
7
|
+
|
|
8
|
+
export function Login() {
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
const [searchParams] = useSearchParams();
|
|
11
|
+
const { t } = useTranslation('auth');
|
|
12
|
+
const { login, loginWithToken } = useAuth();
|
|
13
|
+
const [email, setEmail] = useState('');
|
|
14
|
+
const [password, setPassword] = useState('');
|
|
15
|
+
const [error, setError] = useState('');
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const token = searchParams.get('token');
|
|
20
|
+
const oauthError = searchParams.get('error');
|
|
21
|
+
if (token) {
|
|
22
|
+
loginWithToken(token)
|
|
23
|
+
.then(() => navigate('/dashboard', { replace: true }))
|
|
24
|
+
.catch(() => setError('OAuth login failed'));
|
|
25
|
+
}
|
|
26
|
+
if (oauthError) setError(oauthError);
|
|
27
|
+
}, [searchParams, navigate, loginWithToken]);
|
|
28
|
+
|
|
29
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
setLoading(true);
|
|
32
|
+
setError('');
|
|
33
|
+
try {
|
|
34
|
+
await login(email, password);
|
|
35
|
+
navigate('/dashboard');
|
|
36
|
+
} catch (err) {
|
|
37
|
+
setError(err instanceof Error ? err.message : t('login.failed'));
|
|
38
|
+
} finally {
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="min-h-screen bg-apple-bg flex">
|
|
45
|
+
<LoginLeftPanel />
|
|
46
|
+
<div className="flex-1 flex flex-col items-center justify-center px-6 py-10 border-l border-white/5">
|
|
47
|
+
<Link to="/" className="flex items-center gap-2 mb-8 lg:hidden">
|
|
48
|
+
<img src="/logo.svg" alt="AMLIQ" className="h-8 w-8" />
|
|
49
|
+
</Link>
|
|
50
|
+
<LoginForm
|
|
51
|
+
email={email} password={password} error={error} loading={loading}
|
|
52
|
+
onEmailChange={setEmail} onPasswordChange={setPassword} onSubmit={handleSubmit}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { PageHeader } from '../components/layout/PageHeader'
|
|
3
|
+
import { Card } from '../components/ui/Card'
|
|
4
|
+
import { Button } from '../components/ui/Button'
|
|
5
|
+
import { api } from '../api/client'
|
|
6
|
+
|
|
7
|
+
interface SetupData {
|
|
8
|
+
qr_url: string
|
|
9
|
+
secret: string
|
|
10
|
+
recovery_codes: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function MFASetup() {
|
|
14
|
+
const [setup, setSetup] = useState<SetupData | null>(null)
|
|
15
|
+
const [code, setCode] = useState('')
|
|
16
|
+
const [loading, setLoading] = useState(false)
|
|
17
|
+
const [verified, setVerified] = useState(false)
|
|
18
|
+
const [error, setError] = useState('')
|
|
19
|
+
|
|
20
|
+
const startSetup = async () => {
|
|
21
|
+
setLoading(true); setError('')
|
|
22
|
+
try {
|
|
23
|
+
const d = await api.post<SetupData>('/auth/mfa/setup', {})
|
|
24
|
+
setSetup(d)
|
|
25
|
+
} catch (err) {
|
|
26
|
+
setError(err instanceof Error ? err.message : 'Setup failed')
|
|
27
|
+
} finally {
|
|
28
|
+
setLoading(false)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const verify = async () => {
|
|
33
|
+
if (code.length !== 6) return
|
|
34
|
+
setLoading(true); setError('')
|
|
35
|
+
try {
|
|
36
|
+
await api.post('/auth/mfa/verify', { code })
|
|
37
|
+
setVerified(true)
|
|
38
|
+
} catch (err) {
|
|
39
|
+
setError(err instanceof Error ? err.message : 'Invalid code')
|
|
40
|
+
} finally {
|
|
41
|
+
setLoading(false)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (verified) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="max-w-lg mx-auto">
|
|
48
|
+
<PageHeader title="MFA Enabled" />
|
|
49
|
+
<Card className="text-center py-xxl">
|
|
50
|
+
<span className="text-4xl">🔒</span>
|
|
51
|
+
<p className="sf-headline text-apple-green mt-lg">Two-factor authentication enabled</p>
|
|
52
|
+
<p className="sf-caption text-white/50 mt-sm">Your account is now protected with TOTP</p>
|
|
53
|
+
</Card>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="max-w-lg mx-auto">
|
|
60
|
+
<PageHeader title="Two-Factor Authentication"
|
|
61
|
+
description="Add an extra layer of security to your account" />
|
|
62
|
+
|
|
63
|
+
{!setup ? (
|
|
64
|
+
<Card>
|
|
65
|
+
<p className="sf-body text-white/60 mb-lg">
|
|
66
|
+
Use an authenticator app (Google Authenticator, Authy, 1Password) to generate time-based codes.
|
|
67
|
+
</p>
|
|
68
|
+
<Button onClick={startSetup} disabled={loading} className="w-full">
|
|
69
|
+
{loading ? 'Setting up...' : 'Enable MFA'}
|
|
70
|
+
</Button>
|
|
71
|
+
</Card>
|
|
72
|
+
) : (
|
|
73
|
+
<div className="space-y-lg">
|
|
74
|
+
<Card>
|
|
75
|
+
<p className="sf-headline mb-md">1. Scan QR Code</p>
|
|
76
|
+
<div className="bg-white p-lg rounded-apple-md inline-block">
|
|
77
|
+
<img src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(setup.qr_url)}`}
|
|
78
|
+
alt="TOTP QR Code" className="w-48 h-48" />
|
|
79
|
+
</div>
|
|
80
|
+
<p className="sf-caption text-white/40 mt-md">
|
|
81
|
+
Or enter manually: <code className="text-white/60">{setup.secret}</code>
|
|
82
|
+
</p>
|
|
83
|
+
</Card>
|
|
84
|
+
|
|
85
|
+
<Card>
|
|
86
|
+
<p className="sf-headline mb-md">2. Enter Code</p>
|
|
87
|
+
<input value={code} onChange={e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
88
|
+
placeholder="000000" maxLength={6}
|
|
89
|
+
className="input-field w-full text-center text-2xl tracking-[0.5em] font-mono mb-lg" />
|
|
90
|
+
<Button onClick={verify} disabled={loading || code.length !== 6} className="w-full">
|
|
91
|
+
{loading ? 'Verifying...' : 'Verify & Enable'}
|
|
92
|
+
</Button>
|
|
93
|
+
</Card>
|
|
94
|
+
|
|
95
|
+
<Card>
|
|
96
|
+
<p className="sf-headline mb-md">3. Save Recovery Codes</p>
|
|
97
|
+
<p className="sf-caption text-white/50 mb-md">
|
|
98
|
+
Store these codes safely. Each can be used once if you lose your authenticator.
|
|
99
|
+
</p>
|
|
100
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-sm">
|
|
101
|
+
{setup.recovery_codes.map(c => (
|
|
102
|
+
<code key={c} className="text-sm bg-white/5 p-sm rounded font-mono text-center">{c}</code>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</Card>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{error && <Card className="mt-lg"><p role="alert" className="text-apple-red sf-body">{error}</p></Card>}
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default MFASetup
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MonitorProfile } from '../api/monitoring';
|
|
2
|
+
|
|
3
|
+
interface Props { profile: MonitorProfile; onUpdate?: () => void }
|
|
4
|
+
|
|
5
|
+
export default function MonitorProfileCard({ profile }: Props) {
|
|
6
|
+
const riskColors: Record<string, string> = {
|
|
7
|
+
low: 'text-green-400', medium: 'text-yellow-400',
|
|
8
|
+
high: 'text-orange-400', critical: 'text-red-400',
|
|
9
|
+
};
|
|
10
|
+
return (
|
|
11
|
+
<div className="p-4 bg-white/[0.02] border border-white/[0.06] rounded-xl">
|
|
12
|
+
<div className="flex justify-between items-start">
|
|
13
|
+
<div>
|
|
14
|
+
<h3 className="text-white font-medium">{profile.entity_name}</h3>
|
|
15
|
+
<p className="text-zinc-500 text-sm mt-1">{profile.frequency} | {profile.entity_type}</p>
|
|
16
|
+
</div>
|
|
17
|
+
<span className={`text-xs font-medium uppercase ${riskColors[profile.risk_level] || 'text-zinc-400'}`}>
|
|
18
|
+
{profile.risk_level}
|
|
19
|
+
</span>
|
|
20
|
+
</div>
|
|
21
|
+
<div className="flex gap-4 mt-3 text-xs text-zinc-500">
|
|
22
|
+
<span>Last: {profile.last_screened_at || 'Never'}</span>
|
|
23
|
+
<span>Matches: {profile.match_count}</span>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { PageHeader } from '../components/layout/PageHeader';
|
|
4
|
+
import { Card } from '../components/ui/Card';
|
|
5
|
+
import { Shield, AlertTriangle, Clock, Plus } from 'lucide-react';
|
|
6
|
+
import { monitoringApi, MonitorProfile, MonitorDashboard } from '../api/monitoring';
|
|
7
|
+
import MonitorProfileCard from './MonitorProfileCard';
|
|
8
|
+
import AddMonitorModal from './AddMonitorModal';
|
|
9
|
+
|
|
10
|
+
export function Monitoring() {
|
|
11
|
+
const { t } = useTranslation('monitoring');
|
|
12
|
+
const [profiles, setProfiles] = useState<MonitorProfile[]>([]);
|
|
13
|
+
const [dashboard, setDashboard] = useState<MonitorDashboard | null>(null);
|
|
14
|
+
const [showAdd, setShowAdd] = useState(false);
|
|
15
|
+
|
|
16
|
+
const loadData = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const [pRes, dRes] = await Promise.all([
|
|
19
|
+
monitoringApi.listProfiles(),
|
|
20
|
+
monitoringApi.getDashboard(),
|
|
21
|
+
]);
|
|
22
|
+
setProfiles(pRes.profiles || []);
|
|
23
|
+
setDashboard(dRes);
|
|
24
|
+
} catch { /* handled by client */ }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
useEffect(() => { loadData(); }, []);
|
|
28
|
+
|
|
29
|
+
const stats = [
|
|
30
|
+
{ name: 'Active Profiles', value: dashboard?.active_profiles ?? 0, icon: Shield },
|
|
31
|
+
{ name: 'Pending Alerts', value: dashboard?.pending_alerts ?? 0, icon: AlertTriangle },
|
|
32
|
+
{ name: 'Total Alerts', value: dashboard?.total_alerts ?? 0, icon: Clock },
|
|
33
|
+
{ name: 'Total Profiles', value: dashboard?.total_profiles ?? 0, icon: Shield },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
<PageHeader title={t('title')} description="Ongoing entity monitoring against sanctions lists" />
|
|
39
|
+
<div className="flex justify-end mb-md">
|
|
40
|
+
<button type="button" onClick={() => setShowAdd(true)} className="btn-primary flex items-center gap-sm">
|
|
41
|
+
<Plus className="w-4 h-4" /> Add to Monitoring
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-lg mb-xxl">
|
|
45
|
+
{stats.map((s) => (
|
|
46
|
+
<Card key={s.name}>
|
|
47
|
+
<div className="flex items-center justify-between mb-sm">
|
|
48
|
+
<p className="sf-caption">{s.name}</p>
|
|
49
|
+
<s.icon className="w-5 h-5 text-apple-blue" />
|
|
50
|
+
</div>
|
|
51
|
+
<p className="sf-headline text-lg font-bold">{s.value}</p>
|
|
52
|
+
</Card>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="space-y-md">
|
|
56
|
+
{profiles.map((p) => (
|
|
57
|
+
<MonitorProfileCard key={p.id} profile={p} onUpdate={loadData} />
|
|
58
|
+
))}
|
|
59
|
+
{profiles.length === 0 && (
|
|
60
|
+
<Card><p className="sf-body text-apple-text-secondary text-center py-xl">
|
|
61
|
+
No monitored entities yet. Add one to get started.
|
|
62
|
+
</p></Card>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
{showAdd && <AddMonitorModal onClose={() => setShowAdd(false)} onCreated={loadData} />}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|