"""Admin user-management endpoints (/api/admin/*).""" from __future__ import annotations from fastapi import APIRouter, Cookie, HTTPException from fastapi.responses import JSONResponse from bincio.auth import deps from bincio.auth.db import ( create_reset_code, delete_user, get_user, list_users, purge_expired_sessions, set_suspended, ) router = APIRouter() @router.get("/api/admin/users") async def admin_list_users(bincio_session: str | None = Cookie(default=None)) -> JSONResponse: deps._require_admin(bincio_session) users = list_users(deps._get_db()) return JSONResponse([{ "handle": u.handle, "display_name": u.display_name, "is_admin": u.is_admin, "wiki_access": u.wiki_access, "activity_access": u.activity_access, "suspended": u.suspended, "created_at": u.created_at, } for u in users]) @router.post("/api/admin/users/{handle}/reset-password-code") async def admin_reset_password_code( handle: str, bincio_session: str | None = Cookie(default=None), ) -> JSONResponse: admin = deps._require_admin(bincio_session) db = deps._get_db() if not get_user(db, handle): raise HTTPException(404, f"User '{handle}' not found") code = create_reset_code(db, handle, admin.handle) return JSONResponse({"ok": True, "code": code, "expires_in_hours": 24}) @router.post("/api/admin/users/{handle}/suspend") async def admin_suspend( handle: str, bincio_session: str | None = Cookie(default=None), ) -> JSONResponse: admin = deps._require_admin(bincio_session) if handle == admin.handle: raise HTTPException(400, "Cannot suspend yourself") db = deps._get_db() if not get_user(db, handle): raise HTTPException(404, "User not found") set_suspended(db, handle, True) purge_expired_sessions(db) return JSONResponse({"status": "suspended", "handle": handle}) @router.post("/api/admin/users/{handle}/unsuspend") async def admin_unsuspend( handle: str, bincio_session: str | None = Cookie(default=None), ) -> JSONResponse: deps._require_admin(bincio_session) db = deps._get_db() if not get_user(db, handle): raise HTTPException(404, "User not found") set_suspended(db, handle, False) return JSONResponse({"status": "unsuspended", "handle": handle}) @router.delete("/api/admin/users/{handle}") async def admin_delete_user( handle: str, bincio_session: str | None = Cookie(default=None), ) -> JSONResponse: admin = deps._require_admin(bincio_session) if handle == admin.handle: raise HTTPException(400, "Cannot delete your own account") db = deps._get_db() if not get_user(db, handle): raise HTTPException(404, "User not found") delete_user(db, handle) return JSONResponse({"ok": True, "handle": handle}) @router.patch("/api/admin/users/{handle}/access") async def admin_set_access( handle: str, body: dict, bincio_session: str | None = Cookie(default=None), ) -> JSONResponse: """Set wiki_access and/or activity_access flags for a user.""" deps._require_admin(bincio_session) db = deps._get_db() if not get_user(db, handle): raise HTTPException(404, "User not found") updates = [] params: list = [] for flag in ("wiki_access", "activity_access", "is_admin"): if flag in body: updates.append(f"{flag} = ?") params.append(int(bool(body[flag]))) if not updates: raise HTTPException(400, "No valid fields to update") params.append(handle) db.execute(f"UPDATE users SET {', '.join(updates)} WHERE handle = ?", params) db.commit() return JSONResponse({"ok": True})