113 lines
4.0 KiB
Python
113 lines
4.0 KiB
Python
import logging
|
|
from typing import List
|
|
|
|
from fastapi import Body, Depends, FastAPI, HTTPException, Request, status
|
|
from fastapi.responses import HTMLResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
from backend import actions
|
|
from backend.actions import ActionError
|
|
from backend.auth import authenticate_admin_user, get_current_admin, issue_token, list_manageable_users
|
|
from backend.models import ActionRequest, ActionResponse, LoginRequest, LoginResponse, UserStatus
|
|
from backend.settings import Settings, get_settings
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)s %(name)s - %(message)s",
|
|
)
|
|
logger = logging.getLogger("skd")
|
|
|
|
app = FastAPI(title="Safe Kiddo Daemon", version="1.0.0")
|
|
templates = Jinja2Templates(directory="backend/templates")
|
|
|
|
|
|
def validate_user(username: str, settings: Settings = Depends(get_settings)) -> str:
|
|
allowed = set(list_manageable_users(settings))
|
|
if username not in allowed:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User not allowed")
|
|
return username
|
|
|
|
|
|
@app.get("/health")
|
|
def health(settings: Settings = Depends(get_settings)) -> dict:
|
|
return {"status": "ok", "dry_run": settings.dry_run}
|
|
|
|
|
|
@app.post("/login", response_model=LoginResponse)
|
|
def login(payload: LoginRequest, settings: Settings = Depends(get_settings)) -> LoginResponse:
|
|
authenticate_admin_user(payload.username, payload.password, settings)
|
|
token = issue_token(payload.username, settings)
|
|
return LoginResponse(token=token, expires_in=settings.token_ttl_seconds)
|
|
|
|
|
|
@app.get("/users", response_model=List[UserStatus], dependencies=[Depends(get_current_admin)])
|
|
def users(settings: Settings = Depends(get_settings)) -> List[UserStatus]:
|
|
logged_in = set(actions.list_logged_in_users())
|
|
targets = list_manageable_users(settings)
|
|
return [UserStatus(user=user, logged_in=user in logged_in) for user in targets]
|
|
|
|
|
|
@app.post(
|
|
"/users/{username}/disable",
|
|
response_model=ActionResponse,
|
|
dependencies=[Depends(get_current_admin)],
|
|
)
|
|
def disable_user(
|
|
username: str = Depends(validate_user),
|
|
payload: ActionRequest | None = Body(default=None),
|
|
settings: Settings = Depends(get_settings),
|
|
) -> ActionResponse:
|
|
try:
|
|
steps = actions.disable_user(
|
|
username,
|
|
countdown=payload.countdown if payload else None,
|
|
sound=payload.sound if payload else None,
|
|
message=payload.message if payload else None,
|
|
)
|
|
except ActionError as exc:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
except Exception as exc: # pragma: no cover - safeguard
|
|
logger.exception("Failed to disable %s", username)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="failed") from exc
|
|
|
|
logged_in = username in actions.list_logged_in_users()
|
|
return ActionResponse(
|
|
user=username,
|
|
action="disable",
|
|
dry_run=settings.dry_run,
|
|
steps=steps,
|
|
logged_in=logged_in,
|
|
)
|
|
|
|
|
|
@app.post(
|
|
"/users/{username}/enable",
|
|
response_model=ActionResponse,
|
|
dependencies=[Depends(get_current_admin)],
|
|
)
|
|
def enable_user(
|
|
username: str = Depends(validate_user),
|
|
settings: Settings = Depends(get_settings),
|
|
) -> ActionResponse:
|
|
try:
|
|
steps = actions.enable_user(username)
|
|
except ActionError as exc:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
except Exception as exc: # pragma: no cover - safeguard
|
|
logger.exception("Failed to enable %s", username)
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="failed") from exc
|
|
|
|
logged_in = username in actions.list_logged_in_users()
|
|
return ActionResponse(
|
|
user=username,
|
|
action="enable",
|
|
dry_run=settings.dry_run,
|
|
steps=steps,
|
|
logged_in=logged_in,
|
|
)
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
def index(request: Request) -> HTMLResponse:
|
|
return templates.TemplateResponse("index.html", {"request": request})
|