Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.

admin.tsBlame139 lines · 1 contributor
8f50ed0Claude1/**
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
12import { asc, eq } from "drizzle-orm";
13import { db } from "../db";
14import { 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 */
22export 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
47export 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
63export 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
78export 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
90export 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
103export 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
123export 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). */
132export 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
139export type FlagKey = keyof typeof KNOWN_FLAGS;