feat(/me): subscription data + rw controls
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
40
external/rw.py
vendored
40
external/rw.py
vendored
@@ -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,22 +464,9 @@ 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,
|
||||
@@ -478,3 +486,7 @@ async def sync_subscription_by_telegram_id(
|
||||
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}"
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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
74
routes/users.py
Normal 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)
|
||||
@@ -1,3 +0,0 @@
|
||||
from .me import router as me_router
|
||||
|
||||
users_routers = [me_router]
|
||||
@@ -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
5
schemas/common.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class OperationData(BaseModel):
|
||||
success: bool = Field(False)
|
||||
8
schemas/devices.py
Normal file
8
schemas/devices.py
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user