import os from pathlib import Path import click from sqlalchemy.exc import IntegrityError from sqlalchemy import select from l4d2web.auth import hash_password, validate_new_password from l4d2web.db import session_scope from l4d2web.models import Job, Overlay, User from l4d2web.services.overlay_creation import ( create_overlay_directory, generate_overlay_path, ) @click.command("promote-admin") @click.argument("username") def promote_admin(username: str) -> None: with session_scope() as db: user = db.scalar(select(User).where(User.username == username)) if user is None: raise click.ClickException("user not found") user.admin = True @click.command("create-user") @click.argument("username") @click.option("--admin", is_flag=True, default=False) def create_user(username: str, admin: bool) -> None: password = os.getenv("LEFT4ME_ADMIN_PASSWORD") if password is None: password = click.prompt("Password", hide_input=True, confirmation_prompt=True) policy_error = validate_new_password(password) if policy_error is not None: raise click.ClickException(policy_error) try: with session_scope() as db: existing = db.scalar(select(User).where(User.username == username)) if existing is not None: raise click.ClickException("user already exists") db.add(User(username=username, password_digest=hash_password(password), admin=admin)) except IntegrityError as exc: raise click.ClickException("user already exists") from exc click.echo(f"created user {username}") @click.command("seed-script-overlays") @click.argument( "directory", type=click.Path(exists=True, file_okay=False, path_type=Path), ) def seed_script_overlays(directory: Path) -> None: """Upsert one system-wide script overlay per *.sh file in DIRECTORY. Overlay name = filename stem; user_id stays NULL. Existing rows by name have their script refreshed in place. Hard-errors if a name collides with a non-script overlay. """ sh_files = sorted(p for p in directory.glob("*.sh") if p.stem) if not sh_files: click.echo(f"no *.sh files in {directory}", err=True) return with session_scope() as db: for sh in sh_files: name = sh.stem content = sh.read_text() existing = db.scalar( select(Overlay).where(Overlay.name == name, Overlay.user_id.is_(None)) ) if existing is not None: if existing.type != "script": raise click.ClickException( f"overlay {name!r} exists but is type={existing.type!r}, not script" ) existing.script = content click.echo(f"updated {name} (id={existing.id})") else: overlay = Overlay( name=name, path="", type="script", user_id=None, script=content ) db.add(overlay) db.flush() overlay.path = generate_overlay_path(overlay.id) db.flush() create_overlay_directory(overlay) click.echo(f"created {name} (id={overlay.id})") @click.command("workshop-refresh") def workshop_refresh() -> None: """Enqueue a global workshop refresh job. Idempotent: if a refresh is already queued or running, prints its id and exits 0.""" with session_scope() as db: existing = db.scalar( select(Job) .where( Job.operation == "refresh_workshop_items", Job.state.in_(("queued", "running", "cancelling")), ) .order_by(Job.id.desc()) .limit(1) ) if existing is not None: click.echo( f"refresh_workshop_items job {existing.id} already {existing.state}" ) return job = Job( user_id=None, server_id=None, operation="refresh_workshop_items", state="queued", ) db.add(job) db.flush() click.echo(f"enqueued refresh_workshop_items job {job.id}") def register_cli(app) -> None: app.cli.add_command(promote_admin) app.cli.add_command(create_user) app.cli.add_command(seed_script_overlays) app.cli.add_command(workshop_refresh)