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() pally_token: str = Field()
remnawave_base_url: str = Field() remnawave_base_url: str = Field()
remnawave_sub_url: str = Field()
remnawave_token: str = Field() remnawave_token: str = Field()
remnawave_default_squads_raw: str = Field(alias="REMNAWAVE_DEFAULT_SQUADS_UUIDS") 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) 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( async def sync_subscription_by_telegram_id(
*, *,
expires_at: datetime, expires_at: datetime,
@@ -443,38 +464,29 @@ async def sync_subscription_by_telegram_id(
logger.info("Skipping RW subscription sync: RemnaWave is not configured") logger.info("Skipping RW subscription sync: RemnaWave is not configured")
return False return False
rw_user = None rw_user = await get_rw_user(sdk, telegram_id=telegram_id, username=username)
if telegram_id is not None:
rw_user = await get_user_by_telegram_id(sdk, telegram_id)
if rw_user is None: if rw_user is None:
rw_username = _build_rw_username(telegram_id=telegram_id, username=username) rw_username = _build_rw_username(telegram_id=telegram_id, username=username)
if rw_username is None: rw_user = await create_user(
logger.warning( sdk=sdk,
"Skipping RW subscription sync: cannot build username for telegram_id=%s username=%s", username=rw_username,
telegram_id, telegram_id=telegram_id,
username, expire_at=expires_at,
) hwid_device_limit=None if devices == 0 else devices,
return False squad_uuids=cfg.remnawave_default_squads,
)
rw_user = await get_user_by_username(sdk, rw_username) if rw_user is None:
if rw_user is None: logger.warning(
rw_user = await create_user( "Failed to create RW user for telegram_id=%s username=%s",
sdk=sdk, telegram_id,
username=rw_username, username,
telegram_id=telegram_id, )
expire_at=expires_at, return False
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) expire_synced = await update_expire_at(sdk, rw_user.uuid, expires_at)
devices_synced = await set_hwid_limit(sdk, rw_user.uuid, devices) devices_synced = await set_hwid_limit(sdk, rw_user.uuid, devices)
return expire_synced and devices_synced 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 .orders import router as orders_router
from .payments import payment_routers from .payments import payment_routers
from .plans import router as plans_router from .plans import router as plans_router
from .users import users_routers from .users import router as users_routers
routers: list[APIRouter] = [ routers: list[APIRouter] = [
auth_router, auth_router,
plans_router, plans_router,
orders_router, orders_router,
users_routers,
*payment_routers, *payment_routers,
*users_routers,
] ]

View File

@@ -40,7 +40,6 @@ async def signup(req: UserRegistration, session: AsyncSession = Depends(get_db))
UserInfo( UserInfo(
username=user.username, username=user.username,
telegram_id=user.telegram_id, telegram_id=user.telegram_id,
subscription=None,
referal_code=user.referal_code, referal_code=user.referal_code,
).model_dump(), ).model_dump(),
status_code=201, 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 datetime import datetime
from pydantic import BaseModel, Field from pydantic import BaseModel, Field, computed_field
class SubscriptionData(BaseModel): class SubscriptionData(BaseModel):
devices: int = Field() devices: int | None = Field(None)
expires_at: datetime = Field() expires_at: datetime | None = Field(None)
addon_ids: list[str] = Field() 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): class UserInfo(BaseModel):
username: str | None = Field(None) username: str | None = Field(None)
telegram_id: str | None = Field(None) telegram_id: str | None = Field(None)
subscription: SubscriptionData | None = Field(None)
referal_code: str = Field() referal_code: str = Field()

View File

@@ -3,7 +3,7 @@ from db.models.users import User
from repositories.sessions import SessionsRepository from repositories.sessions import SessionsRepository
from schemas.login import UserLogin from schemas.login import UserLogin
from schemas.providers import ProvidersType from schemas.providers import ProvidersType
from schemas.user import SubscriptionData, UserInfo from schemas.user import UserInfo
async def authorize_user( 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) 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( return UserLogin(
access_token=key_pair.access_token, access_token=key_pair.access_token,
refresh_token=key_pair.refresh_token, refresh_token=key_pair.refresh_token,
@@ -31,7 +22,6 @@ async def authorize_user(
username=user.username, username=user.username,
telegram_id=user.telegram_id, telegram_id=user.telegram_id,
referal_code=user.referal_code, referal_code=user.referal_code,
subscription=sub,
), ),
expires_at=key_pair.expires_at, expires_at=key_pair.expires_at,
) )