|
20 | 20 | */ |
21 | 21 |
|
22 | 22 | import type { PermissionSet } from '@objectstack/spec/security'; |
| 23 | +import { SystemUserId } from '@objectstack/spec/system'; |
23 | 24 |
|
24 | 25 | interface BootstrapOptions { |
25 | 26 | /** Logger from PluginContext. */ |
@@ -117,18 +118,32 @@ export async function bootstrapPlatformAdmin( |
117 | 118 | ql, |
118 | 119 | 'sys_user_permission_set', |
119 | 120 | { permission_set_id: adminPsId }, |
120 | | - 5, |
| 121 | + 50, |
121 | 122 | ); |
122 | | - if (existingAdminLinks.some((r) => !r.organization_id)) { |
| 123 | + // A platform admin "already exists" only if a *human* holds the |
| 124 | + // cross-tenant grant. The seed-data owner `usr_system` (provisioned by |
| 125 | + // the SeedLoader, see runtime/app-plugin.ts `ensureSeedIdentity`) must |
| 126 | + // never count — otherwise a DB where it was wrongly promoted would block |
| 127 | + // every real admin forever. Ignoring it here makes the bootstrap |
| 128 | + // self-healing on restart. |
| 129 | + if (existingAdminLinks.some((r) => !r.organization_id && r.user_id !== SystemUserId.SYSTEM)) { |
123 | 130 | return { seeded: seededCount, adminPromoted: false, reason: 'already_have_admin' }; |
124 | 131 | } |
125 | 132 |
|
126 | 133 | const allUsers = await tryFind(ql, 'sys_user', {}, 50); |
127 | | - if (allUsers.length === 0) { |
128 | | - logger?.info?.('[security] no users yet — first sign-up will be promoted to platform admin'); |
| 134 | + // Exclude the non-loginable system service account. It is created during |
| 135 | + // seed loading — *before* the first human sign-up — so without this filter |
| 136 | + // it is the earliest user and steals the platform-admin promotion, leaving |
| 137 | + // the real admin login without `setup.access` / `studio.access` (Setup and |
| 138 | + // Studio then stay invisible even though login succeeds). |
| 139 | + const humanUsers = allUsers.filter( |
| 140 | + (u) => u.id !== SystemUserId.SYSTEM && u.role !== 'system', |
| 141 | + ); |
| 142 | + if (humanUsers.length === 0) { |
| 143 | + logger?.info?.('[security] no human users yet — first sign-up will be promoted to platform admin'); |
129 | 144 | return { seeded: seededCount, adminPromoted: false, reason: 'no_users' }; |
130 | 145 | } |
131 | | - const sorted = [...allUsers].sort((a, b) => { |
| 146 | + const sorted = [...humanUsers].sort((a, b) => { |
132 | 147 | const ta = a.created_at ? new Date(a.created_at).getTime() : 0; |
133 | 148 | const tb = b.created_at ? new Date(b.created_at).getTime() : 0; |
134 | 149 | return ta - tb; |
|
0 commit comments