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.
| 3c14398 | 1 | /** |
| 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 | ||
| 24 | import { db } from "../src/db"; | |
| 25 | import { users, siteAdmins } from "../src/db/schema"; | |
| 26 | import { eq, or } from "drizzle-orm"; | |
| 27 | import { hashPassword } from "../src/lib/auth"; | |
| 28 | ||
| 29 | function usage(): never { | |
| 30 | console.error( | |
| 31 | "Usage: bun run scripts/reset-admin-password.ts <username> <email> <password>" | |
| 32 | ); | |
| 33 | process.exit(2); | |
| 34 | } | |
| 35 | ||
| 36 | const [, , username, email, password] = process.argv; | |
| 37 | ||
| 38 | if (!username || !email || !password) usage(); | |
| 39 | if (password.length < 8) { | |
| 40 | console.error("Password must be at least 8 characters."); | |
| 41 | process.exit(2); | |
| 42 | } | |
| 43 | if (!email.includes("@")) { | |
| 44 | console.error("Email looks invalid (no @)."); | |
| 45 | process.exit(2); | |
| 46 | } | |
| 47 | ||
| 48 | const passwordHash = await hashPassword(password); | |
| 49 | ||
| 50 | // Find by username OR email — either uniquely identifies an existing user. | |
| 51 | const 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 | ||
| 57 | let userId: string; | |
| 58 | let action: "updated" | "created"; | |
| 59 | ||
| 60 | if (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. | |
| 83 | await db | |
| 84 | .insert(siteAdmins) | |
| 85 | .values({ userId }) | |
| 86 | .onConflictDoNothing({ target: siteAdmins.userId }); | |
| 87 | ||
| 88 | console.log( | |
| 89 | `OK — ${action} user "${username}" <${email}> and ensured site_admins row.` | |
| 90 | ); | |
| 91 | console.log(`Sign in at /login with username "${username}".`); |