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,284 @@
|
|
|
1
|
+
# Admin: devices, login locations & GeoIP
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import {
|
|
5
|
+
// sections (registered in ADMIN_SECTIONS — no app wiring needed)
|
|
6
|
+
DEVICE_INTEL_ADMIN_SECTION, GEOIP_ADMIN_SECTION,
|
|
7
|
+
// pages
|
|
8
|
+
UserDevicesPage, LoginLocationsPage, GeoIpCachePage,
|
|
9
|
+
// the map binding
|
|
10
|
+
LoginLocationMap,
|
|
11
|
+
// KISS detail modals (#1425)
|
|
12
|
+
showUserDeviceDetail, showUserDeviceDetailByDuid,
|
|
13
|
+
showUserDeviceLocationDetail, showLoginEventDetail,
|
|
14
|
+
showGeoIpDossier, showGeoIpDossierForAddress,
|
|
15
|
+
// models — ONE defineModel per endpoint, package-wide
|
|
16
|
+
UserDeviceModel, UserDeviceLocationModel, LoginEventModel, GeoLocatedIPModel,
|
|
17
|
+
// permission clauses
|
|
18
|
+
USER_DEVICE_VIEW_PERMS, DEVICE_LOCATION_VIEW_PERMS,
|
|
19
|
+
LOGIN_EVENT_VIEW_PERMS, LOGIN_SUMMARY_PERMS,
|
|
20
|
+
GEOIP_VIEW_PERMS, GEOIP_MANAGE_PERMS,
|
|
21
|
+
// enforcement + presentation helpers
|
|
22
|
+
blockActive, whitelistActive, blockExpired, whitelistExpired,
|
|
23
|
+
threatTone, riskScoreOf, countryFlag, THREAT_LEVEL_ORDER,
|
|
24
|
+
loginRiskTone, LOGIN_RISK_LEGEND,
|
|
25
|
+
deviceIcon, browserLabel, osLabel, deviceLabel, daysActive, presenceOf,
|
|
26
|
+
// data hooks
|
|
27
|
+
useLoginLocationSummary, useLoginLocationList,
|
|
28
|
+
useUserDeviceSessions, useDeviceSiblings,
|
|
29
|
+
useGeoIpByAddress, useGeoIpRawRecord, lookupGeoIp,
|
|
30
|
+
geoIpSafeExporter, sanitizeGeoIpRow,
|
|
31
|
+
} from 'portal-mojo/admin';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Account-security triage: who logged in from a new country, what is this IP,
|
|
35
|
+
and is this device shared across accounts. Demo: showcase → Admin →
|
|
36
|
+
**Devices, logins & GeoIP**. Styles:
|
|
37
|
+
`apps/{portal,showcase}/src/theme/admin-devices.css` (byte-identical).
|
|
38
|
+
|
|
39
|
+
## Sections
|
|
40
|
+
|
|
41
|
+
Two siblings under the **Security** navigation group, beside Bouncer — so the
|
|
42
|
+
PRE-auth (`BouncerDevice.muid`) and POST-auth (`UserDevice.muid`) views of the
|
|
43
|
+
same physical browser sit next to each other.
|
|
44
|
+
|
|
45
|
+
| Section | id / basePath | Routes |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| Devices & Logins | `device-intel` / `security/devices` | `user-devices` · `login-locations` |
|
|
48
|
+
| IP Intelligence | `geoip` / `security/geoip` | `cache` |
|
|
49
|
+
|
|
50
|
+
All three surfaces are fleet-wide and **groupless** — `GeoLocatedIP` has no
|
|
51
|
+
group FK at all, and the device/login lists are only global with a `sys.`
|
|
52
|
+
grant. The *user-scoped* views stay inside `UserDetail`'s Devices and Logins
|
|
53
|
+
sections, which this module extends rather than forks.
|
|
54
|
+
|
|
55
|
+
`#1287`'s network-security pages (Blocked IPs, firewall log, IP sets,
|
|
56
|
+
geofencing) live in their OWN section at `security/network`; this module
|
|
57
|
+
reserves no space for them and exposes no extension seam. It does export
|
|
58
|
+
everything they import.
|
|
59
|
+
|
|
60
|
+
## Permissions
|
|
61
|
+
|
|
62
|
+
Every clause is `sys.`-pinned, so an active-member grant can never satisfy it.
|
|
63
|
+
A permission-disabled query passes `enabled: false` — a denied surface issues
|
|
64
|
+
**no** request.
|
|
65
|
+
|
|
66
|
+
| Constant | Value | Backend source |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `USER_DEVICE_VIEW_PERMS` | `sys.manage_users`, `sys.users` | `UserDevice.VIEW_PERMS` minus `owner` |
|
|
69
|
+
| `DEVICE_LOCATION_VIEW_PERMS` | `sys.manage_users`, `sys.users` | URL decorator ∩ `UserDeviceLocation.VIEW_PERMS` |
|
|
70
|
+
| `LOGIN_EVENT_VIEW_PERMS` | `sys.manage_users`, `sys.security`, `sys.users` | `UserLoginEvent.VIEW_PERMS` minus `owner` |
|
|
71
|
+
| `LOGIN_SUMMARY_PERMS` | `sys.manage_users`, `sys.security`, `sys.users` | `@requires_global_perms` on `/summary` + `/user` |
|
|
72
|
+
| `GEOIP_VIEW_PERMS` | `sys.manage_users`, `sys.view_security`, `sys.manage_security`, `sys.security`, `sys.users` | `GeoLocatedIP.VIEW_PERMS` |
|
|
73
|
+
| `GEOIP_MANAGE_PERMS` | `sys.manage_users`, `sys.manage_security`, `sys.security` | `GeoLocatedIP.SAVE_PERMS` |
|
|
74
|
+
|
|
75
|
+
**`sys.manage_devices` is deliberately absent everywhere.** It passes the
|
|
76
|
+
`@requires_global_perms` decorator on `/user/device/location` and
|
|
77
|
+
`/user/device/lookup`, then fails the *model's* `VIEW_PERMS`
|
|
78
|
+
(`manage_users | users`). Alone it opens nothing, so offering it as an
|
|
79
|
+
affordance would promise access the server refuses.
|
|
80
|
+
|
|
81
|
+
`UserDevice` and `UserLoginEvent` both carry `owner` in `VIEW_PERMS`: without
|
|
82
|
+
a global grant those endpoints return only the caller's own rows. The admin
|
|
83
|
+
pages are gated so that is never what an operator sees; the mock reproduces it
|
|
84
|
+
so the verifier can assert it.
|
|
85
|
+
|
|
86
|
+
## Endpoints
|
|
87
|
+
|
|
88
|
+
| Endpoint | Gate | Notes |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `GET /api/user/device` `(+/<pk>)` | model `VIEW_PERMS` incl. `owner` | graphs `default` · `basic` · `locations` · `sessions` |
|
|
91
|
+
| `GET /api/user/device/lookup?duid=` | `@requires_global_perms(manage_users, manage_devices, users)` | cross-tenant; `on_rest_get` does not re-gate |
|
|
92
|
+
| `GET /api/user/device/location` `(+/<pk>)` | URL decorator **and** model `VIEW_PERMS` | |
|
|
93
|
+
| `GET /api/account/logins` `(+/<pk>)` | model `VIEW_PERMS` incl. `owner` | `CAN_CREATE/UPDATE/DELETE` all False |
|
|
94
|
+
| `GET /api/account/logins/summary` | `@requires_global_perms(manage_users, security, users)` | country aggregation |
|
|
95
|
+
| `GET /api/account/logins/user?user_id=` | same, `@requires_params('user_id')` | per-user aggregation |
|
|
96
|
+
| `GET /api/system/geoip` `(+/<pk>)` | `GeoLocatedIP.VIEW_PERMS` / `SAVE_PERMS` | graphs `default` · `basic` · `detailed` · `federation` |
|
|
97
|
+
| `GET /api/system/geoip/lookup?ip=` | `@requires_auth()` only, rate-limited 30/ip | see the graph downgrade below |
|
|
98
|
+
|
|
99
|
+
**No model here declares `CAN_DELETE`,** and `rest.py` defaults it to False.
|
|
100
|
+
There is no delete path for a device, a device location, a login event or a
|
|
101
|
+
GeoIP record — the source's "Forget device", "Delete record" and "Delete
|
|
102
|
+
Location Record" items could never have worked.
|
|
103
|
+
|
|
104
|
+
## Model field shapes
|
|
105
|
+
|
|
106
|
+
`UserDeviceRow` is the `default` graph. It has **no** `is_trusted` and **no**
|
|
107
|
+
`is_blocked` — web-mojo's active toggle, "Mark trusted/untrusted" kebab item,
|
|
108
|
+
Trusted/Blocked chips and trust timeline row all wrote a field the model never
|
|
109
|
+
had.
|
|
110
|
+
|
|
111
|
+
`UserDeviceSessionsRow` is the `sessions` graph, keyed separately in the query
|
|
112
|
+
cache (`[endpoint, 'one', id, {graph:'sessions'}]`) so a sparse `default` row
|
|
113
|
+
cannot satisfy it. Its three `extra` properties are real:
|
|
114
|
+
|
|
115
|
+
- `bouncer_device` — the pre-auth `BouncerDevice` for this `muid`.
|
|
116
|
+
- `active_sessions[]` — 24h of `BouncerSignal` grouped by `msid`, each with a
|
|
117
|
+
per-`mtab` breakdown.
|
|
118
|
+
- `recent_locations[]` — the last ten `UserDeviceLocation` rows, flattened.
|
|
119
|
+
|
|
120
|
+
`LoginEventRow` has **no** `event_type`. `GeoLocatedIPRow` has **no**
|
|
121
|
+
`reverse_dns`, **no** `ip_version` and **no** `metadata`, and its `provider`
|
|
122
|
+
and `data` fields are excluded from the `default` graph.
|
|
123
|
+
|
|
124
|
+
## `LoginLocationMap`
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
<LoginLocationMap
|
|
128
|
+
userId={user.id} // scoped → /account/logins/user; omit → /summary
|
|
129
|
+
height={360}
|
|
130
|
+
drStart="2026-07-07" drEnd="2026-08-06" // YYYY-MM-DD, see the two wires
|
|
131
|
+
defaultMode="summary" // 'summary' | 'list'
|
|
132
|
+
showModeToggle
|
|
133
|
+
enabled={can} // false ⇒ no request at all
|
|
134
|
+
onCountrySelect={(cc) => …}
|
|
135
|
+
onOpenLogin={(id) => …}
|
|
136
|
+
onOpenUser={(id) => …} // list mode, global scope only
|
|
137
|
+
/>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Rendering belongs entirely to [`WorldMap`](worldmap.md) (#1426). One import
|
|
141
|
+
line — `from 'portal-mojo/charts'` — is the whole coupling surface, so
|
|
142
|
+
reconciling with the map component touches exactly one file. No projection,
|
|
143
|
+
SVG, marker geometry or colour ramp lives here.
|
|
144
|
+
|
|
145
|
+
- **Summary mode** — one sized marker per country, or per region after a
|
|
146
|
+
drill-down. Position comes from `COUNTRY_CENTROIDS` first and the server's
|
|
147
|
+
`Avg(latitude)`/`Avg(longitude)` only as a fallback: for a country spanning
|
|
148
|
+
the antimeridian that average lands in the wrong ocean. Region rows have no
|
|
149
|
+
centroid and always use the average.
|
|
150
|
+
- **List mode** — one dot per login, bounded at 500, coloured by
|
|
151
|
+
`loginRiskTone(row)`.
|
|
152
|
+
- **Drill-down** is *double-click*, on country markers, in summary mode only.
|
|
153
|
+
A single click fires `onCountrySelect` after WorldMap's 250 ms
|
|
154
|
+
double-click-cancellation window — that quarter-second delay is by design.
|
|
155
|
+
- **Coastlines come from `useWorldLand()`.** The checked-in
|
|
156
|
+
public-domain Natural Earth 110m outlines, no CDN and no tile server (see
|
|
157
|
+
`worldmap.md` → "The `land` seam"). The hook loads them in a separate chunk,
|
|
158
|
+
so pages without a map never carry the geometry. Rendering
|
|
159
|
+
correctly *without* it stays a hard requirement: drop the prop and the ocean +
|
|
160
|
+
graticule fallback still carries the markers, legend, tooltips, drill bar and
|
|
161
|
+
status line.
|
|
162
|
+
|
|
163
|
+
### `loginRiskTone` — and the helper that was deleted
|
|
164
|
+
|
|
165
|
+
`loginRiskTone(row)` reads fields the wire actually sends: `is_new_country` →
|
|
166
|
+
`bad`, `is_new_region` → `warn`, otherwise `ok`.
|
|
167
|
+
|
|
168
|
+
It replaced `loginEventTone(eventType)`, which `portal-mojo/charts` briefly
|
|
169
|
+
exported after being ported from web-mojo. That helper mapped a login
|
|
170
|
+
event-type vocabulary (`success_login`, `failed_login`, `suspicious`, …) — and
|
|
171
|
+
**`UserLoginEvent` has never had an `event_type` field**, so a real row returned
|
|
172
|
+
`'mute'` every time. That is precisely the web-mojo bug where every dot on the
|
|
173
|
+
login map rendered grey.
|
|
174
|
+
|
|
175
|
+
It was **removed from the package 2026-08-06** rather than kept beside this one:
|
|
176
|
+
a plausible-looking name that silently greys out every marker is a trap, and
|
|
177
|
+
there is no series in django-mojo that carries an event type for it to be
|
|
178
|
+
correct about. `verify-worldmap.mjs` asserts both old names stay gone.
|
|
179
|
+
|
|
180
|
+
## The two different date wires
|
|
181
|
+
|
|
182
|
+
They are not interchangeable, and mixing them silently returns everything.
|
|
183
|
+
|
|
184
|
+
| Endpoint | Wire | Format |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `/api/account/logins` (list) | `dr_field` + `dr_start` + `dr_end` | always name `dr_field` explicitly |
|
|
187
|
+
| `/summary` and `/user` | `dr_start` + `dr_end` only | `YYYY-MM-DD`, parsed by `dates.parse` |
|
|
188
|
+
|
|
189
|
+
## Backend traps encoded here
|
|
190
|
+
|
|
191
|
+
- **`region=false` is truthy.** `login_event.py` reads
|
|
192
|
+
`request.DATA.get('region')` with no string coercion, so the *string*
|
|
193
|
+
`"false"` drills. `region=1` is sent only while drilling, never `region=false`.
|
|
194
|
+
- **`user_id` must parse as an int** on `/logins/user`, or the endpoint
|
|
195
|
+
answers `{status: false, code: 400}`.
|
|
196
|
+
- **`country_code` must match `^[A-Z]{2,3}$`** uppercase or it is silently
|
|
197
|
+
ignored — an invalid code falls back to the country branch rather than erroring.
|
|
198
|
+
- **The region drill caps at 500 rows** (`MAX_REGION_RESULTS`).
|
|
199
|
+
- **`/summary` excludes null and empty `country_code`,** and private-range
|
|
200
|
+
logins carry no coordinates — so those logins are invisible on the map *by
|
|
201
|
+
construction*. The status line says so; the Logins tab is where they appear.
|
|
202
|
+
- **The embedded `user_device` graph is `basic` and carries no `id`.**
|
|
203
|
+
Cross-links from a location or login row resolve the device by `duid`
|
|
204
|
+
(`showUserDeviceDetailByDuid`).
|
|
205
|
+
- **`_param_is_true`**: query params arrive verbatim, so `auto_refresh=false`
|
|
206
|
+
would be truthy read naively. `lookupGeoIp` sends `1`/`0`.
|
|
207
|
+
|
|
208
|
+
## Enforcement expiry is STATE
|
|
209
|
+
|
|
210
|
+
`GeoLocatedIP` exposes `block_active` and `whitelist_active` as Python
|
|
211
|
+
properties, but they are serialized only on the `basic` graph — the `default`
|
|
212
|
+
graph carries the raw booleans. `blockActive(row)` / `whitelistActive(row)`
|
|
213
|
+
mirror the model exactly:
|
|
214
|
+
|
|
215
|
+
- a whitelist **wins** over a block;
|
|
216
|
+
- a `blocked_until` in the past means **not blocked**;
|
|
217
|
+
- a `whitelisted_until` in the past means **not whitelisted**.
|
|
218
|
+
|
|
219
|
+
`blockExpired` / `whitelistExpired` surface the lapsed-but-still-flagged state
|
|
220
|
+
as its own badge. web-mojo rendered raw `is_blocked`, so an expired block
|
|
221
|
+
still read "Blocked" and a whitelisted-but-flagged IP read blocked when it was
|
|
222
|
+
not.
|
|
223
|
+
|
|
224
|
+
`block()` on the backend always escalates `threat_level` to at least `high`
|
|
225
|
+
and increments `block_count`; whitelisting clears an active block. The UI
|
|
226
|
+
re-reads the row from the action response rather than optimistically toggling,
|
|
227
|
+
because the backend block is idempotent and race-safe.
|
|
228
|
+
|
|
229
|
+
## Scrubbing rules
|
|
230
|
+
|
|
231
|
+
1. **The raw provider blob never enters the Query cache.** `sanitizeGeoIpRow`
|
|
232
|
+
deletes `data` before any list/detail/save write and runs the shared
|
|
233
|
+
`sanitizeSecurityValue` over the rest.
|
|
234
|
+
2. **The only reader of `graph=detailed`** is `useGeoIpRawRecord` — an
|
|
235
|
+
explicit, `GEOIP_MANAGE_PERMS`-gated, `gcTime: 0` / `staleTime: 0` fetch
|
|
236
|
+
whose payload is sanitized and length-bounded before it is returned. It is
|
|
237
|
+
behind a "Show raw provider record" reveal that is collapsed by default.
|
|
238
|
+
3. **Exports go through `geoIpSafeExporter`** (`createSafeExporter`) with a
|
|
239
|
+
declared field projection, so a crafted `graph` param cannot smuggle
|
|
240
|
+
`data` or `provider` into a CSV.
|
|
241
|
+
4. **Inspecting an IP costs no provider call.** `showGeoIpDossierForAddress`
|
|
242
|
+
resolves through `?ip_address=` on the cache list — read-only, unmetered.
|
|
243
|
+
`/system/geoip/lookup` CREATES and REFRESHES records and is rate-limited
|
|
244
|
+
30/ip, so it runs only on the explicit "Look up IP" action; a 429 surfaces
|
|
245
|
+
as a toast, never a silent no-op.
|
|
246
|
+
5. **`/geoip/lookup` downgrades the graph, not the access.** The endpoint is
|
|
247
|
+
open to any authenticated identity (that is how a downstream instance uses
|
|
248
|
+
this one as its GeoIP provider), but a caller without `VIEW_PERMS` is
|
|
249
|
+
served the `federation` graph whatever it asked for — no enforcement state,
|
|
250
|
+
no raw blob.
|
|
251
|
+
|
|
252
|
+
## Absent by design
|
|
253
|
+
|
|
254
|
+
Do not add these back; each was verified against the backend.
|
|
255
|
+
|
|
256
|
+
- The **trust toggle** and "Mark trusted/untrusted" — `is_trusted` does not exist.
|
|
257
|
+
- **"Forget device"**, "Delete GeoIP record", "Delete location record" — no
|
|
258
|
+
`CAN_DELETE` anywhere.
|
|
259
|
+
- Any **write** to a login event — `CAN_CREATE/UPDATE/DELETE` are all False.
|
|
260
|
+
- The six phantom KPI fields (`event_count`, `incident_count`,
|
|
261
|
+
`login_attempts`, `login_count`, `session_count`, `location_count`), which
|
|
262
|
+
existed on no model and rendered "—" forever. They are replaced with
|
|
263
|
+
`size: 0` count queries against endpoints that do exist.
|
|
264
|
+
- The `reverse_dns` / `ip_version` Network rows and the `provider` row on the
|
|
265
|
+
default graph — all three were permanently blank.
|
|
266
|
+
- A `user` **select** filter on either table: neither endpoint can enumerate
|
|
267
|
+
the directory, so the filter is the FK id.
|
|
268
|
+
- A search placeholder promising user or IP search on `/api/user/device` —
|
|
269
|
+
that model declares no `SEARCH_FIELDS` at all.
|
|
270
|
+
|
|
271
|
+
## Verification
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
node scripts/verify-admin-devices.mjs # or: npm run verify:admin-devices
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Headless assertions over the six permission clauses (`sys.`-pinning,
|
|
278
|
+
member-grant rejection, `manage_devices` absence), route generation for both
|
|
279
|
+
mounts, expiry semantics, the risk tone, the scrubber and the export
|
|
280
|
+
projection, one-`defineModel`-per-endpoint package-wide, the absent-by-design
|
|
281
|
+
list, theme byte-identity and tokens-only CSS, and the full mock wire:
|
|
282
|
+
owner scoping, anonymous denial, all four GeoIP graphs, the `/lookup` graph
|
|
283
|
+
downgrade, every POST_SAVE_ACTION, the verb refusals, the aggregation shape,
|
|
284
|
+
the region drill, the 500 cap, the `user_id` 400 and the `region=false` trap.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Admin DNS certificates and ACME lifecycle
|
|
2
|
+
|
|
3
|
+
Import from `portal-mojo/admin`:
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import {
|
|
7
|
+
CertificatesPage, CertificateDetail, CertificateRequestDialog,
|
|
8
|
+
DomainCertificatesSection, CertificateLifecyclePoller,
|
|
9
|
+
CertificateModel, deriveCertificateRenewalHealth,
|
|
10
|
+
deriveCertificateReadiness,
|
|
11
|
+
} from 'portal-mojo/admin';
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Route, gates, and presentation
|
|
15
|
+
|
|
16
|
+
`DNS_ADMIN_SECTION` contributes `dns/certificates` between DNS Records and
|
|
17
|
+
Provider Credentials. Like the rest of Admin it is global and no-group:
|
|
18
|
+
`sys.view_dns | sys.manage_dns | sys.security` opens the route and
|
|
19
|
+
`sys.manage_dns | sys.security` offers request/revoke. Tables own search,
|
|
20
|
+
status/domain filters, paging, and sort in the URL.
|
|
21
|
+
|
|
22
|
+
Row inspection uses `modal.detail`; requesting and confirming revocation use
|
|
23
|
+
small native KISS modals. `DomainDetail` composes the bounded
|
|
24
|
+
`DomainCertificatesSection` rather than adding a child route.
|
|
25
|
+
|
|
26
|
+
Certificate detail and revoke have an extra backend rule: when the hydrated
|
|
27
|
+
owning Domain has `group=null`, only a literal interactive superuser may open
|
|
28
|
+
or revoke it. A global `manage_dns` grant is not enough. The client hydrates
|
|
29
|
+
the Domain before fetching certificate detail, checks `me.is_superuser ===
|
|
30
|
+
true`, and gives a generic unavailable answer on denial so it does not become
|
|
31
|
+
an additional house-inventory oracle. The server remains authoritative.
|
|
32
|
+
|
|
33
|
+
## Wire contract and cache boundary
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
GET /api/dnsman/certificate?graph=default
|
|
37
|
+
GET /api/dnsman/certificate/<id>
|
|
38
|
+
POST /api/dnsman/certificate/request {domain, names?}
|
|
39
|
+
POST /api/dnsman/certificate/revoke {certificate}
|
|
40
|
+
GET /api/dnsman/delegation?domain=<id>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Only `graph=default` is accepted. Certificate export/download params are
|
|
44
|
+
rejected, not silently forwarded. The graph carries lifecycle metadata and a
|
|
45
|
+
basic Domain relation; it carries no `cert_pem`, `chain_pem`, private key,
|
|
46
|
+
ACME order URL, secret container, or arbitrary metadata. There is deliberately
|
|
47
|
+
no client helper or mock handler for `certificate/material/<id>`.
|
|
48
|
+
|
|
49
|
+
All list/detail rows pass through `sanitizeCertificateRow` in the Query
|
|
50
|
+
function, before TanStack Query stores them. It normalizes lifecycle status,
|
|
51
|
+
lowercase/trailing-dot-free SANs, finite timestamps/counts, a small Domain
|
|
52
|
+
projection, and a bounded/redacted `last_error`. Unknown/equivalent private
|
|
53
|
+
containers drop by construction. Delegation payloads retain only the backend's
|
|
54
|
+
public status shape; credential readiness uses the already-sanitized masked
|
|
55
|
+
credential model.
|
|
56
|
+
|
|
57
|
+
Request and revoke are imperative, single-flight, and non-retrying so an
|
|
58
|
+
ambiguous transport cannot duplicate an external CA operation. They do not use
|
|
59
|
+
MutationCache. A `finally` reconciliation invalidates/refetches active
|
|
60
|
+
certificate queries after success or failure. If authoritative refresh also
|
|
61
|
+
fails, repeat is blocked by an explicit “may have persisted; refresh first”
|
|
62
|
+
error.
|
|
63
|
+
|
|
64
|
+
## Names and readiness
|
|
65
|
+
|
|
66
|
+
The request dialog normalizes names to lowercase ASCII without trailing dots,
|
|
67
|
+
deduplicates them, permits a wildcard only as the leading `*.`, and requires
|
|
68
|
+
every SAN to be the selected zone or a descendant. Empty names intentionally
|
|
69
|
+
omit `names`, letting the backend request the apex plus wildcard.
|
|
70
|
+
|
|
71
|
+
Readiness is backend-authoritative:
|
|
72
|
+
|
|
73
|
+
- ACME must be configured and the Domain active.
|
|
74
|
+
- Route 53 uses direct DNS-01.
|
|
75
|
+
- GoDaddy direct DNS-01 also needs its active, verified masked credential.
|
|
76
|
+
- `provider="mojo"` needs a verified delegated-ACME row.
|
|
77
|
+
- Once a delegation has `verified_at`, it is sticky. A later `broken` row
|
|
78
|
+
blocks issuance; the UI never claims direct-provider fallback.
|
|
79
|
+
|
|
80
|
+
`config.acme.staging` describes the deployment's current directory only. The
|
|
81
|
+
certificate model stores no historical staging provenance, so existing rows
|
|
82
|
+
are never labeled trusted or staging from that flag. New staging requests get
|
|
83
|
+
an explicit “not publicly trusted” warning.
|
|
84
|
+
|
|
85
|
+
## Renewal and polling
|
|
86
|
+
|
|
87
|
+
`renew_after` is the authoritative renewal trigger. `days_remaining` is only
|
|
88
|
+
display. `CertificateLifecyclePoller` owns one timer per mounted certificate
|
|
89
|
+
surface, at 10 seconds and at most 36 ticks:
|
|
90
|
+
|
|
91
|
+
- new issuance follows `pending | issuing` to active/failed/revoked;
|
|
92
|
+
- a due active renewal records attempts/validity/error baseline, waits through
|
|
93
|
+
the initial active state and optional `issuing`, then stops on an observable
|
|
94
|
+
success or failure;
|
|
95
|
+
- an already-active row with `last_error` is a terminal renewal error and does
|
|
96
|
+
not start ordinary polling;
|
|
97
|
+
- timeout, unmount, and terminal state clear the timer.
|
|
98
|
+
|
|
99
|
+
This avoids one interval per row and avoids an endless poll on the backend's
|
|
100
|
+
valid “renewal failed but still-valid certificate remains active” behavior.
|
|
101
|
+
|
|
102
|
+
## Mock, themes, and verification
|
|
103
|
+
|
|
104
|
+
The central mock contains pending, issuing, healthy active, due renewal,
|
|
105
|
+
active renewal-error, failed, revoked, house, verified delegation, broken
|
|
106
|
+
sticky delegation, staging, unconfigured, and malformed-response evidence.
|
|
107
|
+
Its only custody fixture is a private `material_present` boolean; no PEM is
|
|
108
|
+
stored or serialized.
|
|
109
|
+
|
|
110
|
+
Portal and Showcase both import `theme/admin-dns-certificates.css`, which uses
|
|
111
|
+
semantic tokens only. The executable demo is **Develop → Components → DNS
|
|
112
|
+
certificates & ACME** with manager, viewer, platform-house, and unconfigured
|
|
113
|
+
legs. Run:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm run verify:admin-dns-certificates
|
|
117
|
+
npm run verify:admin-dns
|
|
118
|
+
npm run verify:admin-dns-records
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Live verification is read-only. Do not request or revoke a real certificate
|
|
122
|
+
without separate operator authorization.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# DNS domains and live records
|
|
2
|
+
|
|
3
|
+
Import from `portal-mojo/admin`:
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import {
|
|
7
|
+
DNS_ADMIN_SECTION, DomainsPage, DnsRecordsPage, DnsRecordsPanel,
|
|
8
|
+
DnsRecordEditor, DNS_VIEW_PERMISSIONS, DNS_MANAGE_PERMISSIONS,
|
|
9
|
+
registerDnsAdminIntegration, registerDnsDomainLinks,
|
|
10
|
+
} from 'portal-mojo/admin';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Routes and permissions
|
|
14
|
+
|
|
15
|
+
The one canonical DNS section is `id: "dns"`, `basePath: "dns"`, in Infrastructure. Its routes are ordered `domains`, `records`, `certificates`, `purchases`, `registrant`, `credentials`. The read surfaces accept the global, system-pinned any-of view clause `sys.view_dns | sys.manage_dns | sys.security`; registrant and record mutation controls use `sys.manage_dns | sys.security`. Group/member grants never open Admin.
|
|
16
|
+
|
|
17
|
+
Domains uses the server-backed `DomainModel` list and `modal.detail`. Detail composes Overview, DNS Records, and the bounded Certificates section; Records is the same `DnsRecordsPanel` used by the standalone page. That page owns only `domain` and `record_type` in the URL. It may select the first active domain only when `domain` is absent. Invalid, denied, missing, and inactive deep links are preserved and shown as errors.
|
|
18
|
+
|
|
19
|
+
## Wire contract
|
|
20
|
+
|
|
21
|
+
Domain list/default reads are standard model envelopes. List asks only for paging, search, sort, provider, status, group and the `list` graph. Live records are deliberately not a model:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
GET /api/dnsman/dns?domain=8201
|
|
25
|
+
{status:true,data:{domain:"acme.example",provider:"route53",
|
|
26
|
+
records:[{type:"A",name:"acme.example",record_values:["192.0.2.10"],ttl:300}]}}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Records have no database id, paging, search, or graph. The parser fails closed on malformed payloads or an invented `id`. Local identity is `TYPE|normalized-fqdn`.
|
|
30
|
+
|
|
31
|
+
Upsert replaces the complete ordered set and sends exactly
|
|
32
|
+
`{domain,type,name,record_values,ttl}`. Whole-set deletion sends only
|
|
33
|
+
`{domain,type,name}`. Removing some values is an upsert of all survivors,
|
|
34
|
+
never a partial delete.
|
|
35
|
+
|
|
36
|
+
Both write endpoints answer at the top level as
|
|
37
|
+
`{status:true,change_id:string|null,provider:string}`. Route 53 supplies a
|
|
38
|
+
change id; GoDaddy may return `null`. There is no nested `data` write payload.
|
|
39
|
+
|
|
40
|
+
## Editor invariants
|
|
41
|
+
|
|
42
|
+
A, AAAA, CNAME, TXT, MX, SRV, CAA, and delegated non-apex NS use controlled structured rows, intersected with `allowed_record_types`. Unknown types remain readable and have no editor. Existing type/name are immutable.
|
|
43
|
+
|
|
44
|
+
Paste and blur may correct invisible characters, NBSP, curly quotes, URL/path/trailing dots, hostname case, bracketed IPs, and surrounding TXT/CAA quotes. Every correction records field, before, after, and message. Existing TXT values, including meaningful leading, trailing, and repeated whitespace, parse, format, edit, and submit losslessly unless the operator explicitly triggers and acknowledges a paste/blur correction. Submit never trims, dequotes, lowercases, normalizes, or deduplicates unacknowledged state. Blank/duplicate values, incomplete structured rows, invalid IPv4/full IPv6, hostname/range/TTL errors, out-of-zone or malformed names, invalid wildcards, apex NS/SOA/CNAME, and CNAME coexistence fail before the request. Server refusal remains authoritative.
|
|
45
|
+
|
|
46
|
+
Confirmation shows identity, TTL, unchanged/removed/added values, and contextual warnings from the editor opening snapshot. After confirmation the coordinator performs an uncached read, compares the exact set and every same-owner record (including CNAME coexistence), and writes immediately only when unchanged. A `finally` read reconciles cache after success, rejection, or an ambiguous response, and the zone query is invalidated unconditionally even when that read fails so stale cache is never authoritative. The draft and original error survive. This is best-effort preflight: no ETag/CAS exists, so a narrow GET-to-POST race remains.
|
|
47
|
+
|
|
48
|
+
## Provider behavior
|
|
49
|
+
|
|
50
|
+
- Route 53 supports complete-set upsert and separately confirmed whole-set delete.
|
|
51
|
+
- GoDaddy manages DNS here; registrar operations stay in its account. The server clamps refreshed TTLs to 600 and cannot express whole-set deletion. Exact `_acme-challenge.* TXT ["retired"]` rows are spent/inert only for GoDaddy.
|
|
52
|
+
- `provider="mojo"` is certificate-only and fails closed for general DNS.
|
|
53
|
+
- Pending/registering domains, unknown providers, and missing/inactive/unverified credentials render explicit blocked states.
|
|
54
|
+
|
|
55
|
+
## Composition seams
|
|
56
|
+
|
|
57
|
+
`dns-integration.ts` is dependency-free and HMR-safe. Partial registrations merge: production supplies exact-name resolution and `dns/records?domain=<id>`; the central mock supplies `applyManagedDnsRecords` against its sole DNS store. Absence is `null`, so permission is never mistaken for feature availability.
|
|
58
|
+
|
|
59
|
+
`registerDnsDomainLinks` is the reverse registry for certificate, WHOIS, or purchase children. Stable keys replace on HMR. Each entry owns label/icon, mount-relative route, optional system permission, capability/domain predicate, and optional order. Built-in related links are deterministically ordered Domains, DNS Records, Certificates, Provider Credentials.
|
|
60
|
+
|
|
61
|
+
## Mock and verification
|
|
62
|
+
|
|
63
|
+
The central mock is the only DNS state owner and mirrors id-less envelopes, global-versus-tenant authorization, active/provider/credential gates, complete replacement, Route 53 delete, GoDaddy floor/refusal/spent placeholders, Mojo, unknown records, and structured types. Mutation verification is mock-only. Live verification may read a zone and open/cancel an editor, but must never POST or DELETE DNS records.
|
|
64
|
+
|
|
65
|
+
Both Portal and Showcase import `theme/admin-dns.css`; all colors use theme tokens. Run `npm run verify:admin-dns-records` with the orchestration-owned typecheck/build/browser pass.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Admin DNS registrar and registrant contact
|
|
2
|
+
|
|
3
|
+
Import `DomainPurchasesPage`, `DomainPurchaseWizard`, `showDomainOnboarding`, and `RegistrantContactPage` from `portal-mojo/admin`.
|
|
4
|
+
|
|
5
|
+
The global, no-active-group DNS workspace mounts `purchases` and `registrant` between Certificates and Provider Credentials. Purchase-ledger viewing uses the global DNS view clause. Search is read-only; quote, purchase, adoption, and registrant writes require DNS manage authority. House contact and manual House adoption additionally require the literal interactive `is_superuser` identity.
|
|
6
|
+
|
|
7
|
+
## Search, quote, and purchase
|
|
8
|
+
|
|
9
|
+
The wizard asks for one explicit group through bounded `/api/dnsman/credential/group-choice` search. It never reads `/api/group`, active-group state, or free-form group ids. TLDs are operator-typed and bounded by `config.search_batch_limit`; the client has no invented supported-TLD catalog. Search availability stays tri-state: `true`, `false`, or `null` (registry did not answer). Only literal `true` advances.
|
|
10
|
+
|
|
11
|
+
`POST /registrar/quote` returns `{purchase,name,price,currency,years,token,expires,privacy_supported}`. The raw token lives only in an immutable ref in the mounted wizard. It is never state, URL data, Query/Mutation cache data, mock request history, or durable mock data. The exact tuple binds purchase id, token, group, normalized domain, years, decimal price, currency, and expiry. Any upstream change or expiry clears it.
|
|
12
|
+
|
|
13
|
+
The operator types the exact normalized domain before the single-flight, nonretry `POST /registrar/purchase`. The token is removed from the ref before the request. Success and every exception reconcile through an imperative `GET /purchase/<id>?graph=default`; one non-overlapping bounded-backoff loop follows `submitted` to a durable terminal row. Timeout hands the operator to the ledger.
|
|
14
|
+
|
|
15
|
+
After purchase is attempted, the wizard cannot retry or requote. A failed, ambiguous, or timed-out result always says that money may have moved and directs the operator to the AWS Route 53 Domains operation console and durable ledger first.
|
|
16
|
+
|
|
17
|
+
## Purchase ledger
|
|
18
|
+
|
|
19
|
+
`DomainPurchaseModel` lists only `graph=basic`: id, domain, status, price, and currency. Export/download and alternate graphs are rejected. Clicking a row performs an imperative default-graph read into a KISS modal; the detail response and redacted error are modal-local and never Query-cached.
|
|
20
|
+
|
|
21
|
+
## Registrant PII
|
|
22
|
+
|
|
23
|
+
The contact page starts with no scope and no contact. Operators explicitly choose one authorized group, or literal superusers may choose House. Reads and writes are imperative; generation guards discard late responses after scope changes/unmount, and local contact state is cleared immediately.
|
|
24
|
+
|
|
25
|
+
An inherited group response renders a blank form and only the backend-authored boolean status; inherited values are never disclosed. Normal editable values may seed from that scope's database row or the House settings-file response. `Fax` and opaque `ExtraParams` are preserved only when the original response is a direct database row for the exact same scope. They are never carried across scope, inheritance, or deployment-file override boundaries.
|
|
26
|
+
|
|
27
|
+
## House adoption
|
|
28
|
+
|
|
29
|
+
House adoption is a separate, literal-superuser-only menu option. The operator types one exact existing name and submits `/registrar/adopt` without `group`. The UI never calls `/registrar/discover`, auto-discovers account domains, or silently assigns the adopted asset to a tenant.
|
|
30
|
+
|
|
31
|
+
## Themes, showcase, and verification
|
|
32
|
+
|
|
33
|
+
Both themes import the same token-only `admin-dns-registrar.css`. The showcase has manager-ready, missing-contact, viewer-ledger, and platform-House legs. `npm run verify:admin-dns-registrar` is mock-only and performs no real registrar quote or purchase.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Admin DNS
|
|
2
|
+
|
|
3
|
+
Import from `portal-mojo/admin`.
|
|
4
|
+
|
|
5
|
+
`DNS_ADMIN_SECTION` mounts Domains, DNS Records, Certificates, Domain Purchases,
|
|
6
|
+
Registrant Contact, and Provider Credentials under `dns/` (or `system/dns/` embedded). Its global view gate is the ANY-of
|
|
7
|
+
clause `sys.view_dns | sys.manage_dns | sys.security`; mutations use
|
|
8
|
+
`sys.manage_dns | sys.security`. Member grants cannot satisfy either clause.
|
|
9
|
+
|
|
10
|
+
## Capability gate
|
|
11
|
+
|
|
12
|
+
`useDnsCapabilities(group?)` reads `/api/dnsman/config`. The parser is strict:
|
|
13
|
+
every flag, limit, provider capability, ACME field, and delegated-ACME field
|
|
14
|
+
must be present with the correct type. There are no provider or feature
|
|
15
|
+
defaults. Until that contract succeeds, dependent controls do not render and
|
|
16
|
+
the page shows **DNS administration unavailable**.
|
|
17
|
+
|
|
18
|
+
Provider controls are derived only from the response. A provider is offered
|
|
19
|
+
for credential linking only when it reports `requires_credential: true`.
|
|
20
|
+
|
|
21
|
+
## Safe models and operations
|
|
22
|
+
|
|
23
|
+
The package defines `DomainModel`, `DnsCredentialModel`,
|
|
24
|
+
`DomainPurchaseModel`, and `CertificateModel`. Their endpoints and graphs are
|
|
25
|
+
pinned and list params are allowlisted; export params are not accepted.
|
|
26
|
+
Defence-in-depth sanitizers are explicit safe-graph projectors. Only approved
|
|
27
|
+
scalar fields and approved sub-fields of group, user, credential, and domain
|
|
28
|
+
relations survive; unknown or equivalent secret containers drop by default
|
|
29
|
+
before data reaches TanStack Query.
|
|
30
|
+
|
|
31
|
+
DNS records, registrar search/suggest/quote/purchase, house-domain adoption,
|
|
32
|
+
registrant contact, WHOIS, and certificate request/revoke use typed imperative
|
|
33
|
+
helpers. There is deliberately no certificate-material helper.
|
|
34
|
+
|
|
35
|
+
House discovery returns the live `{count,truncated,domains}` contract. Each
|
|
36
|
+
domain includes registration/hosted-zone presence, safe zone metadata,
|
|
37
|
+
tracking/adoption state, and a reason. `untracked=true` returns only untracked
|
|
38
|
+
rows; discovery and assignment remain interactive-superuser operations.
|
|
39
|
+
|
|
40
|
+
The operator registrar workflow is documented in [admin-dns-registrar.md](admin-dns-registrar.md). It does not use discovery: House adoption is manual exact-name only. Purchase tokens remain transient, all post-confirmation outcomes reconcile through the read-only ledger, and contact PII remains imperative/local.
|
|
41
|
+
|
|
42
|
+
These responses require local call-flow handling and must never enter Query or
|
|
43
|
+
Mutation cache, logs, URL state, or mock state:
|
|
44
|
+
|
|
45
|
+
- provider API keys and secrets;
|
|
46
|
+
- registrar confirmation tokens;
|
|
47
|
+
- registrant and WHOIS contact PII;
|
|
48
|
+
- certificate PEM, chains, and private keys.
|
|
49
|
+
|
|
50
|
+
## Provider credentials
|
|
51
|
+
|
|
52
|
+
`ProviderCredentialsPage` is a server-driven `ModelTable`. Search, filters,
|
|
53
|
+
sorting, and paging go to django-mojo. It has no export and no detail route.
|
|
54
|
+
Clicking a row opens a compact `modal.detail`-style KISS surface showing only
|
|
55
|
+
masked key/secret values and verification health.
|
|
56
|
+
|
|
57
|
+
Link and rotation submit raw values through a fresh-auth imperative POST to
|
|
58
|
+
`/api/dnsman/credential/link`. They are held in uncontrolled password refs,
|
|
59
|
+
cleared after every attempt, sanitized before any explicit cache write, and
|
|
60
|
+
the credential queries are invalidated in `finally`.
|
|
61
|
+
|
|
62
|
+
The link picker uses only `/api/dnsman/credential/group-choice` with bounded
|
|
63
|
+
`search/start/size` listing. A stored ID is hydrated through the exact
|
|
64
|
+
`?id=<id>` form. The full selected object is held by `CollectionSelect`, so it
|
|
65
|
+
never invents `/group-choice/<id>`, reads an active group context, calls
|
|
66
|
+
`/api/group`, or accepts a free-form ID.
|
|
67
|
+
|
|
68
|
+
Verification is transactional at the UI boundary: a failed first link adds
|
|
69
|
+
nothing; a failed rotation rejects, preserves the previous masks/encrypted
|
|
70
|
+
pair, and refetches the now-unverified row with its provider error. Successful
|
|
71
|
+
verification replaces the masks and marks the row verified. Viewer-only users
|
|
72
|
+
can inspect safe rows; managers can link, rotate, activate/retire, and arm
|
|
73
|
+
deletion.
|
|
74
|
+
|
|
75
|
+
## Themes and showcase
|
|
76
|
+
|
|
77
|
+
The page composes existing portal primitives and semantic tokens; it adds no
|
|
78
|
+
fixed colors or theme-specific stylesheet. The executable three-leg showcase
|
|
79
|
+
is **Develop → Components → Admin → DNS**: live manager lifecycle,
|
|
80
|
+
viewer-only contract, and malformed-capability unavailable state.
|