Ctrl K
ComunidadGuías…

reset_admin.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
reset_admin.py — Emergency admin password reset (run from the console).

Usage:
    cd D:\\HAMCTRL          (or wherever this script lives)
    py reset_admin.py

Interactively asks for a new password and updates users.json.
If the password is forgotten or the 'admin' user can't log in, run this
script on the server (NOT remotely) to restore access.

Requires:
- Python 3.9+
- users.json reachable via config.py's data directory (dev: next to this
  script; installed EXE: %APPDATA%\\HAMCTRL - see config._writable_data_dir),
  or next to this script / in the current directory as a fallback.
"""

import hashlib
import json
import sys
from pathlib import Path


def hash_pw(pw: str) -> str:
    """Hash the admin password. Prefer scrypt (auth.hash_pw_secure); fall back to
    legacy SHA-256 only if auth can't be imported (standalone emergency use).
    A legacy hash still works — it upgrades to scrypt on next login."""
    try:
        from auth import hash_pw_secure
        return hash_pw_secure(pw)
    except Exception:
        return hashlib.sha256(pw.encode()).hexdigest()


def _candidate_paths() -> list[Path]:
    """users.json locations to try, in priority order. The first one uses
    config.py's own logic for finding the app's writable data directory
    (handles dev / portable-next-to-EXE / %APPDATA%\\HAMCTRL correctly) -
    that's the authoritative location the running app actually reads/writes.
    The rest are plain fallbacks in case config.py can't be imported."""
    candidates = []
    try:
        from config import USR_F
        candidates.append(USR_F)
    except Exception:
        pass
    candidates.append(Path(__file__).parent / "users.json")
    candidates.append(Path.cwd() / "users.json")
    return candidates


def find_users_json() -> Path:
    """Find users.json - see _candidate_paths() for the search order."""
    for c in _candidate_paths():
        if c.exists():
            return c
    return None


def main():
    print("=" * 60)
    print(" ADMIN PASSWORD RESET")
    print("=" * 60)

    usr_f = find_users_json()
    if not usr_f:
        print("ERROR: could not find users.json")
        print("Checked locations:")
        for c in _candidate_paths():
            print(f"  {c}")
        sys.exit(1)

    print(f"Loading: {usr_f}")

    # Load
    try:
        with open(usr_f, "r", encoding="utf-8") as f:
            users = json.load(f)
    except Exception as e:
        print(f"LOAD ERROR: {e}")
        sys.exit(2)

    if not isinstance(users, list):
        # The format may be {"users": [...]}
        if isinstance(users, dict) and "users" in users:
            users_list = users["users"]
        else:
            print(f"ERROR: users.json has an unexpected format")
            sys.exit(3)
    else:
        users_list = users

    # Show current users
    print(f"\nFound {len(users_list)} users:")
    for i, u in enumerate(users_list, 1):
        active = "active" if u.get("active", True) else "INACTIVE"
        print(f"  {i}. {u.get('username', '?'):20s} role={u.get('role', '?'):10s} [{active}]")

    # Find the admin (username == 'admin', case-insensitive)
    admin = next((u for u in users_list if u.get("username", "").lower() == "admin"), None)
    if not admin:
        # Try by role
        admins = [u for u in users_list if u.get("role") == "admin"]
        if not admins:
            print("\nERROR: no user with the admin role exists")
            sys.exit(4)
        if len(admins) == 1:
            admin = admins[0]
            print(f"\nNote: no user with username='admin', but there is one admin: {admin['username']}")
        else:
            print("\nFound multiple admins, pick a number:")
            for i, u in enumerate(admins, 1):
                print(f"  {i}. {u['username']}")
            try:
                idx = int(input("> ")) - 1
                admin = admins[idx]
            except (ValueError, IndexError):
                print("ERROR: invalid choice")
                sys.exit(5)

    print(f"\nResetting password for: {admin.get('username')} (role={admin.get('role')})")

    # Enter the new password
    print()
    print("Enter the new password (press Enter to use the default 'Admin1234!'):")
    new_pw = input("> ").strip()
    if not new_pw:
        new_pw = "Admin1234!"
        print(f"Using default: {new_pw}")

    # Save changes
    admin["password"] = hash_pw(new_pw)
    admin["active"] = True  # make sure the account is active
    # Remove any leftover lock/reset token
    admin.pop("reset_token", None)

    # Backup
    backup = usr_f.with_suffix(".json.bak")
    try:
        with open(backup, "w", encoding="utf-8") as f:
            json.dump(users if isinstance(users, list) else {"users": users_list},
                       f, indent=2, ensure_ascii=False)
        print(f"Backup: {backup}")
    except Exception as e:
        print(f"WARNING: could not create a backup: {e}")

    # Save users.json
    try:
        with open(usr_f, "w", encoding="utf-8") as f:
            if isinstance(users, list):
                json.dump(users_list, f, indent=2, ensure_ascii=False)
            else:
                users["users"] = users_list
                json.dump(users, f, indent=2, ensure_ascii=False)
        print(f"\n✓ Admin password reset: {admin.get('username')} / {new_pw}")
        print(f"✓ Saved: {usr_f}")
        print("\nRESTART Python (webapp.py) for the change to take effect!")
    except Exception as e:
        print(f"WRITE ERROR: {e}")
        sys.exit(6)


if __name__ == "__main__":
    main()