"""POST /api/auth/login|logout; GET /api/auth/me""" from __future__ import annotations import logging from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from pydantic import BaseModel from web.auth import create_jwt, require_auth from web.deps import get_config from web.login_guard import get_guard, get_real_ip router = APIRouter() _COOKIE = "ttb_session" audit = logging.getLogger("audit") class LoginRequest(BaseModel): token: str @router.post("/login") async def login(body: LoginRequest, request: Request, response: Response): """Exchange an invite token for a JWT session cookie.""" from token_store import verify_token config = get_config() token_file = config.web_token_file if config else "invite_tokens.json" expire_hours = config.web_jwt_expire_hours if config else 8 ip = get_real_ip(request) get_guard().check(ip) record = verify_token(body.token, token_file) if record is None: get_guard().record_failure(ip) audit.info("auth.login ip=%s success=False", ip) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") label: str = record["label"] admin: bool = record.get("admin", False) get_guard().record_success(ip) audit.info("auth.login ip=%s success=True label=%s", ip, label) jwt_token = create_jwt(label, admin=admin, expire_hours=expire_hours) response.set_cookie( _COOKIE, jwt_token, httponly=True, secure=True, samesite="strict", max_age=expire_hours * 3600, ) return {"label": label, "admin": admin} @router.post("/logout") async def logout(response: Response): """Clear the session cookie.""" response.delete_cookie(_COOKIE) return {"ok": True} @router.get("/me") async def me(user: dict = Depends(require_auth)): """Return current user info.""" return {"label": user["sub"], "admin": user.get("admin", False)}