There were three answers to "who is the owner" and nothing kept them agreeing: SUPER_ADMIN_EMAIL in .env, the lowest user id, and now the role column. Any two of them part company the moment one is edited, and the failure is silent — an account quietly gains or loses the vault, the platform origin, and the identity the agent sidecar runs as. The column wins; the other two are gone. - isSuperAdmin() reads users.role. SUPER_ADMIN_EMAIL is deleted from the code and from .env. - getOwnerUser() selects on the role instead of `order by id limit 1`. It returns undefined when no row holds it rather than falling back: the agent sidecar refusing to start beats it silently running as the wrong person. - bootstrap creates the first account with role 'Super Admin'. Without this a fresh install would take the column's 'Member' default and come up with NO owner at all — no vault, no agent identity, and the web origin locked to a Super Admin that does not exist. That bug was live the moment the column landed; SUPER_ADMIN_EMAIL was masking it here. Deliberately not cached. The old resolver cached an owner id, justified by "the owner never changes at runtime (bootstrap is closed after user #1)" — which stops being true as soon as roles are editable, and a cache with no invalidation contract is a staleness bug waiting for whoever builds the role UI. It is one primary-key lookup on a request that has already verified a JWT. Verified against the live database: getOwnerUser resolves to id 1, isSuperAdmin is true for it and false for the three Members, for a null payload and for an id that does not exist. Then temporarily set id 13 to 'Admin' (false) and to 'Super Admin' (true) with no restart in between, which is what proves the column is doing the deciding rather than a cache or the old lowest-id path. Reverted. Not solved here: nothing stops the last Super Admin being demoted, which would leave the platform ownerless. The schema cannot express it; whatever eventually edits roles has to. Noted in the column's comment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
25 lines
1.3 KiB
TypeScript
25 lines
1.3 KiB
TypeScript
import { getUserById } from 'officerdb';
|
|
|
|
// Identifies the platform owner. The user's `role` column IS the answer — there is no second mechanism
|
|
// and no environment variable.
|
|
//
|
|
// This used to resolve the owner two other ways: SUPER_ADMIN_EMAIL from .env, falling back to the
|
|
// lowest user id. Both are gone. Three sources of truth for "who owns this server" (those two plus the
|
|
// role column) disagree the moment one is edited, and the failure is silent — an account quietly gains
|
|
// or loses the vault, the platform origin, and the identity the agent sidecar runs as.
|
|
//
|
|
// NOT CACHED, deliberately. The previous version cached an owner id and justified it with "the owner
|
|
// never changes at runtime (bootstrap is closed after user #1)". That stops being true the moment roles
|
|
// are editable, and a cache with no invalidation contract is a staleness bug waiting for whoever builds
|
|
// the role UI. This is one primary-key lookup on a request that has already verified a JWT.
|
|
export async function isSuperAdmin(payload: { id?: number } | null | undefined): Promise<boolean> {
|
|
if (!payload?.id) return false;
|
|
try {
|
|
const user = await getUserById(payload.id);
|
|
return user?.role === 'Super Admin';
|
|
} catch {
|
|
// A transient database error must never promote anyone. Deny, and let the next call retry.
|
|
return false;
|
|
}
|
|
}
|