mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-01 07:42:30 +00:00
386 lines
14 KiB
Python
386 lines
14 KiB
Python
from __future__ import annotations
|
||
|
||
from typing import List, Optional
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
|
||
from sqlalchemy import func, or_, select
|
||
from sqlalchemy.orm import selectinload
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.database.crud.subscription import (
|
||
add_subscription_devices,
|
||
add_subscription_squad,
|
||
add_subscription_traffic,
|
||
extend_subscription,
|
||
get_subscription_by_user_id,
|
||
remove_subscription_squad,
|
||
)
|
||
from app.database.crud.user import (
|
||
add_user_balance,
|
||
create_user,
|
||
get_user_by_id,
|
||
update_user,
|
||
)
|
||
from app.database.models import Subscription, Transaction, User, UserStatus
|
||
from app.webapi.dependencies import get_db, require_permission
|
||
from app.webapi.schemas import (
|
||
Pagination,
|
||
SubscriptionDevicesRequest,
|
||
SubscriptionExtendRequest,
|
||
SubscriptionSchema,
|
||
SubscriptionSquadRequest,
|
||
SubscriptionTrafficRequest,
|
||
TransactionListResponse,
|
||
TransactionSchema,
|
||
UserBalanceUpdateRequest,
|
||
UserCreateRequest,
|
||
UserDetailResponse,
|
||
UserListItem,
|
||
UserListResponse,
|
||
UserStatusUpdateRequest,
|
||
UserUpdateRequest,
|
||
)
|
||
|
||
router = APIRouter(prefix="/users")
|
||
|
||
|
||
def _user_to_list_item(user: User) -> UserListItem:
|
||
subscription = user.subscription
|
||
return UserListItem(
|
||
id=user.id,
|
||
telegram_id=user.telegram_id,
|
||
username=user.username,
|
||
first_name=user.first_name,
|
||
last_name=user.last_name,
|
||
language=user.language,
|
||
status=user.status,
|
||
balance_kopeks=user.balance_kopeks,
|
||
referral_code=user.referral_code,
|
||
created_at=user.created_at,
|
||
updated_at=user.updated_at,
|
||
subscription_status=subscription.status if subscription else None,
|
||
subscription_end_date=subscription.end_date if subscription else None,
|
||
)
|
||
|
||
|
||
def _user_to_detail(user: User, transactions: List[Transaction]) -> UserDetailResponse:
|
||
subscription = user.subscription
|
||
return UserDetailResponse(
|
||
id=user.id,
|
||
telegram_id=user.telegram_id,
|
||
username=user.username,
|
||
first_name=user.first_name,
|
||
last_name=user.last_name,
|
||
language=user.language,
|
||
status=user.status,
|
||
balance_kopeks=user.balance_kopeks,
|
||
referred_by_id=user.referred_by_id,
|
||
referral_code=user.referral_code,
|
||
promo_group_id=user.promo_group_id,
|
||
created_at=user.created_at,
|
||
updated_at=user.updated_at,
|
||
subscription=SubscriptionSchema.model_validate(subscription)
|
||
if subscription
|
||
else None,
|
||
transactions=[TransactionSchema.model_validate(tx) for tx in transactions],
|
||
)
|
||
|
||
|
||
@router.get("", response_model=UserListResponse)
|
||
async def list_users(
|
||
limit: int = Query(50, ge=1, le=200),
|
||
offset: int = Query(0, ge=0),
|
||
status_filter: Optional[str] = Query(None, alias="status"),
|
||
search: Optional[str] = Query(None),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.users:read")),
|
||
) -> UserListResponse:
|
||
stmt = select(User).options(selectinload(User.subscription))
|
||
count_stmt = select(func.count(User.id))
|
||
|
||
filters = []
|
||
|
||
if status_filter:
|
||
valid_statuses = {status.value for status in UserStatus}
|
||
if status_filter not in valid_statuses:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail="Недопустимый статус фильтра",
|
||
)
|
||
filters.append(User.status == status_filter)
|
||
|
||
if search:
|
||
search = search.strip()
|
||
if search.startswith("@"):
|
||
search = search[1:]
|
||
if search.isdigit():
|
||
filters.append(User.telegram_id == int(search))
|
||
else:
|
||
like_pattern = f"%{search.lower()}%"
|
||
filters.append(
|
||
or_(
|
||
func.lower(User.username).like(like_pattern),
|
||
func.lower(User.first_name).like(like_pattern),
|
||
func.lower(User.last_name).like(like_pattern),
|
||
func.lower(User.referral_code).like(like_pattern),
|
||
)
|
||
)
|
||
|
||
for condition in filters:
|
||
stmt = stmt.where(condition)
|
||
count_stmt = count_stmt.where(condition)
|
||
|
||
stmt = stmt.order_by(User.created_at.desc()).offset(offset).limit(limit)
|
||
|
||
result = await db.execute(stmt)
|
||
users = result.scalars().unique().all()
|
||
|
||
total = await db.scalar(count_stmt) or 0
|
||
|
||
items = [_user_to_list_item(user) for user in users]
|
||
pagination = Pagination(total=total, limit=limit, offset=offset)
|
||
return UserListResponse(pagination=pagination, items=items)
|
||
|
||
|
||
@router.get("/{user_id}", response_model=UserDetailResponse)
|
||
async def get_user_detail(
|
||
user_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.users:read")),
|
||
) -> UserDetailResponse:
|
||
user = await get_user_by_id(db, user_id)
|
||
if not user:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден")
|
||
|
||
transactions_stmt = (
|
||
select(Transaction)
|
||
.where(Transaction.user_id == user.id)
|
||
.order_by(Transaction.created_at.desc())
|
||
.limit(50)
|
||
)
|
||
tx_result = await db.execute(transactions_stmt)
|
||
transactions = tx_result.scalars().all()
|
||
|
||
return _user_to_detail(user, transactions)
|
||
|
||
|
||
@router.post("", response_model=UserDetailResponse, status_code=status.HTTP_201_CREATED)
|
||
async def create_user_endpoint(
|
||
payload: UserCreateRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.users:write")),
|
||
) -> UserDetailResponse:
|
||
existing_stmt = select(User).where(User.telegram_id == payload.telegram_id)
|
||
existing = await db.scalar(existing_stmt)
|
||
if existing:
|
||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Пользователь уже существует")
|
||
|
||
user = await create_user(
|
||
db,
|
||
telegram_id=payload.telegram_id,
|
||
username=payload.username,
|
||
first_name=payload.first_name,
|
||
last_name=payload.last_name,
|
||
language=payload.language or "ru",
|
||
referred_by_id=payload.referred_by_id,
|
||
referral_code=payload.referral_code,
|
||
)
|
||
|
||
refreshed = await get_user_by_id(db, user.id)
|
||
return _user_to_detail(refreshed or user, [])
|
||
|
||
|
||
@router.patch("/{user_id}", response_model=UserDetailResponse)
|
||
async def update_user_endpoint(
|
||
payload: UserUpdateRequest,
|
||
user_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.users:write")),
|
||
) -> UserDetailResponse:
|
||
user = await get_user_by_id(db, user_id)
|
||
if not user:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден")
|
||
|
||
updates = payload.model_dump(exclude_unset=True)
|
||
|
||
await update_user(db, user, **updates)
|
||
refreshed = await get_user_by_id(db, user_id)
|
||
|
||
transactions_stmt = (
|
||
select(Transaction)
|
||
.where(Transaction.user_id == user_id)
|
||
.order_by(Transaction.created_at.desc())
|
||
.limit(50)
|
||
)
|
||
tx_result = await db.execute(transactions_stmt)
|
||
transactions = tx_result.scalars().all()
|
||
|
||
return _user_to_detail(refreshed or user, transactions)
|
||
|
||
|
||
@router.post("/{user_id}/balance", response_model=UserDetailResponse)
|
||
async def update_balance(
|
||
payload: UserBalanceUpdateRequest,
|
||
user_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.users:write")),
|
||
) -> UserDetailResponse:
|
||
user = await get_user_by_id(db, user_id)
|
||
if not user:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден")
|
||
|
||
success = await add_user_balance(
|
||
db,
|
||
user,
|
||
amount_kopeks=payload.amount_kopeks,
|
||
description=payload.description or "Изменение баланса через API",
|
||
create_transaction=True,
|
||
)
|
||
|
||
if not success:
|
||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Не удалось обновить баланс")
|
||
|
||
refreshed = await get_user_by_id(db, user_id)
|
||
transactions_stmt = (
|
||
select(Transaction)
|
||
.where(Transaction.user_id == user_id)
|
||
.order_by(Transaction.created_at.desc())
|
||
.limit(50)
|
||
)
|
||
tx_result = await db.execute(transactions_stmt)
|
||
transactions = tx_result.scalars().all()
|
||
|
||
return _user_to_detail(refreshed or user, transactions)
|
||
|
||
|
||
@router.post("/{user_id}/status", response_model=UserDetailResponse)
|
||
async def update_status(
|
||
payload: UserStatusUpdateRequest,
|
||
user_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.users:write")),
|
||
) -> UserDetailResponse:
|
||
user = await get_user_by_id(db, user_id)
|
||
if not user:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден")
|
||
|
||
try:
|
||
new_status = UserStatus(payload.status).value
|
||
except ValueError:
|
||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Недопустимый статус")
|
||
|
||
await update_user(db, user, status=new_status)
|
||
refreshed = await get_user_by_id(db, user_id)
|
||
|
||
transactions_stmt = (
|
||
select(Transaction)
|
||
.where(Transaction.user_id == user_id)
|
||
.order_by(Transaction.created_at.desc())
|
||
.limit(50)
|
||
)
|
||
tx_result = await db.execute(transactions_stmt)
|
||
transactions = tx_result.scalars().all()
|
||
|
||
return _user_to_detail(refreshed or user, transactions)
|
||
|
||
|
||
def _get_subscription_or_404(subscription: Optional[Subscription]) -> Subscription:
|
||
if not subscription:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="У пользователя нет активной подписки")
|
||
return subscription
|
||
|
||
|
||
@router.post("/{user_id}/subscription/extend", response_model=SubscriptionSchema)
|
||
async def extend_user_subscription(
|
||
payload: SubscriptionExtendRequest,
|
||
user_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.subscriptions:write")),
|
||
) -> SubscriptionSchema:
|
||
subscription = await get_subscription_by_user_id(db, user_id)
|
||
subscription = _get_subscription_or_404(subscription)
|
||
updated = await extend_subscription(db, subscription, payload.days)
|
||
return SubscriptionSchema.model_validate(updated)
|
||
|
||
|
||
@router.post("/{user_id}/subscription/traffic", response_model=SubscriptionSchema)
|
||
async def add_subscription_traffic_api(
|
||
payload: SubscriptionTrafficRequest,
|
||
user_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.subscriptions:write")),
|
||
) -> SubscriptionSchema:
|
||
subscription = await get_subscription_by_user_id(db, user_id)
|
||
subscription = _get_subscription_or_404(subscription)
|
||
updated = await add_subscription_traffic(db, subscription, payload.gb)
|
||
return SubscriptionSchema.model_validate(updated)
|
||
|
||
|
||
@router.post("/{user_id}/subscription/devices", response_model=SubscriptionSchema)
|
||
async def add_subscription_devices_api(
|
||
payload: SubscriptionDevicesRequest,
|
||
user_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.subscriptions:write")),
|
||
) -> SubscriptionSchema:
|
||
subscription = await get_subscription_by_user_id(db, user_id)
|
||
subscription = _get_subscription_or_404(subscription)
|
||
updated = await add_subscription_devices(db, subscription, payload.devices)
|
||
return SubscriptionSchema.model_validate(updated)
|
||
|
||
|
||
@router.post("/{user_id}/subscription/squads", response_model=SubscriptionSchema)
|
||
async def add_subscription_squad_api(
|
||
payload: SubscriptionSquadRequest,
|
||
user_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.subscriptions:write")),
|
||
) -> SubscriptionSchema:
|
||
subscription = await get_subscription_by_user_id(db, user_id)
|
||
subscription = _get_subscription_or_404(subscription)
|
||
updated = await add_subscription_squad(db, subscription, payload.squad_uuid)
|
||
return SubscriptionSchema.model_validate(updated)
|
||
|
||
|
||
@router.delete("/{user_id}/subscription/squads/{squad_uuid}", response_model=SubscriptionSchema)
|
||
async def remove_subscription_squad_api(
|
||
squad_uuid: str,
|
||
user_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.subscriptions:write")),
|
||
) -> SubscriptionSchema:
|
||
subscription = await get_subscription_by_user_id(db, user_id)
|
||
subscription = _get_subscription_or_404(subscription)
|
||
updated = await remove_subscription_squad(db, subscription, squad_uuid)
|
||
return SubscriptionSchema.model_validate(updated)
|
||
|
||
|
||
@router.get("/{user_id}/transactions", response_model=TransactionListResponse)
|
||
async def list_user_transactions(
|
||
user_id: int = Path(..., ge=1),
|
||
limit: int = Query(50, ge=1, le=200),
|
||
offset: int = Query(0, ge=0),
|
||
db: AsyncSession = Depends(get_db),
|
||
_: object = Depends(require_permission("webapi.transactions:read")),
|
||
) -> TransactionListResponse:
|
||
user = await get_user_by_id(db, user_id)
|
||
if not user:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден")
|
||
|
||
total_stmt = select(func.count(Transaction.id)).where(Transaction.user_id == user_id)
|
||
total = await db.scalar(total_stmt) or 0
|
||
|
||
stmt = (
|
||
select(Transaction)
|
||
.where(Transaction.user_id == user_id)
|
||
.order_by(Transaction.created_at.desc())
|
||
.offset(offset)
|
||
.limit(limit)
|
||
)
|
||
result = await db.execute(stmt)
|
||
transactions = result.scalars().all()
|
||
|
||
pagination = Pagination(total=total, limit=limit, offset=offset)
|
||
items = [TransactionSchema.model_validate(tx) for tx in transactions]
|
||
return TransactionListResponse(pagination=pagination, items=items)
|