feat: add install guide share tokens and animations
This commit is contained in:
@@ -32,12 +32,14 @@ async def subscription_guides_route(request: web.Request) -> web.Response:
|
||||
|
||||
|
||||
async def public_subscription_guides_route(request: web.Request) -> web.Response:
|
||||
short_uuid = _normalize_short_uuid(request.match_info.get("short_uuid"))
|
||||
if not short_uuid:
|
||||
return web.json_response({"ok": False, "error": "invalid_short_uuid"}, status=404)
|
||||
share_token = subscription_dal.normalize_install_share_token(
|
||||
request.match_info.get("share_token")
|
||||
)
|
||||
if not share_token:
|
||||
return web.json_response({"ok": False, "error": "invalid_share_token"}, status=404)
|
||||
|
||||
status = await _subscription_guides_status_shared(request.app)
|
||||
subscription = await _public_subscription_payload(request, short_uuid)
|
||||
subscription = await _public_subscription_payload(request, share_token)
|
||||
payload = {
|
||||
"enabled": bool(status.get("enabled")),
|
||||
"config": status.get("config") if status.get("enabled") else None,
|
||||
@@ -160,19 +162,19 @@ async def _default_panel_subscription_page_config_uuid(panel_service: Any) -> st
|
||||
|
||||
async def _public_subscription_payload(
|
||||
request: web.Request,
|
||||
short_uuid: str,
|
||||
share_token: str,
|
||||
) -> Dict[str, Any]:
|
||||
settings: Settings = request.app["settings"]
|
||||
panel_service = _panel_service_from_app(request.app)
|
||||
raw_link = ""
|
||||
username = ""
|
||||
resolved_short_uuid = short_uuid
|
||||
resolved_short_uuid = ""
|
||||
|
||||
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
||||
async with async_session_factory() as session:
|
||||
local_sub = await subscription_dal.get_subscription_by_panel_subscription_uuid(
|
||||
local_sub = await subscription_dal.get_subscription_by_install_share_token(
|
||||
session,
|
||||
short_uuid,
|
||||
share_token,
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -185,16 +187,17 @@ async def _public_subscription_payload(
|
||||
if panel_user:
|
||||
raw_link = str(panel_user.get("subscriptionUrl") or "").strip()
|
||||
username = str(panel_user.get("username") or "").strip()
|
||||
resolved_short_uuid = str(panel_user.get("shortUuid") or short_uuid).strip()
|
||||
resolved_short_uuid = str(panel_user.get("shortUuid") or "").strip()
|
||||
|
||||
display_link, connect_url = await prepare_config_links(settings, raw_link)
|
||||
return {
|
||||
"active": bool(display_link),
|
||||
"config_link": display_link,
|
||||
"connect_url": connect_url or display_link,
|
||||
"panel_short_uuid": resolved_short_uuid,
|
||||
"panel_short_uuid": resolved_short_uuid or None,
|
||||
"install_share_token": share_token,
|
||||
"username": username,
|
||||
"share_url": _public_install_url(request, resolved_short_uuid),
|
||||
"share_url": _public_install_url(request, share_token),
|
||||
}
|
||||
|
||||
|
||||
@@ -231,14 +234,7 @@ def _local_subscription_is_publicly_active(subscription: Any) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _normalize_short_uuid(value: Any) -> str:
|
||||
short_uuid = str(value or "").strip()
|
||||
if not re.fullmatch(r"[A-Za-z0-9_-]{8,128}", short_uuid):
|
||||
return ""
|
||||
return short_uuid
|
||||
|
||||
|
||||
def _public_install_url(request: web.Request, short_uuid: str) -> str:
|
||||
def _public_install_url(request: web.Request, share_token: str) -> str:
|
||||
settings: Settings = request.app["settings"]
|
||||
configured_base = str(getattr(settings, "SUBSCRIPTION_MINI_APP_URL", "") or "").strip()
|
||||
if configured_base:
|
||||
@@ -255,7 +251,7 @@ def _public_install_url(request: web.Request, short_uuid: str) -> str:
|
||||
)
|
||||
proto = request.headers.get("X-Forwarded-Proto") or request.scheme or "https"
|
||||
base = f"{proto}://{host}"
|
||||
return f"{base.rstrip('/')}/install/share/{quote(short_uuid)}"
|
||||
return f"{base.rstrip('/')}/s/{quote(share_token)}"
|
||||
|
||||
|
||||
def _subscription_page_request_headers(request: web.Request) -> Dict[str, str]:
|
||||
|
||||
@@ -7,7 +7,7 @@ def setup_subscription_webapp_routes(app: web.Application) -> None:
|
||||
app.router.add_get("/login/password", index_route)
|
||||
app.router.add_get("/home", index_route)
|
||||
app.router.add_get("/install", index_route)
|
||||
app.router.add_get(r"/install/share/{short_uuid:[A-Za-z0-9_-]{8,128}}", index_route)
|
||||
app.router.add_get(r"/s/{share_token:[a-f0-9]{32}}", index_route)
|
||||
app.router.add_get("/invite", index_route)
|
||||
app.router.add_get("/devices", index_route)
|
||||
app.router.add_get("/settings", index_route)
|
||||
@@ -64,7 +64,7 @@ def setup_subscription_webapp_routes(app: web.Application) -> None:
|
||||
app.router.add_get("/api/me", me_route)
|
||||
app.router.add_get("/api/subscription-guides", subscription_guides_route)
|
||||
app.router.add_get(
|
||||
r"/api/subscription-guides/public/{short_uuid:[A-Za-z0-9_-]{8,128}}",
|
||||
r"/api/subscription-guides/public/{share_token:[a-f0-9]{32}}",
|
||||
public_subscription_guides_route,
|
||||
)
|
||||
app.router.add_get("/api/account/avatar", account_avatar_route)
|
||||
|
||||
@@ -53,6 +53,11 @@ async def _build_user_payload(request: web.Request, user_id: int) -> Dict[str, A
|
||||
if db_user.panel_user_uuid
|
||||
else None
|
||||
)
|
||||
install_share_token = (
|
||||
await subscription_dal.ensure_install_share_token(session, local_sub)
|
||||
if active and local_sub
|
||||
else None
|
||||
)
|
||||
trial_available = bool(
|
||||
settings.TRIAL_ENABLED
|
||||
and settings.TRIAL_DURATION_DAYS > 0
|
||||
@@ -83,7 +88,14 @@ async def _build_user_payload(request: web.Request, user_id: int) -> Dict[str, A
|
||||
"language_code": lang,
|
||||
"is_admin": is_admin,
|
||||
},
|
||||
"subscription": _serialize_subscription(request, settings, active, local_sub, lang),
|
||||
"subscription": _serialize_subscription(
|
||||
request,
|
||||
settings,
|
||||
active,
|
||||
local_sub,
|
||||
lang,
|
||||
install_share_token=install_share_token,
|
||||
),
|
||||
"referral": {
|
||||
"code": referral_code,
|
||||
"bot_link": referral_link,
|
||||
@@ -181,12 +193,26 @@ def _build_webapp_referral_link(
|
||||
|
||||
|
||||
def _serialize_subscription(
|
||||
request: web.Request,
|
||||
settings: Settings,
|
||||
active: Optional[Dict[str, Any]],
|
||||
local_sub: Optional[Any],
|
||||
lang: str,
|
||||
request_or_settings: Any,
|
||||
settings_or_active: Any,
|
||||
active_or_local_sub: Optional[Any] = None,
|
||||
local_sub_or_lang: Optional[Any] = None,
|
||||
lang: Optional[str] = None,
|
||||
*,
|
||||
install_share_token: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
if lang is None:
|
||||
request = None
|
||||
settings = request_or_settings
|
||||
active = settings_or_active
|
||||
local_sub = active_or_local_sub
|
||||
lang = str(local_sub_or_lang or "ru")
|
||||
else:
|
||||
request = request_or_settings
|
||||
settings = settings_or_active
|
||||
active = active_or_local_sub
|
||||
local_sub = local_sub_or_lang
|
||||
|
||||
if not active:
|
||||
return {
|
||||
"active": False,
|
||||
@@ -196,6 +222,7 @@ def _serialize_subscription(
|
||||
"config_link": None,
|
||||
"connect_url": None,
|
||||
"panel_short_uuid": None,
|
||||
"install_share_token": None,
|
||||
"install_share_url": None,
|
||||
}
|
||||
|
||||
@@ -237,6 +264,9 @@ def _serialize_subscription(
|
||||
can_topup_devices = False
|
||||
|
||||
panel_short_uuid = str(active.get("panel_short_uuid") or "").strip()
|
||||
share_token = str(
|
||||
install_share_token or getattr(local_sub, "install_share_token", "") or ""
|
||||
).strip()
|
||||
return {
|
||||
"active": seconds_left > 0,
|
||||
"status": active.get("status_from_panel") or "UNKNOWN",
|
||||
@@ -247,7 +277,8 @@ def _serialize_subscription(
|
||||
"config_link": active.get("config_link"),
|
||||
"connect_url": active.get("connect_button_url") or active.get("config_link"),
|
||||
"panel_short_uuid": panel_short_uuid or None,
|
||||
"install_share_url": _build_install_share_link(request, settings, panel_short_uuid),
|
||||
"install_share_token": subscription_dal.normalize_install_share_token(share_token) or None,
|
||||
"install_share_url": _build_install_share_link(request, settings, share_token),
|
||||
"traffic_limit": _format_bytes(active.get("traffic_limit_bytes"), zero_as_unlimited=True),
|
||||
"traffic_used": _format_bytes(active.get("traffic_used_bytes")),
|
||||
"traffic_limit_bytes": _coerce_int_or_none(active.get("traffic_limit_bytes")),
|
||||
@@ -293,12 +324,12 @@ def _serialize_subscription(
|
||||
|
||||
|
||||
def _build_install_share_link(
|
||||
request: web.Request,
|
||||
request: Optional[web.Request],
|
||||
settings: Settings,
|
||||
short_uuid: str,
|
||||
share_token: str,
|
||||
) -> Optional[str]:
|
||||
short_uuid = str(short_uuid or "").strip()
|
||||
if not short_uuid:
|
||||
share_token = subscription_dal.normalize_install_share_token(share_token)
|
||||
if not share_token or request is None:
|
||||
return None
|
||||
configured_base = str(getattr(settings, "SUBSCRIPTION_MINI_APP_URL", "") or "").strip()
|
||||
if configured_base:
|
||||
@@ -315,7 +346,7 @@ def _build_install_share_link(
|
||||
)
|
||||
proto = request.headers.get("X-Forwarded-Proto") or request.scheme or "https"
|
||||
base = f"{proto}://{host}"
|
||||
return f"{base.rstrip('/')}/install/share/{quote(short_uuid)}"
|
||||
return f"{base.rstrip('/')}/s/{quote(share_token)}"
|
||||
|
||||
|
||||
def _serialize_plans(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import logging
|
||||
import re
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -9,6 +11,8 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from db.models import Subscription
|
||||
|
||||
INSTALL_SHARE_TOKEN_BYTES = 16
|
||||
|
||||
|
||||
def _subscription_model_payload(sub_payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
model_columns = Subscription.__mapper__.columns.keys()
|
||||
@@ -42,6 +46,77 @@ async def get_subscription_by_panel_subscription_uuid(
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
def normalize_install_share_token(value: Any) -> str:
|
||||
token = str(value or "").strip().lower()
|
||||
if not re.fullmatch(r"[a-f0-9]{32}", token):
|
||||
return ""
|
||||
return token
|
||||
|
||||
|
||||
async def get_subscription_by_install_share_token(
|
||||
session: AsyncSession,
|
||||
token: str,
|
||||
) -> Optional[Subscription]:
|
||||
normalized = normalize_install_share_token(token)
|
||||
if not normalized:
|
||||
return None
|
||||
stmt = select(Subscription).where(Subscription.install_share_token == normalized)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def ensure_install_share_token(
|
||||
session: AsyncSession,
|
||||
subscription: Subscription,
|
||||
) -> str:
|
||||
raw_existing = str(getattr(subscription, "install_share_token", "") or "").strip()
|
||||
existing = normalize_install_share_token(raw_existing)
|
||||
if existing:
|
||||
if existing != getattr(subscription, "install_share_token", None):
|
||||
subscription.install_share_token = existing
|
||||
await session.flush()
|
||||
return existing
|
||||
|
||||
subscription_id = getattr(subscription, "subscription_id", None)
|
||||
for _attempt in range(10):
|
||||
token = secrets.token_hex(INSTALL_SHARE_TOKEN_BYTES)
|
||||
if await get_subscription_by_install_share_token(session, token):
|
||||
continue
|
||||
if subscription_id:
|
||||
result = await session.execute(
|
||||
update(Subscription)
|
||||
.where(
|
||||
Subscription.subscription_id == subscription_id,
|
||||
or_(
|
||||
Subscription.install_share_token.is_(None),
|
||||
Subscription.install_share_token == "",
|
||||
Subscription.install_share_token == raw_existing,
|
||||
),
|
||||
)
|
||||
.values(install_share_token=token)
|
||||
)
|
||||
await session.flush()
|
||||
if result.rowcount:
|
||||
await session.refresh(subscription)
|
||||
return normalize_install_share_token(
|
||||
getattr(subscription, "install_share_token", None)
|
||||
) or token
|
||||
|
||||
await session.refresh(subscription)
|
||||
raw_existing = str(getattr(subscription, "install_share_token", "") or "").strip()
|
||||
existing = normalize_install_share_token(raw_existing)
|
||||
if existing:
|
||||
return existing
|
||||
continue
|
||||
|
||||
subscription.install_share_token = token
|
||||
await session.flush()
|
||||
await session.refresh(subscription)
|
||||
return token
|
||||
|
||||
raise RuntimeError("Failed to generate a unique install share token")
|
||||
|
||||
|
||||
async def get_active_subscriptions_for_user(
|
||||
session: AsyncSession, user_id: int
|
||||
) -> List[Subscription]:
|
||||
|
||||
@@ -882,6 +882,26 @@ def _migration_0026_add_lifetime_traffic_synced_at(connection: Connection) -> No
|
||||
)
|
||||
|
||||
|
||||
def _migration_0027_add_subscription_install_share_token(connection: Connection) -> None:
|
||||
inspector = inspect(connection)
|
||||
columns: Set[str] = {col["name"] for col in inspector.get_columns("subscriptions")}
|
||||
|
||||
if "install_share_token" not in columns:
|
||||
connection.execute(
|
||||
text("ALTER TABLE subscriptions ADD COLUMN install_share_token VARCHAR(32)")
|
||||
)
|
||||
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_subscriptions_install_share_token
|
||||
ON subscriptions (install_share_token)
|
||||
WHERE install_share_token IS NOT NULL
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
MIGRATIONS: List[Migration] = [
|
||||
Migration(
|
||||
id="0001_add_channel_subscription_fields",
|
||||
@@ -1024,6 +1044,11 @@ MIGRATIONS: List[Migration] = [
|
||||
description="Track when lifetime traffic usage was last synced from panel",
|
||||
upgrade=_migration_0026_add_lifetime_traffic_synced_at,
|
||||
),
|
||||
Migration(
|
||||
id="0027_add_subscription_install_share_token",
|
||||
description="Add stable public share tokens for install instructions",
|
||||
upgrade=_migration_0027_add_subscription_install_share_token,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ class Subscription(Base):
|
||||
user_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=False, index=True)
|
||||
panel_user_uuid = Column(String, nullable=False, index=True)
|
||||
panel_subscription_uuid = Column(String, unique=True, index=True, nullable=True)
|
||||
install_share_token = Column(String(32), unique=True, index=True, nullable=True)
|
||||
start_date = Column(DateTime(timezone=True), nullable=True)
|
||||
end_date = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
duration_months = Column(Integer, nullable=True)
|
||||
|
||||
Reference in New Issue
Block a user