129 lines
4.2 KiB
Python
129 lines
4.2 KiB
Python
import getpass
|
|
import hashlib
|
|
import os
|
|
import secrets
|
|
import tomllib
|
|
from typing import Optional
|
|
|
|
import tomli_w
|
|
|
|
USERS_PATH = os.path.expanduser("~/.config/tueit-transcriber/users.toml")
|
|
|
|
# In-memory session store: token → username
|
|
# Users must re-login after server restart — acceptable for a desktop app.
|
|
_sessions: dict[str, str] = {}
|
|
|
|
|
|
def _hash_password(password: str) -> str:
|
|
salt = secrets.token_hex(16)
|
|
key = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 200_000).hex()
|
|
return f"{salt}:{key}"
|
|
|
|
|
|
def _verify_password(password: str, stored: str) -> bool:
|
|
try:
|
|
salt, key = stored.split(":", 1)
|
|
except ValueError:
|
|
return False
|
|
new_key = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 200_000).hex()
|
|
return secrets.compare_digest(new_key, key)
|
|
|
|
|
|
# ── User store ─────────────────────────────────────────────────────────────────
|
|
|
|
def has_users() -> bool:
|
|
return bool(_load_users())
|
|
|
|
|
|
def _load_users() -> dict:
|
|
if not os.path.exists(USERS_PATH):
|
|
return {}
|
|
with open(USERS_PATH, "rb") as f:
|
|
return tomllib.load(f).get("users", {})
|
|
|
|
|
|
def _save_users(users: dict):
|
|
os.makedirs(os.path.dirname(USERS_PATH), exist_ok=True)
|
|
with open(USERS_PATH, "wb") as f:
|
|
tomli_w.dump({"users": users}, f)
|
|
|
|
|
|
def create_user(username: str, password: str, output_dir: str, is_admin: bool = False):
|
|
users = _load_users()
|
|
users[username] = {
|
|
"password_hash": _hash_password(password),
|
|
"output_dir": output_dir,
|
|
"is_admin": is_admin,
|
|
}
|
|
_save_users(users)
|
|
|
|
|
|
# ── Session management ─────────────────────────────────────────────────────────
|
|
|
|
def authenticate(username: str, password: str) -> Optional[str]:
|
|
"""Verify credentials. Returns a session token on success, None on failure."""
|
|
users = _load_users()
|
|
user = users.get(username)
|
|
if not user:
|
|
return None
|
|
if not _verify_password(password, user["password_hash"]):
|
|
return None
|
|
token = secrets.token_urlsafe(32)
|
|
_sessions[token] = username
|
|
return token
|
|
|
|
|
|
def get_user_for_token(token: str) -> Optional[dict]:
|
|
"""Return user info dict for a valid token, or None."""
|
|
username = _sessions.get(token)
|
|
if not username:
|
|
return None
|
|
users = _load_users()
|
|
user = users.get(username)
|
|
if not user:
|
|
return None
|
|
return {
|
|
"username": username,
|
|
"output_dir": user["output_dir"],
|
|
"is_admin": user.get("is_admin", False),
|
|
}
|
|
|
|
|
|
def invalidate_token(token: str):
|
|
_sessions.pop(token, None)
|
|
|
|
|
|
# ── First-run setup wizard ─────────────────────────────────────────────────────
|
|
|
|
def setup_wizard():
|
|
"""Interactive console setup. Runs when no users exist yet."""
|
|
print("\n=== tüit Transkriptor — Ersteinrichtung ===\n")
|
|
print("Bitte richte den ersten Nutzer ein (wird Administrator).\n")
|
|
|
|
while True:
|
|
username = input("Benutzername: ").strip()
|
|
if username:
|
|
break
|
|
print("Benutzername darf nicht leer sein.")
|
|
|
|
while True:
|
|
password = getpass.getpass("Passwort: ")
|
|
confirm = getpass.getpass("Passwort bestätigen: ")
|
|
if password != confirm:
|
|
print("Passwörter stimmen nicht überein.")
|
|
continue
|
|
if len(password) < 6:
|
|
print("Passwort muss mindestens 6 Zeichen lang sein.")
|
|
continue
|
|
break
|
|
|
|
default_dir = os.path.expanduser(f"~/Transkripte/{username}")
|
|
answer = input(f"Transkripte speichern unter [{default_dir}]: ").strip()
|
|
output_dir = answer if answer else default_dir
|
|
|
|
create_user(username, password, output_dir, is_admin=True)
|
|
|
|
print(f"\nNutzer '{username}' wurde angelegt.")
|
|
print(f"Transkripte werden gespeichert unter: {output_dir}")
|
|
print("\nWeitere Nutzer können später über die Web-Oberfläche hinzugefügt werden.\n")
|