85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
"""Endpoints that stay reachable without a token.
|
|
|
|
`bin/wait-deploy` polls /api/version from whatever machine you pushed from, and
|
|
the browser needs /api/login before it has a session, so these routes stay
|
|
outside the protected API router.
|
|
"""
|
|
import os
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import APIRouter, HTTPException, Request, Response, status
|
|
from fastapi.responses import FileResponse
|
|
from pydantic import BaseModel
|
|
|
|
from app.api.deps import (
|
|
SESSION_COOKIE,
|
|
SESSION_MAX_AGE,
|
|
configured_token,
|
|
create_session,
|
|
password_matches,
|
|
token_matches,
|
|
)
|
|
from app.api.captures import capture_path, delete_capture
|
|
|
|
router = APIRouter(prefix="/api")
|
|
|
|
# Coolify injects the deployed commit; absent when running locally.
|
|
SOURCE_COMMIT = os.environ.get("SOURCE_COMMIT", "dev")
|
|
STARTED_AT = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
password: str
|
|
|
|
|
|
@router.get("/health")
|
|
def health():
|
|
return {"status": "ok", "service": "chart"}
|
|
|
|
|
|
@router.get("/version")
|
|
def version():
|
|
return {"commit": SOURCE_COMMIT, "started_at": STARTED_AT}
|
|
|
|
|
|
@router.post("/login", status_code=status.HTTP_204_NO_CONTENT)
|
|
def login(credentials: LoginRequest, request: Request, response: Response):
|
|
presented_token = request.headers.get("x-chart-token", "")
|
|
token_login = bool(configured_token(request.app)) and token_matches(
|
|
request.app, presented_token
|
|
)
|
|
if not token_login and not password_matches(request.app, credentials.password):
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Incorrect password")
|
|
forwarded_proto = request.headers.get("x-forwarded-proto", "").split(",", 1)[0]
|
|
response.set_cookie(
|
|
SESSION_COOKIE,
|
|
create_session(request.app),
|
|
max_age=SESSION_MAX_AGE,
|
|
httponly=True,
|
|
secure=request.url.scheme == "https" or forwarded_proto == "https",
|
|
samesite="strict",
|
|
path="/",
|
|
)
|
|
|
|
|
|
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
|
|
def logout(response: Response):
|
|
response.delete_cookie(SESSION_COOKIE, path="/", httponly=True, samesite="strict")
|
|
|
|
|
|
@router.get("/debug/captures/{capture_id}")
|
|
def get_debug_capture(capture_id: str):
|
|
"""A short-lived diagnostic screenshot shared by its unguessable id."""
|
|
path = capture_path(capture_id, ".png")
|
|
if path is None:
|
|
raise HTTPException(404, "Capture not found")
|
|
return FileResponse(path, media_type="image/png")
|
|
|
|
|
|
@router.delete("/debug/captures/{capture_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_debug_capture(capture_id: str):
|
|
"""Erase a public diagnostic screenshot after inspection."""
|
|
if not delete_capture(capture_id):
|
|
raise HTTPException(404, "Capture not found")
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|