CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
admin.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 8f50ed0 | 1 | /** |
| 2 | * Block F3 — Site admin helpers. | |
| 3 | * | |
| 4 | * A site admin is a user with a row in `site_admins`. If the table is empty, | |
| 5 | * the very first registered user is the bootstrap admin. This mirrors how | |
| 6 | * many self-hosted apps handle the first-install case without requiring | |
| 7 | * `env`-based provisioning. | |
| 8 | * | |
| 9 | * Writes to system flags go through `setFlag` which records the writer. | |
| 10 | */ | |
| 11 | ||
| 12 | import { asc, eq } from "drizzle-orm"; | |
| 13 | import { db } from "../db"; | |
| 14 | import { siteAdmins, systemFlags, users } from "../db/schema"; | |
| 15 | ||
| 16 | /** | |
| 17 | * Is this user a site admin? Returns true if any of: | |
| 18 | * - they have a row in `site_admins`, OR | |
| 19 | * - no rows exist in `site_admins` and they are the oldest-created user | |
| 20 | * (bootstrap rule). | |
| 21 | */ | |
| 22 | export async function isSiteAdmin( | |
| 23 | userId: string | null | undefined | |
| 24 | ): Promise<boolean> { | |
| 25 | if (!userId) return false; | |
| 26 | try { | |
| 27 | const [row] = await db | |
| 28 | .select({ userId: siteAdmins.userId }) | |
| 29 | .from(siteAdmins) | |
| 30 | .where(eq(siteAdmins.userId, userId)) | |
| 31 | .limit(1); | |
| 32 | if (row) return true; | |
| 33 | // Bootstrap: empty site_admins → oldest user is admin. | |
| 34 | const [anyAdmin] = await db.select().from(siteAdmins).limit(1); | |
| 35 | if (anyAdmin) return false; | |
| 36 | const [first] = await db | |
| 37 | .select({ id: users.id }) | |
| 38 | .from(users) | |
| 39 | .orderBy(asc(users.createdAt)) | |
| 40 | .limit(1); | |
| 41 | return !!first && first.id === userId; | |
| 42 | } catch { | |
| 43 | return false; | |
| 44 | } | |
| 45 | } | |
| 46 | ||
| 47 | export async function listSiteAdmins() { | |
| 48 | try { | |
| 49 | return await db | |
| 50 | .select({ | |
| 51 | userId: siteAdmins.userId, | |
| 52 | username: users.username, | |
| 53 | grantedAt: siteAdmins.grantedAt, | |
| 54 | grantedBy: siteAdmins.grantedBy, | |
| 55 | }) | |
| 56 | .from(siteAdmins) | |
| 57 | .innerJoin(users, eq(siteAdmins.userId, users.id)); | |
| 58 | } catch { | |
| 59 | return []; | |
| 60 | } | |
| 61 | } | |
| 62 | ||
| 63 | export async function grantSiteAdmin( | |
| 64 | userId: string, | |
| 65 | grantedBy: string | null | |
| 66 | ): Promise<boolean> { | |
| 67 | try { | |
| 68 | await db | |
| 69 | .insert(siteAdmins) | |
| 70 | .values({ userId, grantedBy: grantedBy || null }) | |
| 71 | .onConflictDoNothing(); | |
| 72 | return true; | |
| 73 | } catch { | |
| 74 | return false; | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | export async function revokeSiteAdmin(userId: string): Promise<boolean> { | |
| 79 | try { | |
| 80 | const res = await db | |
| 81 | .delete(siteAdmins) | |
| 82 | .where(eq(siteAdmins.userId, userId)) | |
| 83 | .returning({ userId: siteAdmins.userId }); | |
| 84 | return res.length > 0; | |
| 85 | } catch { | |
| 86 | return false; | |
| 87 | } | |
| 88 | } | |
| 89 | ||
| 90 | export async function getFlag(key: string): Promise<string | null> { | |
| 91 | try { | |
| 92 | const [row] = await db | |
| 93 | .select({ value: systemFlags.value }) | |
| 94 | .from(systemFlags) | |
| 95 | .where(eq(systemFlags.key, key)) | |
| 96 | .limit(1); | |
| 97 | return row?.value ?? null; | |
| 98 | } catch { | |
| 99 | return null; | |
| 100 | } | |
| 101 | } | |
| 102 | ||
| 103 | export async function setFlag( | |
| 104 | key: string, | |
| 105 | value: string, | |
| 106 | updatedBy: string | null | |
| 107 | ): Promise<boolean> { | |
| 108 | try { | |
| 109 | await db | |
| 110 | .insert(systemFlags) | |
| 111 | .values({ key, value, updatedBy: updatedBy || null }) | |
| 112 | .onConflictDoUpdate({ | |
| 113 | target: systemFlags.key, | |
| 114 | set: { value, updatedBy: updatedBy || null, updatedAt: new Date() }, | |
| 115 | }); | |
| 116 | return true; | |
| 117 | } catch (err) { | |
| 118 | console.error("[admin] setFlag:", err); | |
| 119 | return false; | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | export async function listFlags() { | |
| 124 | try { | |
| 125 | return await db.select().from(systemFlags); | |
| 126 | } catch { | |
| 127 | return []; | |
| 128 | } | |
| 129 | } | |
| 130 | ||
| 131 | /** Known flag keys with defaults (used by callers + UI rendering). */ | |
| 132 | export const KNOWN_FLAGS = { | |
| 133 | registration_locked: "0", // "1" to block new sign-ups | |
| 134 | site_banner_text: "", // non-empty → show a banner at the top | |
| 135 | site_banner_level: "info", // info | warn | error | |
| 136 | read_only_mode: "0", | |
| 137 | } as const; | |
| 138 | ||
| 139 | export type FlagKey = keyof typeof KNOWN_FLAGS; |