Префикс /api/v1, operationId = "<tag>_<name>", чтобы Dart-клиент получил методы вроде accountsList, а не list_accounts_api_v1_accounts_get. Ошибки — application/problem+json. Auth: access-JWT на час + refresh на 30 дней, который хранится хэшем и ротируется, логин ограничен по частоте в памяти. Деньги в JSON — всегда позиционные строки (api/schemas/common.py): float в проводе потерял бы копейки, которые NUMERIC(24,10) бережёт. Тестовый harness поднимает свой Postgres через pg_ctl (pytest-postgresql), мигрирует его один раз на сессию и усекает таблицы после каждого теста.
52 lines
1.6 KiB
Python
52 lines
1.6 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncIterator
|
|
from typing import Annotated
|
|
|
|
from fastapi import Depends, Request
|
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from fintracker.api.errors import Problem
|
|
from fintracker.api.security import decode_access_token
|
|
from fintracker.config import Settings, get_settings
|
|
from fintracker.db import get_sessionmaker
|
|
from fintracker.models import AppUser
|
|
|
|
_bearer = HTTPBearer(auto_error=False)
|
|
|
|
|
|
async def get_session() -> AsyncIterator[AsyncSession]:
|
|
async with get_sessionmaker()() as session:
|
|
yield session
|
|
|
|
|
|
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
|
SettingsDep = Annotated[Settings, Depends(get_settings)]
|
|
|
|
|
|
async def get_current_user(
|
|
session: SessionDep,
|
|
settings: SettingsDep,
|
|
creds: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)],
|
|
) -> AppUser:
|
|
if creds is None:
|
|
raise Problem(401, "Unauthorized", "Missing bearer token")
|
|
user_id = decode_access_token(creds.credentials, settings)
|
|
if user_id is None:
|
|
raise Problem(401, "Unauthorized", "Invalid or expired token")
|
|
user = await session.get(AppUser, user_id)
|
|
if user is None:
|
|
raise Problem(401, "Unauthorized", "Unknown user")
|
|
return user
|
|
|
|
|
|
CurrentUser = Annotated[AppUser, Depends(get_current_user)]
|
|
|
|
|
|
def client_ip(request: Request) -> str:
|
|
fwd = request.headers.get("x-forwarded-for")
|
|
if fwd:
|
|
return fwd.split(",")[0].strip()
|
|
return request.client.host if request.client else "unknown"
|