- Duplicate and delete from the line context menu.
- Copies shift ten bars right.
- Default names are up and down; copies become up 2, down 2, etc.
- Exact local data receipt time including seconds.
- Deployment timestamp removed.
- Test cleanup no longer deletes drawings created from your browser.
- JWT password session flow.
65 lines
2 KiB
Python
65 lines
2 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
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import APIRouter, HTTPException, Request, Response, status
|
|
from pydantic import BaseModel
|
|
|
|
from app.api.deps import (
|
|
SESSION_COOKIE,
|
|
SESSION_MAX_AGE,
|
|
configured_token,
|
|
create_session,
|
|
password_matches,
|
|
token_matches,
|
|
)
|
|
|
|
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")
|