Compare commits
3 Commits
dee2cad540
...
c06be7b5f2
| Author | SHA1 | Date | |
|---|---|---|---|
| c06be7b5f2 | |||
| 801014c392 | |||
| 680c15414c |
34
alembic/versions/135672cdf14a_user_referal_code.py
Normal file
34
alembic/versions/135672cdf14a_user_referal_code.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""+user.referal_code
|
||||
|
||||
Revision ID: 135672cdf14a
|
||||
Revises: 72e78a8a43cf
|
||||
Create Date: 2026-08-03 10:26:22.290547
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '135672cdf14a'
|
||||
down_revision: Union[str, Sequence[str], None] = '72e78a8a43cf'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('referal_code', sa.TEXT(), nullable=False))
|
||||
op.create_unique_constraint(None, 'users', ['referal_code'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'users', type_='unique')
|
||||
op.drop_column('users', 'referal_code')
|
||||
# ### end Alembic commands ###
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import secrets
|
||||
import string
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import BIGINT, INTEGER, REAL, TEXT, VARCHAR, ForeignKey
|
||||
@@ -9,6 +11,11 @@ if TYPE_CHECKING:
|
||||
from db.models import Order, Session, Subscription
|
||||
|
||||
|
||||
def generate_ref_code(length: int = 8):
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
@@ -19,6 +26,9 @@ class User(Base):
|
||||
hashed_password: Mapped[str] = mapped_column(VARCHAR(255), nullable=True)
|
||||
telegram_id: Mapped[int] = mapped_column(BIGINT, unique=True, nullable=True)
|
||||
referal_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||
referal_code: Mapped[str] = mapped_column(
|
||||
TEXT, nullable=False, unique=True, default=generate_ref_code
|
||||
)
|
||||
|
||||
balance: Mapped[float] = mapped_column(REAL, nullable=False, default=0)
|
||||
|
||||
|
||||
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}"
|
||||
|
||||
@@ -24,17 +24,24 @@ class UserRepository:
|
||||
res = await self.session.execute(stmt)
|
||||
return res.scalar_one_or_none()
|
||||
|
||||
async def get_user_by_ref_code(self, ref_code: str) -> User | None:
|
||||
stmt = select(User).where(User.referal_code == ref_code)
|
||||
res = await self.session.execute(stmt)
|
||||
return res.scalar_one_or_none()
|
||||
|
||||
async def create(
|
||||
self,
|
||||
*,
|
||||
username: str | None = None,
|
||||
hashed_password: str | None = None,
|
||||
telegram_id: int | None = None,
|
||||
referal_id: int | None = None,
|
||||
) -> User:
|
||||
obj = User(
|
||||
username=username,
|
||||
hashed_password=hashed_password,
|
||||
telegram_id=telegram_id,
|
||||
referal_id=referal_id,
|
||||
)
|
||||
self.session.add(obj)
|
||||
await self.session.commit()
|
||||
|
||||
@@ -4,5 +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 router as users_routers
|
||||
|
||||
routers: list[APIRouter] = [auth_router, plans_router, orders_router, *payment_routers]
|
||||
routers: list[APIRouter] = [
|
||||
auth_router,
|
||||
plans_router,
|
||||
orders_router,
|
||||
users_routers,
|
||||
*payment_routers,
|
||||
]
|
||||
|
||||
@@ -28,9 +28,20 @@ async def signup(req: UserRegistration, session: AsyncSession = Depends(get_db))
|
||||
raise HTTPException(status_code=409, detail="User already exists")
|
||||
|
||||
password_hash = hash_password(req.password)
|
||||
user = await users_repo.create(username=req.username, hashed_password=password_hash)
|
||||
referal_id = None
|
||||
if req.referal_code:
|
||||
referal = await users_repo.get_user_by_ref_code(req.referal_code)
|
||||
referal_id = referal.id if referal else None
|
||||
|
||||
user = await users_repo.create(
|
||||
username=req.username, hashed_password=password_hash, referal_id=referal_id
|
||||
)
|
||||
return JSONResponse(
|
||||
UserInfo(username=user.username, telegram_id=user.telegram_id).model_dump(),
|
||||
UserInfo(
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id,
|
||||
referal_code=user.referal_code,
|
||||
).model_dump(),
|
||||
status_code=201,
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="Unsupported provider")
|
||||
|
||||
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)
|
||||
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()
|
||||
@@ -9,4 +9,6 @@ class UserRegistration(BaseModel):
|
||||
username: str | None = Field(None)
|
||||
password: str | None = Field(None)
|
||||
|
||||
referal_code: str | None = Field(None)
|
||||
|
||||
provider: ProvidersType
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field, computed_field
|
||||
|
||||
|
||||
class SubscriptionData(BaseModel):
|
||||
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()
|
||||
telegram_id: str | None = Field()
|
||||
username: str | None = Field(None)
|
||||
telegram_id: str | None = Field(None)
|
||||
referal_code: str = Field()
|
||||
|
||||
@@ -18,6 +18,10 @@ async def authorize_user(
|
||||
return UserLogin(
|
||||
access_token=key_pair.access_token,
|
||||
refresh_token=key_pair.refresh_token,
|
||||
user=UserInfo(username=user.username, telegram_id=user.telegram_id),
|
||||
user=UserInfo(
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id,
|
||||
referal_code=user.referal_code,
|
||||
),
|
||||
expires_at=key_pair.expires_at,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user