Add manage.py CLI for password reset and database wipe

Two commands, run from the server terminal:

  python manage.py reset-admin
    Interactively select an admin account and set a new password.
    The app does not need to be stopped first (SQLite WAL handles
    concurrent access safely). Existing sessions remain valid until
    they expire (8 h); restart the app to invalidate them immediately.

  python manage.py reset-db
    Deletes clubledger.db plus any -wal/-shm sidecar files.
    Requires the app to be stopped first. After restart the app
    recreates a fresh database with the default admin/admin account.
    Asks the user to type RESET to confirm before deleting anything.

https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
This commit is contained in:
Claude 2026-05-30 17:02:40 +00:00
parent b1fcc3dbe9
commit ea03355743
No known key found for this signature in database

141
manage.py Normal file
View file

@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""
ClubLedger management CLI
Run from the server terminal (NOT through the web UI).
The app does not need to be stopped first for reset-admin;
it MUST be stopped before reset-db.
Usage:
python manage.py reset-admin reset an admin account password
python manage.py reset-db wipe all data and start fresh
"""
import sys
import getpass
import sqlite3
from pathlib import Path
DB_PATH = Path("clubledger.db")
def _connect():
if not DB_PATH.exists():
print(f"Database not found: {DB_PATH}")
print("Start the app at least once to create it.")
sys.exit(1)
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
return conn
def cmd_reset_admin():
"""Interactively reset the password for an admin account."""
import bcrypt
conn = _connect()
admins = conn.execute(
"SELECT id, name, username FROM staff_accounts WHERE role='admin' ORDER BY name"
).fetchall()
if not admins:
print("No admin accounts exist.")
print("Start the app — it will create the default admin/admin account automatically.")
conn.close()
sys.exit(0)
if len(admins) == 1:
target = admins[0]
else:
print("Admin accounts:")
for i, a in enumerate(admins, 1):
print(f" {i}. {a['name']} ({a['username']})")
while True:
raw = input("Select account number: ").strip()
try:
target = admins[int(raw) - 1]
break
except (ValueError, IndexError):
print(" Invalid — enter the number shown above.")
print(f"\nResetting password for: {target['name']} ({target['username']})")
while True:
pw = getpass.getpass("New password: ")
if len(pw) < 4:
print(" Password must be at least 4 characters.")
continue
pw2 = getpass.getpass("Confirm password: ")
if pw != pw2:
print(" Passwords do not match — try again.")
continue
break
hashed = bcrypt.hashpw(pw.encode(), bcrypt.gensalt()).decode()
conn.execute(
"UPDATE staff_accounts SET password_hash=? WHERE id=?",
(hashed, target["id"]),
)
conn.commit()
conn.close()
print(f"\nDone. Password updated for '{target['username']}'.")
print("Any existing sessions for this account will still be valid until they expire (8 h).")
print("Restart the app now to invalidate all active sessions immediately.")
def cmd_reset_db():
"""Delete all data files and prepare for a clean start."""
print("=" * 60)
print(" DATABASE RESET")
print("=" * 60)
print()
print("This will permanently delete:")
print(" • All members and their balances")
print(" • All transactions and receipts")
print(" • All staff accounts")
print(" • All app settings")
print()
print("STOP THE APP before continuing.")
print()
confirm = input('Type RESET to confirm (anything else cancels): ').strip()
if confirm != "RESET":
print("Cancelled — nothing was changed.")
sys.exit(0)
deleted = []
for suffix in ("", "-wal", "-shm"):
p = DB_PATH.parent / (DB_PATH.name + suffix)
if p.exists():
p.unlink()
deleted.append(p.name)
if deleted:
print(f"\nDeleted: {', '.join(deleted)}")
else:
print("\nNo database files found — nothing to delete.")
print()
print("Reset complete. Start the app to create a fresh database.")
print("Default admin credentials after restart: username=admin password=admin")
print("Change the password immediately after logging in.")
# ---------------------------------------------------------------------------
COMMANDS = {
"reset-admin": (cmd_reset_admin, "Reset an admin account password"),
"reset-db": (cmd_reset_db, "Wipe all data and start fresh (irreversible)"),
}
def usage():
print(__doc__)
print("Commands:")
for name, (_, desc) in COMMANDS.items():
print(f" {name:<16} {desc}")
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
usage()
COMMANDS[sys.argv[1]][0]()