portal-mojo 0.2.4 → 0.2.6
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/docs/README.md +2 -0
- package/docs/admin-fleet-configuration.md +50 -0
- package/docs/admin-identity-directories-sources.json +130 -0
- package/docs/admin-identity-directories.md +60 -0
- package/docs/admin-identity-users.md +6 -1
- package/docs/admin-members.md +16 -0
- package/docs/admin-network-security.md +1 -1
- package/docs/admin-push.md +85 -6
- package/docs/admin-settings.md +10 -0
- package/docs/admin-signin.md +73 -0
- package/docs/menus-and-access.md +6 -2
- package/docs/realtime.md +5 -0
- package/package.json +1 -1
- package/src/admin/core/index.ts +4 -4
- package/src/admin/domains/identity.ts +25 -1
- package/src/admin/domains/operations.ts +8 -1
- package/src/admin/fleet-configuration/FleetConfigurationPage.tsx +171 -0
- package/src/admin/fleet-configuration/data.ts +89 -0
- package/src/admin/identity/groups/GroupDetail.tsx +287 -0
- package/src/admin/identity/groups/GroupsPage.tsx +125 -0
- package/src/admin/identity/groups/group-permissions.ts +5 -0
- package/src/admin/identity/groups/group-sections/AuditSection.tsx +57 -0
- package/src/admin/identity/groups/group-sections/AuthConfigDialog.tsx +677 -0
- package/src/admin/identity/groups/group-sections/EventsSection.tsx +53 -0
- package/src/admin/identity/groups/group-sections/GeofenceSection.tsx +198 -0
- package/src/admin/identity/groups/group-sections/IdentitySection.tsx +211 -0
- package/src/admin/identity/groups/group-sections/MembersSection.tsx +25 -0
- package/src/admin/identity/groups/group-sections/OverviewSection.tsx +112 -0
- package/src/admin/identity/groups/group-sections/SubGroupsSection.tsx +102 -0
- package/src/admin/identity/groups/group-sections/auth-config.ts +285 -0
- package/src/admin/identity/groups/group-sections/geofence-data.ts +40 -0
- package/src/admin/identity/groups/group-sections/models.ts +110 -0
- package/src/admin/identity/groups/group-sections/shared.ts +21 -0
- package/src/admin/identity/groups/index.ts +6 -0
- package/src/admin/identity/groups/models.ts +70 -0
- package/src/admin/identity/index.ts +1 -0
- package/src/admin/identity/members/MemberDetail.tsx +32 -3
- package/src/admin/identity/members/models.ts +3 -0
- package/src/admin/identity/users/PersonalApiKeysPage.tsx +220 -0
- package/src/admin/identity/users/UserDetail.tsx +2 -2
- package/src/admin/identity/users/index.ts +2 -0
- package/src/admin/identity/users/sections/PermissionsSection.tsx +2 -2
- package/src/admin/index.ts +3 -0
- package/src/admin/messaging/push/PushPage.tsx +61 -13
- package/src/admin/messaging/push/PushTestDialog.tsx +59 -0
- package/src/admin/messaging/push/api.ts +25 -1
- package/src/admin/messaging/push/models.ts +7 -6
- package/src/admin/public/identity.ts +1 -0
- package/src/admin/public/operations.ts +1 -0
- package/src/admin/registry.ts +6 -2
- package/src/admin/settings/SettingDetail.tsx +2 -1
- package/src/admin/settings/SettingValue.tsx +37 -0
- package/src/admin/settings/SettingsPage.tsx +2 -1
- package/src/admin/signin/SigninPage.tsx +397 -0
- package/src/admin/signin/api.ts +83 -0
- package/src/admin/signin/index.ts +4 -0
- package/src/client/fleet-mock.ts +61 -0
- package/src/client/mock.ts +239 -10
- package/src/client/realtime.ts +12 -0
- package/src/ui/SidebarNav.tsx +2 -2
package/docs/README.md
CHANGED
|
@@ -19,12 +19,14 @@ everything: run the showcase (`npm run dev:showcase`) → **Develop → Componen
|
|
|
19
19
|
| [admin-monitoring.md](admin-monitoring.md) | Monitoring Admin registration, logs, and exact Redis-backed metrics-permission mutations |
|
|
20
20
|
| [admin-metrics-explorer.md](admin-metrics-explorer.md) | Read-only recorded-metrics discovery, full-slug adapters, KPI/fan-out/scalar contracts, and cache identity |
|
|
21
21
|
| [admin-settings.md](admin-settings.md) | Runtime settings admin — direction-aware atomic writes, write-only secrets, and global/group scope |
|
|
22
|
+
| [admin-fleet-configuration.md](admin-fleet-configuration.md) | Superuser structured configuration publishing, version restore and per-node application evidence |
|
|
22
23
|
| [admin-bouncer.md](admin-bouncer.md) | Bouncer decisions, device investigations, and bot-signature administration with token-safe caching |
|
|
23
24
|
| [admin-security-tickets.md](admin-security-tickets.md) | Ticket queue, KISS detail modal, notes, approvals, and Maestro sync contract |
|
|
24
25
|
| [admin-incidents.md](admin-incidents.md) | Incident/event triage, forensic detail, merge semantics, and sanitized export boundary |
|
|
25
26
|
| [admin-rules.md](admin-rules.md) | Always-inactive RuleSet authoring and the lossless, runtime-effective handler-chain builder |
|
|
26
27
|
| [admin-members.md](admin-members.md) | Global membership table/detail, no-oracle admission, extensible grants, Group-detail composition, and unsupported-operation boundaries |
|
|
27
28
|
| [admin-identity-users.md](admin-identity-users.md) | Global Users table and 14-section detail, system gates, action contracts, and one-time credential invariants |
|
|
29
|
+
| [admin-signin.md](admin-signin.md) | System sign-in page look & feel plus Google/Apple/GitHub provider credentials — one endpoint, changed-paths saves, write-only secrets |
|
|
28
30
|
| [admin-devices-geoip.md](admin-devices-geoip.md) | Fleet device triage, the login-location world map with country drill-down, and the GeoIP dossier — expiry-aware enforcement, the never-cached raw provider record, and the backend traps the port encodes |
|
|
29
31
|
| [admin-jobs.md](admin-jobs.md) | Jobs engine — runner fleet, per-channel queue depth, the segmented job table with armed cancel and republishing retry, the dry-run-first control plane, scheduled tasks with a global-only gate, and the thirteen backend corrections the port encodes |
|
|
30
32
|
| [admin-network-security.md](admin-network-security.md) | Perimeter control — blocked IPs, the firewall log, kernel IP sets and the geofencing page (posture, rules, simulator, evidence, exemptions), with the DSL projection and every backend correction the port encodes |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Fleet Configuration
|
|
2
|
+
|
|
3
|
+
The Operations → Fleet Configuration page uses `/api/account/admin/fleet`.
|
|
4
|
+
Only literal superusers see the section. An empty permission any-of array uses
|
|
5
|
+
the existing superuser bypass and refuses even the global `admin` grant; the
|
|
6
|
+
page checks `is_superuser === true` before fetching. Backend authority remains
|
|
7
|
+
mandatory, including rejection of diagnostic/key-backed sessions and fresh auth.
|
|
8
|
+
|
|
9
|
+
Applications register their schema in django-mojo. The page renders the returned
|
|
10
|
+
section, label, description, default, type and restart requirement; no application
|
|
11
|
+
setting names are hardcoded. Each field defaults to Keep current value. Set value
|
|
12
|
+
and Remove published override are explicit actions. Clearing falls back to base
|
|
13
|
+
configuration/default; it does not promise erasure of a bootstrap secret.
|
|
14
|
+
|
|
15
|
+
Secret inputs start blank, never hold masked placeholders, and blank replacement
|
|
16
|
+
preserves the existing secret. Values stay in the form until an imperative POST;
|
|
17
|
+
secret-bearing writes bypass TanStack MutationCache. Secret current/default fields
|
|
18
|
+
are removed before Query cache storage. The revision-bound publish/restore/apply
|
|
19
|
+
POSTs use `withFreshAuth` and show failures without claiming success. Schema/type
|
|
20
|
+
validation is repeated by the server; browser validation is only feedback.
|
|
21
|
+
|
|
22
|
+
Publication means published to S3, never applied. Apply now starts a fixed
|
|
23
|
+
asynchronous sync operation and polls its ID until terminal operation status. The
|
|
24
|
+
short job completes after dispatching sync; completed job status does not stop
|
|
25
|
+
polling. Each operation read freshly observes activation until healthy or timed
|
|
26
|
+
out; failed, expired or canceled jobs also stop polling. The expected
|
|
27
|
+
node roster includes missing nodes. Installed, restart requested, restarted and
|
|
28
|
+
healthy remain separate evidence. Only positive installed + restarted + health
|
|
29
|
+
results from every expected node with the explicit
|
|
30
|
+
`request_service_jobs_and_dependencies` health scope justify fleet completion.
|
|
31
|
+
Missing or older health scopes remain unconfirmed, including per-node health and
|
|
32
|
+
restart confirmation, and prompt a backend upgrade. Health covers the
|
|
33
|
+
request service and dependencies where enabled, plus the job engine and scheduler
|
|
34
|
+
with their startup-loaded revision. Worker-only nodes need no request service.
|
|
35
|
+
Draining an old engine is not proof that its replacement has loaded the revision.
|
|
36
|
+
Timed-out, failed, superseded and unknown results stay visible. Refresh status observes the
|
|
37
|
+
normal timer workflow even when Apply now was not used.
|
|
38
|
+
|
|
39
|
+
History contains only version IDs and timestamps. Restore publishes the selected
|
|
40
|
+
version as a new revision using the current expected revision. Secrets are neither
|
|
41
|
+
shown nor compared. Concurrent publication rejects the stale revision; refresh
|
|
42
|
+
before retrying. Settings edits are tied to their loaded publication revision.
|
|
43
|
+
|
|
44
|
+
Verify with `npm run verify:admin-fleet`, `npm run typecheck`, `npm run build`, and
|
|
45
|
+
browser checks of both themes. The Settings showcase includes a partial-convergence
|
|
46
|
+
example. Mock Portal uses representative registered settings and an offline node.
|
|
47
|
+
|
|
48
|
+
Django receives this page only after a canonical Portal artifact is built and
|
|
49
|
+
vendored; follow `docs/admin-artifact.md` and the Django vendor tool. Ordinary
|
|
50
|
+
Vite build output is not the canonical artifact.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"package": "portal-mojo",
|
|
3
|
+
"package_version": "0.2.5",
|
|
4
|
+
"source_repository": "https://github.com/NativeMojo/portal-mojo",
|
|
5
|
+
"source_tag": "v0.2.5",
|
|
6
|
+
"source_commit": "93c7fdfeb3096d33010e72bd4483cf9ca6e3d6f5",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"license_file": "packages/portal-mojo/LICENSE",
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"source_path": "apps/portal/src/group-permissions.ts",
|
|
12
|
+
"source_sha256": "6237ad45328ecb9637a1a07d8954dd27cd019e22fa32942f5e7d05a76037fb5c",
|
|
13
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-permissions.ts",
|
|
14
|
+
"package_sha256": "b909f5a5cf029b8b910a70e6817fea8abd9ded6797af14a16170b2695c0c1699",
|
|
15
|
+
"adaptation": "Moves system-pinned group permission clauses into the canonical package identity surface."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"source_path": "apps/portal/src/models.ts",
|
|
19
|
+
"source_sha256": "0b98a906a774e572f9109703fe22444c3a2e190ca5e02c3a93dfbd02cc3fcb84",
|
|
20
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/models.ts",
|
|
21
|
+
"package_sha256": "6ac12a64d00e409874bc1b13d7941cf54c38a715af5ebcfddd2b2e9257356759",
|
|
22
|
+
"adaptation": "Extracts the Group model/type and group kind options; shared User, Member and Log models remain owned by their package domains."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"source_path": "apps/portal/src/pages/ApiKeysPage.tsx",
|
|
26
|
+
"source_sha256": "3d338cf77c3110b72a9361565c1cced10a7fccc52c486e3dafda119d66c676b2",
|
|
27
|
+
"package_path": "packages/portal-mojo/src/admin/identity/users/PersonalApiKeysPage.tsx",
|
|
28
|
+
"package_sha256": "f377d8a97b5c0ccbce745c333fdd7e4a5fd5f5a89b868b058677b31e3efe64ff",
|
|
29
|
+
"adaptation": "Expanded row facts and actions are presented in a native DetailView modal that observes the current API key record."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"source_path": "apps/portal/src/pages/GroupDetail.tsx",
|
|
33
|
+
"source_sha256": "01a8a6032a59a337636ec28e5e2aef90717e5bc00656807135e02820a92a394b",
|
|
34
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/GroupDetail.tsx",
|
|
35
|
+
"package_sha256": "e26943218e3e8f5ac79ddc7f1b0bd40736bcab0bd6e9c694e01830086349a4e2",
|
|
36
|
+
"adaptation": "Raw metadata is visible in the explicit Metadata section without a collapsible control."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"source_path": "apps/portal/src/pages/GroupsPage.tsx",
|
|
40
|
+
"source_sha256": "82e4aa5e7ebf67832bbd3bcf7e15dcfdeea64383039cc426591fa15262e4d4c8",
|
|
41
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/GroupsPage.tsx",
|
|
42
|
+
"package_sha256": "e16f3d0630fdb1937cf667cd2abe8cd1aabc9bb2b731113112930e66bde2f1df",
|
|
43
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"source_path": "apps/portal/src/pages/group-sections/AuditSection.tsx",
|
|
47
|
+
"source_sha256": "49848f57e5050c0008aa83f7092af9abb8f55d262791a2591e525eaa6d741df4",
|
|
48
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/AuditSection.tsx",
|
|
49
|
+
"package_sha256": "07a40636549a0a1d8636a115ee330313b84c0c82500890c6464301fd39de1e06",
|
|
50
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"source_path": "apps/portal/src/pages/group-sections/AuthConfigDialog.tsx",
|
|
54
|
+
"source_sha256": "f42e18a802f206a2b3f30248c30ad12887598b170deb73b532232a6bf89699dd",
|
|
55
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/AuthConfigDialog.tsx",
|
|
56
|
+
"package_sha256": "e2280185bde7eab3ca7108f7d6da9e0e66c4f4d8323ec97f3fd7d49376932b65",
|
|
57
|
+
"adaptation": "Raw auth overrides remain visible in the explicit configuration dialog without collapsible content."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"source_path": "apps/portal/src/pages/group-sections/EventsSection.tsx",
|
|
61
|
+
"source_sha256": "b8cb12ae99d7c8d5931a090b7be8ea4e0373ff4657e786094e9ac862167d43ec",
|
|
62
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/EventsSection.tsx",
|
|
63
|
+
"package_sha256": "f0004605fbf7ceb2704adb42c0a3dd14dc5a468c445f0f94f2548fbc5f1bfeb4",
|
|
64
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"source_path": "apps/portal/src/pages/group-sections/GeofenceSection.tsx",
|
|
68
|
+
"source_sha256": "d0ce22f5ef922371d7a78c353e1a29e5d9ca4932e45814499e9c3cdeda7dc137",
|
|
69
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/GeofenceSection.tsx",
|
|
70
|
+
"package_sha256": "227850a8ef844867ea55d599997144b462ee058967224c361dd821713d1b677a",
|
|
71
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"source_path": "apps/portal/src/pages/group-sections/IdentitySection.tsx",
|
|
75
|
+
"source_sha256": "cda776a26f1e21b73820ed88ab7dcbafdaf2b146c4c3ee4bf6b24dcbaeecc9f4",
|
|
76
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/IdentitySection.tsx",
|
|
77
|
+
"package_sha256": "888ff5f7a70c80d8a868f13675b39af9a2a7076d6ea5fbc323493c0265ce9880",
|
|
78
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"source_path": "apps/portal/src/pages/group-sections/MembersSection.tsx",
|
|
82
|
+
"source_sha256": "dc2e99924b8ba856ce2ed48dffae082e98cac9ef78df3ddb1f05fe8a71f9725d",
|
|
83
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/MembersSection.tsx",
|
|
84
|
+
"package_sha256": "abccb58bcdb62d0d1de2611cad080ca7237e0dfa781b10c5fc1d7e251398f628",
|
|
85
|
+
"adaptation": "Passes the group navigation callback into UserDetail to keep cross-record navigation available."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"source_path": "apps/portal/src/pages/group-sections/OverviewSection.tsx",
|
|
89
|
+
"source_sha256": "fb395569018b89e6efd45cea241ba665b73efd15549802a6f0d98d90116ce894",
|
|
90
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/OverviewSection.tsx",
|
|
91
|
+
"package_sha256": "08b460272af71ba6209fe388b71f37f79d94a9fecc21c0c2ee3abaf11f01372f",
|
|
92
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"source_path": "apps/portal/src/pages/group-sections/SubGroupsSection.tsx",
|
|
96
|
+
"source_sha256": "81074a2d74d80fda93e40379b87620807c31db7979736f90559e359bdafc0672",
|
|
97
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/SubGroupsSection.tsx",
|
|
98
|
+
"package_sha256": "61e412e709afb4384fdcbaa1d8fbb6df6ba414634ecc6aaa8201c66cf722af93",
|
|
99
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"source_path": "apps/portal/src/pages/group-sections/auth-config.ts",
|
|
103
|
+
"source_sha256": "37d75dff84bca2577bcd1804c77aa0ef1519e4d913dcfdf7914266045fdbabe6",
|
|
104
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/auth-config.ts",
|
|
105
|
+
"package_sha256": "37d75dff84bca2577bcd1804c77aa0ef1519e4d913dcfdf7914266045fdbabe6",
|
|
106
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"source_path": "apps/portal/src/pages/group-sections/geofence-data.ts",
|
|
110
|
+
"source_sha256": "169d5feb72fbe16a0463d591328ce49963e95f1bc0f5f7e5fa9ed271a5ace8e6",
|
|
111
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/geofence-data.ts",
|
|
112
|
+
"package_sha256": "169d5feb72fbe16a0463d591328ce49963e95f1bc0f5f7e5fa9ed271a5ace8e6",
|
|
113
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"source_path": "apps/portal/src/pages/group-sections/models.ts",
|
|
117
|
+
"source_sha256": "ec6a2fd700a0a91e8f1d9e49c364ffe5466db340b435004971e719da08b49371",
|
|
118
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/models.ts",
|
|
119
|
+
"package_sha256": "ec6a2fd700a0a91e8f1d9e49c364ffe5466db340b435004971e719da08b49371",
|
|
120
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"source_path": "apps/portal/src/pages/group-sections/shared.ts",
|
|
124
|
+
"source_sha256": "68aa819b14edcbbd8ae399d5c5a74360cc50ffd281b6d0d3be1c4f8ebc4e4f6c",
|
|
125
|
+
"package_path": "packages/portal-mojo/src/admin/identity/groups/group-sections/shared.ts",
|
|
126
|
+
"package_sha256": "68aa819b14edcbbd8ae399d5c5a74360cc50ffd281b6d0d3be1c4f8ebc4e4f6c",
|
|
127
|
+
"adaptation": "Moved into the package identity domain and updated package-relative imports."
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Admin Groups and Personal API Keys
|
|
2
|
+
|
|
3
|
+
The global Admin registry contributes Groups and Personal API Keys to the
|
|
4
|
+
Identity & Access navigation group. `adminSectionRoutes()` and
|
|
5
|
+
`adminSectionsMenu()` therefore mount the same pages in the standalone
|
|
6
|
+
reference portal and in an embedded System Admin boundary:
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import {
|
|
10
|
+
GROUPS_ADMIN_SECTION,
|
|
11
|
+
GROUP_VIEW_PERMS,
|
|
12
|
+
GroupDetail,
|
|
13
|
+
GroupModel,
|
|
14
|
+
GroupsPage,
|
|
15
|
+
PERSONAL_API_KEYS_ADMIN_SECTION,
|
|
16
|
+
PersonalApiKeysPage,
|
|
17
|
+
} from 'portal-mojo/admin/identity';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The registered routes are `/groups` and `/apikeys`; an embedded mount prefixes
|
|
21
|
+
both routes. Group directory and detail access use the system-pinned
|
|
22
|
+
`sys.groups | sys.view_groups` clause. Group mutations use
|
|
23
|
+
`sys.groups | sys.manage_groups`. Active-group membership cannot satisfy
|
|
24
|
+
these gates. The personal key section uses the system identity-view clause,
|
|
25
|
+
while its API requests remain scoped to the signed-in user's own keys.
|
|
26
|
+
|
|
27
|
+
## Groups
|
|
28
|
+
|
|
29
|
+
`GroupsPage` retains the native server-owned `ModelTable` search, filters,
|
|
30
|
+
presets, paging, sorting, column chooser, persistence and exports against
|
|
31
|
+
`GET /api/group`. Opening a row presents `GroupDetail` in `modal.detail`.
|
|
32
|
+
`GroupDetail` preserves the Overview, Identity, Members, Sub-Groups, API Keys,
|
|
33
|
+
Webhooks, Geofencing, Events, Audit and Metadata surfaces, with each section's
|
|
34
|
+
existing system permission gate and conditional queries. Parent, child, member
|
|
35
|
+
and user navigation opens native detail modals while keeping the directory
|
|
36
|
+
underneath.
|
|
37
|
+
|
|
38
|
+
The group model and section code are adapted from the reference portal at
|
|
39
|
+
`v0.2.5` commit `93c7fdfeb3096d33010e72bd4483cf9ca6e3d6f5`. The source hashes
|
|
40
|
+
and corresponding package hashes are recorded in
|
|
41
|
+
[`admin-identity-directories-sources.json`](admin-identity-directories-sources.json).
|
|
42
|
+
The moved implementation retains the package Apache-2.0 license. Two raw JSON
|
|
43
|
+
surfaces render their values directly in their explicit Metadata/Auth details;
|
|
44
|
+
they do not use collapsible content.
|
|
45
|
+
|
|
46
|
+
## Personal API Keys
|
|
47
|
+
|
|
48
|
+
`PersonalApiKeysPage` uses the existing `ApiKeyModel` at
|
|
49
|
+
`/api/account/api_keys` and the caller-only generation mutation at
|
|
50
|
+
`/api/auth/generate_api_key`. The generated token is shown once and scrubbed
|
|
51
|
+
before it can reach the query or mutation cache. Key facts and actions open in
|
|
52
|
+
a native detail modal. Disable/enable remains reversible with an undo toast;
|
|
53
|
+
revoke remains armed because it rotates the secret and cannot be undone. There
|
|
54
|
+
is no delete action. This page does not use the separate group credential
|
|
55
|
+
endpoint.
|
|
56
|
+
|
|
57
|
+
The Personal API Keys route was added to the package identity section registry
|
|
58
|
+
alongside Users, Members, Groups, Credentials and Sign-in. Compatibility page
|
|
59
|
+
paths in the reference app re-export the package-owned pages, and its menu and
|
|
60
|
+
route composition do not inject duplicate directory entries.
|
|
@@ -25,7 +25,12 @@ reason per batch; rows already in the target state are ineligible.
|
|
|
25
25
|
|
|
26
26
|
The detail preserves all 14 sections: Overview, Profile, Personal, Security,
|
|
27
27
|
OAuth, Groups, Sys Perms, conditional App Perms, API Keys, Devices, Logins,
|
|
28
|
-
Audit, Notifications, and Metadata.
|
|
28
|
+
Audit, Notifications, and Metadata. Permission sections are labeled **System
|
|
29
|
+
permissions** and **Product permissions** in the UI. Consuming apps install
|
|
30
|
+
their product tabs with `registerFormTabs(USER_APP_PERMS_TABSET, tabs)`; the
|
|
31
|
+
section stays absent when none are registered. User grants can cover multiple
|
|
32
|
+
groups; use a Member product permission for a single group.
|
|
33
|
+
Subsidiary count/list queries are enabled
|
|
29
34
|
only when their exact system permission is present. Logs use
|
|
30
35
|
`sys.view_logs | sys.manage_logs | sys.security`; incident events use
|
|
31
36
|
`sys.view_security | sys.security`. Permission revocation therefore removes
|
package/docs/admin-members.md
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
MEMBERS_ADMIN_SECTION,
|
|
13
13
|
openMemberAdmissionDialog,
|
|
14
14
|
registerMemberPermissions,
|
|
15
|
+
MEMBER_APP_PERMS_TABSET,
|
|
15
16
|
} from 'portal-mojo/admin';
|
|
16
17
|
```
|
|
17
18
|
|
|
@@ -103,6 +104,16 @@ and read-only rather than disappearing. Permission switches autosave through
|
|
|
103
104
|
`FormView`; a deployment's `MEMBER_PERMS_PROTECTION` remains authoritative,
|
|
104
105
|
and rejected changes revert with the server error shown.
|
|
105
106
|
|
|
107
|
+
For a larger product catalog, also register native `FormView` tabs under
|
|
108
|
+
`MEMBER_APP_PERMS_TABSET` with `registerFormTabs` from `portal-mojo/ui`.
|
|
109
|
+
Use dotted `permissions.<grant>` fields and retain the corresponding
|
|
110
|
+
`registerMemberPermissions` definitions. A separate **Product permissions**
|
|
111
|
+
detail section appears when this tabset is populated. Fields covered by those
|
|
112
|
+
tabs leave the ordinary **Permissions** section, so each grant has one editor.
|
|
113
|
+
Both forms save the same membership row with the same gates, partial-dict
|
|
114
|
+
merge, escalation confirmation and rejection handling. The tabset creates no
|
|
115
|
+
new grant semantics: the backend still decides what each key authorizes.
|
|
116
|
+
|
|
106
117
|
## Composition
|
|
107
118
|
|
|
108
119
|
`MEMBERS_ADMIN_SECTION` is root-relative and dual-mounts as `/members` in the
|
|
@@ -110,3 +121,8 @@ standalone Admin and `/system/members` when embedded. It never consumes active
|
|
|
110
121
|
group context. `GroupMembersPanel` is the reusable Group-detail composition;
|
|
111
122
|
pass optional `onNavigateUser` and `onNavigateGroup` callbacks to connect
|
|
112
123
|
sibling identity details without coupling the package to an app.
|
|
124
|
+
|
|
125
|
+
Member product tabs only render `permissions.<grant>` fields whose grant is
|
|
126
|
+
registered through `registerMemberPermissions()` and editable. System/derived
|
|
127
|
+
grants, unregistered names and unrelated fields (such as `group` or `user`)
|
|
128
|
+
are ignored; tabs with no valid fields do not appear.
|
|
@@ -163,7 +163,7 @@ flag is detected, `true` REQUIRES the flag (rare), absent/`null` means don't
|
|
|
163
163
|
care.
|
|
164
164
|
|
|
165
165
|
`geofence-data.ts` is the ONE definition of the lossy projection between that
|
|
166
|
-
DSL and the friendly form. `
|
|
166
|
+
DSL and the friendly form. `packages/portal-mojo/src/admin/identity/groups/group-sections/geofence-data.ts`
|
|
167
167
|
is a re-export shim over it, so the platform page and the group panel cannot
|
|
168
168
|
drift. `GeofenceRuleEditor` is the ONE editor both render.
|
|
169
169
|
|
package/docs/admin-push.md
CHANGED
|
@@ -6,38 +6,117 @@ Import `PushPage`, its individual panels/pages, the canonical push models, API h
|
|
|
6
6
|
|
|
7
7
|
The page has six possible tabs: Your stats, Global metrics, Devices, Deliveries, Templates, and Config. Stats are authenticated caller data. Every other tab is independently filtered by its exact permission clause, and only the active authorized panel mounts, so hidden/inactive surfaces issue no requests.
|
|
8
8
|
|
|
9
|
-
`push_surface` is registered with `registerNonFilterParams` at module load. It survives table writes but never becomes a Django lookup or filter pill. Changing tabs
|
|
9
|
+
`push_surface` is registered with `registerNonFilterParams` at module load. It survives table writes but never becomes a Django lookup or filter pill. Changing tabs retains only `push_surface` and the showcase host's `demo` selector, clearing search, sort, paging, and filters belonging to the prior model. Showcase registers `demo` as a non-filter param at boot, so table writes keep the selected demo without sending it as a model lookup.
|
|
10
10
|
|
|
11
11
|
## Exact permissions
|
|
12
12
|
|
|
13
|
-
All UI clauses carry the `sys.` namespace
|
|
13
|
+
All UI clauses carry the `sys.` namespace. Alternatives within a clause are ANY-of:
|
|
14
14
|
|
|
15
15
|
- Devices view: `view_devices | manage_devices | comms | owner | manage_users`; save: `manage_devices | comms | owner`.
|
|
16
16
|
- Deliveries view: `view_notifications | manage_notifications | comms | owner | manage_users`; save: `manage_notifications | comms`.
|
|
17
17
|
- Templates view: `manage_notifications | manage_groups | comms | owner | manage_users`; save: `manage_notifications | manage_groups | comms`.
|
|
18
18
|
- Config view/save: `manage_push_config | manage_groups | comms`.
|
|
19
19
|
- Config connection test: `manage_push_config | comms`.
|
|
20
|
+
- Device test: `send_notifications | comms` **AND** the Devices view clause above.
|
|
20
21
|
- Global metrics: `view_metrics | metrics`, with `account="global"`.
|
|
21
22
|
|
|
22
23
|
`sys.users` is deliberately absent: it is not in `RegisteredDevice.RestMeta.VIEW_PERMS`. The Stats endpoint requires authentication only and always scopes all five keys to `request.user`.
|
|
23
24
|
|
|
25
|
+
Device inspection or config management alone never enables a test send. The backend separately enforces global `send_notifications | comms` and visibility of the selected registration; invisible and missing devices both return 404. Config checks require global `manage_push_config | comms`, independently of config editing. Group/member grants, API keys, and key-backed sessions cannot satisfy these test-action gates.
|
|
26
|
+
|
|
24
27
|
## Projection and ownership boundaries
|
|
25
28
|
|
|
26
29
|
Every model pins a `basic` or `default` graph, allowlists list params, and positively projects rows before Query or Mutation cache reconciliation. RegisteredDevice never retains `device_token`; Delivery never retains `platform_data`, and its arbitrary `data_payload` is not retained by the UI projection. Related users are reduced to id plus a short display label—never email, phone, metadata, or an expanded user graph. Template JSON objects accept only a bounded shallow scalar projection on reads.
|
|
27
30
|
|
|
31
|
+
The additional evidence retained by the UI is:
|
|
32
|
+
|
|
33
|
+
| Model / result | Retained fields and fallback |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `PushDeviceRow` | `is_active` is boolean or null; an older backend with no field shows “Activity unknown”. |
|
|
36
|
+
| `PushConfigRow` | `fcm_project_id` (max 200 characters), `fcm_client_email` (max 254), and boolean/null `has_fcm_credentials`. Missing metadata is “Not reported” / “Presence not reported”; credential presence is not verification. |
|
|
37
|
+
| `PushDeliveryRow` | `push_outcome` allowlists `accepted`, `delivered`, `simulated`, `rejected`, `blocked`, `unknown`, `pending`, `sent`, and `failed`; otherwise null, with display falling back to stored `status`. |
|
|
38
|
+
| `PushTestConfig` | Positive safe-integer `id`, `name` (max 100), `group` (safe-integer id/name up to 120 characters, or explicit null for system default), `fcm_project_id` (max 200), `test_mode`, `is_active`, `has_fcm_credentials`. No service-account email or secret material. |
|
|
39
|
+
| `PushTestResult` | `success`, `outcome`, local `message`, allowlisted/null `error_code`, success-only `message_id` (max 500), positive safe-integer/null `delivery_id` and `device_id`, and projected/null `config`. |
|
|
40
|
+
|
|
41
|
+
Test results live in the open dialog, outside Query and Mutation caches. Provider messages and raw error bodies are replaced by local copy; unknown error codes are discarded. `PushReadiness` retains only `ready`, local `message`, `device_id`, and projected `config`.
|
|
42
|
+
|
|
28
43
|
`PushDeviceModel` now lives canonically in `admin/messaging/push`. The former `admin/identity/users/models.ts` definition is a compatibility re-export of the same object, so User Detail and the global Push page share one endpoint definition and Query key.
|
|
29
44
|
|
|
30
45
|
## FCM credentials and actions
|
|
31
46
|
|
|
32
47
|
FCM writes are imperative and cache-free. A service account textarea is an uncontrolled DOM ref and never prefills. Blank means untouched and is omitted. Replacement must parse to an object with `type="service_account"`, `project_id`, `client_email`, and a PEM `private_key`. Clear sends `null` only after explicit confirmation. Empty replacement is impossible, and server responses are sanitized before cache reconciliation.
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
### Saved configuration check
|
|
50
|
+
|
|
51
|
+
`testPushConfigConnection(id)` calls `POST /api/account/devices/push/config/<id>/test` with `{}`. The backend authenticates with FCM and submits a validation-only message, including when `test_mode=true` or the selected config is inactive. It sends no device notification and does not activate the config. The portal never accepts a raw device token.
|
|
52
|
+
|
|
53
|
+
The adapter accepts success only when the envelope has not failed, `success === true`, `outcome === "validated"`, `validation_only === true`, and `message_id` matches `projects/<project>/messages/<reference>`. Missing/malformed verdicts, simulated legacy responses, and provider rejections cannot display a verified connection. Backend HTTP 400 failures retain safe diagnostic data; the adapter returns a failed `PushTestResult` for the dialog to display.
|
|
54
|
+
|
|
55
|
+
“Check FCM connection” lives in the config header menu. The result and check time remain visible in Overview and Connection for that saved record/revision. Checks lock duplicate checks, editing, and active-state changes while pending. Editing or changing active state clears the result; record revision, permission, caller, or mount changes invalidate late completions. This is evidence from the current view, not a persisted credential-verification flag.
|
|
56
|
+
|
|
57
|
+
### One registered device
|
|
58
|
+
|
|
59
|
+
The device header's “Send test push” opens a focused dialog. The API helpers use the existing test endpoint:
|
|
60
|
+
|
|
61
|
+
| Helper | Request | Purpose |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `fetchPushDeviceReadiness(42)` | `GET /api/account/devices/push/test?device_id=42` | Local readiness; no FCM traffic. |
|
|
64
|
+
| `sendPushDeviceTest(42, title, message)` | `POST /api/account/devices/push/test` with `{"device_id":42,"title":"Push test","message":"This is a test notification."}` | One real notification to the selected registration. |
|
|
65
|
+
|
|
66
|
+
`device_id` here is the numeric database `id`, not the app-provided string `device_id` used for registration. The portal never calls the caller-wide POST variant without a selector. The backend resolves the device owner's active org config, falling back to the active system default; the dialog shows the device, user, config ID/name, scope, and FCM project actually reported by readiness or the send result.
|
|
67
|
+
|
|
68
|
+
Readiness must explicitly match the requested device and include an active config with credentials present and simulation disabled. Inactive registration, push disabled, an opted-out `test` category, missing token/config/credentials, or simulation prevents sending. Readiness does not verify provider credentials or device reachability. Unknown/malformed readiness disables Send rather than guessing.
|
|
69
|
+
|
|
70
|
+
The dialog defaults to title `Push test` and message `This is a test notification.`; inputs must be nonblank and fit 200/1000 characters. A single pending guard prevents duplicate submissions and locks fields, Cancel, Escape, and backdrop dismissal during sending. Each dialog permits one completed attempt; reopen it to obtain fresh readiness before another. Permission loss hides stale results; settled requests still unlock dismissal. Every attempt invalidates device, delivery, and caller-stat queries in `finally`.
|
|
71
|
+
|
|
72
|
+
Device results require explicit `success === true`, `outcome === "accepted"`, a provider message reference, and the requested `device_id`. Handled backend failures may be HTTP 200 with `data.success=false`; inspect the projected result. A failed envelope cannot establish success. Uncertain transport/malformed responses become `unknown`, and known HTTP 400/401/403/404 refusals without structured evidence become blocked. There is no automatic resend.
|
|
73
|
+
|
|
74
|
+
The portal exposes no general `/send`, caller-wide test, delivery retry, device registration/unregistration, or delete action.
|
|
75
|
+
|
|
76
|
+
All four REST models have live `CAN_DELETE = false`; the mock returns 403 without removing or cascading anything. Templates/configs can be created and edited under their save clauses. Device record editing and delivery mutation remain absent from this page.
|
|
77
|
+
|
|
78
|
+
## Outcomes and delivery evidence
|
|
79
|
+
|
|
80
|
+
Test dialogs keep a visible result label, local explanation, check time, and delivery record ID when available:
|
|
81
|
+
|
|
82
|
+
| Outcome | Meaning |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `validated` | FCM connection verified; no notification delivered. |
|
|
85
|
+
| `accepted` | FCM accepted the notification; the operator must check the selected device for receipt. |
|
|
86
|
+
| `rejected` | FCM refused the request; show the safe diagnostic. |
|
|
87
|
+
| `blocked` | Readiness, credentials, authentication, or request refusal prevented a successful test. |
|
|
88
|
+
| `unknown` | Acceptance is uncertain. Check the device and delivery history before another test; do not label it failed or retry automatically. |
|
|
35
89
|
|
|
36
|
-
|
|
90
|
+
Delivery lists/details prefer `push_outcome` over `status`. `simulated` is explicitly shown as no FCM send, even though the stored status is `sent`. `unknown` remains pending; provider acceptance alone does not mark a record delivered. Older `sent` records are described as lacking acceptance/receipt evidence. Status filtering and sorting still use the stored status field, not the derived outcome. The caller-stat tile is “Recorded sent” because its count may include simulations.
|
|
37
91
|
|
|
38
92
|
## Modal lifecycle convention
|
|
39
93
|
|
|
40
94
|
See [Admin modal policy](admin-modals.md) for header actions, content-only
|
|
41
95
|
sections, supported lifecycle fields and retained irreversible exceptions.
|
|
42
|
-
Config/template active-state controls live in the detail header.
|
|
43
|
-
|
|
96
|
+
Config/template active-state controls live in the detail header. Device details
|
|
97
|
+
use Overview/Preferences with active and push-state chips; delivery details use
|
|
98
|
+
Notification/Delivery with outcome explanations. Templates use
|
|
99
|
+
Overview/Notification/Variables; configs use Overview/Connection with scope,
|
|
100
|
+
send-mode, credential-presence, and service-account metadata.
|
|
101
|
+
|
|
102
|
+
Config/template editors keep one pending save guard and protect Escape/backdrop
|
|
103
|
+
dismissal as well as Cancel. Config saves retain write-only credential handling;
|
|
104
|
+
device test sends use their separate permission AND and dismissal lock.
|
|
105
|
+
|
|
106
|
+
## Showcase and verification
|
|
107
|
+
|
|
108
|
+
Open `#/?demo=admin-push` in Showcase. Its banner explicitly identifies
|
|
109
|
+
local fixtures: connection checks and test pushes there never contact FCM or
|
|
110
|
+
deliver notifications. Fixtures exercise system/org config selection, disabled
|
|
111
|
+
push, simulation blockers, safe metadata, and a selected-device accepted result
|
|
112
|
+
with a delivery row and no delivered timestamp. Switching Push tabs preserves
|
|
113
|
+
`demo=admin-push`; model filtering never consumes that host selector.
|
|
114
|
+
|
|
115
|
+
`npm run verify:admin-push` runs both `scripts/verify-admin-push.mjs` (mock wire,
|
|
116
|
+
permissions, projections, credentials, and target selection) and
|
|
117
|
+
`scripts/verify-push-tests.mjs` (mounted dialog/adapters). The latter covers strict
|
|
118
|
+
provider verdicts, malformed/mismatched responses, revision/permission ownership,
|
|
119
|
+
persistent results, duplicate-send prevention, pending dismissal, and cache
|
|
120
|
+
isolation. JSDOM assertions do not prove native dialog layout or stacking;
|
|
121
|
+
complete browser verification in both themes and a narrow viewport, including
|
|
122
|
+
the device test, configuration check, and nested editors.
|
package/docs/admin-settings.md
CHANGED
|
@@ -33,6 +33,16 @@ queries `/api/group`, and preserves that scope on save. New settings default to
|
|
|
33
33
|
Global. Delete is intentionally absent because live `Setting.RestMeta` does not
|
|
34
34
|
declare deletion.
|
|
35
35
|
|
|
36
|
+
## Viewing values
|
|
37
|
+
|
|
38
|
+
JSON objects and arrays show a compact type/count summary in the list. Long or
|
|
39
|
+
multiline text shows a single-line preview, capped at 80 characters. Select the
|
|
40
|
+
row to open the full value in the existing detail
|
|
41
|
+
modal. JSON uses the shared `JsonBlock` viewer, expanded initially, with syntax
|
|
42
|
+
highlighting, Show/Hide, Copy, and a scrollable body. Other text preserves line
|
|
43
|
+
breaks in a scrollable area; invalid JSON remains plain text. Secret values stay
|
|
44
|
+
masked in both views.
|
|
45
|
+
|
|
36
46
|
## Atomic write contract
|
|
37
47
|
|
|
38
48
|
Use `saveSettingAtomic()` / `buildSettingPayload()` rather than a generic
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Sign-in admin (#5547)
|
|
2
|
+
|
|
3
|
+
Import `SigninPage`, `SIGNIN_ADMIN_SECTION`, the permission clause and the API
|
|
4
|
+
helpers from `portal-mojo/admin` (or `portal-mojo/admin/identity`). The section
|
|
5
|
+
registers one global route, `/system/sign-in`, under **Identity & Access**,
|
|
6
|
+
gated by `SIGNIN_ADMIN_PERMISSIONS` = `['sys.manage_settings', 'sys.admin']`
|
|
7
|
+
(superusers pass). Admin global scope masks any outer product group.
|
|
8
|
+
|
|
9
|
+
The page has two parts:
|
|
10
|
+
|
|
11
|
+
- **Look & feel** — the SYSTEM hosted login/registration page: branding,
|
|
12
|
+
layout/appearance/accent color, hero panel, sign-in heading/copy/methods,
|
|
13
|
+
registration, custom CSS. It edits the same keys a group admin edits in the
|
|
14
|
+
group **Configure Auth** dialog; a group's own settings still override these.
|
|
15
|
+
- **Providers** — one card each for Google, Apple and GitHub: an Enabled
|
|
16
|
+
switch, a read-only callback URL with a Copy button, the credential fields,
|
|
17
|
+
a Ready / Needs setup badge, and a link to the provider's console.
|
|
18
|
+
|
|
19
|
+
KISS by design: trusted admins, no step-up auth, no confirmation beyond a
|
|
20
|
+
browser `confirm()` before clearing a stored value.
|
|
21
|
+
|
|
22
|
+
## Wire contract
|
|
23
|
+
|
|
24
|
+
One endpoint, both methods, django-mojo envelope.
|
|
25
|
+
|
|
26
|
+
`GET /api/account/admin/signin` → `{schema_version, auth, editable, options, providers}`
|
|
27
|
+
|
|
28
|
+
- `auth` is `public_auth_config` of the resolved system config
|
|
29
|
+
(`{theme, login, registration}`).
|
|
30
|
+
- `editable` lists the flat dotted paths the POST accepts. The page renders
|
|
31
|
+
only fields whose path is listed, so the backend can narrow it.
|
|
32
|
+
- `options` carries the allowed tokens for methods, layouts, appearances,
|
|
33
|
+
hero image positions and passkey prompts. Legacy layouts `card` /
|
|
34
|
+
`fullscreen` display as `compact` / `branded-panel`; any other unknown select
|
|
35
|
+
value falls back to the first option with a `console.warn`.
|
|
36
|
+
- `providers[]`: `{name, label, enabled, ready, missing, callback_url,
|
|
37
|
+
console_url, help, fields[]}`; each field is `{key, label, secret,
|
|
38
|
+
multiline, configured, source, value, hint}`. `source` is `admin`,
|
|
39
|
+
`deployment` (shown as "Set in server config") or `none`. Secret fields
|
|
40
|
+
never carry `value`; `hint` (e.g. `…a1b2`) is shown when present.
|
|
41
|
+
|
|
42
|
+
`POST /api/account/admin/signin` returns the same full payload, which the page
|
|
43
|
+
writes straight into its query cache (`SIGNIN_QUERY_KEY`).
|
|
44
|
+
|
|
45
|
+
- Look & feel: `{auth: {"theme.app_title": "…", "login.methods": […]}}` —
|
|
46
|
+
only the paths whose value differs from the loaded payload, sent by the
|
|
47
|
+
explicit **Save** button. Password is locked on in the sign-in methods picker;
|
|
48
|
+
the server rejects a list without it (400).
|
|
49
|
+
- Provider: `{provider, values, enabled}`. In `values`, an omitted key or
|
|
50
|
+
`""` keeps the stored value and `null` clears it. The card's **Save** sends
|
|
51
|
+
only fields the admin typed into; **Clear…** (shown for `source: "admin"`)
|
|
52
|
+
confirms, then sends `null` at once. The **Enabled** switch saves
|
|
53
|
+
immediately with `{provider, enabled}`, which adds/removes the provider from
|
|
54
|
+
both `login.methods` and `registration.methods`.
|
|
55
|
+
- Errors are `{status: false, error}` (HTTP 400). The client rejects them; the
|
|
56
|
+
page shows the message inline and as a toast and keeps the unsaved edits.
|
|
57
|
+
|
|
58
|
+
Secrets are entered in password inputs (the Apple `.p8` key in a textarea)
|
|
59
|
+
and cleared from local state after a successful save.
|
|
60
|
+
|
|
61
|
+
## Mock
|
|
62
|
+
|
|
63
|
+
`src/client/mock.ts` implements the endpoint: global `manage_settings` /
|
|
64
|
+
`admin` gate (401 anonymous, 403 otherwise), `editable`-only writes with the
|
|
65
|
+
option and password rules, and credential storage. Seed: Apple has Team ID and
|
|
66
|
+
Services ID stored in the Admin (missing Key ID and private key), Google has
|
|
67
|
+
nothing, GitHub is configured by the deployment. Saved look & feel lands in a
|
|
68
|
+
system override that `/api/auth/config` layers between the deployment config
|
|
69
|
+
and group overrides, so the public config reflects saves. An Admin-stored
|
|
70
|
+
credential wins over the deployment value; clearing it falls back to the
|
|
71
|
+
deployment value.
|
|
72
|
+
|
|
73
|
+
Verify with `npm run verify:admin-signin`.
|
package/docs/menus-and-access.md
CHANGED
|
@@ -26,8 +26,10 @@ setDefaultMenu('admin');
|
|
|
26
26
|
Resolution: route containment in registration order (group menus eligible
|
|
27
27
|
only under a kind-matched active group) → defaultMenu → first non-group
|
|
28
28
|
visible. Items support `permissions` (any-of) ∧ `requiresGroupKind`;
|
|
29
|
-
dividers with no visible children drop
|
|
30
|
-
|
|
29
|
+
dividers with no visible children drop. Only the destination row is selected;
|
|
30
|
+
an accordion parent is a neutral disclosure control and opens for its active
|
|
31
|
+
child. `exact: true` end-matches both the visual selection and `aria-current`.
|
|
32
|
+
`scope: 'admin' | 'group' | 'account' | 'global'` makes ownership
|
|
31
33
|
explicit; legacy `groupKind` menus still infer group scope. Static rendering
|
|
32
34
|
remains the default for compatibility. `presentation: 'accordion'` enables
|
|
33
35
|
searchable, single-open-section navigation. Give accordion parents stable
|
|
@@ -57,6 +59,8 @@ Admin section bundles contribute through `adminSectionsMenu`. Use
|
|
|
57
59
|
`{grouped: true, presentation: 'accordion'}` for the domain-grouped Admin
|
|
58
60
|
experience. Routes remain mount-relative; multiple embedded root sections
|
|
59
61
|
share one `/system` landing instead of emitting duplicate index routes.
|
|
62
|
+
Generated Admin destinations are exact matched, so an embedded mount’s root
|
|
63
|
+
Dashboard cannot remain selected on every nested page.
|
|
60
64
|
|
|
61
65
|
Import the route/menu machinery from `portal-mojo/admin/core`, then import only
|
|
62
66
|
the registry domains the shell installs. The stable domains are `identity`,
|
package/docs/realtime.md
CHANGED
|
@@ -19,6 +19,11 @@ disable this shared provider.
|
|
|
19
19
|
`auth_success`. An auth error closes and suspends the connection until the
|
|
20
20
|
token value changes.
|
|
21
21
|
- Heartbeat is a `ping` every 20 seconds with a 10-second `pong` deadline.
|
|
22
|
+
A server-initiated `{type:'ping'}` (django-mojo 1.32+ sends one every 20
|
|
23
|
+
seconds and culls a socket silent for 90) is answered at once with
|
|
24
|
+
`{type:'pong'}` while ready, clears the pending `pong` deadline, and is never
|
|
25
|
+
dispatched to consumers. Answering from the message handler keeps the socket
|
|
26
|
+
alive while a background tab throttles the client's own ping timer.
|
|
22
27
|
Reconnect uses capped jittered backoff. Pre-accept close code 4429 enforces at
|
|
23
28
|
least 60 seconds, and focus/visibility cannot bypass that deadline.
|
|
24
29
|
- Desired topics are refcounted and replay after authentication. Operations are
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portal-mojo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "React portal toolkit for django-mojo — typed client + params store, mission-control UI, dependency-free SVG charts, admin section bundles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/src/admin/core/index.ts
CHANGED
|
@@ -207,8 +207,8 @@ export function adminSectionsMenu(sections: readonly AdminSection[], opts: {
|
|
|
207
207
|
const labeled = section.routes.filter((route) => route.label);
|
|
208
208
|
const destinations: MenuItem[] = labeled.length > 0 ? labeled.map((route) => ({
|
|
209
209
|
id: `admin:${section.id}:${route.path || 'index'}`, label: route.label!, keywords: [section.title],
|
|
210
|
-
route: absolutePath(base, route.path), permissionClauses: [section.permissions, ...(route.permissions ? [route.permissions] : [])],
|
|
211
|
-
})) : [{ id: `admin:${section.id}`, label: section.title, keywords: [section.title], route: absolutePath(base), permissionClauses: [section.permissions] }];
|
|
210
|
+
route: absolutePath(base, route.path), exact: true, permissionClauses: [section.permissions, ...(route.permissions ? [route.permissions] : [])],
|
|
211
|
+
})) : [{ id: `admin:${section.id}`, label: section.title, keywords: [section.title], route: absolutePath(base), exact: true, permissionClauses: [section.permissions] }];
|
|
212
212
|
if (section.navigationGroup === null) {
|
|
213
213
|
items.push(...destinations.map((destination) => ({ ...destination, icon: section.icon })));
|
|
214
214
|
continue;
|
|
@@ -227,8 +227,8 @@ export function adminSectionsMenu(sections: readonly AdminSection[], opts: {
|
|
|
227
227
|
const base = relativePath(mount, section.basePath ?? section.id);
|
|
228
228
|
items.push({
|
|
229
229
|
label: section.title, icon: section.icon, permissions: section.permissions,
|
|
230
|
-
...(base ? { route: absolutePath(base) } : {}),
|
|
231
|
-
children: labeled.map((route) => ({ label: route.label!, route: absolutePath(base, route.path), permissions: route.permissions })),
|
|
230
|
+
...(base ? { route: absolutePath(base), exact: true } : {}),
|
|
231
|
+
children: labeled.map((route) => ({ label: route.label!, route: absolutePath(base, route.path), exact: true, permissions: route.permissions })),
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
234
|
return { name: opts.name ?? 'admin', scope: 'admin', presentation: opts.presentation ?? 'static', items };
|