fix: validate roles, guard last owner, scope bootstrap status
Security review of e70b2f0. The UI gating was correctly backed by
RequireRole everywhere; these are the missing validation gaps behind it.
- UpdateUserRole and createOrgUser accepted any role string verbatim, so
an admin could self-promote to owner, create an owner outright, or set
a junk role that silently stripped a user's access. Roles are now
whitelisted, only an owner may grant or remove the owner role, and an
actor cannot change their own.
- Neither demote nor delete guarded the last owner, so an org could reach
zero owners. Both now refuse when no owner would remain, returning 409.
Self-delete rejected.
- CountUsers counted across all orgs, so a locked-out org could never
re-bootstrap once another tenant existed, and the unauthenticated
bootstrap-status endpoint reported instance-wide state. It now answers
per-org on an org host, falling back to global only on the apex.
- HandleMe repeats the middleware's host/org check; it sits outside the
middleware so it can still return its own 401.
- Post-bootstrap now sends the new owner to their org host's login page.
The session cookie is deliberately scoped to the exact host, so the old
redirect landed them unauthenticated.
- AuthProvider renders an error state instead of mounting the shell with
a null user when /auth/me fails for a reason other than 401.
- api.ts unwraps {"error": ...} so these messages render as text.
This commit is contained in:
+11
-2
@@ -385,8 +385,17 @@ async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => res.statusText);
|
||||
throw new ApiError(res.status, text || `HTTP ${res.status}`);
|
||||
const text = await res.text().catch(() => "");
|
||||
// Handlers report failures as {"error": "..."} — unwrap it so the message
|
||||
// reaching the UI is the sentence the backend wrote, not raw JSON.
|
||||
let message = text || res.statusText || `HTTP ${res.status}`;
|
||||
try {
|
||||
const body = JSON.parse(text);
|
||||
if (body?.error) message = body.error;
|
||||
} catch {
|
||||
// non-JSON body — keep the text as-is
|
||||
}
|
||||
throw new ApiError(res.status, message);
|
||||
}
|
||||
|
||||
if (res.status === 204) {
|
||||
|
||||
Reference in New Issue
Block a user