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,78 @@
|
|
|
1
|
+
# Admin settings
|
|
2
|
+
|
|
3
|
+
`portal-mojo/admin` provides a real django-mojo runtime-settings administrator
|
|
4
|
+
for standalone and embedded portals. It replaces static settings-demo data with
|
|
5
|
+
the `/api/settings` model while keeping write-only values out of client caches.
|
|
6
|
+
|
|
7
|
+
## Routes and access
|
|
8
|
+
|
|
9
|
+
Register `SettingsPage` at the section index. Selecting a row opens
|
|
10
|
+
`SettingDetail` through the shared KISS modal; there is no `:id` child route.
|
|
11
|
+
The route and sidebar contribution require either `sys.manage_settings`
|
|
12
|
+
or `sys.groups`, mirroring `Setting.RestMeta` while ensuring active-group member
|
|
13
|
+
permissions cannot open a fleet-wide settings surface.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
{
|
|
17
|
+
id: 'settings',
|
|
18
|
+
title: 'Settings',
|
|
19
|
+
icon: 'bi-gear',
|
|
20
|
+
permissions: SETTINGS_PERMISSIONS,
|
|
21
|
+
routes: [
|
|
22
|
+
{ path: '', component: SettingsPage, permissions: SETTINGS_PERMISSIONS },
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`SettingsPage` searches only the key (the backend's sole `SEARCH_FIELDS`
|
|
28
|
+
entry), and offers server filters for `is_secret` and `group__isnull`. Keys are
|
|
29
|
+
immutable after creation. Operators with `sys.groups` may change scope, sending
|
|
30
|
+
a group id or explicit `null` for Global. A `sys.manage_settings`-only operator
|
|
31
|
+
sees the current scope read-only; the editor never mounts the group picker or
|
|
32
|
+
queries `/api/group`, and preserves that scope on save. New settings default to
|
|
33
|
+
Global. Delete is intentionally absent because live `Setting.RestMeta` does not
|
|
34
|
+
declare deletion.
|
|
35
|
+
|
|
36
|
+
## Atomic write contract
|
|
37
|
+
|
|
38
|
+
Use `saveSettingAtomic()` / `buildSettingPayload()` rather than a generic
|
|
39
|
+
autosave. django-mojo processes fields against the model's current secrecy
|
|
40
|
+
state in request insertion order. Create-secret remains `is_secret → value`.
|
|
41
|
+
An existing Plain→Secret transition must be `value → is_secret`, so the current
|
|
42
|
+
plain row still holds the replacement when the flag flips and the pre-save hook
|
|
43
|
+
encrypts it. Secret→Plain is the reverse: `is_secret → value`, so the explicit
|
|
44
|
+
replacement lands as plain text. Same-secret replacement sends only `value`.
|
|
45
|
+
|
|
46
|
+
Both transitions require an explicit replacement; a new secret or a
|
|
47
|
+
Plain→Secret transition requires a non-empty secret, while Secret→Plain may
|
|
48
|
+
explicitly replace it with an empty plain value. An unchanged secret with a
|
|
49
|
+
blank replacement omits `value`; an unchanged plain setting may intentionally
|
|
50
|
+
save an empty string. A no-op makes no request.
|
|
51
|
+
|
|
52
|
+
The custom save bypasses TanStack MutationCache because mutation variables
|
|
53
|
+
retain submitted bodies. Its response passes through `SettingModel` sanitation
|
|
54
|
+
before it may enter Query cache. For secret rows, the cache contains only the
|
|
55
|
+
masked `display_value`; the `value` property is removed. Success toasts are
|
|
56
|
+
generic and backend failures stay in the form; neither interpolates a submitted
|
|
57
|
+
value.
|
|
58
|
+
|
|
59
|
+
## Editor behavior
|
|
60
|
+
|
|
61
|
+
The replacement textarea is deliberately uncontrolled. Existing secret rows
|
|
62
|
+
seed it with an empty string—never `******`—and switching secret status clears
|
|
63
|
+
the control and marks it as needing an explicit replacement. Backend collision
|
|
64
|
+
and registered-key validation messages stay visible in the modal.
|
|
65
|
+
|
|
66
|
+
## Exports
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import {
|
|
70
|
+
SettingModel,
|
|
71
|
+
SettingsPage,
|
|
72
|
+
SettingDetail,
|
|
73
|
+
SETTINGS_PERMISSIONS,
|
|
74
|
+
SETTINGS_ADMIN_SECTION,
|
|
75
|
+
buildSettingPayload,
|
|
76
|
+
saveSettingAtomic,
|
|
77
|
+
} from 'portal-mojo/admin';
|
|
78
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Shortlinks Admin
|
|
2
|
+
|
|
3
|
+
Import from `portal-mojo/admin`. `SHORTLINKS_ADMIN_SECTION` contributes
|
|
4
|
+
`shortlinks/links` and `shortlinks/history` to the global, no-group
|
|
5
|
+
Communications navigation. Both routes require `sys.manage_shortlinks`.
|
|
6
|
+
|
|
7
|
+
## Privacy boundary
|
|
8
|
+
|
|
9
|
+
django-mojo's `graph=list` includes the destination `url`. `ShortlinkModel`
|
|
10
|
+
therefore applies a positive row projection inside the query function before
|
|
11
|
+
TanStack Query receives a row. Destinations and metadata are omitted.
|
|
12
|
+
|
|
13
|
+
Click history is manage-only and projects raw records into: click id, shortlink
|
|
14
|
+
id/code, bot classification, timestamp, a bounded browser/bot summary, and an
|
|
15
|
+
HTTP(S) referrer origin. IP addresses, raw user-agent strings, referrer
|
|
16
|
+
path/query/fragment, metadata, and nested `shortlink.url` are never cached.
|
|
17
|
+
Neither table supports search or export, and list parameters are allowlisted.
|
|
18
|
+
|
|
19
|
+
## Mutations and short URLs
|
|
20
|
+
|
|
21
|
+
Create, active-state changes, and delete are imperative one-shot requests. They
|
|
22
|
+
do not claim fresh-auth protection the backend does not provide and do not use
|
|
23
|
+
retrying mutation hooks. Every outcome runs an authoritative reconciliation in
|
|
24
|
+
`finally` semantics. If reconciliation itself fails, the UI warns that the
|
|
25
|
+
change may have persisted and blocks retry until a successful refresh.
|
|
26
|
+
|
|
27
|
+
Short URLs are `${apiOrigin()}/s/{code}` (or `/s/{code}` for the same-origin
|
|
28
|
+
mock). The package makes no canonical-host claim.
|
|
29
|
+
|
|
30
|
+
Human and bot totals are counts of retained tracking records. The displayed
|
|
31
|
+
remainder is `max(0, hit_count - human - bot)`; `hit_count` also includes hits
|
|
32
|
+
made while tracking was disabled.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Storage Admin
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import {
|
|
5
|
+
STORAGE_ADMIN_SECTION, BucketsPage, BackendsPage, FilesPage, FileView,
|
|
6
|
+
FileUploadSurface, FileManagerModel, FileManagerUploadPolicyModel, FileModel,
|
|
7
|
+
} from 'portal-mojo/admin';
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`STORAGE_ADMIN_SECTION` contributes global/no-group Infrastructure routes at
|
|
11
|
+
`storage/buckets`, `storage/backends`, and `storage/files` (or the same paths
|
|
12
|
+
under an embedded `/system` mount). There are no record routes: buckets,
|
|
13
|
+
backends, nested files, and files use `modal.detail`.
|
|
14
|
+
|
|
15
|
+
## Permissions
|
|
16
|
+
|
|
17
|
+
- Buckets: `sys.manage_aws | sys.files`.
|
|
18
|
+
- Backends/Files view: `sys.view_fileman | sys.manage_files | sys.files`.
|
|
19
|
+
- Backend/File mutations: `sys.manage_files | sys.files`.
|
|
20
|
+
- Add File, its whole-page drop surface, and upload destination requests mount
|
|
21
|
+
only with the File mutation grant.
|
|
22
|
+
- Group and user selectors mount only with their respective system-pinned
|
|
23
|
+
directory grants. Admin never reads an active group.
|
|
24
|
+
|
|
25
|
+
The section audience is the union of child audiences; each route retains its
|
|
26
|
+
exact clause. A member-only grant cannot satisfy any Storage route.
|
|
27
|
+
|
|
28
|
+
## Buckets
|
|
29
|
+
|
|
30
|
+
`GET /api/aws/s3/bucket` is one complete account inventory, rendered with
|
|
31
|
+
explicitly local name search/order and no paging claims. Failed inventory is
|
|
32
|
+
an Error/Retry state, never “No buckets.”
|
|
33
|
+
|
|
34
|
+
All writes are `POST /api/aws/s3/bucket/<encoded-name>`:
|
|
35
|
+
|
|
36
|
+
- create: `{}` → `{id,name,created_new:true|false|null}`;
|
|
37
|
+
- access: `{set_public:boolean}` → verified `is_public`, `complete:true`,
|
|
38
|
+
`mutation_state:'complete'`;
|
|
39
|
+
- empty: `{empty:{confirm_name:name}}` under `withFreshAuth`, after an
|
|
40
|
+
`ArmedButton` and exact case-sensitive UI confirmation.
|
|
41
|
+
|
|
42
|
+
Every POST invalidates and actively refetches in `finally`. A primary failure
|
|
43
|
+
is preserved if refresh also fails. HTTP 409
|
|
44
|
+
`s3_operation_incomplete` retains finite `none|partial|unknown` evidence,
|
|
45
|
+
acknowledged counts, classified failures, nullable remaining values, and the
|
|
46
|
+
access safety-lock posture without fabricating zeros or atomic completion.
|
|
47
|
+
Bucket DELETE is absent.
|
|
48
|
+
|
|
49
|
+
## Backends
|
|
50
|
+
|
|
51
|
+
`FileManagerModel` forces `graph=list` and allowlists graph fields before any
|
|
52
|
+
cache write. Readable credentials are masks only. Credential inputs begin
|
|
53
|
+
blank; blank means preserve. `saveFileManagerAtomic` runs imperatively outside
|
|
54
|
+
MutationCache, strips masks and blank secrets, fresh-auth retries the same
|
|
55
|
+
closure, refetches authoritatively, and compares requested owner FKs.
|
|
56
|
+
|
|
57
|
+
Creation supports runtime backends `file` and `s3`, explicitly starts private,
|
|
58
|
+
and requires an authorized group/user owner unless the caller is superuser and
|
|
59
|
+
chooses System. Supported payload actions are `test_connection`, `check_cors`,
|
|
60
|
+
`fix_cors`, and `clone`; inner `{status:false}` rejects. CORS is S3-only.
|
|
61
|
+
FileManager DELETE and `check_public_access` are absent.
|
|
62
|
+
|
|
63
|
+
## Files and capability URLs
|
|
64
|
+
|
|
65
|
+
`FileModel` forces the list graph and normal saves to `filename`, `is_public`,
|
|
66
|
+
and explicit `group`. Group moves refetch and compare the authoritative FK.
|
|
67
|
+
Selection supports public/private, explicit prepared group move, safe
|
|
68
|
+
same-gesture download, and all-settled confirmed delete. Authorized operators
|
|
69
|
+
also get **Add File** and a drag-only whole-page drop overlay. Both open one
|
|
70
|
+
configuration modal; the bounded queue lives above it, so closing the modal
|
|
71
|
+
does not cancel work or discard outcomes.
|
|
72
|
+
|
|
73
|
+
Every batch chooses an explicit manager. System choices request only active
|
|
74
|
+
`graph=upload_policy` rows with null group/user owners. Group choices first use
|
|
75
|
+
the independently gated active-group directory, then request null-user policy
|
|
76
|
+
rows for that exact group. User-owned and dual-owned managers never enter these
|
|
77
|
+
Admin lists. The safe policy projection contains only name/use, size,
|
|
78
|
+
extension/MIME guidance, and transfer-route support. A false
|
|
79
|
+
`supports_direct_upload` means a local API target, not an unavailable manager.
|
|
80
|
+
|
|
81
|
+
The queue caps at six files and three concurrent transfers. It reports actual
|
|
82
|
+
byte progress, truthful cancel/retry/recovery, partial acceptance, and uncertain
|
|
83
|
+
remote state. Permission loss closes the destination modal, stops its queries,
|
|
84
|
+
and best-effort cancels queued transfers. Completion compares authoritative
|
|
85
|
+
scalar `file_manager_id`/`group_id` with the immutable destination. Terminal
|
|
86
|
+
changes coalesce into one authoritative active Files refetch plus one trailing
|
|
87
|
+
pass; refresh failure remains visible and current filters may hide the new row.
|
|
88
|
+
|
|
89
|
+
Each task owns a private strict `idempotency_key`. A lost initiation response
|
|
90
|
+
reuses it to recover the same File. A server-proven failed/expired lifecycle
|
|
91
|
+
rotates it and creates a fresh attempt on the same Retry click. Keys, browser
|
|
92
|
+
`File` objects, targets/tokens, and completion payloads never enter Query or
|
|
93
|
+
persistent storage.
|
|
94
|
+
|
|
95
|
+
File, thumbnail, rendition, and share URLs are bearer capabilities. The shared
|
|
96
|
+
validator accepts ordinary relative paths or explicit `http:`/`https:` URLs
|
|
97
|
+
without userinfo, and rejects protocol-relative, malformed, `javascript:`, and
|
|
98
|
+
`data:` values. Open/Download uses an anchor with `rel=noreferrer`. Safe exports
|
|
99
|
+
omit every URL, storage path, mask, metadata object, token, and transient.
|
|
100
|
+
|
|
101
|
+
`FileView` sections are Overview, Preview, Details, Renditions, Shares, and
|
|
102
|
+
Metadata. Category precedes MIME fallback: image inline; native audio/video;
|
|
103
|
+
PDF open; office-like documents use the best safe image rendition; archive
|
|
104
|
+
downloads; text/CSV/unknown remain generic. No returned HTML is embedded.
|
|
105
|
+
`StableMediaPreview` captures its safe source for the mounted File id so normal
|
|
106
|
+
query refreshes do not replace the media node; reopening refreshes policy.
|
|
107
|
+
|
|
108
|
+
## Renditions and shares
|
|
109
|
+
|
|
110
|
+
Role-keyed rendition objects normalize to stable rows. Signatures include
|
|
111
|
+
role, rendition id, status, modified, width, height, and size. Recursive
|
|
112
|
+
non-overlapping polling waits five seconds, makes at most 12 fetch attempts,
|
|
113
|
+
and stops on signature change/arrival, failed or expired upload, close/id
|
|
114
|
+
change, or timeout. Timeout leaves manual Refresh.
|
|
115
|
+
|
|
116
|
+
Regeneration sends `{regenerate_renditions:true|string[]}` with deduped roles
|
|
117
|
+
capped at 20. Share creation sends `{share:true|options}` imperatively; options
|
|
118
|
+
and the newly returned capability URL stay in one modal-local result and never
|
|
119
|
+
enter Query/Mutation caches. The response parser uses the collision-safe
|
|
120
|
+
`shortlink_code` key and validates the ISO `expires_at`. Separately authorized
|
|
121
|
+
rows are labeled “Visible shares”; the backend owner fallback remains available
|
|
122
|
+
without granting global Shortlinks Admin. Safe `track_clicks` and bounded note
|
|
123
|
+
values are retained, and operators can use the live active toggle or permanent
|
|
124
|
+
DELETE contract.
|
|
125
|
+
|
|
126
|
+
## Mock and verification
|
|
127
|
+
|
|
128
|
+
The central mock owns bucket, FileManager, File, upload attempts/byte sessions,
|
|
129
|
+
canonical rendition, and shortlink rows. It implements the safe policy graph,
|
|
130
|
+
flat lifecycle/target response, idempotent replay, terminal retry, and local or
|
|
131
|
+
provider paths. Credential writes derive masks and discard raw values. Stable
|
|
132
|
+
personas are `storage.viewer@nativemojo.com`,
|
|
133
|
+
`storage.manager@nativemojo.com`, `bucket.manager@nativemojo.com`, and
|
|
134
|
+
`storage.member@nativemojo.com` (password `mojo`); the showcase operator has
|
|
135
|
+
the broad demo grants.
|
|
136
|
+
|
|
137
|
+
Run `npm run verify:admin-storage`, `npm run verify:file-upload`, and
|
|
138
|
+
`npm run verify:upload-ux` for the focused route, permission, upload,
|
|
139
|
+
cache-safety, URL, FK-reconciliation, and bounded-polling contracts. Browser
|
|
140
|
+
evidence belongs in the Portal mock for viewer/member gates and in Showcase
|
|
141
|
+
for the exported surface, in both themes.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# auth-pages — in-app sign-in surfaces, step-up challenges, password tools
|
|
2
|
+
|
|
3
|
+
The C3 layer over [auth.md](auth.md)'s client: the portal's own login /
|
|
4
|
+
forgot / reset / magic-link pages (for deployments serving the portal
|
|
5
|
+
same-origin with the API), the fresh-auth (step-up) modal, and the password
|
|
6
|
+
strength/generator pieces. Board #1259.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// The package pieces
|
|
10
|
+
import {
|
|
11
|
+
// MFA completion (consumes the mfa_token from login()'s 'mfa' result)
|
|
12
|
+
completeMfaTotp, completeMfaRecovery, sendMfaSms, completeMfaSms,
|
|
13
|
+
// Fresh-auth (step-up) challenge surface
|
|
14
|
+
REAUTH_STATUS, isReauthRequired, setFreshAuthHandler, requestFreshAuth,
|
|
15
|
+
withFreshAuth, sessionIsPersistent,
|
|
16
|
+
type MfaChallenge, type FreshAuthHandler, type MfaCompletionOptions,
|
|
17
|
+
} from 'portal-mojo/client';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
checkPasswordStrength, generatePassword, PasswordStrengthMeter,
|
|
21
|
+
type PasswordStrength, type PasswordStrengthClass, type GeneratePasswordOptions,
|
|
22
|
+
} from 'portal-mojo/ui';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The pages themselves are APP surfaces (`apps/portal/src/pages/auth/`):
|
|
26
|
+
`LoginPage`, `ForgotPage`, `ResetPage`, `MagicPage`, `MfaPanel`,
|
|
27
|
+
`FreshAuthHost`, `AuthLayout` — wired through `routes.tsx`, which exports
|
|
28
|
+
`authRoutes`, `RequireAuth`, and `handleAuthTokenLanding()`.
|
|
29
|
+
|
|
30
|
+
## Config switch
|
|
31
|
+
|
|
32
|
+
`VITE_MOJO_AUTH` (env, same idiom as `VITE_MOJO_API`):
|
|
33
|
+
|
|
34
|
+
| Value | Behavior |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `inapp` | Unauthenticated visits route to `#/auth/login` (these pages) |
|
|
37
|
+
| `hosted` | Unauthenticated visits bounce to the django-mojo hosted `/auth` pages (bouncer pages, incl. the bot-check interstitial — which exists on the hosted path ONLY) |
|
|
38
|
+
| unset | **hosted when `VITE_MOJO_API` is set** (the default for real deployments), in-app under the mock (there is no hosted page to bounce to) |
|
|
39
|
+
|
|
40
|
+
Unknown values fall back to the unset default WITH a `console.warn`.
|
|
41
|
+
`hosted` + mock also warns and falls back to in-app. Resolution lives in
|
|
42
|
+
`pages/auth/config.ts` (`authMode()` / `inAppAuthEnabled()`).
|
|
43
|
+
|
|
44
|
+
## Route guard
|
|
45
|
+
|
|
46
|
+
`RequireAuth` wraps the App route element (`<RequireAuth><App/></RequireAuth>`;
|
|
47
|
+
`authRoutes` mount as top-level SIBLINGS — auth pages are full-screen, no app
|
|
48
|
+
chrome). Unauthenticated (per the A1 snapshot: no valid access OR refresh
|
|
49
|
+
token) → in-app login (intended route stashed in
|
|
50
|
+
`sessionStorage['portal:auth-return']`, restored after any successful flow)
|
|
51
|
+
or the hosted redirect (which keeps auth.ts's own stash). Authenticated →
|
|
52
|
+
children plus `<FreshAuthHost/>`. A signed-in visit to `#/auth/login`
|
|
53
|
+
bounces to `/` (web-mojo AuthApp guard parity); the forgot/reset/magic pages
|
|
54
|
+
stay reachable signed-in — completing them re-logs-in by design (the server
|
|
55
|
+
answers with a full TokenGrant).
|
|
56
|
+
|
|
57
|
+
## Flows (all on the A1 client — no new wire code in pages)
|
|
58
|
+
|
|
59
|
+
- **Password**: `login()` → tokens stored, or `{kind:'mfa'}` → `MfaPanel`.
|
|
60
|
+
Remember-me checkbox maps to `LoginOptions.remember` (localStorage vs
|
|
61
|
+
sessionStorage).
|
|
62
|
+
- **MFA step** (`MfaPanel`, shared with the fresh-auth modal). Methods from
|
|
63
|
+
the server: `totp` (code → `completeMfaTotp`; recovery-code toggle →
|
|
64
|
+
`completeMfaRecovery`), `sms` (`sendMfaSms` **consumes and re-issues the
|
|
65
|
+
mfa_token — always replace your stored token with the returned one**, then
|
|
66
|
+
`completeMfaSms`), `passkey` (a full `loginWithPasskey()` ceremony IS the
|
|
67
|
+
second factor — django-mojo's passkey login never routes through the MFA
|
|
68
|
+
gate). Unknown methods are dropped with a `console.warn`. The challenge
|
|
69
|
+
expiry (`expiresIn`, default 300s) is counted down visibly.
|
|
70
|
+
- **Forgot/reset**: `forgotPassword(email, 'code'|'link')`; code → ResetPage
|
|
71
|
+
code mode (email carried via router state + a sessionStorage stash so
|
|
72
|
+
reloads survive); link → lands on ResetPage token mode. Both resets log
|
|
73
|
+
the user in on success. The reset form carries the live
|
|
74
|
+
`PasswordStrengthMeter` + a generator affordance (fills both fields,
|
|
75
|
+
reveals the value). Strength is ADVISORY — submission is never blocked on
|
|
76
|
+
it; the server owns password policy.
|
|
77
|
+
- **Magic link**: request via `sendMagicLink`; the landing auto-exchanges
|
|
78
|
+
`loginWithMagicToken` ONCE (StrictMode-proof ref guard — `ml:` tokens are
|
|
79
|
+
single-use) with progress/error UI.
|
|
80
|
+
- **Passkey**: `loginWithPasskey(username?, {remember})` behind `isPasskeySupported()`;
|
|
81
|
+
no username → discoverable-credential prompt. The ceremony is fully built;
|
|
82
|
+
REAL-authenticator verification is deferred (mock validates shape only).
|
|
83
|
+
The remember choice is explicit because MFA and fresh-auth passkey paths
|
|
84
|
+
must not promote a sessionStorage session into localStorage.
|
|
85
|
+
|
|
86
|
+
## Token-landing URL shapes
|
|
87
|
+
|
|
88
|
+
django-mojo's `build_token_url` produces
|
|
89
|
+
`{WEBAPP_BASE_URL}{WEBAPP_AUTH_PATH}?flow=<flow>&token=<tok>` with `pr:`
|
|
90
|
+
(password_reset) / `ml:` (magic_login) token prefixes. Both landing shapes
|
|
91
|
+
work:
|
|
92
|
+
|
|
93
|
+
- **Real search** (`/?flow=password_reset&token=pr:…` — WEBAPP_AUTH_PATH
|
|
94
|
+
points at the portal root): `handleAuthTokenLanding()` at boot scrubs
|
|
95
|
+
flow/token from the real URL **synchronously, before any network call**
|
|
96
|
+
(the auth_code discipline) and hash-routes to the landing page.
|
|
97
|
+
- **Hash query** (`/#/auth?flow=…&token=…` — WEBAPP_AUTH_PATH points at
|
|
98
|
+
`/#/auth`): the `/auth` index route dispatches by token prefix, then flow.
|
|
99
|
+
|
|
100
|
+
The landing pages capture the token once at mount and scrub it from the
|
|
101
|
+
hash query before exchanging.
|
|
102
|
+
|
|
103
|
+
## Fresh-auth (step-up) challenges
|
|
104
|
+
|
|
105
|
+
django-mojo's freshness gate (`account/services/fresh_auth.py`,
|
|
106
|
+
`FRESH_AUTH_WINDOW`, default off): sensitive operations answer **HTTP 440 /
|
|
107
|
+
`reauth_required`** when the JWT's `auth_time` is too old. A token refresh
|
|
108
|
+
can NEVER satisfy it — refreshes carry `auth_time` forward unchanged; only
|
|
109
|
+
a genuine re-login mints a new one. That is why 440 is not 401: it must not
|
|
110
|
+
trigger the refresh path.
|
|
111
|
+
|
|
112
|
+
Client surface (`portal-mojo/client`):
|
|
113
|
+
|
|
114
|
+
- `isReauthRequired(error)` — MojoError with status/`error_code` 440 or
|
|
115
|
+
message `reauth_required`.
|
|
116
|
+
- `setFreshAuthHandler(handler)` — the app registers ONE UI handler;
|
|
117
|
+
`FreshAuthHost` (mounted by `RequireAuth`) does this.
|
|
118
|
+
- `requestFreshAuth()` — single-flight prompt: a burst of gated calls
|
|
119
|
+
shares one modal. No handler → `console.warn` + resolve false.
|
|
120
|
+
- `withFreshAuth(fn)` — run fn; on a 440 prompt, then retry **once** after a
|
|
121
|
+
successful re-login; a dismissed prompt rejects with the ORIGINAL error.
|
|
122
|
+
|
|
123
|
+
The modal re-prompts credentials OVER the current screen — page state
|
|
124
|
+
survives. It pins the identity to the current session's email (step-up is
|
|
125
|
+
"prove you are still you"), offers password + passkey + the MFA step, and
|
|
126
|
+
keeps the session in its original storage (`sessionIsPersistent()` →
|
|
127
|
+
`remember`). Dev affordance: `__mojo.requestFreshAuth()` in the console
|
|
128
|
+
exercises the modal without a 440.
|
|
129
|
+
|
|
130
|
+
Call-site pattern:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
await withFreshAuth(() => save(changes)); // a 440 prompts + retries once
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`defineModel().useSave()` and `.useAction()` wrap their mutation functions in
|
|
137
|
+
this flow globally. Mutation variables are closed over unchanged and the
|
|
138
|
+
normal success/cache path runs only after the retry succeeds. `useDelete()`
|
|
139
|
+
and direct `mojoCall()` calls remain explicit opt-ins: their sensitivity and
|
|
140
|
+
retry safety are endpoint-specific.
|
|
141
|
+
|
|
142
|
+
Deployment note: a deployment enforcing `BOUNCER_REQUIRE_TOKEN` gates
|
|
143
|
+
`/api/login` behind the bouncer token, which in-app surfaces do not carry
|
|
144
|
+
(`LoginOptions.extra.bouncer_token` is the seam) — the modal will surface
|
|
145
|
+
the server's 403 verbatim there; hosted mode is the supported posture for
|
|
146
|
+
bouncer-enforcing deployments.
|
|
147
|
+
|
|
148
|
+
## Password tools (`portal-mojo/ui`)
|
|
149
|
+
|
|
150
|
+
- `checkPasswordStrength(password)` → `{score, strength, feedback, details}`
|
|
151
|
+
— web-mojo MOJOUtils scoring, EXACT: length bands (+1/+3/+4), variety
|
|
152
|
+
(+1 lower, +1 upper, +1 digits, +2 special), one -1 common-pattern
|
|
153
|
+
penalty, common-password `max(0, score-3)`, thresholds <2/<4/<6/<8 →
|
|
154
|
+
very-weak/weak/fair/good/strong, same feedback strings. 22 headless
|
|
155
|
+
assertion groups verified the port (entropy classes, floors, praise
|
|
156
|
+
lines, edge inputs).
|
|
157
|
+
- `generatePassword(options?)` — same option surface and guarantees
|
|
158
|
+
(defaults incl. length 12, one char per included class, `customChars`
|
|
159
|
+
override skips the guarantee, `excludeAmbiguous` strips i/l · I/O/L ·
|
|
160
|
+
0/1 · |, length < 4 throws, empty pool throws). ONE documented deviation:
|
|
161
|
+
crypto.getRandomValues + Fisher–Yates replaces web-mojo's Math.random and
|
|
162
|
+
biased sort-shuffle — a non-crypto RNG in a password generator is a trap,
|
|
163
|
+
not a feature.
|
|
164
|
+
- `<PasswordStrengthMeter password showFeedback? />` — 5-segment bar +
|
|
165
|
+
label + feedback, colored via tokens through `data-strength`
|
|
166
|
+
(bad/warn/info/ok), both themes free. Empty input renders an idle unlit
|
|
167
|
+
state instead of the scorer's non-empty-string complaint.
|
|
168
|
+
|
|
169
|
+
## Mock affordances
|
|
170
|
+
|
|
171
|
+
Under the mock transport the pages show inline dev hints: password `mojo`
|
|
172
|
+
for any seeded user (e.g. `ian@mojoverify.com`); Maya (seeded user 2) requires
|
|
173
|
+
MFA; TOTP `123456`, recovery `mock-recovery`, and SMS `654321`; reset code
|
|
174
|
+
`123456`; token formats `pr:mock-<id>` / `ml:mock-<id>`. MFA tokens are
|
|
175
|
+
expiring and single-use. The mock follows the measured runtime behavior:
|
|
176
|
+
TOTP/recovery/SMS attempts burn the token before code validation, and
|
|
177
|
+
`sms/send` burns then re-issues it (older web docs claiming otherwise are
|
|
178
|
+
stale). Login's mock-only `__mock_mfa_ttl` can make expiry immediate.
|
|
179
|
+
|
|
180
|
+
Arm a real one-shot 440 path from the dev console with
|
|
181
|
+
`__mojo.armMockReauth('POST', '/api/user/1')`. Matching includes method and
|
|
182
|
+
exact path, runs after bearer authentication, and is consumed only by the
|
|
183
|
+
first authenticated match. This makes anonymous 401, unrelated requests,
|
|
184
|
+
the first matching 440, retry success, and later normal calls reproducible.
|
|
185
|
+
|
|
186
|
+
## Deferred (explicitly)
|
|
187
|
+
|
|
188
|
+
- **OAuth redirect flows** — re-deferred with a code-level seam
|
|
189
|
+
(`TODO(C3-deferred)` in auth.ts documents the full verified wire:
|
|
190
|
+
`GET /api/auth/oauth/<provider>/begin` → `{auth_url, state}` → provider →
|
|
191
|
+
backend callback → 302 to the frontend with `?code=&state=` →
|
|
192
|
+
`POST …/complete`). Blocked on: server-side redirect-uri allowlisting,
|
|
193
|
+
deployment-specific providers (`GET /api/auth/config` → `login.methods`),
|
|
194
|
+
and no way to exercise the round-trip against the mock or without real
|
|
195
|
+
provider credentials. Hosted pages cover OAuth deployments today.
|
|
196
|
+
- **Passkey real-authenticator verification** — the ceremony is fully
|
|
197
|
+
built on auth.ts; verifying against a real authenticator + a live
|
|
198
|
+
django-mojo passkey registration is Ian's post-merge pass.
|
|
199
|
+
|
|
200
|
+
## Pitfalls
|
|
201
|
+
|
|
202
|
+
- `auth/sms/send` BURNS the mfa_token it received and returns a new one —
|
|
203
|
+
verifying with the old token fails. MfaPanel handles this; any new UI
|
|
204
|
+
must too.
|
|
205
|
+
- Never route a 440 into the token-refresh path; the server chose 440
|
|
206
|
+
precisely so 401-refresh machinery ignores it.
|
|
207
|
+
- `ml:`/`pr:` tokens are single-use: exchange exactly once (StrictMode
|
|
208
|
+
double-effects will burn them — use a ref guard, as MagicPage does).
|
|
209
|
+
- The auth pages must stay OUTSIDE `RequireAuth` and OUTSIDE the App shell
|
|
210
|
+
(they are route siblings, not children).
|
|
211
|
+
- Keep the `?auth_code=` scrub-before-network behavior in auth.ts intact —
|
|
212
|
+
`handleAuthTokenLanding()` follows the same discipline for `?token=`.
|
package/docs/auth.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# auth — session client + hosted-auth bridge
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import {
|
|
5
|
+
initAuth, login, logout, onAuth, useAuthSnapshot,
|
|
6
|
+
handleAuthCodeFromURL, hostedAuthUrl, redirectToHostedAuth,
|
|
7
|
+
sendMagicLink, loginWithMagicToken, handleMagicTokenFromURL,
|
|
8
|
+
forgotPassword, resetPasswordWithCode, resetPasswordWithToken,
|
|
9
|
+
loginWithPasskey, isPasskeySupported,
|
|
10
|
+
} from 'portal-mojo/client';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Boot (once, before render)
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
initAuth(); // installs the pre-request gate + Authorization header + refresh watcher
|
|
17
|
+
void handleAuthCodeFromURL(); // returning from the hosted /auth pages (no-op otherwise)
|
|
18
|
+
onAuth('login', () => queryClient.invalidateQueries()); // identity changed →
|
|
19
|
+
onAuth('logout', () => queryClient.invalidateQueries()); // every cached answer is suspect
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Session mechanics
|
|
23
|
+
|
|
24
|
+
- JWT pair (`access_token`/`refresh_token`); single-flight refresh (3
|
|
25
|
+
concurrent 401s → ONE refresh POST); pre-request gate throws a synthetic
|
|
26
|
+
`AuthRequiredError` instead of firing doomed requests.
|
|
27
|
+
- `useAuthSnapshot()` → `{authenticated, uid, email}` — live, cross-tab.
|
|
28
|
+
- `logout()` is client-side (django-mojo has no logout endpoint) and emits
|
|
29
|
+
`'logout'`.
|
|
30
|
+
- Events for `onAuth`: `'login' | 'logout' | 'refreshed' | 'refresh-failed'
|
|
31
|
+
| 'unauthorized'`.
|
|
32
|
+
|
|
33
|
+
## Hosted auth pages (the django-mojo "bouncer" pages)
|
|
34
|
+
|
|
35
|
+
The backend serves themed login/register pages at `<origin>/auth` (behind
|
|
36
|
+
its bot-check interstitial). Flow:
|
|
37
|
+
|
|
38
|
+
1. `redirectToHostedAuth()` — stashes the current hash route in
|
|
39
|
+
sessionStorage, then navigates to
|
|
40
|
+
`<origin>/auth?redirect=<HASH-FREE current url>`. Hash-free is
|
|
41
|
+
load-bearing: the page string-appends `?auth_code=<code>` to the
|
|
42
|
+
redirect, and a `#` in it would swallow the code.
|
|
43
|
+
2. User signs in on the hosted page; it mints a single-use 60s handoff code
|
|
44
|
+
(`POST /api/auth/handoff`) and navigates back with `?auth_code=`.
|
|
45
|
+
3. `handleAuthCodeFromURL()` (boot) exchanges it (`POST /api/auth/exchange`),
|
|
46
|
+
scrubs the code from the URL synchronously, restores the stashed route.
|
|
47
|
+
Handles a hash-embedded `#/route?auth_code=` landing too.
|
|
48
|
+
|
|
49
|
+
`hostedAuthUrl()` returns null under the mock transport — there is no page
|
|
50
|
+
to host; the UI should fall back to a dev affordance (`__mojo.login(...)`).
|
|
51
|
+
|
|
52
|
+
## Direct flows (for in-app auth pages)
|
|
53
|
+
|
|
54
|
+
`login(username, password)` → `{kind: 'authenticated', user}` or an MFA challenge;
|
|
55
|
+
magic-link (`sendMagicLink` → `loginWithMagicToken`), password reset (code
|
|
56
|
+
and token variants), passkeys (`loginWithPasskey`; ceremony fully ported,
|
|
57
|
+
mock validates shape only). All reject with the server's message on
|
|
58
|
+
failure.
|
package/docs/calendar.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Calendar engine + date fns
|
|
2
|
+
|
|
3
|
+
The picker core ported from web-mojo's dependency-free calendar
|
|
4
|
+
(`calendar/Calendar.js` + `utils/dateFns.js`): ONE engine covering
|
|
5
|
+
day/month/year precisions, single + range selection, drill-down zoom, and
|
|
6
|
+
1–2 day panes. Popover, trigger inputs, and preset rails are separate
|
|
7
|
+
components built ON this — the engine renders inline and owns no
|
|
8
|
+
positioning.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { Calendar } from 'portal-mojo/ui';
|
|
12
|
+
import { dateFns } from 'portal-mojo/ui'; // namespace, like fmt
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Value shapes (the contract)
|
|
16
|
+
|
|
17
|
+
- Canonical strings at each precision: `YYYY-MM-DD` / `YYYY-MM` / `YYYY`.
|
|
18
|
+
Everything the engine emits and accepts (`value`, `startValue`, `min`,
|
|
19
|
+
`disabledDates`, callback payloads) speaks these.
|
|
20
|
+
- Parsed shapes are plain int objects — `{y}`, `{y,m}`, `{y,m,d}`
|
|
21
|
+
(`ParsedYear/ParsedYm/ParsedYmd`, union `ParsedDate`). `Date` objects
|
|
22
|
+
never leak out of the fns; all math is browser-local (DST-safe), never
|
|
23
|
+
UTC.
|
|
24
|
+
|
|
25
|
+
## `<Calendar>` API
|
|
26
|
+
|
|
27
|
+
| Prop | Notes |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `precision` | `'day' \| 'month' \| 'year'` (default `day`) — the grid that COMMITS. Unknown values warn + fall back to `day`. |
|
|
30
|
+
| `mode` | `'single' \| 'range'` (default `single`) |
|
|
31
|
+
| `months` | `1 \| 2` day-view panes (default 1; clamped) |
|
|
32
|
+
| `value` | single mode — controlled canonical string (or null) |
|
|
33
|
+
| `startValue` / `endValue` | range mode — controlled canonical strings |
|
|
34
|
+
| `min` / `max` | bounds, compared at the PANE precision being painted: with `min="2026-03-15"` the March-2026 month tile and 2026 year tile stay enabled; only cells wholly out of bounds disable |
|
|
35
|
+
| `disabledDates` | `YYYY-MM-DD[]`, day grids only |
|
|
36
|
+
| `firstDay` | 0=Sun … (default 1=Mon); reorders weekday header + grid offsets |
|
|
37
|
+
| `locale` | BCP-47 for month/weekday names (default `en-US`) |
|
|
38
|
+
| `year` / `month` | initial page override (otherwise the selection, else today) |
|
|
39
|
+
| `onSelect` | `{value, parsed}` — single-mode commit at precision |
|
|
40
|
+
| `onRangeSelect` | `{start, end}` — ordered (ends swap if picked backwards) |
|
|
41
|
+
| `onRangeStart` | `{anchor}` — first range click; nothing committed yet |
|
|
42
|
+
| `onRangeCancel` | Escape pressed with an anchor in progress |
|
|
43
|
+
| `onViewChange` | `{view}` — zoom changed (drill-down or header click) |
|
|
44
|
+
| `onNavigate` | `{delta}` — page changed (buttons or PageUp/PageDown) |
|
|
45
|
+
|
|
46
|
+
**Selection is controlled.** A commit fires the callback; the cell paints
|
|
47
|
+
selected only after the owner writes the prop back. Prop writes carry the
|
|
48
|
+
source's programmatic semantics: a new `value` re-pages the calendar to it;
|
|
49
|
+
a new `startValue`/`endValue` pair cancels any in-progress anchor
|
|
50
|
+
(`setRange`). View, page, anchor and hover are internal state — paging and
|
|
51
|
+
zooming never round-trip through the owner.
|
|
52
|
+
|
|
53
|
+
## Behavior invariants (from source, keep them true)
|
|
54
|
+
|
|
55
|
+
- **Drill-down zoom:** header label zooms out day→month→year (a `<button>`;
|
|
56
|
+
inert at the year grid). A tile ABOVE precision drills down
|
|
57
|
+
(year→month→day); a tile AT precision commits. Reaching day view is only
|
|
58
|
+
possible when `precision='day'`, so range/selected tint on month/year
|
|
59
|
+
grids paints only at matching precision.
|
|
60
|
+
- **Range machine:** first commit places the anchor (`onRangeStart`),
|
|
61
|
+
second commits the ordered pair (`onRangeSelect`). The anchor SURVIVES
|
|
62
|
+
paging/zooming — commit a start, page forward, commit the end elsewhere.
|
|
63
|
+
Hover paints a live preview from anchor to cursor; preview cells use
|
|
64
|
+
`-in-preview` classes, DISTINCT from committed `-in-range`, with
|
|
65
|
+
`-anchor-start/-anchor-end/-anchor-solo` end caps (day grids add
|
|
66
|
+
directional chevrons; solo hides them).
|
|
67
|
+
- While an anchor is active the committed range is suppressed from painting
|
|
68
|
+
(source parity). **Deviation from source:** after Escape the committed
|
|
69
|
+
props paint again — web-mojo left the calendar blank until the next
|
|
70
|
+
commit (nothing consumed `range:cancel`). Corollary: do NOT write range
|
|
71
|
+
props from `onRangeStart` — a range prop write means `setRange` and kills
|
|
72
|
+
the anchor.
|
|
73
|
+
- **Keyboard:** every cell is a real `<button>` (focusable, Enter/Space
|
|
74
|
+
commit). With focus anywhere inside: PageUp/PageDown page, Escape cancels
|
|
75
|
+
an in-progress anchor. There is no roving arrow-key focus — that was not
|
|
76
|
+
in the source engine either.
|
|
77
|
+
- **Today** wears `-today` (red, day grids only). `disabledDates` are
|
|
78
|
+
unclickable and skipped by hover preview painting (the span still covers
|
|
79
|
+
them — `excludeDisabledFromRange` was accepted-but-unused in source and
|
|
80
|
+
is not carried).
|
|
81
|
+
- Blank leading cells are inert `<div>`s, not buttons.
|
|
82
|
+
|
|
83
|
+
## dateFns (`dateFns.*`)
|
|
84
|
+
|
|
85
|
+
Pure, framework-free, `Date`-free shapes. The tuned edge cases are the
|
|
86
|
+
point — do not "simplify":
|
|
87
|
+
|
|
88
|
+
| Group | Functions |
|
|
89
|
+
|---|---|
|
|
90
|
+
| parse/format | `parseYmd/parseYm/parseYear` (+`parseByPrecision`), `formatYmd/formatYm/formatYear` (+`formatByPrecision`), `formatForDisplay(parsed, 'MMM D, YYYY')` |
|
|
91
|
+
| calendar math | `daysInMonth(y,m)` (1-indexed), `weekdayWithFirstDay(y,m,d,firstDay)`, `addMonths/addYears` (shape-preserving, day CLAMPS: Jan 31 +1mo → Feb 28/29) |
|
|
92
|
+
| compare | `compareYmd/Ym/Year/ByPrecision` (−1/0/1; cross-precision compares shared parts — how min/max pane-gating works), `isSameDay/Month/Year` |
|
|
93
|
+
| counts | `daysBetweenInclusive` (1 for same day), `monthsBetweenInclusive`, `yearsBetweenInclusive`, `unitsBetweenInclusive` |
|
|
94
|
+
| time | `parseTime` ("14:30", "2:30 pm", seconds/TZ tails dropped; strict 12h: hour 1–12 with am/pm, else null), `formatTime(t, '24h'\|'12h')`, `compareTime`, `addMinutes` (wraps 24h) |
|
|
95
|
+
| datetime | `parseDateTime` (ISO±offset / "date time IANA" / date-only→00:00), `formatDateTime` ("YYYY-MM-DD HH:MM[ TZ]"), `formatDateTimeForDisplay`, `ianaOffset(zone, refDate)` (DST-aware "+05:30", null on bad zone) |
|
|
96
|
+
| names/now | `monthNames(locale, 'short'\|'long')`, `weekdayNames(locale, firstDay, len)` (Intl-backed, cached, static-English fast path), `today()`, `_setFrozenToday(p)` test hook |
|
|
97
|
+
|
|
98
|
+
Parse fns return `null` on invalid input, never throw; format fns return
|
|
99
|
+
`''` for null. `parseYmd` range-checks day 1–31 but not month length
|
|
100
|
+
(source behavior; the engine never emits an out-of-month day).
|
|
101
|
+
|
|
102
|
+
## Styling
|
|
103
|
+
|
|
104
|
+
`apps/portal/src/theme/calendar.css` — engine anatomy only
|
|
105
|
+
(`mojo-calendar-*`: pane/head/nav/grid/weekday/cell + state classes), all
|
|
106
|
+
colors via theme tokens; component tokens `--mojo-cal-anchor-fill`,
|
|
107
|
+
`--mojo-cal-range-fill`, `--mojo-cal-range-fill-hover` (accent tints:
|
|
108
|
+
10%/18% light, 18%/30% dark). The calendar fills its container's width —
|
|
109
|
+
give it a sized wrapper (~300px per pane; the popover shells do this for
|
|
110
|
+
you later).
|
|
111
|
+
|
|
112
|
+
## Pitfalls
|
|
113
|
+
|
|
114
|
+
- Don't parse engine output with `new Date('YYYY-MM-DD')` — that's UTC
|
|
115
|
+
midnight and shifts a day in western zones. Use `dateFns.parseYmd`.
|
|
116
|
+
- `min`/`max` are compared at pane precision — a `min` mid-month still
|
|
117
|
+
allows that month's tile; check day-level rules at day level.
|
|
118
|
+
- Escape/PageUp/PageDown require focus inside the calendar (click one cell
|
|
119
|
+
first); the demo copy says so too.
|
|
120
|
+
- The wire's daterange triple (`dr_field/dr_start/dr_end`) and epoch-second
|
|
121
|
+
datetimes are PICKER-shell concerns — the engine only speaks canonical
|
|
122
|
+
local strings.
|