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.
Files changed (369) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -0
  3. package/docs/DetailView.md +173 -0
  4. package/docs/ModelTable.md +128 -0
  5. package/docs/README.md +98 -0
  6. package/docs/admin-assistant.md +72 -0
  7. package/docs/admin-bouncer.md +120 -0
  8. package/docs/admin-cloudwatch.md +38 -0
  9. package/docs/admin-credentials.md +116 -0
  10. package/docs/admin-dashboard.md +15 -0
  11. package/docs/admin-devices-geoip.md +284 -0
  12. package/docs/admin-dns-certificates.md +122 -0
  13. package/docs/admin-dns-records.md +65 -0
  14. package/docs/admin-dns-registrar.md +33 -0
  15. package/docs/admin-dns.md +80 -0
  16. package/docs/admin-identity-users.md +103 -0
  17. package/docs/admin-incidents.md +71 -0
  18. package/docs/admin-jobs.md +426 -0
  19. package/docs/admin-members.md +112 -0
  20. package/docs/admin-messaging.md +56 -0
  21. package/docs/admin-metrics-explorer.md +63 -0
  22. package/docs/admin-monitoring.md +99 -0
  23. package/docs/admin-network-security.md +393 -0
  24. package/docs/admin-phonehub.md +27 -0
  25. package/docs/admin-push.md +36 -0
  26. package/docs/admin-rules.md +87 -0
  27. package/docs/admin-security-tickets.md +40 -0
  28. package/docs/admin-settings.md +78 -0
  29. package/docs/admin-shortlinks.md +32 -0
  30. package/docs/admin-storage.md +141 -0
  31. package/docs/auth-pages.md +212 -0
  32. package/docs/auth.md +58 -0
  33. package/docs/calendar.md +122 -0
  34. package/docs/charts.md +303 -0
  35. package/docs/client.md +206 -0
  36. package/docs/collection-multiselect.md +117 -0
  37. package/docs/collection-select.md +132 -0
  38. package/docs/combobox.md +105 -0
  39. package/docs/dataview.md +226 -0
  40. package/docs/datepicker.md +151 -0
  41. package/docs/daterange.md +170 -0
  42. package/docs/datetimepicker.md +249 -0
  43. package/docs/defineModel.md +72 -0
  44. package/docs/detail-primitives.md +174 -0
  45. package/docs/feedback.md +85 -0
  46. package/docs/file-upload-ux.md +146 -0
  47. package/docs/forms.md +435 -0
  48. package/docs/grouping-and-fmt.md +98 -0
  49. package/docs/idioms.md +112 -0
  50. package/docs/image-editor.md +101 -0
  51. package/docs/loading.md +242 -0
  52. package/docs/location-address.md +76 -0
  53. package/docs/markdown.md +203 -0
  54. package/docs/menus-and-access.md +89 -0
  55. package/docs/multiselect-dropdown.md +153 -0
  56. package/docs/params.md +73 -0
  57. package/docs/popover.md +130 -0
  58. package/docs/realtime.md +61 -0
  59. package/docs/record-feed.md +238 -0
  60. package/docs/right-panel.md +42 -0
  61. package/docs/tabs-and-form-wizard.md +60 -0
  62. package/docs/taginput.md +120 -0
  63. package/docs/timepicker.md +173 -0
  64. package/docs/timezone-select.md +147 -0
  65. package/docs/worldmap.md +302 -0
  66. package/package.json +55 -0
  67. package/src/admin/assistant/AssistantFeed.tsx +29 -0
  68. package/src/admin/assistant/AssistantPanel.tsx +65 -0
  69. package/src/admin/assistant/api.ts +17 -0
  70. package/src/admin/assistant/data.ts +63 -0
  71. package/src/admin/assistant/index.ts +21 -0
  72. package/src/admin/assistant/launchers.tsx +33 -0
  73. package/src/admin/assistant/pages.tsx +78 -0
  74. package/src/admin/assistant/streaming.ts +238 -0
  75. package/src/admin/assistant/types.ts +30 -0
  76. package/src/admin/bouncer/devices.tsx +230 -0
  77. package/src/admin/bouncer/index.ts +34 -0
  78. package/src/admin/bouncer/models.ts +199 -0
  79. package/src/admin/bouncer/signals.tsx +182 -0
  80. package/src/admin/bouncer/signatures.tsx +220 -0
  81. package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
  82. package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
  83. package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
  84. package/src/admin/cloudwatch/data.ts +163 -0
  85. package/src/admin/cloudwatch/index.ts +16 -0
  86. package/src/admin/credentials/group-api-keys.tsx +433 -0
  87. package/src/admin/credentials/index.ts +4 -0
  88. package/src/admin/credentials/models.ts +244 -0
  89. package/src/admin/credentials/secret-dialog.tsx +77 -0
  90. package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
  91. package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
  92. package/src/admin/dashboard/data.ts +60 -0
  93. package/src/admin/dashboard/index.ts +2 -0
  94. package/src/admin/dns/CertificateDetail.tsx +115 -0
  95. package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
  96. package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
  97. package/src/admin/dns/CertificatesPage.tsx +85 -0
  98. package/src/admin/dns/DnsRecordEditor.tsx +110 -0
  99. package/src/admin/dns/DnsRecordsPage.tsx +36 -0
  100. package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
  101. package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
  102. package/src/admin/dns/DomainDetail.tsx +41 -0
  103. package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
  104. package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
  105. package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
  106. package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
  107. package/src/admin/dns/DomainsPage.tsx +27 -0
  108. package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
  109. package/src/admin/dns/RegistrantContactPage.tsx +42 -0
  110. package/src/admin/dns/api.ts +515 -0
  111. package/src/admin/dns/certificate-data.ts +221 -0
  112. package/src/admin/dns/data.ts +32 -0
  113. package/src/admin/dns/dns-data.ts +266 -0
  114. package/src/admin/dns/dns-integration.ts +24 -0
  115. package/src/admin/dns/domain-links.ts +37 -0
  116. package/src/admin/dns/index.ts +86 -0
  117. package/src/admin/dns/models.ts +418 -0
  118. package/src/admin/dns/purchase-data.ts +67 -0
  119. package/src/admin/dns/registrant-data.ts +52 -0
  120. package/src/admin/identity/index.ts +5 -0
  121. package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
  122. package/src/admin/identity/members/MemberDetail.tsx +226 -0
  123. package/src/admin/identity/members/MembersPage.tsx +144 -0
  124. package/src/admin/identity/members/index.ts +24 -0
  125. package/src/admin/identity/members/member-flows.tsx +132 -0
  126. package/src/admin/identity/members/models.ts +204 -0
  127. package/src/admin/identity/users/UserDetail.tsx +257 -0
  128. package/src/admin/identity/users/UsersPage.tsx +163 -0
  129. package/src/admin/identity/users/index.ts +27 -0
  130. package/src/admin/identity/users/models.ts +232 -0
  131. package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
  132. package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
  133. package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
  134. package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
  135. package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
  136. package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
  137. package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
  138. package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
  139. package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
  140. package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
  141. package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
  142. package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
  143. package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
  144. package/src/admin/identity/users/sections/actions.tsx +752 -0
  145. package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
  146. package/src/admin/identity/users/sections/queries.ts +74 -0
  147. package/src/admin/identity/users/sections/shared.tsx +299 -0
  148. package/src/admin/incidents/EventDetail.tsx +43 -0
  149. package/src/admin/incidents/EventsPage.tsx +52 -0
  150. package/src/admin/incidents/IncidentDetail.tsx +68 -0
  151. package/src/admin/incidents/IncidentsPage.tsx +102 -0
  152. package/src/admin/incidents/forensics.tsx +73 -0
  153. package/src/admin/incidents/index.ts +16 -0
  154. package/src/admin/incidents/models.ts +113 -0
  155. package/src/admin/incidents/sanitize.ts +71 -0
  156. package/src/admin/index.ts +383 -0
  157. package/src/admin/jobs/JobDashboardPage.tsx +111 -0
  158. package/src/admin/jobs/JobDetail.tsx +721 -0
  159. package/src/admin/jobs/JobRunnersPage.tsx +216 -0
  160. package/src/admin/jobs/JobsTablePage.tsx +122 -0
  161. package/src/admin/jobs/RunnerDetail.tsx +576 -0
  162. package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
  163. package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
  164. package/src/admin/jobs/columns.tsx +297 -0
  165. package/src/admin/jobs/control.ts +488 -0
  166. package/src/admin/jobs/index.ts +85 -0
  167. package/src/admin/jobs/models.ts +599 -0
  168. package/src/admin/jobs/queries.ts +272 -0
  169. package/src/admin/jobs/scheduled-task-form.tsx +493 -0
  170. package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
  171. package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
  172. package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
  173. package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
  174. package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
  175. package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
  176. package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
  177. package/src/admin/messaging/MailboxesPage.tsx +15 -0
  178. package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
  179. package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
  180. package/src/admin/messaging/SentMessagesPage.tsx +11 -0
  181. package/src/admin/messaging/api.ts +17 -0
  182. package/src/admin/messaging/data.ts +10 -0
  183. package/src/admin/messaging/index.ts +23 -0
  184. package/src/admin/messaging/models.ts +76 -0
  185. package/src/admin/messaging/push/PushPage.tsx +44 -0
  186. package/src/admin/messaging/push/api.ts +17 -0
  187. package/src/admin/messaging/push/index.ts +9 -0
  188. package/src/admin/messaging/push/models.ts +43 -0
  189. package/src/admin/messaging/sanitize.ts +44 -0
  190. package/src/admin/monitoring/LogInspector.tsx +160 -0
  191. package/src/admin/monitoring/LogsPage.tsx +135 -0
  192. package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
  193. package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
  194. package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
  195. package/src/admin/monitoring/index.ts +55 -0
  196. package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
  197. package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
  198. package/src/admin/monitoring/models.ts +155 -0
  199. package/src/admin/network/BlockedIPsPage.tsx +194 -0
  200. package/src/admin/network/BlockedIpDetail.tsx +262 -0
  201. package/src/admin/network/FirewallLogPage.tsx +115 -0
  202. package/src/admin/network/IPSetDetail.tsx +280 -0
  203. package/src/admin/network/IPSetEditor.tsx +191 -0
  204. package/src/admin/network/IPSetsPage.tsx +177 -0
  205. package/src/admin/network/geofence/BlocksTab.tsx +289 -0
  206. package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
  207. package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
  208. package/src/admin/network/geofence/PostureHeader.tsx +184 -0
  209. package/src/admin/network/geofence/RuleEditor.tsx +176 -0
  210. package/src/admin/network/geofence/RulesTab.tsx +254 -0
  211. package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
  212. package/src/admin/network/geofence/geofence-data.ts +674 -0
  213. package/src/admin/network/index.ts +68 -0
  214. package/src/admin/network/models.ts +607 -0
  215. package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
  216. package/src/admin/phonehub/api.ts +47 -0
  217. package/src/admin/phonehub/data.ts +6 -0
  218. package/src/admin/phonehub/index.ts +10 -0
  219. package/src/admin/phonehub/models.ts +68 -0
  220. package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
  221. package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
  222. package/src/admin/rules/RuleSetsPage.tsx +33 -0
  223. package/src/admin/rules/editors.tsx +54 -0
  224. package/src/admin/rules/handler-dsl.ts +213 -0
  225. package/src/admin/rules/index.ts +13 -0
  226. package/src/admin/rules/models.ts +112 -0
  227. package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
  228. package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
  229. package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
  230. package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
  231. package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
  232. package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
  233. package/src/admin/security/devices/index.ts +40 -0
  234. package/src/admin/security/devices/models.ts +470 -0
  235. package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
  236. package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
  237. package/src/admin/security/geoip/geoip-forms.ts +102 -0
  238. package/src/admin/security/geoip/index.ts +27 -0
  239. package/src/admin/security/geoip/models.ts +397 -0
  240. package/src/admin/security/index.ts +31 -0
  241. package/src/admin/security/models.ts +137 -0
  242. package/src/admin/security/tickets.tsx +562 -0
  243. package/src/admin/security-permissions.ts +5 -0
  244. package/src/admin/settings/SettingDetail.tsx +57 -0
  245. package/src/admin/settings/SettingEditor.tsx +157 -0
  246. package/src/admin/settings/SettingsPage.tsx +72 -0
  247. package/src/admin/settings/index.ts +19 -0
  248. package/src/admin/settings/model.ts +143 -0
  249. package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
  250. package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
  251. package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
  252. package/src/admin/shortlinks/index.ts +18 -0
  253. package/src/admin/shortlinks/models.ts +246 -0
  254. package/src/admin/storage/BackendsPage.tsx +43 -0
  255. package/src/admin/storage/BucketDetail.tsx +77 -0
  256. package/src/admin/storage/BucketsPage.tsx +64 -0
  257. package/src/admin/storage/FileManagerDetail.tsx +36 -0
  258. package/src/admin/storage/FilePreview.tsx +54 -0
  259. package/src/admin/storage/FileUploadSurface.tsx +250 -0
  260. package/src/admin/storage/FileView.tsx +109 -0
  261. package/src/admin/storage/FilesPage.tsx +53 -0
  262. package/src/admin/storage/file-renditions.ts +99 -0
  263. package/src/admin/storage/index.ts +31 -0
  264. package/src/admin/storage/models.ts +585 -0
  265. package/src/admin/storage/storage-dialogs.tsx +80 -0
  266. package/src/charts/CircularProgress.tsx +281 -0
  267. package/src/charts/KPIStrip.tsx +162 -0
  268. package/src/charts/KPITile.tsx +125 -0
  269. package/src/charts/MetricsChart.tsx +459 -0
  270. package/src/charts/MetricsMiniWidget.tsx +588 -0
  271. package/src/charts/MiniChart.tsx +283 -0
  272. package/src/charts/PieChart.tsx +354 -0
  273. package/src/charts/SeriesChart.tsx +378 -0
  274. package/src/charts/chart-dialogs.tsx +149 -0
  275. package/src/charts/exportChart.ts +144 -0
  276. package/src/charts/index.ts +28 -0
  277. package/src/charts/pie-math.ts +127 -0
  278. package/src/charts/stats.ts +265 -0
  279. package/src/charts/worldmap/WorldMap.tsx +649 -0
  280. package/src/charts/worldmap/countryCentroids.ts +304 -0
  281. package/src/charts/worldmap/geo.ts +293 -0
  282. package/src/charts/worldmap/use-world-land.ts +47 -0
  283. package/src/charts/worldmap/world-land.ts +18 -0
  284. package/src/charts/worldmap/worldmap-data.ts +285 -0
  285. package/src/client/auth.ts +737 -0
  286. package/src/client/client.ts +268 -0
  287. package/src/client/duid.ts +31 -0
  288. package/src/client/errors.ts +33 -0
  289. package/src/client/group-context.ts +28 -0
  290. package/src/client/group.tsx +126 -0
  291. package/src/client/hooks.ts +50 -0
  292. package/src/client/index.ts +37 -0
  293. package/src/client/jwt.ts +75 -0
  294. package/src/client/location.ts +285 -0
  295. package/src/client/lookups.ts +41 -0
  296. package/src/client/markdown-fallback.tsx +115 -0
  297. package/src/client/markdown-parse.ts +386 -0
  298. package/src/client/markdown-sanitize.ts +217 -0
  299. package/src/client/markdown.ts +122 -0
  300. package/src/client/me.ts +188 -0
  301. package/src/client/mock.ts +9984 -0
  302. package/src/client/model.ts +227 -0
  303. package/src/client/params.ts +292 -0
  304. package/src/client/realtime-mock.ts +221 -0
  305. package/src/client/realtime.ts +561 -0
  306. package/src/client/record-feed.ts +329 -0
  307. package/src/client/safe-export.ts +71 -0
  308. package/src/client/types.ts +278 -0
  309. package/src/client/upload.ts +421 -0
  310. package/src/ui/AddressField.tsx +273 -0
  311. package/src/ui/AttachmentQueue.tsx +92 -0
  312. package/src/ui/CollectionMultiSelect.tsx +622 -0
  313. package/src/ui/CollectionSelect.tsx +517 -0
  314. package/src/ui/ComboBox.tsx +336 -0
  315. package/src/ui/DetailView.tsx +438 -0
  316. package/src/ui/FileDrop.tsx +194 -0
  317. package/src/ui/FileField.tsx +369 -0
  318. package/src/ui/FilterBar.tsx +269 -0
  319. package/src/ui/FormFields.tsx +77 -0
  320. package/src/ui/FormView.tsx +360 -0
  321. package/src/ui/FormWizard.tsx +336 -0
  322. package/src/ui/GroupSwitcher.tsx +202 -0
  323. package/src/ui/Guarded.tsx +16 -0
  324. package/src/ui/MarkdownView.tsx +89 -0
  325. package/src/ui/ModelTable.tsx +848 -0
  326. package/src/ui/MultiSelectDropdown.tsx +390 -0
  327. package/src/ui/Popover.tsx +204 -0
  328. package/src/ui/RecordFeed.tsx +627 -0
  329. package/src/ui/RequiresGroup.tsx +32 -0
  330. package/src/ui/RightPanel.tsx +107 -0
  331. package/src/ui/SidebarNav.tsx +330 -0
  332. package/src/ui/Tabs.tsx +161 -0
  333. package/src/ui/TagInput.tsx +381 -0
  334. package/src/ui/ThemeProvider.tsx +52 -0
  335. package/src/ui/UploadQueue.tsx +603 -0
  336. package/src/ui/armed-button.tsx +91 -0
  337. package/src/ui/date/Calendar.tsx +498 -0
  338. package/src/ui/date/DatePicker.tsx +289 -0
  339. package/src/ui/date/DateRangePicker.tsx +280 -0
  340. package/src/ui/date/DateTimePicker.tsx +587 -0
  341. package/src/ui/date/PresetRail.tsx +165 -0
  342. package/src/ui/date/TimePicker.tsx +624 -0
  343. package/src/ui/date/TimezoneSelect.tsx +267 -0
  344. package/src/ui/date/fns.ts +679 -0
  345. package/src/ui/detail/DataView.tsx +692 -0
  346. package/src/ui/detail/FlowStrip.tsx +80 -0
  347. package/src/ui/detail/KnownFieldsCard.tsx +121 -0
  348. package/src/ui/detail/MetadataSection.tsx +219 -0
  349. package/src/ui/detail/StackTraceView.tsx +150 -0
  350. package/src/ui/detail/StatusPanel.tsx +73 -0
  351. package/src/ui/detail/Timeline.tsx +52 -0
  352. package/src/ui/detail/index.ts +12 -0
  353. package/src/ui/field-registry.tsx +428 -0
  354. package/src/ui/field-wire.ts +322 -0
  355. package/src/ui/form-autosave.ts +583 -0
  356. package/src/ui/format.ts +444 -0
  357. package/src/ui/grouping.ts +150 -0
  358. package/src/ui/image-editor/ImageEditor.tsx +484 -0
  359. package/src/ui/image-editor/canvas.ts +254 -0
  360. package/src/ui/image-editor/index.ts +3 -0
  361. package/src/ui/image-editor/math.ts +416 -0
  362. package/src/ui/index.ts +58 -0
  363. package/src/ui/loading.tsx +604 -0
  364. package/src/ui/menu-registry.ts +213 -0
  365. package/src/ui/modal.tsx +297 -0
  366. package/src/ui/password.tsx +302 -0
  367. package/src/ui/schema-form-core.tsx +315 -0
  368. package/src/ui/toast.tsx +219 -0
  369. package/src/ui/ui.tsx +45 -0
