ddd15cae0f
Steps 3–7 of the migration plan: - models.py: Pydantic request/response types - deps.py: shared state, JWT-based auth helpers, rate limiting - server.py: FastAPI app with CORS + gzip - routers/auth.py: login, logout, /api/me, reset-password, register - routers/invites.py: GET/POST /api/invites - routers/admin.py: user listing, suspend/unsuspend, delete, access flags, reset-password-code - cli.py: `bincio-auth init` (creates DB + admin + JWT secret) and `bincio-auth serve` Cookie carries a signed JWT (HS256); consumers validate locally with shared secret.
119 lines
3.6 KiB
Python
119 lines
3.6 KiB
Python
"""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})
|