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

reset-admin-password.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.

reset-admin-password.tsBlame91 lines · 1 contributor
3c14398Claude1/**
2 * Reset (or create) the site admin's password.
3 *
4 * Use this when you have shell access to the deployment but have lost the
5 * site-admin password. Idempotent: if the user exists, updates the password
6 * hash; if not, creates the user. Either way, ensures the user is in
7 * `site_admins` so they bypass the bootstrap-rule chicken-and-egg.
8 *
9 * Run against the live database via:
10 *
11 * bun run scripts/reset-admin-password.ts <username> <email> <password>
12 *
13 * On Fly.io that looks like:
14 *
15 * fly ssh console -C "bun run scripts/reset-admin-password.ts admin you@example.com 'new-password'"
16 *
17 * Requires DATABASE_URL set in the environment (which is true inside the
18 * Fly machine; locally you'd export it first).
19 *
20 * Password is bcrypt-hashed with the same parameters as `src/lib/auth.ts`
21 * (cost 10). Plaintext is never persisted or logged.
22 */
23
24import { db } from "../src/db";
25import { users, siteAdmins } from "../src/db/schema";
26import { eq, or } from "drizzle-orm";
27import { hashPassword } from "../src/lib/auth";
28
29function usage(): never {
30 console.error(
31 "Usage: bun run scripts/reset-admin-password.ts <username> <email> <password>"
32 );
33 process.exit(2);
34}
35
36const [, , username, email, password] = process.argv;
37
38if (!username || !email || !password) usage();
39if (password.length < 8) {
40 console.error("Password must be at least 8 characters.");
41 process.exit(2);
42}
43if (!email.includes("@")) {
44 console.error("Email looks invalid (no @).");
45 process.exit(2);
46}
47
48const passwordHash = await hashPassword(password);
49
50// Find by username OR email — either uniquely identifies an existing user.
51const existing = await db
52 .select({ id: users.id, username: users.username, email: users.email })
53 .from(users)
54 .where(or(eq(users.username, username), eq(users.email, email)))
55 .limit(1);
56
57let userId: string;
58let action: "updated" | "created";
59
60if (existing.length > 0) {
61 const row = existing[0]!;
62 userId = row.id;
63 await db
64 .update(users)
65 .set({ passwordHash, email, username, updatedAt: new Date() })
66 .where(eq(users.id, userId));
67 action = "updated";
68 if (row.username !== username || row.email !== email) {
69 console.log(
70 `(Note: existing user matched on ${row.username === username ? "username" : "email"}; username/email fields realigned to the values you passed.)`
71 );
72 }
73} else {
74 const inserted = await db
75 .insert(users)
76 .values({ username, email, passwordHash })
77 .returning({ id: users.id });
78 userId = inserted[0]!.id;
79 action = "created";
80}
81
82// Ensure site-admin row so we don't depend on the oldest-user bootstrap rule.
83await db
84 .insert(siteAdmins)
85 .values({ userId })
86 .onConflictDoNothing({ target: siteAdmins.userId });
87
88console.log(
89 `OK — ${action} user "${username}" <${email}> and ensured site_admins row.`
90);
91console.log(`Sign in at /login with username "${username}".`);