Reveiw
This commit is contained in:
parent
ad7968dadd
commit
5bf8e752e7
22 changed files with 81 additions and 112 deletions
|
|
@ -2,11 +2,10 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
from .db import db_connection
|
||||
from .models import _canonical_uuidv4
|
||||
|
||||
# Single generic detail used for ALL 401 responses. Per spec, the response
|
||||
# must not distinguish between missing / malformed / unknown tokens — that
|
||||
|
|
@ -21,26 +20,28 @@ def _unauthorized() -> HTTPException:
|
|||
)
|
||||
|
||||
|
||||
def get_current_user(request: Request) -> str:
|
||||
"""Dependency that extracts and validates a Bearer token, returns user_id."""
|
||||
def extract_bearer_token(request: Request) -> str | None:
|
||||
"""Return the raw Bearer token from the Authorization header, or None."""
|
||||
auth_header = request.headers.get("Authorization") or request.headers.get(
|
||||
"authorization"
|
||||
)
|
||||
if not auth_header:
|
||||
raise _unauthorized()
|
||||
|
||||
return None
|
||||
parts = auth_header.split()
|
||||
if len(parts) != 2 or parts[0].lower() != "bearer":
|
||||
raise _unauthorized()
|
||||
if len(parts) == 2 and parts[0].lower() == "bearer":
|
||||
return parts[1]
|
||||
return None
|
||||
|
||||
token = parts[1]
|
||||
|
||||
def get_current_user(request: Request) -> str:
|
||||
"""Dependency that extracts and validates a Bearer token, returns user_id."""
|
||||
token = extract_bearer_token(request)
|
||||
if token is None:
|
||||
raise _unauthorized()
|
||||
|
||||
try:
|
||||
u = uuid.UUID(token)
|
||||
if u.version != 4:
|
||||
raise ValueError("Not v4")
|
||||
token = str(u)
|
||||
except (ValueError, AttributeError):
|
||||
token = _canonical_uuidv4(token)
|
||||
except ValueError:
|
||||
raise _unauthorized()
|
||||
|
||||
with db_connection() as conn:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from fastapi import Request, Response
|
|||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
from .auth import extract_bearer_token
|
||||
|
||||
PAYLOAD_LIMIT_BYTES = 2 * 1024 * 1024 # 2 MiB
|
||||
|
||||
_TOO_LARGE_BODY = json.dumps(
|
||||
|
|
@ -20,12 +22,7 @@ _TOO_LARGE_BODY = json.dumps(
|
|||
|
||||
def _get_token_or_ip(request: Request) -> str:
|
||||
"""Key function for rate limiting: use Bearer token if present, else IP."""
|
||||
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
||||
if auth:
|
||||
parts = auth.split()
|
||||
if len(parts) == 2 and parts[0].lower() == "bearer":
|
||||
return parts[1]
|
||||
return get_remote_address(request)
|
||||
return extract_bearer_token(request) or get_remote_address(request)
|
||||
|
||||
|
||||
limiter = Limiter(key_func=_get_token_or_ip, default_limits=[])
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ from hashlib import sha256
|
|||
import structlog
|
||||
from fastapi import Request, Response
|
||||
|
||||
from .auth import extract_bearer_token
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
"""Configure structlog for JSON output."""
|
||||
|
|
@ -38,12 +40,8 @@ async def request_logging_middleware(request: Request, call_next) -> Response:
|
|||
structlog.contextvars.bind_contextvars(request_id=request_id)
|
||||
|
||||
# Extract user_id from Authorization header for logging (no DB call here)
|
||||
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
||||
user_id: str | None = None
|
||||
if auth:
|
||||
parts = auth.split()
|
||||
if len(parts) == 2 and parts[0].lower() == "bearer":
|
||||
user_id = token_log_id(parts[1])
|
||||
token = extract_bearer_token(request)
|
||||
user_id = token_log_id(token) if token else None
|
||||
|
||||
start = time.monotonic()
|
||||
response = await call_next(request)
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ def create_app() -> FastAPI:
|
|||
if field
|
||||
)
|
||||
if fields:
|
||||
detail_str = "Validation failed for: " + ", ".join(f for f in fields if f)
|
||||
detail_str = "Validation failed for: " + ", ".join(fields)
|
||||
else:
|
||||
detail_str = "Validation failed"
|
||||
return JSONResponse(
|
||||
|
|
@ -116,7 +116,7 @@ def create_app() -> FastAPI:
|
|||
code = STATUS_CODE_MAP.get(exc.status_code, "server_error")
|
||||
detail = {"error": code, "detail": str(exc.detail)}
|
||||
|
||||
headers = getattr(exc, "headers", None) or {}
|
||||
headers = exc.headers or {}
|
||||
return JSONResponse(status_code=exc.status_code, content=detail, headers=headers)
|
||||
|
||||
# Generic 500 handler
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
-- Life Towers v4 initial schema.
|
||||
-- SQLite with WAL mode and foreign keys enabled at connection time.
|
||||
-- WAL mode, foreign keys, and busy_timeout are applied per-connection in
|
||||
-- db._apply_pragmas(), so they are not (and need not be) set here.
|
||||
-- All timestamps are unix epoch seconds (INTEGER).
|
||||
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at INTEGER NOT NULL,
|
||||
|
|
|
|||
|
|
@ -51,17 +51,6 @@ async def client(tmp_path: Path) -> AsyncGenerator[AsyncClient, None]:
|
|||
db_module._DB_PATH = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Health
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health(client: AsyncClient) -> None:
|
||||
resp = await client.get("/api/v1/health")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"status": "ok"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_spa_index_injects_absolute_open_graph_urls(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue