feat(api): FastAPI — auth, RFC 7807 и роуты фазы 1
Префикс /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), мигрирует его один раз на сессию и усекает таблицы после каждого теста.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
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"
|
||||
Reference in New Issue
Block a user