feat(/me): subscription data + rw controls
This commit is contained in:
@@ -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")
|
||||||
|
|
||||||
|
|||||||
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)
|
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,22 +464,9 @@ 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:
|
|
||||||
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(
|
rw_user = await create_user(
|
||||||
sdk=sdk,
|
sdk=sdk,
|
||||||
username=rw_username,
|
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)
|
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}"
|
||||||
|
|||||||
@@ -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,
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
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 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()
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user