root 42443e1f23 fix(cli): chdir nach lxc-frontend, damit pydantic-settings die .env findet
Beim Aufruf aus /root oder anderen Verzeichnissen scheiterte der CLI mit
PermissionError, weil pydantic-settings .env relativ zum Arbeitsverzeichnis
liest und z. B. /root/.env stat-en wollte (für deploy nicht lesbar).
2026-05-14 02:52:13 +00:00

171 lines
5.8 KiB
Python

"""CLI-Tool für Notfall-User-Verwaltung.
Aufruf am LXC (von überall, absoluter Pfad):
sudo -u deploy /var/www/voice-agent/lxc-frontend/.venv/bin/python \\
/var/www/voice-agent/lxc-frontend/app/cli.py <befehl> [...]
Bequemer ist der Wrapper (vom install.sh angelegt):
sudo voice-agent-admin <befehl> [...]
Befehle:
list — alle User auflisten
create <user> <pass> [--admin] — User anlegen
reset <user> <pass> — Passwort setzen
promote <user> — zum Admin machen
delete <user> — User löschen (nicht den letzten Admin)
verify <user> <pass> — Passwort testen (sagt nur OK / NEIN)
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
# Damit das Skript aus jedem Verzeichnis funktioniert:
# 1) lxc-frontend/ auf sys.path → `from app.…` Imports funktionieren
# 2) chdir nach lxc-frontend/ → pydantic-settings findet ./.env (statt z. B. /root/.env)
_LXC_FRONTEND = Path(__file__).resolve().parent.parent
if str(_LXC_FRONTEND) not in sys.path:
sys.path.insert(0, str(_LXC_FRONTEND))
os.chdir(_LXC_FRONTEND)
from sqlmodel import Session, select # noqa: E402
from app.core.auth import hash_password, verify_password # noqa: E402
from app.core.db import engine, init_db # noqa: E402
from app.core.migrate import run_migrations # noqa: E402
from app.models.user import User # noqa: E402
def _setup() -> None:
init_db()
run_migrations()
def cmd_list() -> int:
_setup()
with Session(engine) as s:
users = s.exec(select(User).order_by(User.id)).all()
if not users:
print("(keine User in der DB)")
return 0
print(f"{'ID':>4} {'USERNAME':<30} {'ADMIN':<6} {'HASH':<10} CREATED")
for u in users:
print(
f"{u.id:>4} {u.username:<30} {'ja' if u.is_admin else 'nein':<6} "
f"len={len(u.password_hash):<5} {u.created_at.isoformat(timespec='seconds')}"
)
return 0
def cmd_create(username: str, password: str, is_admin: bool) -> int:
_setup()
if len(password) < 8:
print("FEHLER: Passwort muss min. 8 Zeichen haben.", file=sys.stderr)
return 2
with Session(engine) as s:
if s.exec(select(User).where(User.username == username)).first():
print(f"FEHLER: User '{username}' existiert bereits.", file=sys.stderr)
return 2
u = User(username=username, password_hash=hash_password(password), is_admin=is_admin)
s.add(u)
s.commit()
s.refresh(u)
print(f"OK — User #{u.id} '{u.username}' angelegt (admin={u.is_admin}).")
return 0
def cmd_reset(username: str, password: str) -> int:
_setup()
if len(password) < 8:
print("FEHLER: Passwort muss min. 8 Zeichen haben.", file=sys.stderr)
return 2
with Session(engine) as s:
u = s.exec(select(User).where(User.username == username)).first()
if not u:
print(f"FEHLER: User '{username}' nicht gefunden.", file=sys.stderr)
return 2
u.password_hash = hash_password(password)
s.add(u)
s.commit()
print(f"OK — Passwort für '{username}' gesetzt.")
return 0
def cmd_promote(username: str) -> int:
_setup()
with Session(engine) as s:
u = s.exec(select(User).where(User.username == username)).first()
if not u:
print(f"FEHLER: User '{username}' nicht gefunden.", file=sys.stderr)
return 2
if u.is_admin:
print(f"OK — '{username}' war bereits Admin.")
return 0
u.is_admin = True
s.add(u)
s.commit()
print(f"OK — '{username}' ist jetzt Admin.")
return 0
def cmd_delete(username: str) -> int:
_setup()
with Session(engine) as s:
u = s.exec(select(User).where(User.username == username)).first()
if not u:
print(f"FEHLER: User '{username}' nicht gefunden.", file=sys.stderr)
return 2
if u.is_admin:
admins = s.exec(select(User).where(User.is_admin == True)).all() # noqa: E712
if len(admins) <= 1:
print("FEHLER: letzten Admin kann man nicht löschen.", file=sys.stderr)
return 2
s.delete(u)
s.commit()
print(f"OK — '{username}' gelöscht.")
return 0
def cmd_verify(username: str, password: str) -> int:
_setup()
with Session(engine) as s:
u = s.exec(select(User).where(User.username == username)).first()
if not u:
print(f"User '{username}' existiert nicht.")
return 1
if verify_password(password, u.password_hash):
print(f"OK — Passwort für '{username}' stimmt.")
return 0
print(f"NEIN — Passwort stimmt nicht. Hash-Länge in DB: {len(u.password_hash)}, Präfix: {u.password_hash[:7]}")
return 1
def main(argv: list[str]) -> int:
if len(argv) < 2:
print(__doc__)
return 1
cmd = argv[1]
try:
if cmd == "list":
return cmd_list()
if cmd == "create" and len(argv) >= 4:
return cmd_create(argv[2], argv[3], "--admin" in argv[4:])
if cmd == "reset" and len(argv) == 4:
return cmd_reset(argv[2], argv[3])
if cmd == "promote" and len(argv) == 3:
return cmd_promote(argv[2])
if cmd == "delete" and len(argv) == 3:
return cmd_delete(argv[2])
if cmd == "verify" and len(argv) == 4:
return cmd_verify(argv[2], argv[3])
except Exception as e: # noqa: BLE001
print(f"FEHLER: {e}", file=sys.stderr)
return 3
print(__doc__)
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv))