first commit

This commit is contained in:
2025-12-15 12:07:32 +01:00
commit 6f49528bbd
21 changed files with 1258 additions and 0 deletions

112
backend/app.py Normal file
View File

@ -0,0 +1,112 @@
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})