8375d7ecc5
- Multi-channel subscription enforcement via middleware, events, and cabinet API - 3-layer cache architecture: Redis -> PostgreSQL -> rate-limited Telegram API - ChatMemberUpdated event-driven tracking with automatic VPN access control - Admin management via bot FSM handler and REST API with full CRUD - Channel ID normalization: @username resolved to numeric ID at creation time - Fail-closed error handling: API errors deny access (security-first) - Background reconciliation with keyset pagination (100 per batch) - Per-user rate limiting on subscription check button (5s cooldown) - Redis connection pooling via cache singleton (no per-request connections) - Database: channel_id index, multi-row upsert optimization - Localization: en, ru, zh, fa, ua translations for all new strings - Frontend blocking UI with channel list and subscription status - Admin channel management page with toggle, delete, and create
96 lines
3.4 KiB
Python
96 lines
3.4 KiB
Python
"""Admin API for managing required channels."""
|
|
|
|
import structlog
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database.crud.required_channel import (
|
|
add_channel,
|
|
delete_channel,
|
|
get_all_channels,
|
|
toggle_channel,
|
|
update_channel,
|
|
)
|
|
from app.database.models import User
|
|
from app.services.channel_subscription_service import channel_subscription_service
|
|
|
|
from ..dependencies import get_cabinet_db, get_current_admin_user
|
|
from ..schemas.channel import (
|
|
ChannelCreateRequest,
|
|
ChannelListResponse,
|
|
ChannelResponse,
|
|
ChannelUpdateRequest,
|
|
)
|
|
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
router = APIRouter(prefix='/admin/channel-subscriptions', tags=['Cabinet Admin Channels'])
|
|
|
|
|
|
@router.get('', response_model=ChannelListResponse)
|
|
async def list_channels(
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
_admin: User = Depends(get_current_admin_user),
|
|
) -> ChannelListResponse:
|
|
channels = await get_all_channels(db)
|
|
return ChannelListResponse(
|
|
items=[ChannelResponse.model_validate(ch) for ch in channels],
|
|
total=len(channels),
|
|
)
|
|
|
|
|
|
@router.post('', response_model=ChannelResponse, status_code=201)
|
|
async def create_channel(
|
|
data: ChannelCreateRequest,
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
_admin: User = Depends(get_current_admin_user),
|
|
) -> ChannelResponse:
|
|
# NOTE: REST API does not resolve @username to numeric channel ID (no Bot instance available).
|
|
# The bot FSM handler is the primary creation path and resolves automatically.
|
|
# When using REST API, callers should provide numeric channel IDs (e.g. -1001234567890)
|
|
# to ensure ChatMemberUpdated event matching works correctly.
|
|
ch = await add_channel(db, channel_id=data.channel_id, channel_link=data.channel_link, title=data.title)
|
|
await channel_subscription_service.invalidate_channels_cache()
|
|
return ChannelResponse.model_validate(ch)
|
|
|
|
|
|
@router.patch('/{channel_db_id}', response_model=ChannelResponse)
|
|
async def update_channel_endpoint(
|
|
channel_db_id: int,
|
|
data: ChannelUpdateRequest,
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
_admin: User = Depends(get_current_admin_user),
|
|
) -> ChannelResponse:
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
ch = await update_channel(db, channel_db_id, **update_data)
|
|
if not ch:
|
|
raise HTTPException(status_code=404, detail='Channel not found')
|
|
await channel_subscription_service.invalidate_channels_cache()
|
|
return ChannelResponse.model_validate(ch)
|
|
|
|
|
|
@router.post('/{channel_db_id}/toggle', response_model=ChannelResponse)
|
|
async def toggle_channel_endpoint(
|
|
channel_db_id: int,
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
_admin: User = Depends(get_current_admin_user),
|
|
) -> ChannelResponse:
|
|
ch = await toggle_channel(db, channel_db_id)
|
|
if not ch:
|
|
raise HTTPException(status_code=404, detail='Channel not found')
|
|
await channel_subscription_service.invalidate_channels_cache()
|
|
return ChannelResponse.model_validate(ch)
|
|
|
|
|
|
@router.delete('/{channel_db_id}', status_code=204)
|
|
async def delete_channel_endpoint(
|
|
channel_db_id: int,
|
|
db: AsyncSession = Depends(get_cabinet_db),
|
|
_admin: User = Depends(get_current_admin_user),
|
|
) -> None:
|
|
ok = await delete_channel(db, channel_db_id)
|
|
if not ok:
|
|
raise HTTPException(status_code=404, detail='Channel not found')
|
|
await channel_subscription_service.invalidate_channels_cache()
|