Files
remnawave-bedolaga-telegram…/app/cabinet/routes/admin_channels.py
T
Fringg 8375d7ecc5 feat: add multi-channel mandatory subscription system
- 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
2026-02-24 02:50:31 +03:00

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()