@@ -0,0 +1,102 @@
1
+ // GeoIPForms — web-mojo `src/core/models/System.js:8-67`, ported field for
2
+ // field into portal-mojo's `Field` language. Nothing is dropped; `cols: 6/12`
3
+ // becomes `columns: 6/12`, `switch` stays `switch`, and the two numeric
4
+ // coordinate fields keep `type: 'number'`.
5
+ //
6
+ // Two deviations, both forced:
7
+ // 1. `editNetwork` in the source offered a `provider` text input, but
8
+ // `provider` is excluded from the `default` graph, so the field always
9
+ // rendered EMPTY and saving it wrote a blank over the real value. It is
10
+ // dropped — provider is provenance the enrichment pipeline owns, and it
11
+ // is visible read-only in the manage-gated raw reveal.
12
+ // 2. The source's `type: 'number'` becomes `type: 'text'`. portal-mojo's
13
+ // field registry has no `number` renderer, so `'number'` would fall back
14
+ // to a text input WITH a console.warn — the same control, plus noise. The
15
+ // backend coerces FloatField/IntegerField from strings, so the wire is
16
+ // unchanged; the expected format moves into `help`.
17
+ import type { Field, ModelForm } from '../../../client';
18
+ import { THREAT_LEVEL_OPTIONS } from './models';
19
+
20
+ export const GEOIP_EDIT_LOCATION_FIELDS: Field[] = [
21
+ { name: 'ip_address', type: 'text', label: 'IP address', required: true, disabled: true, columns: 6 },
22
+ { name: 'subnet', type: 'text', label: 'Subnet', columns: 6 },
23
+ { name: 'country_name', type: 'text', label: 'Country', columns: 6 },
24
+ { name: 'country_code', type: 'text', label: 'Country code', columns: 6, help: 'ISO 3166-1 alpha-2 (or alpha-3)' },
25
+ { name: 'region', type: 'text', label: 'Region', columns: 6 },
26
+ { name: 'region_code', type: 'text', label: 'Region code', columns: 6, help: 'ISO 3166-2 subdivision, e.g. US-CA' },
27
+ { name: 'city', type: 'text', label: 'City', columns: 6 },
28
+ { name: 'postal_code', type: 'text', label: 'Postal code', columns: 6 },
29
+ { name: 'timezone', type: 'text', label: 'Timezone', columns: 6 },
30
+ { name: 'latitude', type: 'text', label: 'Latitude', columns: 6, placeholder: '-90 … 90', help: 'Decimal degrees' },
31
+ { name: 'longitude', type: 'text', label: 'Longitude', columns: 6, placeholder: '-180 … 180', help: 'Decimal degrees' },
32
+ ];
33
+
34
+ export const GEOIP_EDIT_NETWORK_FIELDS: Field[] = [
35
+ { name: 'asn', type: 'text', label: 'ASN', columns: 6 },
36
+ { name: 'asn_org', type: 'text', label: 'ASN organization', columns: 6 },
37
+ { name: 'isp', type: 'text', label: 'ISP', columns: 12 },
38
+ { name: 'connection_type', type: 'text', label: 'Connection type', columns: 6, help: 'residential · business · hosting · cellular' },
39
+ { name: 'mobile_carrier', type: 'text', label: 'Mobile carrier', columns: 6 },
40
+ { name: 'is_mobile', type: 'switch', label: 'Mobile connection', columns: 6 },
41
+ { name: 'is_cloud', type: 'switch', label: 'Cloud provider', columns: 6 },
42
+ { name: 'is_datacenter', type: 'switch', label: 'Datacenter', columns: 6 },
43
+ ];
44
+
45
+ export const GEOIP_EDIT_SECURITY_FIELDS: Field[] = [
46
+ { name: 'threat_level', type: 'select', label: 'Threat level', columns: 12, options: [...THREAT_LEVEL_OPTIONS] },
47
+ { name: 'risk_score', type: 'text', label: 'Risk score', columns: 6, placeholder: '0 … 100', help: 'Recomputed server-side from the flags below' },
48
+ { name: 'is_known_attacker', type: 'switch', label: 'Known attacker', columns: 6 },
49
+ { name: 'is_known_abuser', type: 'switch', label: 'Known abuser', columns: 6 },
50
+ { name: 'is_tor', type: 'switch', label: 'Tor exit node', columns: 6 },
51
+ { name: 'is_vpn', type: 'switch', label: 'VPN', columns: 6 },
52
+ { name: 'is_proxy', type: 'switch', label: 'Proxy', columns: 6 },
53
+ ];
54
+
55
+ export const GEOIP_LOOKUP_FIELDS: Field[] = [
56
+ {
57
+ name: 'ip', type: 'text', label: 'IP address', required: true,
58
+ placeholder: 'e.g. 203.0.113.9',
59
+ help: 'Creates the record when absent and refreshes it when stale. Rate limited to 30 lookups per source IP.',
60
+ },
61
+ ];
62
+
63
+ /** Block TTLs, exactly the source set (GeoIPView.js:1118-1128). */
64
+ export const GEOIP_BLOCK_TTL_OPTIONS: readonly { value: string; label: string }[] = [
65
+ { value: '3600', label: '1 hour' },
66
+ { value: '21600', label: '6 hours' },
67
+ { value: '86400', label: '24 hours' },
68
+ { value: '604800', label: '7 days' },
69
+ { value: '2592000', label: '30 days' },
70
+ { value: '0', label: 'Permanent' },
71
+ ];
72
+
73
+ export const GEOIP_BLOCK_FIELDS: Field[] = [
74
+ { name: 'reason', type: 'text', label: 'Reason', required: true, placeholder: 'e.g. Suspicious activity' },
75
+ { name: 'ttl', type: 'select', label: 'Duration', options: [...GEOIP_BLOCK_TTL_OPTIONS] },
76
+ ];
77
+
78
+ export const GEOIP_UNBLOCK_FIELDS: Field[] = [
79
+ { name: 'reason', type: 'text', label: 'Reason', placeholder: 'e.g. False positive' },
80
+ ];
81
+
82
+ export const GEOIP_WHITELIST_FIELDS: Field[] = [
83
+ { name: 'reason', type: 'text', label: 'Reason', required: true, placeholder: 'e.g. Known office IP' },
84
+ {
85
+ name: 'ttl', type: 'select', label: 'Expires', options: [
86
+ { value: '0', label: 'Permanent' },
87
+ { value: '86400', label: '24 hours' },
88
+ { value: '604800', label: '7 days' },
89
+ { value: '2592000', label: '30 days' },
90
+ ],
91
+ },
92
+ ];
93
+
94
+ export const GEOIP_FORMS: Record<string, ModelForm> = {
95
+ edit_location: { title: 'Edit location', submitText: 'Save', fields: GEOIP_EDIT_LOCATION_FIELDS },
96
+ edit_network: { title: 'Edit network', submitText: 'Save', fields: GEOIP_EDIT_NETWORK_FIELDS },
97
+ edit_security: { title: 'Edit security', submitText: 'Save', fields: GEOIP_EDIT_SECURITY_FIELDS },
98
+ lookup: { title: 'Look up IP', submitText: 'Look up', fields: GEOIP_LOOKUP_FIELDS },
99
+ block: { title: 'Block IP', submitText: 'Block', fields: GEOIP_BLOCK_FIELDS },
100
+ unblock: { title: 'Unblock IP', submitText: 'Unblock', fields: GEOIP_UNBLOCK_FIELDS },
101
+ whitelist: { title: 'Whitelist IP', submitText: 'Whitelist', fields: GEOIP_WHITELIST_FIELDS },
102
+ };
@@ -0,0 +1,27 @@
1
+ // IP Intelligence — the GeoIP cache surfaces. This module OWNS
2
+ // `/api/system/geoip` for the whole toolkit: #1287's network-security section
3
+ // imports the model, the permission constants, the enforcement helpers and
4
+ // the dossier from here rather than redefining any of them.
5
+ import type { AdminSection } from '../../index';
6
+ import { GeoIpCachePage } from './GeoIpCachePage';
7
+ import { GEOIP_VIEW_PERMS } from './models';
8
+
9
+ export * from './models';
10
+ export * from './geoip-forms';
11
+ export * from './GeoIpDossier';
12
+ export * from './GeoIpCachePage';
13
+
14
+ export const GEOIP_ADMIN_SECTION: AdminSection = {
15
+ id: 'geoip',
16
+ basePath: 'security/geoip',
17
+ title: 'IP Intelligence',
18
+ icon: 'bi-globe2',
19
+ navigationGroup: 'security',
20
+ permissions: GEOIP_VIEW_PERMS,
21
+ routes: [
22
+ {
23
+ path: 'cache', label: 'GeoIP Cache',
24
+ component: GeoIpCachePage, permissions: GEOIP_VIEW_PERMS,
25
+ },
26
+ ],
27
+ };
@@ -0,0 +1,397 @@
1
+ // GeoIP model layer — the CANONICAL definition of `/api/system/geoip` for the
2
+ // whole admin toolkit. #1287 (network security: Blocked IPs, firewall log, IP
3
+ // sets, geofencing) imports `GeoLocatedIPModel`, `GEOIP_VIEW_PERMS`,
4
+ // `GEOIP_MANAGE_PERMS`, the enforcement helpers and the dossier from here
5
+ // rather than redefining any of them — one endpoint, one defineModel, one
6
+ // cache key.
7
+ //
8
+ // Ported from web-mojo `src/core/models/System.js` (GeoLocatedIP +
9
+ // GeoLocatedIPList + GeoIPForms) and `admin/account/devices/GeoIPView.js`
10
+ // (1255 lines, read in full 2026-08-06), reconciled against the authoritative
11
+ // backend `django-mojo/mojo/apps/account/models/geolocated_ip.py` +
12
+ // `rest/device.py`.
13
+ //
14
+ // Backend-forced corrections carried here (see docs/admin-devices-geoip.md):
15
+ // · `reverse_dns` and `ip_version` DO NOT EXIST on GeoLocatedIP — the source
16
+ // rendered two permanently blank Network rows and a subtitle fragment
17
+ // that never appeared.
18
+ // · `provider` is EXCLUDED from the `default` graph, so the source's
19
+ // Provider row was blank on every normal read. It rides `detailed` only.
20
+ // · There is no `metadata` field — `KnownFieldsCard` is fed the record.
21
+ // · `block_active` / `whitelist_active` are Python properties that are only
22
+ // serialized on the `basic` graph. The default graph carries the raw
23
+ // booleans, so expiry has to be evaluated client-side: `blockActive()` /
24
+ // `whitelistActive()` mirror the model properties exactly.
25
+ // · `CAN_DELETE` is unset ⇒ `rest.py` defaults it to False. There is no
26
+ // delete path; the source's "Delete record" kebab item could never work.
27
+ import { useQuery, type QueryClient } from '@tanstack/react-query';
28
+ import {
29
+ createSafeExporter, defineModel, mojoCall, mojoList,
30
+ type Params,
31
+ } from '../../../client';
32
+ import { boundedSecurityText, sanitizeSecurityValue } from '../../incidents/sanitize';
33
+
34
+ // ── Permissions (backend-derived, `sys.`-pinned, fail-closed) ─────────
35
+
36
+ /** `GeoLocatedIP.RestMeta.VIEW_PERMS`, system-pinned. */
37
+ export const GEOIP_VIEW_PERMS = [
38
+ 'sys.manage_users',
39
+ 'sys.view_security',
40
+ 'sys.manage_security',
41
+ 'sys.security',
42
+ 'sys.users',
43
+ ];
44
+
45
+ /** `GeoLocatedIP.RestMeta.SAVE_PERMS`, system-pinned. Gates every mutation. */
46
+ export const GEOIP_MANAGE_PERMS = [
47
+ 'sys.manage_users',
48
+ 'sys.manage_security',
49
+ 'sys.security',
50
+ ];
51
+
52
+ // ── Row shapes ────────────────────────────────────────────────────────
53
+
54
+ export type ThreatLevel = 'low' | 'medium' | 'high' | 'critical';
55
+
56
+ /** THREAT_LEVEL_ORDER from the model, with `null` at index 0. */
57
+ export const THREAT_LEVEL_ORDER: readonly (ThreatLevel | null)[] = [null, 'low', 'medium', 'high', 'critical'];
58
+
59
+ export const THREAT_LEVEL_OPTIONS: readonly { value: string; label: string }[] = [
60
+ { value: '', label: 'None' },
61
+ { value: 'low', label: 'Low' },
62
+ { value: 'medium', label: 'Medium' },
63
+ { value: 'high', label: 'High' },
64
+ { value: 'critical', label: 'Critical' },
65
+ ];
66
+
67
+ /**
68
+ * The `default` graph: every model field EXCEPT `data` and `provider`, plus
69
+ * the three `extra` properties. Datetimes are epoch SECONDS.
70
+ */
71
+ export interface GeoLocatedIPRow {
72
+ id: number;
73
+ created: number;
74
+ modified: number;
75
+ last_seen: number;
76
+
77
+ ip_address: string;
78
+ subnet: string | null;
79
+
80
+ country_code: string | null;
81
+ country_name: string | null;
82
+ region: string | null;
83
+ region_code: string | null;
84
+ city: string | null;
85
+ postal_code: string | null;
86
+ latitude: number | null;
87
+ longitude: number | null;
88
+ timezone: string | null;
89
+
90
+ is_tor: boolean;
91
+ is_vpn: boolean;
92
+ is_proxy: boolean;
93
+ is_cloud: boolean;
94
+ is_datacenter: boolean;
95
+ is_mobile: boolean;
96
+ is_known_attacker: boolean;
97
+ is_known_abuser: boolean;
98
+
99
+ threat_level: string | null;
100
+ asn: string | null;
101
+ asn_org: string | null;
102
+ isp: string | null;
103
+ mobile_carrier: string | null;
104
+ connection_type: string | null;
105
+
106
+ is_blocked: boolean;
107
+ blocked_at: number | null;
108
+ blocked_until: number | null;
109
+ blocked_reason: string | null;
110
+ block_count: number;
111
+
112
+ is_whitelisted: boolean;
113
+ whitelisted_reason: string | null;
114
+ whitelisted_until: number | null;
115
+
116
+ expires_at: number | null;
117
+
118
+ /** `extra` on every graph — computed Python properties. */
119
+ is_threat: boolean;
120
+ is_suspicious: boolean;
121
+ risk_score: number;
122
+ }
123
+
124
+ /**
125
+ * The `detailed` graph adds the two fields the default graph excludes. It is
126
+ * read ONLY through `useGeoIpRawRecord` — an explicit, manage-gated,
127
+ * `gcTime: 0` fetch — so the raw provider blob never lands in the persistent
128
+ * Query cache.
129
+ */
130
+ export interface GeoLocatedIPDetailedRow extends GeoLocatedIPRow {
131
+ provider: string | null;
132
+ data: Record<string, unknown> | null;
133
+ }
134
+
135
+ // ── Scrubbing ─────────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * `data` is the raw provider blob. It can carry firewall hints (the backend's
139
+ * own federation graph excludes it for exactly that reason) and arbitrary
140
+ * third-party text, so it is deleted before the row reaches any cache; the
141
+ * rest goes through the shared security scrubber.
142
+ */
143
+ export function sanitizeGeoIpRow<T>(row: T): T {
144
+ const safe = { ...(row as unknown as Record<string, unknown>) };
145
+ delete safe.data;
146
+ return sanitizeSecurityValue(safe) as unknown as T;
147
+ }
148
+
149
+ // ── Enforcement state (the model's Python properties, verbatim) ───────
150
+
151
+ const nowSec = (): number => Math.floor(Date.now() / 1000);
152
+
153
+ /**
154
+ * `GeoLocatedIP.whitelist_active`: whitelisted AND not expired. Every
155
+ * consumer of whitelist state must use this, never the raw `is_whitelisted`.
156
+ */
157
+ export function whitelistActive(row: Pick<GeoLocatedIPRow, 'is_whitelisted' | 'whitelisted_until'> | null | undefined): boolean {
158
+ if (!row?.is_whitelisted) return false;
159
+ if (row.whitelisted_until != null && nowSec() > row.whitelisted_until) return false;
160
+ return true;
161
+ }
162
+
163
+ /**
164
+ * `GeoLocatedIP.block_active`: blocked, not whitelisted, and not expired.
165
+ * web-mojo rendered raw `is_blocked`, so an expired block still read
166
+ * "Blocked" and a whitelisted-but-flagged IP read blocked when it was not.
167
+ */
168
+ export function blockActive(row: Pick<GeoLocatedIPRow, 'is_blocked' | 'blocked_until' | 'is_whitelisted' | 'whitelisted_until'> | null | undefined): boolean {
169
+ if (!row?.is_blocked) return false;
170
+ if (whitelistActive(row)) return false;
171
+ if (row.blocked_until != null && nowSec() > row.blocked_until) return false;
172
+ return true;
173
+ }
174
+
175
+ /** True when `is_blocked` is set but the block has lapsed — a real UI state. */
176
+ export function blockExpired(row: Pick<GeoLocatedIPRow, 'is_blocked' | 'blocked_until'> | null | undefined): boolean {
177
+ return Boolean(row?.is_blocked && row.blocked_until != null && nowSec() > row.blocked_until);
178
+ }
179
+
180
+ /** True when `is_whitelisted` is set but the whitelist has lapsed. */
181
+ export function whitelistExpired(row: Pick<GeoLocatedIPRow, 'is_whitelisted' | 'whitelisted_until'> | null | undefined): boolean {
182
+ return Boolean(row?.is_whitelisted && row.whitelisted_until != null && nowSec() > row.whitelisted_until);
183
+ }
184
+
185
+ export type GeoTone = 'success' | 'warning' | 'danger' | 'muted' | 'info' | 'primary';
186
+
187
+ /** THREAT_TONE from GeoIPView.js:46-52, mapped onto portal-mojo's Tone. */
188
+ export function threatTone(level: string | null | undefined): GeoTone {
189
+ switch (String(level ?? '').toLowerCase()) {
190
+ case 'none':
191
+ case 'low':
192
+ return 'success';
193
+ case 'medium':
194
+ return 'warning';
195
+ case 'high':
196
+ case 'critical':
197
+ return 'danger';
198
+ default:
199
+ return 'muted';
200
+ }
201
+ }
202
+
203
+ /** `risk_score` rides the default graph; this is the null-safe read. */
204
+ export function riskScoreOf(row: Pick<GeoLocatedIPRow, 'risk_score'> | null | undefined): number | null {
205
+ return typeof row?.risk_score === 'number' ? row.risk_score : null;
206
+ }
207
+
208
+ /**
209
+ * ISO-3166 alpha-2 → the regional-indicator pair (GeoIPView.js COUNTRY_FLAG).
210
+ * Anything that is not exactly two ASCII letters returns '' — the backend's
211
+ * `country_code` is `max_length=3` and can legitimately hold a 3-letter code.
212
+ */
213
+ export function countryFlag(code: string | null | undefined): string {
214
+ if (!code || typeof code !== 'string' || code.length !== 2) return '';
215
+ const upper = code.toUpperCase();
216
+ if (!/^[A-Z]{2}$/.test(upper)) return '';
217
+ const BASE = 0x1F1E6;
218
+ const A = 0x41;
219
+ return String.fromCodePoint(BASE + (upper.charCodeAt(0) - A))
220
+ + String.fromCodePoint(BASE + (upper.charCodeAt(1) - A));
221
+ }
222
+
223
+ // ── Model ─────────────────────────────────────────────────────────────
224
+
225
+ const COMMON_PARAMS = new Set(['start', 'size', 'sort', 'search', 'dr_field', 'dr_start', 'dr_end']);
226
+
227
+ /** Filterable fields. An unlisted key never reaches the wire — and neither
228
+ * can a crafted `graph`, which is pinned to `default`. */
229
+ const GEOIP_FILTERS = new Set([
230
+ 'id', 'id__in', 'ip_address', 'ip_address__icontains', 'subnet',
231
+ 'country_code', 'country_code__in', 'region', 'region__icontains', 'region_code',
232
+ 'city', 'city__icontains', 'postal_code',
233
+ 'threat_level', 'threat_level__in', 'asn', 'asn_org__icontains',
234
+ 'isp', 'isp__icontains', 'connection_type', 'mobile_carrier',
235
+ 'is_tor', 'is_vpn', 'is_proxy', 'is_cloud', 'is_datacenter', 'is_mobile',
236
+ 'is_known_attacker', 'is_known_abuser', 'is_threat',
237
+ 'is_blocked', 'is_whitelisted', 'block_count__gte',
238
+ 'last_seen__gte', 'last_seen__lte', 'created__gte', 'created__lte',
239
+ // #1287 (Blocked IPs) additions. `threat_level` is nullable and
240
+ // `blocked_until` null means PERMANENT, so both need an isnull lookup to
241
+ // be filterable at all — web-mojo offered a literal `none` threat value,
242
+ // which matched no row on any deployment.
243
+ 'threat_level__isnull', 'blocked_until__isnull',
244
+ ]);
245
+
246
+ export function normalizeGeoIpListParams(params: Params): Params {
247
+ const out: Params = { graph: 'default' };
248
+ for (const [key, value] of Object.entries(params)) {
249
+ if ((COMMON_PARAMS.has(key) || GEOIP_FILTERS.has(key)) && value != null && value !== '') out[key] = value;
250
+ }
251
+ out.sort = String(out.sort ?? '-last_seen');
252
+ if (out.dr_start != null || out.dr_end != null) out.dr_field = String(out.dr_field ?? 'last_seen');
253
+ return out;
254
+ }
255
+
256
+ /**
257
+ * Every action is a declared `POST_SAVE_ACTIONS` key on the backend RestMeta.
258
+ * All six handlers return nothing, so `on_rest_save` answers with the
259
+ * refreshed record — `response: 'row'` (the default) is correct for all of
260
+ * them and the UI re-reads state rather than optimistically toggling.
261
+ */
262
+ export const GeoLocatedIPModel = defineModel<GeoLocatedIPRow>({
263
+ name: 'geoip',
264
+ endpoint: '/api/system/geoip',
265
+ permissions: { view: GEOIP_VIEW_PERMS, manage: GEOIP_MANAGE_PERMS },
266
+ actions: {
267
+ refresh: { permissions: GEOIP_MANAGE_PERMS },
268
+ threat_analysis: { permissions: GEOIP_MANAGE_PERMS },
269
+ block: { permissions: GEOIP_MANAGE_PERMS },
270
+ unblock: { permissions: GEOIP_MANAGE_PERMS },
271
+ whitelist: { permissions: GEOIP_MANAGE_PERMS },
272
+ unwhitelist: { permissions: GEOIP_MANAGE_PERMS },
273
+ },
274
+ normalizeListParams: normalizeGeoIpListParams,
275
+ sanitizeRow: sanitizeGeoIpRow,
276
+ });
277
+
278
+ // ── Reading a dossier from an IP ──────────────────────────────────────
279
+
280
+ /**
281
+ * The DEFAULT path for "show me what we know about this IP": a filtered read
282
+ * of the cache table. Read-only, no provider cost, no rate limit.
283
+ *
284
+ * `/system/geoip/lookup` is NOT this — it CREATES and REFRESHES records and
285
+ * is rate-limited to 30/ip server-side, so it runs only on the explicit
286
+ * operator action below and never as a background query.
287
+ */
288
+ export function useGeoIpByAddress(ip: string | null | undefined, enabled = true) {
289
+ const address = ip ?? '';
290
+ return useQuery({
291
+ queryKey: [GeoLocatedIPModel.endpoint, { ip_address: address, size: 1, graph: 'default' }] as const,
292
+ queryFn: async () => {
293
+ const page = await mojoList<GeoLocatedIPRow>(GeoLocatedIPModel.endpoint, {
294
+ ip_address: address, size: 1, graph: 'default',
295
+ });
296
+ const row = page.rows[0];
297
+ return row ? sanitizeGeoIpRow(row) : null;
298
+ },
299
+ enabled: enabled && address.length > 0,
300
+ });
301
+ }
302
+
303
+ export interface GeoIpLookupOptions {
304
+ /** Server default is true; `false` skips the provider refresh entirely. */
305
+ autoRefresh?: boolean;
306
+ }
307
+
308
+ /**
309
+ * Explicit operator lookup. `GET /api/system/geoip/lookup?ip=` creates the
310
+ * record when it is absent and refreshes it when it is expired, behind a
311
+ * 30-per-IP rate limit — a 429 must surface, never be swallowed.
312
+ *
313
+ * `auto_refresh` is sent as `1`/`0`: the backend's `_param_is_true` coerces
314
+ * the STRING "false" correctly, but "0"/"1" is unambiguous either way.
315
+ */
316
+ export async function lookupGeoIp(
317
+ qc: QueryClient,
318
+ ip: string,
319
+ opts: GeoIpLookupOptions = {},
320
+ ): Promise<GeoLocatedIPRow> {
321
+ const body = await mojoCall(`${GeoLocatedIPModel.endpoint}/lookup`, {
322
+ params: { ip, auto_refresh: opts.autoRefresh === false ? '0' : '1' },
323
+ });
324
+ const row = sanitizeGeoIpRow(body.data as GeoLocatedIPRow);
325
+ await GeoLocatedIPModel.invalidate(qc);
326
+ return row;
327
+ }
328
+
329
+ /** Graph is part of the key so a sparse default row cannot satisfy detail. */
330
+ export function geoIpRawKey(id: number) {
331
+ return [GeoLocatedIPModel.endpoint, 'one', id, { graph: 'detailed' }] as const;
332
+ }
333
+
334
+ export interface GeoIpRawRecord {
335
+ provider: string | null;
336
+ /** Already sanitized AND length-bounded — safe to render in a <pre>. */
337
+ text: string;
338
+ }
339
+
340
+ /**
341
+ * The ONLY reader of `graph=detailed`, and therefore the only path that ever
342
+ * sees `data`/`provider`. Deliberately:
343
+ * · `enabled` is folded with the caller's GEOIP_MANAGE_PERMS check;
344
+ * · `gcTime: 0` + `staleTime: 0` so the blob never persists in the cache;
345
+ * · the payload is scrubbed and bounded before it is returned.
346
+ */
347
+ export function useGeoIpRawRecord(id: number | null, enabled: boolean) {
348
+ return useQuery({
349
+ queryKey: id == null
350
+ ? [GeoLocatedIPModel.endpoint, 'one', null, { graph: 'detailed' }] as const
351
+ : geoIpRawKey(id),
352
+ queryFn: async (): Promise<GeoIpRawRecord> => {
353
+ const body = await mojoCall(`${GeoLocatedIPModel.endpoint}/${id!}`, { params: { graph: 'detailed' } });
354
+ const raw = sanitizeSecurityValue(body.data) as GeoLocatedIPDetailedRow;
355
+ return {
356
+ provider: raw?.provider ?? null,
357
+ text: boundedSecurityText(raw?.data ?? {}),
358
+ };
359
+ },
360
+ enabled: enabled && id != null,
361
+ gcTime: 0,
362
+ staleTime: 0,
363
+ });
364
+ }
365
+
366
+ // ── Export ────────────────────────────────────────────────────────────
367
+
368
+ /**
369
+ * Declared projection only. The server-side `download_format` path would
370
+ * serialize whatever graph the params asked for; this cannot — `data` and
371
+ * `provider` are not in the field list, and every row is sanitized before it
372
+ * is projected.
373
+ */
374
+ export const geoIpSafeExporter = createSafeExporter<GeoLocatedIPRow>({
375
+ endpoint: GeoLocatedIPModel.endpoint,
376
+ filename: 'geoip-cache',
377
+ sanitizeRow: sanitizeGeoIpRow,
378
+ fields: [
379
+ { key: 'id' }, { key: 'ip_address' }, { key: 'subnet' },
380
+ { key: 'country_code' }, { key: 'country_name' }, { key: 'region' },
381
+ { key: 'region_code' }, { key: 'city' }, { key: 'postal_code' },
382
+ { key: 'latitude' }, { key: 'longitude' }, { key: 'timezone' },
383
+ { key: 'asn' }, { key: 'asn_org' }, { key: 'isp' },
384
+ { key: 'connection_type' }, { key: 'mobile_carrier' },
385
+ { key: 'threat_level' }, { key: 'risk_score' },
386
+ { key: 'is_threat' }, { key: 'is_suspicious' },
387
+ { key: 'is_tor' }, { key: 'is_vpn' }, { key: 'is_proxy' },
388
+ { key: 'is_cloud' }, { key: 'is_datacenter' }, { key: 'is_mobile' },
389
+ { key: 'is_known_attacker' }, { key: 'is_known_abuser' },
390
+ { key: 'is_blocked' }, { key: 'blocked_at' }, { key: 'blocked_until' },
391
+ { key: 'blocked_reason' }, { key: 'block_count' },
392
+ { key: 'is_whitelisted' }, { key: 'whitelisted_reason' }, { key: 'whitelisted_until' },
393
+ { key: 'block_active', label: 'block_active', value: (row) => blockActive(row) },
394
+ { key: 'whitelist_active', label: 'whitelist_active', value: (row) => whitelistActive(row) },
395
+ { key: 'last_seen' }, { key: 'created' }, { key: 'modified' }, { key: 'expires_at' },
396
+ ],
397
+ });
@@ -0,0 +1,31 @@
1
+ import type { AdminSection } from '../index';
2
+ import { SECURITY_VIEW_PERMS } from '../security-permissions';
3
+ import { TicketsPage } from './tickets';
4
+ import { INCIDENTS_ADMIN_ROUTES } from '../incidents';
5
+ import { RULES_ADMIN_ROUTES } from '../rules';
6
+
7
+ export * from './models';
8
+ export * from './tickets';
9
+ // Two sibling sections under the same Security navigation group (#1291):
10
+ // Devices & Logins at security/devices, IP Intelligence at security/geoip.
11
+ export * from './devices';
12
+ export * from './geoip';
13
+
14
+ export const SECURITY_OPERATIONS_ADMIN_SECTION: AdminSection = {
15
+ id: 'security-operations',
16
+ basePath: 'security',
17
+ title: 'Security Operations',
18
+ icon: 'bi-shield-check',
19
+ navigationGroup: 'security',
20
+ permissions: SECURITY_VIEW_PERMS,
21
+ routes: [
22
+ {
23
+ path: 'tickets',
24
+ label: 'Tickets',
25
+ component: TicketsPage,
26
+ permissions: SECURITY_VIEW_PERMS,
27
+ },
28
+ ...INCIDENTS_ADMIN_ROUTES,
29
+ ...RULES_ADMIN_ROUTES,
30
+ ],
31
+ };