portal-mojo 0.1.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/LICENSE +201 -0
- package/README.md +55 -0
- package/docs/DetailView.md +173 -0
- package/docs/ModelTable.md +128 -0
- package/docs/README.md +98 -0
- package/docs/admin-assistant.md +72 -0
- package/docs/admin-bouncer.md +120 -0
- package/docs/admin-cloudwatch.md +38 -0
- package/docs/admin-credentials.md +116 -0
- package/docs/admin-dashboard.md +15 -0
- package/docs/admin-devices-geoip.md +284 -0
- package/docs/admin-dns-certificates.md +122 -0
- package/docs/admin-dns-records.md +65 -0
- package/docs/admin-dns-registrar.md +33 -0
- package/docs/admin-dns.md +80 -0
- package/docs/admin-identity-users.md +103 -0
- package/docs/admin-incidents.md +71 -0
- package/docs/admin-jobs.md +426 -0
- package/docs/admin-members.md +112 -0
- package/docs/admin-messaging.md +56 -0
- package/docs/admin-metrics-explorer.md +63 -0
- package/docs/admin-monitoring.md +99 -0
- package/docs/admin-network-security.md +393 -0
- package/docs/admin-phonehub.md +27 -0
- package/docs/admin-push.md +36 -0
- package/docs/admin-rules.md +87 -0
- package/docs/admin-security-tickets.md +40 -0
- package/docs/admin-settings.md +78 -0
- package/docs/admin-shortlinks.md +32 -0
- package/docs/admin-storage.md +141 -0
- package/docs/auth-pages.md +212 -0
- package/docs/auth.md +58 -0
- package/docs/calendar.md +122 -0
- package/docs/charts.md +303 -0
- package/docs/client.md +206 -0
- package/docs/collection-multiselect.md +117 -0
- package/docs/collection-select.md +132 -0
- package/docs/combobox.md +105 -0
- package/docs/dataview.md +226 -0
- package/docs/datepicker.md +151 -0
- package/docs/daterange.md +170 -0
- package/docs/datetimepicker.md +249 -0
- package/docs/defineModel.md +72 -0
- package/docs/detail-primitives.md +174 -0
- package/docs/feedback.md +85 -0
- package/docs/file-upload-ux.md +146 -0
- package/docs/forms.md +435 -0
- package/docs/grouping-and-fmt.md +98 -0
- package/docs/idioms.md +112 -0
- package/docs/image-editor.md +101 -0
- package/docs/loading.md +242 -0
- package/docs/location-address.md +76 -0
- package/docs/markdown.md +203 -0
- package/docs/menus-and-access.md +89 -0
- package/docs/multiselect-dropdown.md +153 -0
- package/docs/params.md +73 -0
- package/docs/popover.md +130 -0
- package/docs/realtime.md +61 -0
- package/docs/record-feed.md +238 -0
- package/docs/right-panel.md +42 -0
- package/docs/tabs-and-form-wizard.md +60 -0
- package/docs/taginput.md +120 -0
- package/docs/timepicker.md +173 -0
- package/docs/timezone-select.md +147 -0
- package/docs/worldmap.md +302 -0
- package/package.json +55 -0
- package/src/admin/assistant/AssistantFeed.tsx +29 -0
- package/src/admin/assistant/AssistantPanel.tsx +65 -0
- package/src/admin/assistant/api.ts +17 -0
- package/src/admin/assistant/data.ts +63 -0
- package/src/admin/assistant/index.ts +21 -0
- package/src/admin/assistant/launchers.tsx +33 -0
- package/src/admin/assistant/pages.tsx +78 -0
- package/src/admin/assistant/streaming.ts +238 -0
- package/src/admin/assistant/types.ts +30 -0
- package/src/admin/bouncer/devices.tsx +230 -0
- package/src/admin/bouncer/index.ts +34 -0
- package/src/admin/bouncer/models.ts +199 -0
- package/src/admin/bouncer/signals.tsx +182 -0
- package/src/admin/bouncer/signatures.tsx +220 -0
- package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
- package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
- package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
- package/src/admin/cloudwatch/data.ts +163 -0
- package/src/admin/cloudwatch/index.ts +16 -0
- package/src/admin/credentials/group-api-keys.tsx +433 -0
- package/src/admin/credentials/index.ts +4 -0
- package/src/admin/credentials/models.ts +244 -0
- package/src/admin/credentials/secret-dialog.tsx +77 -0
- package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
- package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
- package/src/admin/dashboard/data.ts +60 -0
- package/src/admin/dashboard/index.ts +2 -0
- package/src/admin/dns/CertificateDetail.tsx +115 -0
- package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
- package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
- package/src/admin/dns/CertificatesPage.tsx +85 -0
- package/src/admin/dns/DnsRecordEditor.tsx +110 -0
- package/src/admin/dns/DnsRecordsPage.tsx +36 -0
- package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
- package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
- package/src/admin/dns/DomainDetail.tsx +41 -0
- package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
- package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
- package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
- package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
- package/src/admin/dns/DomainsPage.tsx +27 -0
- package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
- package/src/admin/dns/RegistrantContactPage.tsx +42 -0
- package/src/admin/dns/api.ts +515 -0
- package/src/admin/dns/certificate-data.ts +221 -0
- package/src/admin/dns/data.ts +32 -0
- package/src/admin/dns/dns-data.ts +266 -0
- package/src/admin/dns/dns-integration.ts +24 -0
- package/src/admin/dns/domain-links.ts +37 -0
- package/src/admin/dns/index.ts +86 -0
- package/src/admin/dns/models.ts +418 -0
- package/src/admin/dns/purchase-data.ts +67 -0
- package/src/admin/dns/registrant-data.ts +52 -0
- package/src/admin/identity/index.ts +5 -0
- package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
- package/src/admin/identity/members/MemberDetail.tsx +226 -0
- package/src/admin/identity/members/MembersPage.tsx +144 -0
- package/src/admin/identity/members/index.ts +24 -0
- package/src/admin/identity/members/member-flows.tsx +132 -0
- package/src/admin/identity/members/models.ts +204 -0
- package/src/admin/identity/users/UserDetail.tsx +257 -0
- package/src/admin/identity/users/UsersPage.tsx +163 -0
- package/src/admin/identity/users/index.ts +27 -0
- package/src/admin/identity/users/models.ts +232 -0
- package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
- package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
- package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
- package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
- package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
- package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
- package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
- package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
- package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
- package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
- package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
- package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
- package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
- package/src/admin/identity/users/sections/actions.tsx +752 -0
- package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
- package/src/admin/identity/users/sections/queries.ts +74 -0
- package/src/admin/identity/users/sections/shared.tsx +299 -0
- package/src/admin/incidents/EventDetail.tsx +43 -0
- package/src/admin/incidents/EventsPage.tsx +52 -0
- package/src/admin/incidents/IncidentDetail.tsx +68 -0
- package/src/admin/incidents/IncidentsPage.tsx +102 -0
- package/src/admin/incidents/forensics.tsx +73 -0
- package/src/admin/incidents/index.ts +16 -0
- package/src/admin/incidents/models.ts +113 -0
- package/src/admin/incidents/sanitize.ts +71 -0
- package/src/admin/index.ts +383 -0
- package/src/admin/jobs/JobDashboardPage.tsx +111 -0
- package/src/admin/jobs/JobDetail.tsx +721 -0
- package/src/admin/jobs/JobRunnersPage.tsx +216 -0
- package/src/admin/jobs/JobsTablePage.tsx +122 -0
- package/src/admin/jobs/RunnerDetail.tsx +576 -0
- package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
- package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
- package/src/admin/jobs/columns.tsx +297 -0
- package/src/admin/jobs/control.ts +488 -0
- package/src/admin/jobs/index.ts +85 -0
- package/src/admin/jobs/models.ts +599 -0
- package/src/admin/jobs/queries.ts +272 -0
- package/src/admin/jobs/scheduled-task-form.tsx +493 -0
- package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
- package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
- package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
- package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
- package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
- package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
- package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
- package/src/admin/messaging/MailboxesPage.tsx +15 -0
- package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
- package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
- package/src/admin/messaging/SentMessagesPage.tsx +11 -0
- package/src/admin/messaging/api.ts +17 -0
- package/src/admin/messaging/data.ts +10 -0
- package/src/admin/messaging/index.ts +23 -0
- package/src/admin/messaging/models.ts +76 -0
- package/src/admin/messaging/push/PushPage.tsx +44 -0
- package/src/admin/messaging/push/api.ts +17 -0
- package/src/admin/messaging/push/index.ts +9 -0
- package/src/admin/messaging/push/models.ts +43 -0
- package/src/admin/messaging/sanitize.ts +44 -0
- package/src/admin/monitoring/LogInspector.tsx +160 -0
- package/src/admin/monitoring/LogsPage.tsx +135 -0
- package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
- package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
- package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
- package/src/admin/monitoring/index.ts +55 -0
- package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
- package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
- package/src/admin/monitoring/models.ts +155 -0
- package/src/admin/network/BlockedIPsPage.tsx +194 -0
- package/src/admin/network/BlockedIpDetail.tsx +262 -0
- package/src/admin/network/FirewallLogPage.tsx +115 -0
- package/src/admin/network/IPSetDetail.tsx +280 -0
- package/src/admin/network/IPSetEditor.tsx +191 -0
- package/src/admin/network/IPSetsPage.tsx +177 -0
- package/src/admin/network/geofence/BlocksTab.tsx +289 -0
- package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
- package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
- package/src/admin/network/geofence/PostureHeader.tsx +184 -0
- package/src/admin/network/geofence/RuleEditor.tsx +176 -0
- package/src/admin/network/geofence/RulesTab.tsx +254 -0
- package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
- package/src/admin/network/geofence/geofence-data.ts +674 -0
- package/src/admin/network/index.ts +68 -0
- package/src/admin/network/models.ts +607 -0
- package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
- package/src/admin/phonehub/api.ts +47 -0
- package/src/admin/phonehub/data.ts +6 -0
- package/src/admin/phonehub/index.ts +10 -0
- package/src/admin/phonehub/models.ts +68 -0
- package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
- package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
- package/src/admin/rules/RuleSetsPage.tsx +33 -0
- package/src/admin/rules/editors.tsx +54 -0
- package/src/admin/rules/handler-dsl.ts +213 -0
- package/src/admin/rules/index.ts +13 -0
- package/src/admin/rules/models.ts +112 -0
- package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
- package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
- package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
- package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
- package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
- package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
- package/src/admin/security/devices/index.ts +40 -0
- package/src/admin/security/devices/models.ts +470 -0
- package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
- package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
- package/src/admin/security/geoip/geoip-forms.ts +102 -0
- package/src/admin/security/geoip/index.ts +27 -0
- package/src/admin/security/geoip/models.ts +397 -0
- package/src/admin/security/index.ts +31 -0
- package/src/admin/security/models.ts +137 -0
- package/src/admin/security/tickets.tsx +562 -0
- package/src/admin/security-permissions.ts +5 -0
- package/src/admin/settings/SettingDetail.tsx +57 -0
- package/src/admin/settings/SettingEditor.tsx +157 -0
- package/src/admin/settings/SettingsPage.tsx +72 -0
- package/src/admin/settings/index.ts +19 -0
- package/src/admin/settings/model.ts +143 -0
- package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
- package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
- package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
- package/src/admin/shortlinks/index.ts +18 -0
- package/src/admin/shortlinks/models.ts +246 -0
- package/src/admin/storage/BackendsPage.tsx +43 -0
- package/src/admin/storage/BucketDetail.tsx +77 -0
- package/src/admin/storage/BucketsPage.tsx +64 -0
- package/src/admin/storage/FileManagerDetail.tsx +36 -0
- package/src/admin/storage/FilePreview.tsx +54 -0
- package/src/admin/storage/FileUploadSurface.tsx +250 -0
- package/src/admin/storage/FileView.tsx +109 -0
- package/src/admin/storage/FilesPage.tsx +53 -0
- package/src/admin/storage/file-renditions.ts +99 -0
- package/src/admin/storage/index.ts +31 -0
- package/src/admin/storage/models.ts +585 -0
- package/src/admin/storage/storage-dialogs.tsx +80 -0
- package/src/charts/CircularProgress.tsx +281 -0
- package/src/charts/KPIStrip.tsx +162 -0
- package/src/charts/KPITile.tsx +125 -0
- package/src/charts/MetricsChart.tsx +459 -0
- package/src/charts/MetricsMiniWidget.tsx +588 -0
- package/src/charts/MiniChart.tsx +283 -0
- package/src/charts/PieChart.tsx +354 -0
- package/src/charts/SeriesChart.tsx +378 -0
- package/src/charts/chart-dialogs.tsx +149 -0
- package/src/charts/exportChart.ts +144 -0
- package/src/charts/index.ts +28 -0
- package/src/charts/pie-math.ts +127 -0
- package/src/charts/stats.ts +265 -0
- package/src/charts/worldmap/WorldMap.tsx +649 -0
- package/src/charts/worldmap/countryCentroids.ts +304 -0
- package/src/charts/worldmap/geo.ts +293 -0
- package/src/charts/worldmap/use-world-land.ts +47 -0
- package/src/charts/worldmap/world-land.ts +18 -0
- package/src/charts/worldmap/worldmap-data.ts +285 -0
- package/src/client/auth.ts +737 -0
- package/src/client/client.ts +268 -0
- package/src/client/duid.ts +31 -0
- package/src/client/errors.ts +33 -0
- package/src/client/group-context.ts +28 -0
- package/src/client/group.tsx +126 -0
- package/src/client/hooks.ts +50 -0
- package/src/client/index.ts +37 -0
- package/src/client/jwt.ts +75 -0
- package/src/client/location.ts +285 -0
- package/src/client/lookups.ts +41 -0
- package/src/client/markdown-fallback.tsx +115 -0
- package/src/client/markdown-parse.ts +386 -0
- package/src/client/markdown-sanitize.ts +217 -0
- package/src/client/markdown.ts +122 -0
- package/src/client/me.ts +188 -0
- package/src/client/mock.ts +9984 -0
- package/src/client/model.ts +227 -0
- package/src/client/params.ts +292 -0
- package/src/client/realtime-mock.ts +221 -0
- package/src/client/realtime.ts +561 -0
- package/src/client/record-feed.ts +329 -0
- package/src/client/safe-export.ts +71 -0
- package/src/client/types.ts +278 -0
- package/src/client/upload.ts +421 -0
- package/src/ui/AddressField.tsx +273 -0
- package/src/ui/AttachmentQueue.tsx +92 -0
- package/src/ui/CollectionMultiSelect.tsx +622 -0
- package/src/ui/CollectionSelect.tsx +517 -0
- package/src/ui/ComboBox.tsx +336 -0
- package/src/ui/DetailView.tsx +438 -0
- package/src/ui/FileDrop.tsx +194 -0
- package/src/ui/FileField.tsx +369 -0
- package/src/ui/FilterBar.tsx +269 -0
- package/src/ui/FormFields.tsx +77 -0
- package/src/ui/FormView.tsx +360 -0
- package/src/ui/FormWizard.tsx +336 -0
- package/src/ui/GroupSwitcher.tsx +202 -0
- package/src/ui/Guarded.tsx +16 -0
- package/src/ui/MarkdownView.tsx +89 -0
- package/src/ui/ModelTable.tsx +848 -0
- package/src/ui/MultiSelectDropdown.tsx +390 -0
- package/src/ui/Popover.tsx +204 -0
- package/src/ui/RecordFeed.tsx +627 -0
- package/src/ui/RequiresGroup.tsx +32 -0
- package/src/ui/RightPanel.tsx +107 -0
- package/src/ui/SidebarNav.tsx +330 -0
- package/src/ui/Tabs.tsx +161 -0
- package/src/ui/TagInput.tsx +381 -0
- package/src/ui/ThemeProvider.tsx +52 -0
- package/src/ui/UploadQueue.tsx +603 -0
- package/src/ui/armed-button.tsx +91 -0
- package/src/ui/date/Calendar.tsx +498 -0
- package/src/ui/date/DatePicker.tsx +289 -0
- package/src/ui/date/DateRangePicker.tsx +280 -0
- package/src/ui/date/DateTimePicker.tsx +587 -0
- package/src/ui/date/PresetRail.tsx +165 -0
- package/src/ui/date/TimePicker.tsx +624 -0
- package/src/ui/date/TimezoneSelect.tsx +267 -0
- package/src/ui/date/fns.ts +679 -0
- package/src/ui/detail/DataView.tsx +692 -0
- package/src/ui/detail/FlowStrip.tsx +80 -0
- package/src/ui/detail/KnownFieldsCard.tsx +121 -0
- package/src/ui/detail/MetadataSection.tsx +219 -0
- package/src/ui/detail/StackTraceView.tsx +150 -0
- package/src/ui/detail/StatusPanel.tsx +73 -0
- package/src/ui/detail/Timeline.tsx +52 -0
- package/src/ui/detail/index.ts +12 -0
- package/src/ui/field-registry.tsx +428 -0
- package/src/ui/field-wire.ts +322 -0
- package/src/ui/form-autosave.ts +583 -0
- package/src/ui/format.ts +444 -0
- package/src/ui/grouping.ts +150 -0
- package/src/ui/image-editor/ImageEditor.tsx +484 -0
- package/src/ui/image-editor/canvas.ts +254 -0
- package/src/ui/image-editor/index.ts +3 -0
- package/src/ui/image-editor/math.ts +416 -0
- package/src/ui/index.ts +58 -0
- package/src/ui/loading.tsx +604 -0
- package/src/ui/menu-registry.ts +213 -0
- package/src/ui/modal.tsx +297 -0
- package/src/ui/password.tsx +302 -0
- package/src/ui/schema-form-core.tsx +315 -0
- package/src/ui/toast.tsx +219 -0
- package/src/ui/ui.tsx +45 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// ExemptionsTab — the auditor's "who is exempt" surface. Port of web-mojo
|
|
2
|
+
// `GeofenceExemptionsView.js`. Every entry shows who it covers, why, and until
|
|
3
|
+
// when; expired entries render inactive and are NEVER hidden (the backend
|
|
4
|
+
// lists them with `active: false` for exactly that reason).
|
|
5
|
+
//
|
|
6
|
+
// Three planes, three different write paths:
|
|
7
|
+
// 1. Network ranges — the GEOFENCE_ALLOWLIST setting, a FULL-REPLACE POST.
|
|
8
|
+
// Every write refetches and compares first (see useSaveGeoAllowlist):
|
|
9
|
+
// the source posted from possibly-stale local state and could silently
|
|
10
|
+
// drop a concurrent editor's entry.
|
|
11
|
+
// 2. Individual IPs — GeoLocatedIP whitelist rows, written through the
|
|
12
|
+
// standard geoip magic-field actions (#1291's model).
|
|
13
|
+
// 3. Bypass holders — read-only. Grants are managed on user records; the
|
|
14
|
+
// endpoint deliberately returns id/username only so a geofence-only
|
|
15
|
+
// grant cannot be used to enumerate user PII.
|
|
16
|
+
import {
|
|
17
|
+
ArmedButton, Badge, formModal, modal, toast, type Field,
|
|
18
|
+
} from '../../../ui';
|
|
19
|
+
import { useCan } from '../../../client';
|
|
20
|
+
import { GEOIP_MANAGE_PERMS, GeoLocatedIPModel } from '../../security/geoip';
|
|
21
|
+
import {
|
|
22
|
+
AllowlistRaceError, findGeoIpIdByAddress, resolveGeoIpIdForAddress,
|
|
23
|
+
useBypassHolders, useGeoAllowlist, useSaveGeoAllowlist,
|
|
24
|
+
type AllowlistWriteEntry,
|
|
25
|
+
} from '../models';
|
|
26
|
+
import {
|
|
27
|
+
GEOFENCE_MANAGE_PERMS,
|
|
28
|
+
type AllowlistEntry, type BypassHolder, type GeoIpAllowlistEntry,
|
|
29
|
+
} from './geofence-data';
|
|
30
|
+
|
|
31
|
+
const RANGE_FIELDS: Field[] = [
|
|
32
|
+
{
|
|
33
|
+
name: 'cidr', type: 'text', label: 'Range (CIDR or IP)', required: true, columns: 12,
|
|
34
|
+
placeholder: '198.51.100.0/24',
|
|
35
|
+
help: 'Validated server-side — an unparseable value is refused with the entry index.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'reason', type: 'text', label: 'Reason', required: true, columns: 12,
|
|
39
|
+
placeholder: 'Office egress — Seattle HQ',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'until', type: 'datetimepicker', label: 'Expires (optional)', columns: 12,
|
|
43
|
+
outputFormat: 'iso',
|
|
44
|
+
help: 'Leave empty for a permanent exemption.',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const IP_FIELDS: Field[] = [
|
|
49
|
+
{ name: 'ip', type: 'text', label: 'IP address', required: true, columns: 12, placeholder: '203.0.113.7' },
|
|
50
|
+
{ name: 'reason', type: 'text', label: 'Reason', required: true, columns: 12, placeholder: 'Dev box — Paris' },
|
|
51
|
+
{
|
|
52
|
+
name: 'until', type: 'datetimepicker', label: 'Expires (optional)', columns: 12,
|
|
53
|
+
outputFormat: 'iso',
|
|
54
|
+
help: 'Leave empty for a permanent exemption.',
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function StatusPill({ active }: { active: boolean }) {
|
|
59
|
+
return <Badge tone={active ? 'success' : 'muted'}>{active ? 'Active' : 'Expired'}</Badge>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function untilText(until: string | null | undefined): string {
|
|
63
|
+
return until ? until : 'Never';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The wire form of one stored entry: only `{cidr, reason, until}` is legal. */
|
|
67
|
+
function toWriteEntry(entry: AllowlistEntry): AllowlistWriteEntry {
|
|
68
|
+
const out: AllowlistWriteEntry = { cidr: entry.cidr };
|
|
69
|
+
if (entry.reason) out.reason = entry.reason;
|
|
70
|
+
if (entry.until) out.until = entry.until;
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function ExemptionsTab() {
|
|
75
|
+
const { can: canManage } = useCan(GEOFENCE_MANAGE_PERMS);
|
|
76
|
+
const canGeoIpManage = useCan(GEOIP_MANAGE_PERMS).can;
|
|
77
|
+
const allowlist = useGeoAllowlist();
|
|
78
|
+
const bypass = useBypassHolders();
|
|
79
|
+
const save = useSaveGeoAllowlist();
|
|
80
|
+
const whitelist = GeoLocatedIPModel.useAction('whitelist');
|
|
81
|
+
const unwhitelist = GeoLocatedIPModel.useAction('unwhitelist');
|
|
82
|
+
|
|
83
|
+
const setting: AllowlistEntry[] = allowlist.data?.setting ?? [];
|
|
84
|
+
const geoips: GeoIpAllowlistEntry[] = allowlist.data?.geoip ?? [];
|
|
85
|
+
const holders: BypassHolder[] = bypass.data?.holders ?? [];
|
|
86
|
+
|
|
87
|
+
const writeAllowlist = async (entries: AllowlistWriteEntry[], label: string) => {
|
|
88
|
+
try {
|
|
89
|
+
await save.mutateAsync({ entries, baseline: setting });
|
|
90
|
+
toast.success(label);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (err instanceof AllowlistRaceError) {
|
|
93
|
+
toast.error(err.message);
|
|
94
|
+
void allowlist.refetch();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
toast.error(err instanceof Error ? err.message : 'Failed to save the allowlist');
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const onAddRange = async () => {
|
|
102
|
+
const data = await formModal({
|
|
103
|
+
title: 'Add network exemption',
|
|
104
|
+
fields: RANGE_FIELDS,
|
|
105
|
+
submitText: 'Add',
|
|
106
|
+
intro: 'An allowlisted address passes EVERY rule, including the VPN and Tor checks.',
|
|
107
|
+
});
|
|
108
|
+
if (!data) return;
|
|
109
|
+
const until = typeof data.until === 'string' && data.until ? data.until : undefined;
|
|
110
|
+
await writeAllowlist(
|
|
111
|
+
[...setting.map(toWriteEntry), { cidr: String(data.cidr).trim(), reason: String(data.reason), ...(until ? { until } : {}) }],
|
|
112
|
+
'Exemption added',
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const onEditRange = async (index: number) => {
|
|
117
|
+
const entry = setting[index];
|
|
118
|
+
if (!entry) return;
|
|
119
|
+
const data = await formModal({
|
|
120
|
+
title: 'Edit network exemption',
|
|
121
|
+
fields: RANGE_FIELDS,
|
|
122
|
+
submitText: 'Save',
|
|
123
|
+
initial: { cidr: entry.cidr, reason: entry.reason ?? '', until: entry.until ?? '' },
|
|
124
|
+
});
|
|
125
|
+
if (!data) return;
|
|
126
|
+
const until = typeof data.until === 'string' && data.until ? data.until : undefined;
|
|
127
|
+
const next = setting.map(toWriteEntry);
|
|
128
|
+
next[index] = { cidr: String(data.cidr).trim(), reason: String(data.reason), ...(until ? { until } : {}) };
|
|
129
|
+
await writeAllowlist(next, 'Exemption saved');
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const onRemoveRange = async (index: number) => {
|
|
133
|
+
const entry = setting[index];
|
|
134
|
+
if (!entry) return;
|
|
135
|
+
const next = setting.filter((_, i) => i !== index).map(toWriteEntry);
|
|
136
|
+
// Clearing the LAST entry posts `entries: []`, which is a legitimate
|
|
137
|
+
// clear — not "no change". It is confirmed explicitly.
|
|
138
|
+
if (next.length === 0) {
|
|
139
|
+
const ok = await modal.confirm({
|
|
140
|
+
title: 'Clear every network exemption?',
|
|
141
|
+
message: 'This removes the last entry, so the allowlist will be empty. Every range is evaluated against the rules again immediately.',
|
|
142
|
+
confirmText: 'Clear the allowlist',
|
|
143
|
+
danger: true,
|
|
144
|
+
});
|
|
145
|
+
if (!ok) return;
|
|
146
|
+
}
|
|
147
|
+
await writeAllowlist(next, `Exemption for ${entry.cidr} removed`);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const onWhitelistIp = async () => {
|
|
151
|
+
const data = await formModal({
|
|
152
|
+
title: 'Whitelist an IP',
|
|
153
|
+
fields: IP_FIELDS,
|
|
154
|
+
submitText: 'Whitelist',
|
|
155
|
+
intro: 'The address is resolved through the GeoIP provider (creating the record if it is new) and then whitelisted.',
|
|
156
|
+
});
|
|
157
|
+
if (!data) return;
|
|
158
|
+
const ip = String(data.ip).trim();
|
|
159
|
+
const until = typeof data.until === 'string' && data.until ? data.until : null;
|
|
160
|
+
try {
|
|
161
|
+
const id = await resolveGeoIpIdForAddress(GeoLocatedIPModel.endpoint, ip);
|
|
162
|
+
await whitelist.mutateAsync({
|
|
163
|
+
id,
|
|
164
|
+
payload: until ? { reason: String(data.reason), until } : { reason: String(data.reason) },
|
|
165
|
+
});
|
|
166
|
+
await allowlist.refetch();
|
|
167
|
+
toast.success(`${ip} whitelisted`);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
toast.error(err instanceof Error ? err.message : `Could not whitelist ${ip}`);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const onUnwhitelistIp = async (ip: string) => {
|
|
174
|
+
try {
|
|
175
|
+
// A cached read, not `/lookup` — the record exists by definition,
|
|
176
|
+
// so removal costs no provider call and no rate-limit budget.
|
|
177
|
+
const id = await findGeoIpIdByAddress(GeoLocatedIPModel.endpoint, ip);
|
|
178
|
+
await unwhitelist.mutateAsync({ id, payload: 1 });
|
|
179
|
+
await allowlist.refetch();
|
|
180
|
+
toast.success(`Whitelist removed for ${ip}`);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
toast.error(err instanceof Error ? err.message : `Could not remove the whitelist for ${ip}`);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="geo-exemptions">
|
|
188
|
+
<div className="netsec-note netsec-note-info">
|
|
189
|
+
<i className="bi bi-shield-exclamation" />
|
|
190
|
+
<div>
|
|
191
|
+
Exemptions are compliance-sensitive. Every entry shows <b>who it covers, why, and
|
|
192
|
+
until when</b> — and any exemption that bypasses a block is recorded in the audit trail.
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* ── 1. Network ranges ───────────────────────────────── */}
|
|
197
|
+
<div className="panel netsec-card">
|
|
198
|
+
<div className="netsec-card-head">
|
|
199
|
+
<span>Network ranges (allowlist)</span>
|
|
200
|
+
{canManage && (
|
|
201
|
+
<button type="button" className="btn btn-primary btn-compact" onClick={() => void onAddRange()}>
|
|
202
|
+
<i className="bi bi-plus-lg" /> Add range
|
|
203
|
+
</button>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
<div className="netsec-card-body">
|
|
207
|
+
{allowlist.isPending && <p className="dim">Loading exemptions…</p>}
|
|
208
|
+
{allowlist.error && <p className="text-bad">{allowlist.error.message}</p>}
|
|
209
|
+
{!allowlist.isPending && !allowlist.error && (
|
|
210
|
+
setting.length === 0
|
|
211
|
+
? <p className="dim">No network exemptions.</p>
|
|
212
|
+
: (
|
|
213
|
+
<div className="netsec-table-scroll">
|
|
214
|
+
<table className="tbl geo-exempt-table">
|
|
215
|
+
<thead>
|
|
216
|
+
<tr>
|
|
217
|
+
<th>Range</th><th>Reason</th><th>Expires</th><th>Status</th>
|
|
218
|
+
{canManage && <th aria-label="Actions" />}
|
|
219
|
+
</tr>
|
|
220
|
+
</thead>
|
|
221
|
+
<tbody>
|
|
222
|
+
{setting.map((entry, index) => (
|
|
223
|
+
<tr key={`${entry.cidr}-${index}`} className={entry.active === false ? 'geo-row-expired' : undefined}>
|
|
224
|
+
<td><code>{entry.cidr}</code></td>
|
|
225
|
+
<td>{entry.reason || <span className="dim">—</span>}</td>
|
|
226
|
+
<td>{untilText(entry.until)}</td>
|
|
227
|
+
<td><StatusPill active={entry.active !== false} /></td>
|
|
228
|
+
{canManage && (
|
|
229
|
+
<td className="netsec-row-actions">
|
|
230
|
+
<button type="button" className="btn-link" onClick={() => void onEditRange(index)}>Edit</button>
|
|
231
|
+
<ArmedButton
|
|
232
|
+
className="btn-compact"
|
|
233
|
+
label="Remove"
|
|
234
|
+
armedLabel="Click again — this range is evaluated against the rules again immediately"
|
|
235
|
+
onConfirm={() => onRemoveRange(index)}
|
|
236
|
+
/>
|
|
237
|
+
</td>
|
|
238
|
+
)}
|
|
239
|
+
</tr>
|
|
240
|
+
))}
|
|
241
|
+
</tbody>
|
|
242
|
+
</table>
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
)}
|
|
246
|
+
<p className="dim">
|
|
247
|
+
Allowlisted addresses pass <b>every</b> rule, including the VPN and Tor checks.
|
|
248
|
+
Expired entries stay listed until they are removed.
|
|
249
|
+
</p>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* ── 2. Individual IPs ───────────────────────────────── */}
|
|
254
|
+
<div className="panel netsec-card">
|
|
255
|
+
<div className="netsec-card-head">
|
|
256
|
+
<span>Individual IPs</span>
|
|
257
|
+
{canManage && canGeoIpManage && (
|
|
258
|
+
<button type="button" className="btn btn-compact" onClick={() => void onWhitelistIp()}>
|
|
259
|
+
<i className="bi bi-shield-check" /> Whitelist an IP…
|
|
260
|
+
</button>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
<div className="netsec-card-body">
|
|
264
|
+
{geoips.length === 0
|
|
265
|
+
? <p className="dim">No individually whitelisted IPs.</p>
|
|
266
|
+
: (
|
|
267
|
+
<div className="netsec-table-scroll">
|
|
268
|
+
<table className="tbl geo-exempt-table">
|
|
269
|
+
<thead>
|
|
270
|
+
<tr>
|
|
271
|
+
<th>IP address</th><th>Reason</th><th>Expires</th><th>Status</th>
|
|
272
|
+
{canManage && canGeoIpManage && <th aria-label="Actions" />}
|
|
273
|
+
</tr>
|
|
274
|
+
</thead>
|
|
275
|
+
<tbody>
|
|
276
|
+
{geoips.map((entry) => (
|
|
277
|
+
<tr key={entry.ip} className={entry.active === false ? 'geo-row-expired' : undefined}>
|
|
278
|
+
<td><code>{entry.ip}</code></td>
|
|
279
|
+
<td>{entry.reason || <span className="dim">—</span>}</td>
|
|
280
|
+
<td>{untilText(entry.until)}</td>
|
|
281
|
+
<td><StatusPill active={entry.active !== false} /></td>
|
|
282
|
+
{canManage && canGeoIpManage && (
|
|
283
|
+
<td className="netsec-row-actions">
|
|
284
|
+
<ArmedButton
|
|
285
|
+
className="btn-compact"
|
|
286
|
+
label="Remove"
|
|
287
|
+
armedLabel="Click again — this address is evaluated against the rules again immediately"
|
|
288
|
+
onConfirm={() => onUnwhitelistIp(entry.ip)}
|
|
289
|
+
/>
|
|
290
|
+
</td>
|
|
291
|
+
)}
|
|
292
|
+
</tr>
|
|
293
|
+
))}
|
|
294
|
+
</tbody>
|
|
295
|
+
</table>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
<p className="dim">
|
|
299
|
+
Whitelisting takes a reason and an optional expiry. Full enforcement management
|
|
300
|
+
lives on the <b>Blocked IPs</b> page.
|
|
301
|
+
</p>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
{/* ── 3. Bypass holders ───────────────────────────────── */}
|
|
306
|
+
<div className="panel netsec-card">
|
|
307
|
+
<div className="netsec-card-head">
|
|
308
|
+
<span>Users who bypass geofencing</span>
|
|
309
|
+
<span className="eyebrow">read-only</span>
|
|
310
|
+
</div>
|
|
311
|
+
<div className="netsec-card-body">
|
|
312
|
+
{bypass.isPending && <p className="dim">Loading bypass holders…</p>}
|
|
313
|
+
{bypass.error && <p className="text-bad">{bypass.error.message}</p>}
|
|
314
|
+
{!bypass.isPending && !bypass.error && (
|
|
315
|
+
holders.length === 0
|
|
316
|
+
? <p className="dim">No users hold a geofence bypass.</p>
|
|
317
|
+
: (
|
|
318
|
+
<div className="netsec-table-scroll">
|
|
319
|
+
<table className="tbl geo-exempt-table">
|
|
320
|
+
<thead><tr><th>User</th><th>Status</th><th>Source</th></tr></thead>
|
|
321
|
+
<tbody>
|
|
322
|
+
{holders.map((holder) => (
|
|
323
|
+
<tr key={holder.id}>
|
|
324
|
+
<td>{holder.username}</td>
|
|
325
|
+
<td><Badge tone={holder.is_active ? 'success' : 'muted'}>{holder.is_active ? 'Active' : 'Inactive'}</Badge></td>
|
|
326
|
+
<td>
|
|
327
|
+
<Badge tone={holder.source === 'superuser' ? 'primary' : 'info'}>
|
|
328
|
+
{holder.source === 'superuser' ? 'Superuser' : 'Permission grant'}
|
|
329
|
+
</Badge>
|
|
330
|
+
</td>
|
|
331
|
+
</tr>
|
|
332
|
+
))}
|
|
333
|
+
</tbody>
|
|
334
|
+
</table>
|
|
335
|
+
</div>
|
|
336
|
+
)
|
|
337
|
+
)}
|
|
338
|
+
<p className="dim">
|
|
339
|
+
{/* The honest cap, from the response's own count/capped pair. */}
|
|
340
|
+
{bypass.data?.capped
|
|
341
|
+
? `Showing the first ${holders.length} of more than ${bypass.data.count ?? holders.length} — the endpoint caps at 200.`
|
|
342
|
+
: `Showing all ${bypass.data?.count ?? holders.length}.`}
|
|
343
|
+
{' '}Grants are managed on user records, not here.
|
|
344
|
+
</p>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// GeofencingPage — posture header over four tabs (Rules · Simulator ·
|
|
2
|
+
// Blocks log · Exemptions). Port of web-mojo `GeofencingPage.js`.
|
|
3
|
+
//
|
|
4
|
+
// This is the ONE surface in this module that stays a PAGE rather than a
|
|
5
|
+
// KISS detail modal: the requested presentation is exactly "a config page that
|
|
6
|
+
// states what is in force and lets you test it before it bites".
|
|
7
|
+
//
|
|
8
|
+
// ONE `useGeoConfig()` query feeds the header and every tab. TanStack Query
|
|
9
|
+
// replaces the source's manual onEnter/onAfterMount fetch dance entirely — a
|
|
10
|
+
// rules save or an override removal invalidates the key and every consumer
|
|
11
|
+
// re-reads, with no `config-changed` event plumbing.
|
|
12
|
+
import { useState } from 'react';
|
|
13
|
+
import { useCan } from '../../../client';
|
|
14
|
+
import type { EventRow } from '../../incidents/models';
|
|
15
|
+
import {
|
|
16
|
+
GEOFENCE_API_MISSING_MESSAGE, GEOFENCE_EVENT_CATEGORIES, GeofenceEventModel,
|
|
17
|
+
isGeofenceApiMissing, useGeoConfig,
|
|
18
|
+
} from '../models';
|
|
19
|
+
import { GEOFENCE_VIEW_PERMS, SECURITY_EVENTS_PERMS } from './geofence-data';
|
|
20
|
+
import { PostureHeader } from './PostureHeader';
|
|
21
|
+
import { RulesTab } from './RulesTab';
|
|
22
|
+
import { SimulatorTab } from './SimulatorTab';
|
|
23
|
+
import { BlocksTab } from './BlocksTab';
|
|
24
|
+
import { ExemptionsTab } from './ExemptionsTab';
|
|
25
|
+
|
|
26
|
+
type TabKey = 'rules' | 'simulator' | 'blocks' | 'exemptions';
|
|
27
|
+
|
|
28
|
+
const TABS: { key: TabKey; label: string; icon: string }[] = [
|
|
29
|
+
{ key: 'rules', label: 'Rules', icon: 'bi-sliders' },
|
|
30
|
+
{ key: 'simulator', label: 'Simulator', icon: 'bi-play-circle' },
|
|
31
|
+
{ key: 'blocks', label: 'Blocks log', icon: 'bi-journal-text' },
|
|
32
|
+
{ key: 'exemptions', label: 'Exemptions', icon: 'bi-shield-check' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export function GeofencingPage() {
|
|
36
|
+
const { can: canView } = useCan(GEOFENCE_VIEW_PERMS);
|
|
37
|
+
const { can: canEvents } = useCan(SECURITY_EVENTS_PERMS);
|
|
38
|
+
const config = useGeoConfig();
|
|
39
|
+
const [tab, setTab] = useState<TabKey>('rules');
|
|
40
|
+
|
|
41
|
+
// The Last-change chip is OPTIONAL: omitted silently — and with no request
|
|
42
|
+
// issued — for an operator without security-events access.
|
|
43
|
+
const lastChangeQuery = GeofenceEventModel.useList(
|
|
44
|
+
{ category: GEOFENCE_EVENT_CATEGORIES.config, size: 1, sort: '-created' },
|
|
45
|
+
{ enabled: canView && canEvents },
|
|
46
|
+
);
|
|
47
|
+
const lastChange: EventRow | null = lastChangeQuery.data?.rows[0] ?? null;
|
|
48
|
+
|
|
49
|
+
if (!canView) {
|
|
50
|
+
return (
|
|
51
|
+
<div className="geofencing-page">
|
|
52
|
+
<div className="panel">
|
|
53
|
+
<div className="empty-state">
|
|
54
|
+
<i className="bi bi-shield-lock" />
|
|
55
|
+
<h3>Access denied</h3>
|
|
56
|
+
<p>Geofencing configuration requires a global geofence grant.</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (config.error) {
|
|
64
|
+
// A backend without the config plane simply does not register the
|
|
65
|
+
// /api/geo routes, so the wire answer is a 404 — not an error
|
|
66
|
+
// envelope. Anything else is a real failure and shows its own message.
|
|
67
|
+
const missing = isGeofenceApiMissing(config.error);
|
|
68
|
+
return (
|
|
69
|
+
<div className="geofencing-page">
|
|
70
|
+
<div className="panel">
|
|
71
|
+
<div className="empty-state">
|
|
72
|
+
<i className={`bi ${missing ? 'bi-plug' : 'bi-exclamation-triangle'}`} />
|
|
73
|
+
<h3>{missing ? 'Geofencing administration unavailable' : 'Could not load the geofence configuration'}</h3>
|
|
74
|
+
<p>{missing ? GEOFENCE_API_MISSING_MESSAGE : config.error.message}</p>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="geofencing-page">
|
|
83
|
+
<PostureHeader config={config.data} lastChange={lastChange} loading={config.isPending} />
|
|
84
|
+
|
|
85
|
+
<div className="geo-tabs">
|
|
86
|
+
<div className="seg">
|
|
87
|
+
{TABS.map((entry) => (
|
|
88
|
+
<button
|
|
89
|
+
key={entry.key}
|
|
90
|
+
type="button"
|
|
91
|
+
className={`seg-btn${tab === entry.key ? ' seg-active' : ''}`}
|
|
92
|
+
onClick={() => setTab(entry.key)}
|
|
93
|
+
>
|
|
94
|
+
<i className={`bi ${entry.icon}`} /> {entry.label}
|
|
95
|
+
</button>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="geo-tab-body">
|
|
101
|
+
{config.isPending ? (
|
|
102
|
+
<div className="panel">
|
|
103
|
+
<div className="empty-state">
|
|
104
|
+
<i className="bi bi-hourglass-split" />
|
|
105
|
+
<p>Waiting for the geofence configuration…</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
) : (
|
|
109
|
+
<>
|
|
110
|
+
{tab === 'rules' && <RulesTab config={config.data} />}
|
|
111
|
+
{tab === 'simulator' && <SimulatorTab config={config.data} />}
|
|
112
|
+
{tab === 'blocks' && <BlocksTab />}
|
|
113
|
+
{tab === 'exemptions' && <ExemptionsTab />}
|
|
114
|
+
</>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// PostureHeader — the read-only "is geofencing actually enforcing" answer.
|
|
2
|
+
// Port of web-mojo `GeofencePostureHeader.js`.
|
|
3
|
+
//
|
|
4
|
+
// Read-only ON PURPOSE. Everything it shows (GEOFENCE_ENABLED,
|
|
5
|
+
// GEOFENCE_FAIL_CLOSED, GEOFENCE_FAIL_CLOSED_SCOPES, GEOFENCE_CACHE_TTL,
|
|
6
|
+
// GEOFENCE_STRICT_POSTURE) is a plain `Setting` row with no validated
|
|
7
|
+
// geofence-specific endpoint. Writing them through the generic settings path
|
|
8
|
+
// would bypass the validation seam the config plane exists to provide, so the
|
|
9
|
+
// header points at Runtime Settings instead. Same call web-mojo made.
|
|
10
|
+
import type { ReactNode } from 'react';
|
|
11
|
+
import { fmt } from '../../../ui';
|
|
12
|
+
import type { EventRow } from '../../incidents/models';
|
|
13
|
+
import { configChangedBy } from '../models';
|
|
14
|
+
import { collectScopes, scopeLabel, type GeoRulesConfig } from './geofence-data';
|
|
15
|
+
|
|
16
|
+
type ChipTone = '' | 'warn' | 'bad' | 'ok';
|
|
17
|
+
|
|
18
|
+
interface PostureChip {
|
|
19
|
+
label: string;
|
|
20
|
+
value: ReactNode;
|
|
21
|
+
tone: ChipTone;
|
|
22
|
+
title?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SOURCE_LABELS: Record<string, string> = {
|
|
26
|
+
setting: 'Portal (database)',
|
|
27
|
+
conf: 'Deploy file',
|
|
28
|
+
none: 'No rules configured',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** `cache_ttl` is seconds; 0 or negative means the decision cache is off. */
|
|
32
|
+
export function ttlLabel(ttl: number | null | undefined): string {
|
|
33
|
+
const n = Number(ttl);
|
|
34
|
+
if (!Number.isFinite(n) || n <= 0) return 'off';
|
|
35
|
+
if (n % 60 === 0) return `${n / 60} min`;
|
|
36
|
+
return `${n}s`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildPostureChips(
|
|
40
|
+
config: GeoRulesConfig | null | undefined,
|
|
41
|
+
lastChange: EventRow | null,
|
|
42
|
+
): PostureChip[] {
|
|
43
|
+
const posture = config?.posture ?? {};
|
|
44
|
+
const chips: PostureChip[] = [];
|
|
45
|
+
|
|
46
|
+
const source = config?.system?.source || 'none';
|
|
47
|
+
chips.push({
|
|
48
|
+
label: 'Rules source',
|
|
49
|
+
value: SOURCE_LABELS[source] ?? source,
|
|
50
|
+
tone: source === 'conf' ? 'warn' : '',
|
|
51
|
+
title: source === 'conf'
|
|
52
|
+
? 'These rules come from the deployment file. Saving in the portal creates an override that takes precedence.'
|
|
53
|
+
: undefined,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// One chip per scope the config ACTUALLY DECLARES — scopes are
|
|
57
|
+
// deployment-defined strings and are never hardcoded here.
|
|
58
|
+
const failClosedScopes = posture.fail_closed_scopes ?? [];
|
|
59
|
+
for (const scope of collectScopes(config)) {
|
|
60
|
+
const closed = Boolean(posture.fail_closed) || failClosedScopes.includes(scope);
|
|
61
|
+
chips.push({
|
|
62
|
+
label: scopeLabel(scope),
|
|
63
|
+
value: closed ? 'fail closed' : 'fail open',
|
|
64
|
+
tone: closed ? 'warn' : '',
|
|
65
|
+
title: closed
|
|
66
|
+
? 'A failed location lookup DENIES the request on this scope.'
|
|
67
|
+
: 'A failed location lookup lets the request through on this scope.',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Not in the source. `strict_posture` is what turns an EMPTY ruleset into
|
|
72
|
+
// a denial (`no_rules_strict`), so an operator staring at "no rules
|
|
73
|
+
// configured" needs to know which of the two worlds they are in.
|
|
74
|
+
if (posture.strict_posture) {
|
|
75
|
+
chips.push({
|
|
76
|
+
label: 'Strict posture',
|
|
77
|
+
value: 'on',
|
|
78
|
+
tone: 'warn',
|
|
79
|
+
title: 'With no rules configured, requests are DENIED rather than passed through.',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (posture.cache_ttl !== undefined && posture.cache_ttl !== null) {
|
|
84
|
+
chips.push({ label: 'Decision cache', value: ttlLabel(posture.cache_ttl), tone: '' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const summary = config?.allowlist_summary ?? {};
|
|
88
|
+
const ranges = summary.setting_entries ?? 0;
|
|
89
|
+
const ips = summary.geoip_active ?? 0;
|
|
90
|
+
chips.push({
|
|
91
|
+
label: 'Exemptions',
|
|
92
|
+
value: `${ranges} range${ranges === 1 ? '' : 's'} · ${ips} IP${ips === 1 ? '' : 's'}`,
|
|
93
|
+
tone: '',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (lastChange) {
|
|
97
|
+
// BACKEND CORRECTION: `evidence.report_config_change` writes
|
|
98
|
+
// `changed_by`; `reporter._create_event_dict` writes `user_name`.
|
|
99
|
+
// web-mojo read `metadata.username`, which neither writes — the chip
|
|
100
|
+
// rendered blank on every deployment.
|
|
101
|
+
const who = configChangedBy(lastChange.metadata);
|
|
102
|
+
const when = fmt.datetime(lastChange.created);
|
|
103
|
+
chips.push({ label: 'Last change', value: who ? `${when} · ${who}` : when, tone: '' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return chips;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function PostureHeader({ config, lastChange, loading }: {
|
|
110
|
+
config: GeoRulesConfig | null | undefined;
|
|
111
|
+
lastChange: EventRow | null;
|
|
112
|
+
loading?: boolean;
|
|
113
|
+
}) {
|
|
114
|
+
if (loading) {
|
|
115
|
+
return (
|
|
116
|
+
<div className="panel geo-posture">
|
|
117
|
+
<div className="dim"><i className="bi bi-hourglass-split" /> Loading geofencing status…</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const enabled = Boolean(config?.posture?.enabled);
|
|
123
|
+
const chips = buildPostureChips(config, lastChange);
|
|
124
|
+
const endpoints = (config?.enforced_endpoints ?? []).map((entry) => ({
|
|
125
|
+
// Strip the framework prefix so the list scans; keep enough of the
|
|
126
|
+
// path to stay unambiguous in a compliance screenshot.
|
|
127
|
+
endpoint: String(entry.endpoint ?? '').replace(/^mojo\.apps\./, ''),
|
|
128
|
+
scope: entry.scope || '—',
|
|
129
|
+
afterAuth: entry.after_auth === true,
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="panel geo-posture">
|
|
134
|
+
<div className="geo-posture-head">
|
|
135
|
+
<span className="geo-posture-icon"><i className="bi bi-globe-americas" /></span>
|
|
136
|
+
<div className="geo-posture-title">
|
|
137
|
+
<h2>Geofencing</h2>
|
|
138
|
+
<p className="dim">Location-based access rules for this deployment’s protected endpoints.</p>
|
|
139
|
+
</div>
|
|
140
|
+
<span className={`chip ${enabled ? 'chip-success' : 'chip-muted'} geo-status-pill`}>
|
|
141
|
+
<i className={`bi ${enabled ? 'bi-shield-check' : 'bi-shield-slash'}`} />
|
|
142
|
+
{enabled ? 'Enforcing' : 'Off — rules staged, not enforcing'}
|
|
143
|
+
</span>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div className="geo-chip-row">
|
|
147
|
+
{chips.map((chip, index) => (
|
|
148
|
+
<span key={`${chip.label}-${index}`} className={`geo-chip ${chip.tone}`} title={chip.title}>
|
|
149
|
+
{chip.label} <b>{chip.value}</b>
|
|
150
|
+
</span>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{endpoints.length > 0 ? (
|
|
155
|
+
<details className="geo-endpoints">
|
|
156
|
+
<summary>{endpoints.length} protected endpoint{endpoints.length === 1 ? '' : 's'}</summary>
|
|
157
|
+
<div className="geo-endpoint-list">
|
|
158
|
+
{endpoints.map((entry) => (
|
|
159
|
+
<div key={entry.endpoint} className="geo-endpoint">
|
|
160
|
+
<span className="chip chip-muted">{entry.scope}</span>
|
|
161
|
+
<code>{entry.endpoint}</code>
|
|
162
|
+
{entry.afterAuth && (
|
|
163
|
+
<span className="chip chip-warning" title="This endpoint is gated AFTER credentials are verified.">
|
|
164
|
+
enforced after credentials
|
|
165
|
+
</span>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
</details>
|
|
171
|
+
) : (
|
|
172
|
+
<p className="dim geo-endpoints-empty">
|
|
173
|
+
<i className="bi bi-info-circle" /> No endpoints on this server are gated by geofencing.
|
|
174
|
+
</p>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
<p className="dim geo-posture-foot">
|
|
178
|
+
Posture values (enforcement, fail-closed scopes, cache TTL, strict posture) are
|
|
179
|
+
deployment settings and are edited in Runtime Settings, not here — this page owns
|
|
180
|
+
the rules, exemptions and evidence.
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|