From d023b255a75dbaec8f360bf97901251b8e5872a0 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Tue, 24 Feb 2026 09:45:06 +0530 Subject: [PATCH] Add admin CLI password reset flow --- README.md | 14 +++++++ package.json | 1 + scripts/manage-db.ts | 98 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e82f634..390db02 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,20 @@ bun run build - Never stored in plaintext - Secure cookie-based session management +### Admin Password Recovery (CLI) +If email delivery is not configured, an admin with server access can reset a user password from the command line: + +```bash +bun run reset-password -- --email=user@example.com --new-password='new-secure-password' +``` + +What this does: +- Updates the credential password hash for the matching user +- Creates a credential account if one does not already exist +- Invalidates all active sessions for that user (forces re-login) + +Use this only from trusted server/admin environments. + ## Authentication Gitea Mirror supports multiple authentication methods. **Email/password authentication is the default and always enabled.** diff --git a/package.json b/package.json index dc68391..1dae0a1 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "check-db": "bun scripts/manage-db.ts check", "fix-db": "bun scripts/manage-db.ts fix", "reset-users": "bun scripts/manage-db.ts reset-users", + "reset-password": "bun scripts/manage-db.ts reset-password", "db:generate": "bun drizzle-kit generate", "db:migrate": "bun drizzle-kit migrate", "db:push": "bun drizzle-kit push", diff --git a/scripts/manage-db.ts b/scripts/manage-db.ts index ae858de..cb09c5c 100644 --- a/scripts/manage-db.ts +++ b/scripts/manage-db.ts @@ -4,9 +4,9 @@ import { Database } from "bun:sqlite"; import { drizzle } from "drizzle-orm/bun-sqlite"; import { migrate } from "drizzle-orm/bun-sqlite/migrator"; import { v4 as uuidv4 } from "uuid"; -import { users, configs, repositories, organizations, mirrorJobs, events } from "../src/lib/db/schema"; -import bcrypt from "bcryptjs"; -import { eq } from "drizzle-orm"; +import { users, configs, repositories, organizations, mirrorJobs, events, accounts, sessions } from "../src/lib/db/schema"; +import { and, eq } from "drizzle-orm"; +import { hashPassword } from "better-auth/crypto"; // Command line arguments const args = process.argv.slice(2); @@ -194,6 +194,92 @@ async function fixDatabase() { console.log("✅ Database location fixed"); } +/** + * Reset a single user's password (admin recovery flow) + */ +async function resetPassword() { + const emailArg = args.find((arg) => arg.startsWith("--email=")); + const passwordArg = args.find((arg) => arg.startsWith("--new-password=")); + const email = emailArg?.split("=")[1]?.trim().toLowerCase(); + const newPassword = passwordArg?.split("=")[1]; + + if (!email || !newPassword) { + console.log("❌ Missing required arguments"); + console.log("Usage:"); + console.log(" bun run manage-db reset-password --email=user@example.com --new-password='new-secure-password'"); + process.exit(1); + } + + if (newPassword.length < 8) { + console.log("❌ Password must be at least 8 characters"); + process.exit(1); + } + + if (!fs.existsSync(dbPath)) { + console.log("❌ Database does not exist"); + process.exit(1); + } + + const sqlite = new Database(dbPath); + const db = drizzle({ client: sqlite }); + + try { + const user = await db.query.users.findFirst({ + where: eq(users.email, email), + }); + + if (!user) { + console.log(`❌ No user found for email: ${email}`); + sqlite.close(); + process.exit(1); + } + + const hashedPassword = await hashPassword(newPassword); + const now = new Date(); + + const credentialAccount = await db.query.accounts.findFirst({ + where: and( + eq(accounts.userId, user.id), + eq(accounts.providerId, "credential"), + ), + }); + + if (credentialAccount) { + await db + .update(accounts) + .set({ + password: hashedPassword, + updatedAt: now, + }) + .where(eq(accounts.id, credentialAccount.id)); + } else { + await db.insert(accounts).values({ + id: uuidv4(), + accountId: user.id, + userId: user.id, + providerId: "credential", + password: hashedPassword, + createdAt: now, + updatedAt: now, + }); + } + + const deletedSessions = await db + .delete(sessions) + .where(eq(sessions.userId, user.id)) + .returning({ id: sessions.id }); + + console.log(`✅ Password reset for ${email}`); + console.log(`🔒 Cleared ${deletedSessions.length} active session(s)`); + + sqlite.close(); + } catch (error) { + console.error("❌ Error resetting password:", error); + sqlite.close(); + process.exit(1); + } +} + /** * Auto mode - check and initialize if needed */ @@ -224,6 +310,9 @@ switch (command) { case "cleanup": await cleanupDatabase(); break; + case "reset-password": + await resetPassword(); + break; case "auto": await autoMode(); break; @@ -233,7 +322,8 @@ switch (command) { console.log(" check - Check database status"); console.log(" fix - Fix database location issues"); console.log(" reset-users - Remove all users and related data"); + console.log(" reset-password - Reset one user's password and clear sessions"); console.log(" cleanup - Remove all database files"); console.log(" auto - Auto initialize if needed"); process.exit(1); -} \ No newline at end of file +}