Commite6f7b90
feat(admin): scripts to check + promote site admin
feat(admin): scripts to check + promote site admin - scripts/check-admin.ts — reports users.is_admin AND site_admins membership for an email. Idempotent. Tells you exactly which level of admin promotion is missing. - scripts/promote-admin.ts — sets users.is_admin = true AND inserts into site_admins. Idempotent. Use when the bootstrap-first-user rule didn't apply (e.g. someone else registered first) or when you want to be explicit instead of relying on bootstrap. - DO_THIS_NOW.md — 6-step one-pager for the metal-box bring-up: SSH → clone → compose up → migrate → register → promote-admin → verify. Plus troubleshooting for the three failure modes that actually happen (DNS/Cloudflare proxy on, DB url wrong, migrations not run). Closes the \"I can log in but I'm not sure if I'm admin\" gap and gives a deterministic path to admin even if the bootstrap rule didn't kick in.
3 files changed+220−0e6f7b906a0015c46b14165317879d5e86c9ba4a1
3 changed files+220−0
AddedDO_THIS_NOW.md+90−0View fileUnifiedSplit
@@ -0,0 +1,90 @@
1# Do this now (gluecron live + admin)
2
3One page. Five steps. Copy-paste in order.
4
5## 1. SSH to the metal box
6
7```sh
8ssh root@45.76.171.37
9```
10
11If SSH key-only is enabled and your key isn't loaded, use Vultr's vSerial
12console from the dashboard — it accepts the root password from server
13Overview → "Show Password".
14
15## 2. Clone + configure (first-time only)
16
17```sh
18git clone https://github.com/ccantynz-alt/Gluecron.com.git /opt/gluecron
19cd /opt/gluecron
20git checkout claude/new-session-xk1l7 # the deploy + admin scripts live here
21cp .env.example .env
22vim .env # set DATABASE_URL at minimum (Neon connection string)
23```
24
25Redeploy on later visits:
26
27```sh
28cd /opt/gluecron && git pull && docker compose up -d --build
29```
30
31## 3. Bring up the stack
32
33```sh
34docker compose up -d --build
35docker compose logs -f caddy # ctrl-c once you see "certificate obtained"
36```
37
38Then run migrations:
39
40```sh
41docker compose exec gluecron bun run db:migrate
42```
43
44## 4. Register your account
45
46- Open https://gluecron.com/register in a browser
47- Email + password. Use the same email you want to be admin under.
48- First registered user auto-promotes per bootstrap rule — but to be safe:
49
50## 5. Confirm + promote yourself to admin
51
52```sh
53docker compose exec gluecron bun run scripts/check-admin.ts you@example.com
54```
55
56If it says "NOT admin":
57
58```sh
59docker compose exec gluecron bun run scripts/promote-admin.ts you@example.com
60```
61
62Log out, log back in, visit https://gluecron.com/admin — should render.
63
64## 6. Verify (optional but quick)
65
66```sh
67bash scripts/verify-deploy.sh https://gluecron.com
68```
69
70All lines should show OK. If any FAIL, look at the line and debug just that.
71
72---
73
74## Troubleshooting
75
76**Caddy logs say "acme: error issuing certificate":** DNS is wrong or
77Cloudflare proxy is on. Check `dig +short gluecron.com` returns
78`45.76.171.37` and that the orange cloud is grey in Cloudflare.
79
80**`docker compose exec` says "service is not running":** check
81`docker compose ps`. If gluecron is restarting, `docker compose logs gluecron`.
82Most common cause: `DATABASE_URL` wrong or Neon project paused.
83
84**`/register` returns 500:** DB migrations probably weren't run. Re-run
85`docker compose exec gluecron bun run db:migrate`.
86
87**"Cannot find module 'src/db/client'":** the scripts assume the working
88directory is the repo root inside the container, which is the default for
89`docker compose exec`. If you're running them on the host, prefix with
90`bun --cwd /opt/gluecron run scripts/...`.
Addedscripts/check-admin.ts+72−0View fileUnifiedSplit
@@ -0,0 +1,72 @@
1/**
2 * Check whether a user is a site admin.
3 *
4 * Usage:
5 * docker compose exec gluecron bun run scripts/check-admin.ts <email>
6 *
7 * Reports both `users.is_admin` and `site_admins` table presence so you can
8 * see if your account is admin-promoted at either level. Use
9 * `scripts/promote-admin.ts` to grant admin if it isn't.
10 */
11import { db } from "../src/db/client";
12import { users, siteAdmins } from "../src/db/schema";
13import { eq } from "drizzle-orm";
14
15const emailArg = process.argv[2];
16if (!emailArg) {
17 console.error("Usage: bun run scripts/check-admin.ts <email>");
18 process.exit(2);
19}
20const email = emailArg.toLowerCase().trim();
21
22const userRow = await db
23 .select({ id: users.id, email: users.email, isAdmin: users.isAdmin, createdAt: users.createdAt })
24 .from(users)
25 .where(eq(users.email, email))
26 .limit(1);
27
28if (userRow.length === 0) {
29 console.error(`No user found for email: ${email}`);
30 console.error("Register at /register first, then re-run this script.");
31 process.exit(1);
32}
33
34const u = userRow[0];
35const inSiteAdmins = await db
36 .select({ userId: siteAdmins.userId })
37 .from(siteAdmins)
38 .where(eq(siteAdmins.userId, u.id))
39 .limit(1);
40
41const totalSiteAdmins = await db.select({ userId: siteAdmins.userId }).from(siteAdmins);
42
43console.log(`User: ${u.email}`);
44console.log(` id: ${u.id}`);
45console.log(` created_at: ${u.createdAt.toISOString()}`);
46console.log(` users.is_admin: ${u.isAdmin ? "YES" : "no"}`);
47console.log(` in site_admins: ${inSiteAdmins.length > 0 ? "YES" : "no"}`);
48console.log(` total site_admins: ${totalSiteAdmins.length}`);
49
50if (totalSiteAdmins.length === 0) {
51 console.log("");
52 console.log("site_admins table is empty — the bootstrap rule applies:");
53 console.log(" the oldest user in `users` is treated as admin by app code.");
54 console.log(" Run scripts/promote-admin.ts to make this explicit and durable.");
55}
56
57if (u.isAdmin && inSiteAdmins.length > 0) {
58 console.log("");
59 console.log("✓ This account is a full site admin.");
60 process.exit(0);
61}
62
63if (u.isAdmin || inSiteAdmins.length > 0) {
64 console.log("");
65 console.log("~ Partial admin state. Run scripts/promote-admin.ts to normalise.");
66 process.exit(0);
67}
68
69console.log("");
70console.log("× This account is NOT admin. Run:");
71console.log(` docker compose exec gluecron bun run scripts/promote-admin.ts ${email}`);
72process.exit(0);
Addedscripts/promote-admin.ts+58−0View fileUnifiedSplit
@@ -0,0 +1,58 @@
1/**
2 * Promote a user to site admin.
3 *
4 * Usage:
5 * docker compose exec gluecron bun run scripts/promote-admin.ts <email>
6 *
7 * Sets both `users.is_admin = true` AND inserts into `site_admins`, so the
8 * account is admin under both the bootstrap rule and the explicit-list rule.
9 *
10 * Idempotent: safe to re-run. Reports what changed.
11 */
12import { db } from "../src/db/client";
13import { users, siteAdmins } from "../src/db/schema";
14import { and, eq } from "drizzle-orm";
15
16const emailArg = process.argv[2];
17if (!emailArg) {
18 console.error("Usage: bun run scripts/promote-admin.ts <email>");
19 process.exit(2);
20}
21const email = emailArg.toLowerCase().trim();
22
23const userRow = await db
24 .select({ id: users.id, email: users.email, isAdmin: users.isAdmin })
25 .from(users)
26 .where(eq(users.email, email))
27 .limit(1);
28
29if (userRow.length === 0) {
30 console.error(`No user found for email: ${email}`);
31 console.error("Register at /register first, then re-run this script.");
32 process.exit(1);
33}
34const u = userRow[0];
35
36let flippedIsAdmin = false;
37if (!u.isAdmin) {
38 await db.update(users).set({ isAdmin: true }).where(eq(users.id, u.id));
39 flippedIsAdmin = true;
40}
41
42const existing = await db
43 .select({ userId: siteAdmins.userId })
44 .from(siteAdmins)
45 .where(eq(siteAdmins.userId, u.id))
46 .limit(1);
47
48let insertedSiteAdmin = false;
49if (existing.length === 0) {
50 await db.insert(siteAdmins).values({ userId: u.id, grantedBy: u.id });
51 insertedSiteAdmin = true;
52}
53
54console.log(`User: ${u.email} (${u.id})`);
55console.log(` users.is_admin : ${flippedIsAdmin ? "flipped FALSE -> TRUE" : "already TRUE"}`);
56console.log(` site_admins row : ${insertedSiteAdmin ? "INSERTED" : "already present"}`);
57console.log("");
58console.log("✓ This account is now a site admin. Log out and back in if /admin still 403s.");
059