feat(/me): subscription data + rw controls

This commit is contained in:
2026-08-03 11:15:11 +07:00
parent 801014c392
commit c06be7b5f2
11 changed files with 142 additions and 73 deletions

View File

@@ -22,6 +22,7 @@ class Settings(BaseSettings):
pally_token: str = Field()
remnawave_base_url: str = Field()
remnawave_sub_url: str = Field()
remnawave_token: str = Field()
remnawave_default_squads_raw: str = Field(alias="REMNAWAVE_DEFAULT_SQUADS_UUIDS")

70
external/rw.py vendored
View File

@@ -431,6 +431,27 @@ def get_sdk() -> RemnawaveSDK | None:
return RemnawaveSDK(base_url=cfg.remnawave_base_url, token=cfg.remnawave_token)
async def get_rw_user(
sdk: RemnawaveSDK, telegram_id: int | None = None, username: str | None = None
) -> RWUserInfo | None:
rw_user = None
if telegram_id is not None:
rw_user = await get_user_by_telegram_id(sdk, telegram_id)
if rw_user is None:
rw_username = _build_rw_username(telegram_id=telegram_id, username=username)
if rw_username is None:
logger.warning(
"Cannot build username for telegram_id=%s username=%s",
telegram_id,
username,
)
return None
rw_user = await get_user_by_username(sdk, rw_username)
return rw_user
async def sync_subscription_by_telegram_id(
*,
expires_at: datetime,
@@ -443,38 +464,29 @@ async def sync_subscription_by_telegram_id(
logger.info("Skipping RW subscription sync: RemnaWave is not configured")
return False
rw_user = None
if telegram_id is not None:
rw_user = await get_user_by_telegram_id(sdk, telegram_id)
rw_user = await get_rw_user(sdk, telegram_id=telegram_id, username=username)
if rw_user is None:
rw_username = _build_rw_username(telegram_id=telegram_id, username=username)
if rw_username is None:
logger.warning(
"Skipping RW subscription sync: cannot build username for telegram_id=%s username=%s",
telegram_id,
username,
)
return False
rw_user = await get_user_by_username(sdk, rw_username)
if rw_user is None:
rw_user = await create_user(
sdk=sdk,
username=rw_username,
telegram_id=telegram_id,
expire_at=expires_at,
hwid_device_limit=None if devices == 0 else devices,
squad_uuids=cfg.remnawave_default_squads,
)
if rw_user is None:
logger.warning(
"Failed to create RW user for telegram_id=%s username=%s",
telegram_id,
username,
)
return False
rw_user = await create_user(
sdk=sdk,
username=rw_username,
telegram_id=telegram_id,
expire_at=expires_at,
hwid_device_limit=None if devices == 0 else devices,
squad_uuids=cfg.remnawave_default_squads,
)
if rw_user is None:
logger.warning(
"Failed to create RW user for telegram_id=%s username=%s",
telegram_id,
username,
)
return False
expire_synced = await update_expire_at(sdk, rw_user.uuid, expires_at)
devices_synced = await set_hwid_limit(sdk, rw_user.uuid, devices)
return expire_synced and devices_synced
def build_subscription_link(short_uuid: str):
return cfg.remnawave_sub_url.rstrip("/") + f"/{short_uuid}"

View File

@@ -4,12 +4,12 @@ from .auth import router as auth_router
from .orders import router as orders_router
from .payments import payment_routers
from .plans import router as plans_router
from .users import users_routers
from .users import router as users_routers
routers: list[APIRouter] = [
auth_router,
plans_router,
orders_router,
users_routers,
*payment_routers,
*users_routers,
]

View File

@@ -40,7 +40,6 @@ async def signup(req: UserRegistration, session: AsyncSession = Depends(get_db))
UserInfo(
username=user.username,
telegram_id=user.telegram_id,
subscription=None,
referal_code=user.referal_code,
).model_dump(),
status_code=201,

74
routes/users.py Normal file
View File

@@ -0,0 +1,74 @@
from fastapi import APIRouter, Depends, HTTPException
from core.deps import get_auth_context
from external.rw import build_subscription_link, delete_hwid, get_hwid_list, get_rw_user, get_sdk
from schemas.common import OperationData
from schemas.devices import Device
from schemas.dto import AuthContext
from schemas.user import SubscriptionData, UserInfo
router = APIRouter(prefix="/users")
@router.get("/me", response_model=UserInfo)
async def get_me(ctx: AuthContext = Depends(get_auth_context)):
return UserInfo(
username=ctx.user.username,
telegram_id=ctx.user.telegram_id,
referal_code=ctx.user.referal_code,
)
@router.get("/subscription")
async def get_subscription(ctx: AuthContext = Depends(get_auth_context)):
sub = ctx.user.subscription
if not sub:
return SubscriptionData(
devices=None,
expires_at=None,
addon_ids=[],
subscription_link=None,
)
rw_user = await get_rw_user(get_sdk(), ctx.user.telegram_id, ctx.user.username)
return SubscriptionData(
devices=sub.devices,
expires_at=sub.expires_at,
addon_ids=[a.id for a in sub.addons],
subscription_link=build_subscription_link(rw_user.short_uuid),
)
@router.get("/subscription/hwid", response_model=list[Device])
async def get_hwid(ctx: AuthContext = Depends(get_auth_context)):
rw_user = await get_rw_user(get_sdk(), ctx.user.telegram_id, ctx.user.username)
if not rw_user:
raise HTTPException(403, detail="RW user not found.")
hwid_list = await get_hwid_list(get_sdk(), user_uuid=rw_user.uuid)
devices = []
if not hwid_list:
return devices
for device in hwid_list:
devices.append(
Device(
os=device.platform,
model=device.device_model,
client=device.user_agent.split("/")[0],
hwid=device.hwid,
)
)
return devices
@router.delete("/subscription/hwid")
async def delete_hwid_endpoint(hwid: str, ctx: AuthContext = Depends(get_auth_context)):
rw_user = await get_rw_user(get_sdk(), ctx.user.telegram_id, ctx.user.username)
if not rw_user:
raise HTTPException(403, detail="RW user not found.")
success = await delete_hwid(get_sdk(), rw_user.uuid, hwid)
return OperationData(success=success)

View File

@@ -1,3 +0,0 @@
from .me import router as me_router
users_routers = [me_router]

View File

@@ -1,22 +0,0 @@
from fastapi import APIRouter, Depends
from core.deps import get_auth_context
from schemas.dto import AuthContext
from schemas.user import SubscriptionData, UserInfo
router = APIRouter(prefix="/users/me")
@router.get("", response_model=UserInfo)
async def get_me(ctx: AuthContext = Depends(get_auth_context)):
sub_data = SubscriptionData(
devices=ctx.user.subscription.devices,
expires_at=ctx.user.subscription.expires_at,
addon_ids=[a.id for a in ctx.user.subscription.addons],
)
return UserInfo(
username=ctx.user.username,
telegram_id=ctx.user.telegram_id,
subscription=sub_data,
referal_code=ctx.user.referal_code,
)

5
schemas/common.py Normal file
View File

@@ -0,0 +1,5 @@
from pydantic import BaseModel, Field
class OperationData(BaseModel):
success: bool = Field(False)

8
schemas/devices.py Normal file
View File

@@ -0,0 +1,8 @@
from pydantic import BaseModel, Field
class Device(BaseModel):
os: str | None = Field(None)
model: str | None = Field(None)
client: str | None = Field(None)
hwid: str = Field()

View File

@@ -1,16 +1,21 @@
from datetime import datetime
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, computed_field
class SubscriptionData(BaseModel):
devices: int = Field()
expires_at: datetime = Field()
addon_ids: list[str] = Field()
devices: int | None = Field(None)
expires_at: datetime | None = Field(None)
addon_ids: list[str] = Field(default_factory=list)
subscription_link: str | None = Field(None)
@property
@computed_field
def has_subscription(self) -> bool:
return all([self.devices, self.expires_at, self.subscription_link, self.addon_ids])
class UserInfo(BaseModel):
username: str | None = Field(None)
telegram_id: str | None = Field(None)
subscription: SubscriptionData | None = Field(None)
referal_code: str = Field()

View File

@@ -3,7 +3,7 @@ from db.models.users import User
from repositories.sessions import SessionsRepository
from schemas.login import UserLogin
from schemas.providers import ProvidersType
from schemas.user import SubscriptionData, UserInfo
from schemas.user import UserInfo
async def authorize_user(
@@ -15,15 +15,6 @@ async def authorize_user(
await sessions_repo.create(user_id=user.id, refresh_token_hash=refresh_token_hash, iss=iss)
sub = (
SubscriptionData(
devices=user.subscription.devices,
expires_at=user.subscription.expires_at,
addon_ids=[a.id for a in user.subscription.addons],
)
if user.subscription
else None
)
return UserLogin(
access_token=key_pair.access_token,
refresh_token=key_pair.refresh_token,
@@ -31,7 +22,6 @@ async def authorize_user(
username=user.username,
telegram_id=user.telegram_id,
referal_code=user.referal_code,
subscription=sub,
),
expires_at=key_pair.expires_at,
)