Commit1f28d38unknown_key
Merge pull request #64 from ccantynz-alt/claude/review-readme-docs-ulqPK
Merge pull request #64 from ccantynz-alt/claude/review-readme-docs-ulqPK Claude/review readme docs ulq pk
2 files changed+109−01f28d3891f7fc2908a9222b34775aad2d91f1ced
2 changed files+109−0
Addedscripts/reset-admin-password.ts+91−0View fileUnifiedSplit
@@ -0,0 +1,91 @@
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
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}".`);
Modifiedsrc/routes/auth.tsx+18−0View fileUnifiedSplit
@@ -28,6 +28,7 @@ import {
2828 FormGroup,
2929 Input,
3030 Button,
31 LinkButton,
3132 Alert,
3233 Text,
3334} from "../views/ui";
@@ -221,6 +222,23 @@ auth.get("/login", async (c) => {
221222 Sign in
222223 </Button>
223224 </Form>
225 {ssoEnabled && (
226 <div class="auth-sso">
227 <div class="auth-divider">or</div>
228 <LinkButton href="/login/sso">Sign in with {ssoLabel}</LinkButton>
229 </div>
230 )}
231 <div class="auth-passkey">
232 <div class="auth-divider">or</div>
233 <button type="button" id="pk-signin-btn" class="btn">
234 Sign in with passkey
235 </button>
236 <div
237 id="pk-signin-status"
238 class="auth-status"
239 aria-live="polite"
240 ></div>
241 </div>
224242 <p class="auth-switch">
225243 <Text>New to gluecron? <a href="/register">Create an account</a></Text>
226244 </p>
227245