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,122 @@
|
|
|
1
|
+
// Markdown rendering, the platform way: django-mojo owns the renderer, so the
|
|
2
|
+
// portal asks it instead of shipping a second one that drifts.
|
|
3
|
+
//
|
|
4
|
+
// Wire contract — mojo/apps/docit/rest/render.py, verified against the source:
|
|
5
|
+
// POST /api/docit/render {markdown: string} (@requires_auth)
|
|
6
|
+
// → the handler returns a bare dict, so mojo/decorators/http.py wraps it:
|
|
7
|
+
// {status: true, code: 200, data: {html: string}}
|
|
8
|
+
// → errors: 400 `markdown field is required`, 405 non-POST,
|
|
9
|
+
// 413 over MAX_MARKDOWN_BYTES.
|
|
10
|
+
// Unwrapped at the ONE boundary (mojoCall) like every other endpoint — no
|
|
11
|
+
// `resp.data.data.html || resp.data.html` sniffing, which is what web-mojo's
|
|
12
|
+
// three copies of this call did (do-not-recreate list).
|
|
13
|
+
//
|
|
14
|
+
// What this module guarantees to its callers:
|
|
15
|
+
// · The string it resolves is SANITIZED (markdown-sanitize.ts). Unsanitized
|
|
16
|
+
// server HTML never leaves this file, so there is no way to hold it.
|
|
17
|
+
// · A failure REJECTS. The fallback decision belongs to the component,
|
|
18
|
+
// because the fallback path renders React ELEMENTS, not a string —
|
|
19
|
+
// "resolve the fallback as success" would hand back a string that no
|
|
20
|
+
// longer says which renderer produced it.
|
|
21
|
+
// · Identical sources are rendered once: an in-memory cache plus in-flight
|
|
22
|
+
// de-duplication, so a chat feed re-rendering 50 messages is 0 requests
|
|
23
|
+
// and two mounts of the same message are 1.
|
|
24
|
+
import { mojoCall } from './client';
|
|
25
|
+
import { MojoError } from './errors';
|
|
26
|
+
import { sanitizeMarkdownHtml } from './markdown-sanitize';
|
|
27
|
+
|
|
28
|
+
export { sanitizeMarkdownHtml, safeLinkUrl, safeImgUrl, type SanitizeDrop } from './markdown-sanitize';
|
|
29
|
+
export { MarkdownFallback, markdownToNodes } from './markdown-fallback';
|
|
30
|
+
export { markdownToHtml, parseMarkdown, type MdBlock, type MdInline } from './markdown-parse';
|
|
31
|
+
|
|
32
|
+
export const MARKDOWN_RENDER_ENDPOINT = '/api/docit/render';
|
|
33
|
+
/** The server's own cap (rest/render.py) — over it, skip a guaranteed 413. */
|
|
34
|
+
export const MAX_MARKDOWN_BYTES = 400_000;
|
|
35
|
+
/** Enough for a long chat scrollback; oldest entries fall off the end. */
|
|
36
|
+
const CACHE_LIMIT = 200;
|
|
37
|
+
|
|
38
|
+
interface CacheEntry { source: string; html: string }
|
|
39
|
+
|
|
40
|
+
const cache = new Map<string, CacheEntry>();
|
|
41
|
+
const inflight = new Map<string, { source: string; p: Promise<string> }>();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* FNV-1a over the source, suffixed with its length. The key is only an index:
|
|
45
|
+
* every entry stores its source and is verified on hit, so a hash collision
|
|
46
|
+
* costs a re-render, never a wrong document.
|
|
47
|
+
*/
|
|
48
|
+
function cacheKey(source: string): string {
|
|
49
|
+
let h = 0x811c9dc5;
|
|
50
|
+
for (let i = 0; i < source.length; i++) {
|
|
51
|
+
h ^= source.charCodeAt(i);
|
|
52
|
+
h = Math.imul(h, 0x01000193);
|
|
53
|
+
}
|
|
54
|
+
return `${(h >>> 0).toString(36)}:${source.length}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function remember(key: string, source: string, html: string): void {
|
|
58
|
+
if (cache.size >= CACHE_LIMIT) {
|
|
59
|
+
const oldest = cache.keys().next();
|
|
60
|
+
if (!oldest.done) cache.delete(oldest.value);
|
|
61
|
+
}
|
|
62
|
+
cache.set(key, { source, html });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function utf8Bytes(source: string): number {
|
|
66
|
+
// UTF-8 is at most 4 bytes per UTF-16 code unit — skip the encode when the
|
|
67
|
+
// answer cannot possibly exceed the cap.
|
|
68
|
+
if (source.length * 4 <= MAX_MARKDOWN_BYTES) return source.length;
|
|
69
|
+
return new TextEncoder().encode(source).length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Sanitized HTML for this source if it has already been rendered — else null. */
|
|
73
|
+
export function peekMarkdown(source: string): string | null {
|
|
74
|
+
if (!source) return null;
|
|
75
|
+
const hit = cache.get(cacheKey(source));
|
|
76
|
+
return hit && hit.source === source ? hit.html : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Drop the memo (demos, tests, a backend swap mid-session). */
|
|
80
|
+
export function clearMarkdownCache(): void {
|
|
81
|
+
cache.clear();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Render markdown on the server and sanitize the result.
|
|
86
|
+
*
|
|
87
|
+
* REJECTS on any failure (endpoint missing, offline, 401, oversized body, a
|
|
88
|
+
* response without `html`, or no DOM to sanitize with). Callers render
|
|
89
|
+
* `<MarkdownFallback source>` instead — see ui/MarkdownView.tsx.
|
|
90
|
+
*/
|
|
91
|
+
export function renderMarkdown(source: string): Promise<string> {
|
|
92
|
+
if (!source.trim()) return Promise.resolve('');
|
|
93
|
+
|
|
94
|
+
const key = cacheKey(source);
|
|
95
|
+
const hit = cache.get(key);
|
|
96
|
+
if (hit && hit.source === source) return Promise.resolve(hit.html);
|
|
97
|
+
|
|
98
|
+
const pending = inflight.get(key);
|
|
99
|
+
if (pending && pending.source === source) return pending.p;
|
|
100
|
+
|
|
101
|
+
if (utf8Bytes(source) > MAX_MARKDOWN_BYTES) {
|
|
102
|
+
return Promise.reject(new MojoError('markdown input too large', 413));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const p = (async () => {
|
|
106
|
+
const body = await mojoCall(MARKDOWN_RENDER_ENDPOINT, { method: 'POST', body: { markdown: source } });
|
|
107
|
+
const html = (body.data as { html?: unknown } | undefined)?.html;
|
|
108
|
+
if (typeof html !== 'string') throw new MojoError('docit/render returned no html', 0);
|
|
109
|
+
const safe = sanitizeMarkdownHtml(html);
|
|
110
|
+
remember(key, source, safe);
|
|
111
|
+
return safe;
|
|
112
|
+
})();
|
|
113
|
+
|
|
114
|
+
// Only the registered promise clears the slot — a colliding key that was
|
|
115
|
+
// never registered must not evict someone else's in-flight render.
|
|
116
|
+
const slot = { source, p };
|
|
117
|
+
inflight.set(key, slot);
|
|
118
|
+
void p.catch(() => undefined).then(() => {
|
|
119
|
+
if (inflight.get(key) === slot) inflight.delete(key);
|
|
120
|
+
});
|
|
121
|
+
return p;
|
|
122
|
+
}
|
package/src/client/me.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Me + permissions — the CHECK side of web-mojo's permission model, ported
|
|
2
|
+
// from User.hasPermission (src/core/models/User.js:13-61) and
|
|
3
|
+
// Member.hasPermission (Member.js:15-28). UX gating only: the client may err
|
|
4
|
+
// permissive (category rollups), the server stays authoritative on every
|
|
5
|
+
// request.
|
|
6
|
+
//
|
|
7
|
+
// Semantics, in check order:
|
|
8
|
+
// 1. `is_superuser` passes everything.
|
|
9
|
+
// 2. An array means ANY-of.
|
|
10
|
+
// 3. A `sys.` prefix pins the check to system-level (user) permissions —
|
|
11
|
+
// group/member permissions are never consulted for sys.* checks.
|
|
12
|
+
// 4. Direct grant, or a category grant covering the granular name
|
|
13
|
+
// (CATEGORY_GRANULAR_MAP rollup).
|
|
14
|
+
// 5. System-level `admin` is the permission-driven equivalent of
|
|
15
|
+
// is_superuser — passes every gate.
|
|
16
|
+
// 6. Otherwise the active-group member: direct grant, or the member
|
|
17
|
+
// `admin` wildcard (full access within the group — never sys.*).
|
|
18
|
+
import { useQuery } from '@tanstack/react-query';
|
|
19
|
+
import { useContext, useSyncExternalStore } from 'react';
|
|
20
|
+
import { mojoGet } from './client';
|
|
21
|
+
import { getAuthSnapshot, subscribeAuth, type AuthSnapshot } from './auth';
|
|
22
|
+
import { GroupContext } from './group-context';
|
|
23
|
+
import { sanitizeAvatarRelation } from './types';
|
|
24
|
+
|
|
25
|
+
/** Server permission dicts use true AND 1 as granted (web-mojo checked `== true`). */
|
|
26
|
+
export type PermissionsDict = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
export interface Me {
|
|
29
|
+
id: number;
|
|
30
|
+
display_name?: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
is_superuser?: boolean;
|
|
33
|
+
permissions?: PermissionsDict | null;
|
|
34
|
+
avatar?: { id: number } | null;
|
|
35
|
+
[field: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `me` has its own query key and therefore its own mandatory sanitizer. */
|
|
39
|
+
export function sanitizeMe(row: Me): Me {
|
|
40
|
+
return { ...row, avatar: sanitizeAvatarRelation(row.avatar) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Active-group membership context — provided by the A3 GroupProvider. */
|
|
44
|
+
export interface MemberLike {
|
|
45
|
+
permissions?: PermissionsDict | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Category → granular rollup map ────────────────────────────────────
|
|
49
|
+
// A category grant implicitly grants every granular permission it covers.
|
|
50
|
+
// Ported verbatim from User.CATEGORY_GRANULAR_MAP (incl. the dnsman note:
|
|
51
|
+
// `security` maps view_dns/manage_dns because dnsman's RestMeta accepts
|
|
52
|
+
// `security` for both VIEW_PERMS and SAVE_PERMS — mirroring, not over-granting).
|
|
53
|
+
|
|
54
|
+
const BASE_CATEGORY_GRANULAR_MAP: Record<string, string[]> = {
|
|
55
|
+
security: ['view_security', 'manage_security', 'view_dns', 'manage_dns'],
|
|
56
|
+
users: ['view_users', 'manage_users', 'view_members'],
|
|
57
|
+
groups: ['view_groups', 'manage_groups', 'manage_group'],
|
|
58
|
+
comms: ['manage_chat', 'manage_aws', 'view_notifications', 'manage_notifications', 'send_notifications',
|
|
59
|
+
'view_devices', 'manage_devices', 'manage_push_config',
|
|
60
|
+
'view_phone_numbers', 'manage_phone_numbers', 'manage_phone_config',
|
|
61
|
+
'view_sms', 'manage_sms', 'send_sms'],
|
|
62
|
+
jobs: ['view_jobs', 'manage_jobs'],
|
|
63
|
+
metrics: ['view_metrics', 'manage_metrics', 'write_metrics'],
|
|
64
|
+
files: ['view_fileman', 'manage_files', 'view_vault', 'manage_vault'],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let categoryGranularMap: Record<string, string[]> = { ...BASE_CATEGORY_GRANULAR_MAP };
|
|
68
|
+
let granularToCategory: Record<string, string> = invertMap(categoryGranularMap);
|
|
69
|
+
|
|
70
|
+
function invertMap(map: Record<string, string[]>): Record<string, string> {
|
|
71
|
+
const out: Record<string, string> = {};
|
|
72
|
+
for (const [category, granulars] of Object.entries(map)) {
|
|
73
|
+
for (const g of granulars) out[g] = category;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* App extension point: add category → granular coverage (web-mojo's
|
|
80
|
+
* APP_CATEGORY_PERMISSIONS + rebuildPermissions, as a registry call instead
|
|
81
|
+
* of live-mutated arrays). Existing categories are extended, not replaced.
|
|
82
|
+
*/
|
|
83
|
+
export function registerPermissionCategories(extra: Record<string, string[]>): void {
|
|
84
|
+
for (const [category, granulars] of Object.entries(extra)) {
|
|
85
|
+
const current = categoryGranularMap[category] ?? [];
|
|
86
|
+
categoryGranularMap[category] = [...new Set([...current, ...granulars])];
|
|
87
|
+
}
|
|
88
|
+
granularToCategory = invertMap(categoryGranularMap);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getCategoryGranularMap(): Readonly<Record<string, readonly string[]>> {
|
|
92
|
+
return categoryGranularMap;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── The check ─────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/** The backend stores grants as true or 1 (web-mojo's loose `== true`). */
|
|
98
|
+
function granted(dict: PermissionsDict | null | undefined, name: string): boolean {
|
|
99
|
+
const v = dict?.[name];
|
|
100
|
+
return v === true || v === 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Direct grant or category rollup on one permissions dict (User._hasPermission). */
|
|
104
|
+
function dictHasPermission(dict: PermissionsDict | null | undefined, name: string): boolean {
|
|
105
|
+
if (!dict) return false;
|
|
106
|
+
if (granted(dict, name)) return true;
|
|
107
|
+
const category = granularToCategory[name];
|
|
108
|
+
return category != null && granted(dict, category);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Member.hasPermission: direct grant, or the group `admin` wildcard (never sys.*). */
|
|
112
|
+
export function memberHasPermission(member: MemberLike | null | undefined, permission: string | string[]): boolean {
|
|
113
|
+
if (!member) return false;
|
|
114
|
+
if (Array.isArray(permission)) return permission.some((p) => memberHasPermission(member, p));
|
|
115
|
+
// A member dictionary is never a system authority. Fail before the direct
|
|
116
|
+
// lookup too: malformed/stale rows containing a literal `sys.*` key must
|
|
117
|
+
// not satisfy an Admin gate. User.hasPermission still resolves sys.* from
|
|
118
|
+
// the user's global dictionary in hasPermission() below.
|
|
119
|
+
if (permission.startsWith('sys.')) return false;
|
|
120
|
+
const dict = member.permissions;
|
|
121
|
+
if (!dict) return false;
|
|
122
|
+
if (granted(dict, permission)) return true;
|
|
123
|
+
return granted(dict, 'admin');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** User.hasPermission, full semantics. `member` is the active-group membership. */
|
|
127
|
+
export function hasPermission(
|
|
128
|
+
me: Me | null | undefined,
|
|
129
|
+
permission: string | string[],
|
|
130
|
+
member?: MemberLike | null,
|
|
131
|
+
): boolean {
|
|
132
|
+
if (!me) return false;
|
|
133
|
+
if (me.is_superuser) return true;
|
|
134
|
+
if (Array.isArray(permission)) return permission.some((p) => hasPermission(me, p, member));
|
|
135
|
+
|
|
136
|
+
const isSys = permission.startsWith('sys.');
|
|
137
|
+
const name = isSys ? permission.slice(4) : permission;
|
|
138
|
+
|
|
139
|
+
if (dictHasPermission(me.permissions, name)) return true;
|
|
140
|
+
// System-level `admin` is a full-access grant — the permission-driven
|
|
141
|
+
// equivalent of is_superuser.
|
|
142
|
+
if (dictHasPermission(me.permissions, 'admin')) return true;
|
|
143
|
+
// Member permissions are only consulted for non-sys checks.
|
|
144
|
+
if (!isSys && member && memberHasPermission(member, permission)) return true;
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Hooks ─────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/** Live auth-session snapshot (re-renders on login/logout, incl. cross-tab). */
|
|
151
|
+
export function useAuthSnapshot(): AuthSnapshot {
|
|
152
|
+
return useSyncExternalStore(subscribeAuth, getAuthSnapshot, getAuthSnapshot);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The signed-in user. Disabled (no fetch, no data) while anonymous; keyed by
|
|
157
|
+
* uid so a login/logout/login sequence never bleeds one identity's data into
|
|
158
|
+
* the next.
|
|
159
|
+
*/
|
|
160
|
+
export function useMe() {
|
|
161
|
+
const auth = useAuthSnapshot();
|
|
162
|
+
return useQuery<Me>({
|
|
163
|
+
queryKey: ['me', auth.uid],
|
|
164
|
+
queryFn: async () => sanitizeMe(await mojoGet<Me>('/api/user', 'me')),
|
|
165
|
+
enabled: auth.authenticated,
|
|
166
|
+
staleTime: 5 * 60_000,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface CanResult {
|
|
171
|
+
can: boolean;
|
|
172
|
+
/** True only during the first `me` fetch — anonymous is NOT loading. */
|
|
173
|
+
isLoading: boolean;
|
|
174
|
+
me: Me | null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Permission check against the signed-in user PLUS the active-group member
|
|
179
|
+
* when a <GroupProvider> is mounted (member `admin` grants everything in the
|
|
180
|
+
* group; sys.* checks never consult it). Resolves false while loading or
|
|
181
|
+
* anonymous (fail-closed rendering).
|
|
182
|
+
*/
|
|
183
|
+
export function useCan(permission: string | string[]): CanResult {
|
|
184
|
+
const { data, isLoading } = useMe();
|
|
185
|
+
const groupCtx = useContext(GroupContext);
|
|
186
|
+
const me = data ?? null;
|
|
187
|
+
return { can: hasPermission(me, permission, groupCtx?.member ?? null), isLoading, me };
|
|
188
|
+
}
|