Merge pull request #504 from Fr1ngg/bedolaga/-docs

Enable API key auth in web API docs
This commit is contained in:
Egor
2025-09-28 02:59:25 +03:00
committed by GitHub
11 changed files with 58 additions and 52 deletions

View File

@@ -28,6 +28,7 @@ def create_web_api_app() -> FastAPI:
docs_url=docs_config.get("docs_url"),
redoc_url=docs_config.get("redoc_url"),
openapi_url=docs_config.get("openapi_url"),
swagger_ui_parameters={"persistAuthorization": True},
)
allowed_origins = settings.get_web_api_allowed_origins()

View File

@@ -2,8 +2,8 @@ from __future__ import annotations
from typing import AsyncGenerator
from fastapi import Depends, HTTPException, Request, status
from fastapi.security.utils import get_authorization_scheme_param
from fastapi import Depends, HTTPException, Request, Security, status
from fastapi.security import APIKeyHeader
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.database import AsyncSessionLocal
@@ -11,6 +11,9 @@ from app.database.models import WebApiToken
from app.services.web_api_token_service import web_api_token_service
api_key_header_scheme = APIKeyHeader(name="X-API-Key", auto_error=False)
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
@@ -21,15 +24,17 @@ async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
async def require_api_token(
request: Request,
api_key_header: str | None = Security(api_key_header_scheme),
db: AsyncSession = Depends(get_db_session),
) -> WebApiToken:
api_key = request.headers.get("X-API-Key")
api_key = api_key_header
if not api_key:
authorization = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
if scheme.lower() == "bearer" and param:
api_key = param
if authorization:
scheme, _, credentials = authorization.partition(" ")
if scheme.lower() == "bearer" and credentials:
api_key = credentials
if not api_key:
raise HTTPException(

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, Query, Security, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.system_settings_service import bot_configuration_service
@@ -100,7 +100,7 @@ def _serialize_definition(definition, include_choices: bool = True) -> SettingDe
@router.get("/categories", response_model=list[SettingCategorySummary])
async def list_categories(
_: object = Depends(require_api_token),
_: object = Security(require_api_token),
) -> list[SettingCategorySummary]:
categories = bot_configuration_service.get_categories()
return [
@@ -111,7 +111,7 @@ async def list_categories(
@router.get("", response_model=list[SettingDefinition])
async def list_settings(
_: object = Depends(require_api_token),
_: object = Security(require_api_token),
category: Optional[str] = Query(default=None, alias="category_key"),
) -> list[SettingDefinition]:
items: list[SettingDefinition] = []
@@ -130,7 +130,7 @@ async def list_settings(
@router.get("/{key}", response_model=SettingDefinition)
async def get_setting(
key: str,
_: object = Depends(require_api_token),
_: object = Security(require_api_token),
) -> SettingDefinition:
try:
definition = bot_configuration_service.get_definition(key)
@@ -144,7 +144,7 @@ async def get_setting(
async def update_setting(
key: str,
payload: SettingUpdateRequest,
_: object = Depends(require_api_token),
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> SettingDefinition:
try:
@@ -162,7 +162,7 @@ async def update_setting(
@router.delete("/{key}", response_model=SettingDefinition)
async def reset_setting(
key: str,
_: object = Depends(require_api_token),
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> SettingDefinition:
try:

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Security
from app.config import settings
from app.services.version_service import version_service
@@ -12,7 +12,7 @@ router = APIRouter()
@router.get("/health", tags=["health"], response_model=HealthCheckResponse)
async def health_check(_: object = Depends(require_api_token)) -> HealthCheckResponse:
async def health_check(_: object = Security(require_api_token)) -> HealthCheckResponse:
return HealthCheckResponse(
status="ok",
api_version=settings.WEB_API_VERSION,

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Response, status
from fastapi import APIRouter, Depends, HTTPException, Response, Security, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.crud.promo_group import (
@@ -56,7 +56,7 @@ def _serialize(group: PromoGroup, members_count: int = 0) -> PromoGroupResponse:
@router.get("", response_model=list[PromoGroupResponse])
async def list_promo_groups(
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> list[PromoGroupResponse]:
groups_with_counts = await get_promo_groups_with_counts(db)
@@ -66,7 +66,7 @@ async def list_promo_groups(
@router.get("/{group_id}", response_model=PromoGroupResponse)
async def get_promo_group(
group_id: int,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> PromoGroupResponse:
group = await get_promo_group_by_id(db, group_id)
@@ -80,7 +80,7 @@ async def get_promo_group(
@router.post("", response_model=PromoGroupResponse, status_code=status.HTTP_201_CREATED)
async def create_promo_group_endpoint(
payload: PromoGroupCreateRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> PromoGroupResponse:
group = await create_promo_group(
@@ -100,7 +100,7 @@ async def create_promo_group_endpoint(
async def update_promo_group_endpoint(
group_id: int,
payload: PromoGroupUpdateRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> PromoGroupResponse:
group = await get_promo_group_by_id(db, group_id)
@@ -125,7 +125,7 @@ async def update_promo_group_endpoint(
@router.delete("/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_promo_group_endpoint(
group_id: int,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> Response:
group = await get_promo_group_by_id(db, group_id)

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import datetime
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Security
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -24,7 +24,7 @@ router = APIRouter()
@router.get("/overview")
async def stats_overview(
_: object = Depends(require_api_token),
_: object = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> dict[str, object]:
total_users = await db.scalar(select(func.count()).select_from(User)) or 0

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, Query, Security, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -69,7 +69,7 @@ async def _get_subscription(db: AsyncSession, subscription_id: int) -> Subscript
@router.get("", response_model=list[SubscriptionResponse])
async def list_subscriptions(
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
@@ -95,7 +95,7 @@ async def list_subscriptions(
@router.get("/{subscription_id}", response_model=SubscriptionResponse)
async def get_subscription(
subscription_id: int,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> SubscriptionResponse:
subscription = await _get_subscription(db, subscription_id)
@@ -105,7 +105,7 @@ async def get_subscription(
@router.post("", response_model=SubscriptionResponse, status_code=status.HTTP_201_CREATED)
async def create_subscription(
payload: SubscriptionCreateRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> SubscriptionResponse:
existing = await get_subscription_by_user_id(db, payload.user_id)
@@ -141,7 +141,7 @@ async def create_subscription(
async def extend_subscription_endpoint(
subscription_id: int,
payload: SubscriptionExtendRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> SubscriptionResponse:
subscription = await _get_subscription(db, subscription_id)
@@ -154,7 +154,7 @@ async def extend_subscription_endpoint(
async def add_subscription_traffic_endpoint(
subscription_id: int,
payload: SubscriptionTrafficRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> SubscriptionResponse:
subscription = await _get_subscription(db, subscription_id)
@@ -167,7 +167,7 @@ async def add_subscription_traffic_endpoint(
async def add_subscription_devices_endpoint(
subscription_id: int,
payload: SubscriptionDevicesRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> SubscriptionResponse:
subscription = await _get_subscription(db, subscription_id)
@@ -180,7 +180,7 @@ async def add_subscription_devices_endpoint(
async def add_subscription_squad_endpoint(
subscription_id: int,
payload: SubscriptionSquadRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> SubscriptionResponse:
if not payload.squad_uuid:
@@ -196,7 +196,7 @@ async def add_subscription_squad_endpoint(
async def remove_subscription_squad_endpoint(
subscription_id: int,
squad_uuid: str,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> SubscriptionResponse:
subscription = await _get_subscription(db, subscription_id)

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import datetime
from typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, Query, Security, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.crud.ticket import TicketCRUD
@@ -56,7 +56,7 @@ def _serialize_ticket(ticket: Ticket, include_messages: bool = False) -> TicketR
@router.get("", response_model=list[TicketResponse])
async def list_tickets(
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
@@ -89,7 +89,7 @@ async def list_tickets(
@router.get("/{ticket_id}", response_model=TicketResponse)
async def get_ticket(
ticket_id: int,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> TicketResponse:
ticket = await TicketCRUD.get_ticket_by_id(db, ticket_id, load_messages=True, load_user=False)
@@ -102,7 +102,7 @@ async def get_ticket(
async def update_ticket_status(
ticket_id: int,
payload: TicketStatusUpdateRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> TicketResponse:
try:
@@ -123,7 +123,7 @@ async def update_ticket_status(
async def update_ticket_priority(
ticket_id: int,
payload: TicketPriorityUpdateRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> TicketResponse:
allowed_priorities = {"low", "normal", "high", "urgent"}
@@ -146,7 +146,7 @@ async def update_ticket_priority(
async def update_reply_block(
ticket_id: int,
payload: TicketReplyBlockRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> TicketResponse:
until = payload.until
@@ -169,7 +169,7 @@ async def update_reply_block(
@router.delete("/{ticket_id}/reply-block", response_model=TicketResponse)
async def clear_reply_block(
ticket_id: int,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> TicketResponse:
success = await TicketCRUD.set_user_reply_block(

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Response, status
from fastapi import APIRouter, Depends, HTTPException, Response, Security, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.crud.web_api_token import (
@@ -35,7 +35,7 @@ def _serialize(token: WebApiToken) -> TokenResponse:
@router.get("", response_model=list[TokenResponse])
async def get_tokens(
_: WebApiToken = Depends(require_api_token),
_: WebApiToken = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> list[TokenResponse]:
tokens = await list_tokens(db, include_inactive=True)
@@ -45,7 +45,7 @@ async def get_tokens(
@router.post("", response_model=TokenCreateResponse, status_code=status.HTTP_201_CREATED)
async def create_token(
payload: TokenCreateRequest,
actor: WebApiToken = Depends(require_api_token),
actor: WebApiToken = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> TokenCreateResponse:
token_value, token = await web_api_token_service.create_token(
@@ -65,7 +65,7 @@ async def create_token(
@router.post("/{token_id}/revoke", response_model=TokenResponse)
async def revoke_token(
token_id: int,
_: WebApiToken = Depends(require_api_token),
_: WebApiToken = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> TokenResponse:
token = await get_token_by_id(db, token_id)
@@ -80,7 +80,7 @@ async def revoke_token(
@router.post("/{token_id}/activate", response_model=TokenResponse)
async def activate_token(
token_id: int,
_: WebApiToken = Depends(require_api_token),
_: WebApiToken = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> TokenResponse:
token = await get_token_by_id(db, token_id)
@@ -95,7 +95,7 @@ async def activate_token(
@router.delete("/{token_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_token_endpoint(
token_id: int,
_: WebApiToken = Depends(require_api_token),
_: WebApiToken = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> Response:
token = await get_token_by_id(db, token_id)

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import datetime
from typing import Any, Optional
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, Query, Security
from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -33,7 +33,7 @@ def _serialize(transaction: Transaction) -> TransactionResponse:
@router.get("", response_model=TransactionListResponse)
async def list_transactions(
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, Query, Security, status
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -111,7 +111,7 @@ def _apply_search_filter(query, search: str):
@router.get("", response_model=UserListResponse)
async def list_users(
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
@@ -155,7 +155,7 @@ async def list_users(
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> UserResponse:
user = await get_user_by_id(db, user_id)
@@ -168,7 +168,7 @@ async def get_user(
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user_endpoint(
payload: UserCreateRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> UserResponse:
existing = await get_user_by_telegram_id(db, payload.telegram_id)
@@ -199,7 +199,7 @@ async def create_user_endpoint(
async def update_user_endpoint(
user_id: int,
payload: UserUpdateRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> UserResponse:
user = await get_user_by_id(db, user_id)
@@ -252,7 +252,7 @@ async def update_user_endpoint(
async def update_balance(
user_id: int,
payload: BalanceUpdateRequest,
_: Any = Depends(require_api_token),
_: Any = Security(require_api_token),
db: AsyncSession = Depends(get_db_session),
) -> UserResponse:
if payload.amount_kopeks == 0: