Ruff format/ Cleanup
This commit is contained in:
@@ -5,5 +5,6 @@ __all__ = ("router", "VERSION")
|
||||
def __getattr__(name: str):
|
||||
if name == "router":
|
||||
from api.v2.router import router
|
||||
|
||||
return router
|
||||
raise AttributeError(name)
|
||||
|
||||
+3
-7
@@ -34,7 +34,7 @@ def generate_crud_router(
|
||||
u = await resolve_user_optional(session, int(value))
|
||||
if u is None:
|
||||
return None
|
||||
return getattr(model, "user_id"), u.id
|
||||
return model.user_id, u.id
|
||||
field = getattr(model, identifier_field)
|
||||
return field, cast_identifier_type(field, value)
|
||||
|
||||
@@ -77,9 +77,7 @@ def generate_crud_router(
|
||||
if resolved is None:
|
||||
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
|
||||
field, casted = resolved
|
||||
result = await session.execute(
|
||||
_apply_user_relationship_loader(model, select(model).where(field == casted))
|
||||
)
|
||||
result = await session.execute(_apply_user_relationship_loader(model, select(model).where(field == casted)))
|
||||
obj = result.scalar_one_or_none()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
|
||||
@@ -97,9 +95,7 @@ def generate_crud_router(
|
||||
if resolved is None:
|
||||
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
|
||||
field, casted = resolved
|
||||
result = await session.execute(
|
||||
_apply_user_relationship_loader(model, select(model).where(field == casted))
|
||||
)
|
||||
result = await session.execute(_apply_user_relationship_loader(model, select(model).where(field == casted)))
|
||||
objs = result.scalars().all()
|
||||
if not objs:
|
||||
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
|
||||
|
||||
+16
-15
@@ -1,27 +1,28 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from api.v2.routes import (
|
||||
root_router,
|
||||
auth,
|
||||
users,
|
||||
keys,
|
||||
coupons,
|
||||
servers,
|
||||
tariffs,
|
||||
gifts,
|
||||
referrals,
|
||||
misc,
|
||||
partners,
|
||||
modules,
|
||||
management,
|
||||
settings,
|
||||
payment_links,
|
||||
identities,
|
||||
web,
|
||||
flows,
|
||||
gifts,
|
||||
identities,
|
||||
keys,
|
||||
management,
|
||||
misc,
|
||||
modules,
|
||||
notifications,
|
||||
partners,
|
||||
payment_links,
|
||||
referrals,
|
||||
root_router,
|
||||
servers,
|
||||
settings,
|
||||
tariffs,
|
||||
users,
|
||||
web,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(root_router)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from collections import deque
|
||||
from threading import Lock
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ async def verify_email(
|
||||
from sqlalchemy import update
|
||||
|
||||
from database.models import Identity as IdentityModel
|
||||
await session.execute(
|
||||
update(IdentityModel).where(IdentityModel.id == identity.id).values(email_verified=True)
|
||||
)
|
||||
|
||||
await session.execute(update(IdentityModel).where(IdentityModel.id == identity.id).values(email_verified=True))
|
||||
return {"ok": True}
|
||||
|
||||
@@ -3,9 +3,11 @@ import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
import time
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -192,7 +194,9 @@ async def google_callback(
|
||||
token = await idb.issue_token_for_identity(session, identity)
|
||||
logger.info(
|
||||
"[Auth] Login success: identity={}, google_sub={}, ip={}, method=google",
|
||||
identity.id, google_sub, _client_ip(request),
|
||||
identity.id,
|
||||
google_sub,
|
||||
_client_ip(request),
|
||||
)
|
||||
redirect = RedirectResponse(return_to, status_code=302)
|
||||
set_auth_cookie(redirect, token, request)
|
||||
|
||||
@@ -49,9 +49,7 @@ async def link_email_send_code(
|
||||
if existing and existing.id != identity.id:
|
||||
our_tg = identity.tg_id
|
||||
their_tg = existing.tg_id
|
||||
can_merge = their_tg is None or (
|
||||
our_tg is not None and int(their_tg) == int(our_tg)
|
||||
)
|
||||
can_merge = their_tg is None or (our_tg is not None and int(their_tg) == int(our_tg))
|
||||
if not can_merge:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
|
||||
@@ -75,8 +75,9 @@ async def register_by_email(
|
||||
)
|
||||
ip = _client_ip(request)
|
||||
try:
|
||||
from core.redis_cache import cache_incr_checked
|
||||
from api.v2.routes.auth._fallback_limiter import check_and_increment
|
||||
from core.redis_cache import cache_incr_checked
|
||||
|
||||
count, redis_ok = await cache_incr_checked(f"register_rate:{ip}", 3600)
|
||||
if not redis_ok:
|
||||
count = check_and_increment(f"register_rate:{ip}", 5, 3600)
|
||||
@@ -144,8 +145,9 @@ async def login(
|
||||
raise HTTPException(status_code=400, detail="Email обязателен")
|
||||
ip = _client_ip(request)
|
||||
try:
|
||||
from core.redis_cache import cache_get, cache_incr_checked
|
||||
from api.v2.routes.auth._fallback_limiter import check_and_increment
|
||||
from core.redis_cache import cache_get, cache_incr_checked
|
||||
|
||||
lockout_key = f"login_lockout:{email}"
|
||||
locked = await cache_get(lockout_key)
|
||||
if locked:
|
||||
@@ -164,6 +166,7 @@ async def login(
|
||||
if not result:
|
||||
try:
|
||||
from core.redis_cache import cache_incr, cache_set
|
||||
|
||||
fail_key = f"login_fail:{email}"
|
||||
fails = await cache_incr(fail_key, 900)
|
||||
if fails >= 10:
|
||||
@@ -173,6 +176,7 @@ async def login(
|
||||
raise HTTPException(status_code=401, detail="Неверный email или пароль")
|
||||
try:
|
||||
from core.redis_cache import cache_delete
|
||||
|
||||
await cache_delete(f"login_fail:{email}")
|
||||
except Exception:
|
||||
pass
|
||||
@@ -193,8 +197,9 @@ async def send_login_code(
|
||||
"""Отправить код входа на email (SMTP + Redis)."""
|
||||
ip = _client_ip(request)
|
||||
try:
|
||||
from core.redis_cache import cache_incr_checked
|
||||
from api.v2.routes.auth._fallback_limiter import check_and_increment
|
||||
from core.redis_cache import cache_incr_checked
|
||||
|
||||
count, redis_ok = await cache_incr_checked(f"send_code_rate:{ip}", 3600)
|
||||
if not redis_ok:
|
||||
count = check_and_increment(f"send_code_rate:{ip}", 10, 3600)
|
||||
@@ -292,7 +297,10 @@ async def login_by_code(
|
||||
from sqlalchemy import update as sa_update
|
||||
|
||||
from database.models import Identity as IdentityModel
|
||||
await session.execute(sa_update(IdentityModel).where(IdentityModel.id == identity.id).values(email_verified=True))
|
||||
|
||||
await session.execute(
|
||||
sa_update(IdentityModel).where(IdentityModel.id == identity.id).values(email_verified=True)
|
||||
)
|
||||
await bind_identity_actor(request, session, identity)
|
||||
token = await idb.issue_token_for_identity(session, identity)
|
||||
logger.info("[Auth] Login success: identity={}, email={}, method=code", identity.id, email_norm)
|
||||
|
||||
@@ -50,7 +50,12 @@ async def login_telegram(
|
||||
identity = await idb.get_or_create_identity_for_tg(session, body.id)
|
||||
await bind_identity_actor(request, session, identity)
|
||||
token = await idb.issue_token_for_identity(session, identity)
|
||||
logger.info("[Auth] Login success: identity={}, tg_id={}, ip={}, method=telegram_widget", identity.id, body.id, _client_ip(request))
|
||||
logger.info(
|
||||
"[Auth] Login success: identity={}, tg_id={}, ip={}, method=telegram_widget",
|
||||
identity.id,
|
||||
body.id,
|
||||
_client_ip(request),
|
||||
)
|
||||
set_auth_cookie(response, token, request)
|
||||
set_is_admin_cookie(response, identity, request)
|
||||
return LoginResponse(identity_id=identity.id)
|
||||
@@ -65,6 +70,7 @@ async def login_telegram_webapp(
|
||||
):
|
||||
"""Вход через Telegram WebApp initData. Валидирует HMAC, находит/создаёт Identity по tg_id."""
|
||||
from utils.telegram_login import verify_webapp_init_data
|
||||
|
||||
result = verify_webapp_init_data(body.init_data, API_TOKEN)
|
||||
if not result:
|
||||
raise HTTPException(status_code=401, detail="Неверная подпись initData")
|
||||
@@ -74,7 +80,12 @@ async def login_telegram_webapp(
|
||||
identity = await idb.get_or_create_identity_for_tg(session, int(tg_id))
|
||||
await bind_identity_actor(request, session, identity)
|
||||
token = await idb.issue_token_for_identity(session, identity)
|
||||
logger.info("[Auth] Login success: identity={}, tg_id={}, ip={}, method=telegram_webapp", identity.id, tg_id, _client_ip(request))
|
||||
logger.info(
|
||||
"[Auth] Login success: identity={}, tg_id={}, ip={}, method=telegram_webapp",
|
||||
identity.id,
|
||||
tg_id,
|
||||
_client_ip(request),
|
||||
)
|
||||
set_auth_cookie(response, token, request)
|
||||
set_is_admin_cookie(response, identity, request)
|
||||
return LoginResponse(identity_id=identity.id)
|
||||
|
||||
@@ -3,9 +3,11 @@ import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
import time
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -189,7 +191,9 @@ async def yandex_callback(
|
||||
token = await idb.issue_token_for_identity(session, identity)
|
||||
logger.info(
|
||||
"[Auth] Login success: identity={}, yandex_sub={}, ip={}, method=yandex",
|
||||
identity.id, yandex_sub, _client_ip(request),
|
||||
identity.id,
|
||||
yandex_sub,
|
||||
_client_ip(request),
|
||||
)
|
||||
redirect = RedirectResponse(return_to, status_code=302)
|
||||
set_auth_cookie(redirect, token, request)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.depends import get_request_actor, get_session, verify_identity_token
|
||||
from api.v2.base_crud import generate_crud_router
|
||||
from api.v2.schemas import CouponBase, CouponResponse, CouponUpdate
|
||||
from api.v2.schemas.web_public import CouponApplyRequest, CouponApplyResponse
|
||||
from api.depends import get_request_actor, get_session, verify_identity_token
|
||||
from database import identities as idb
|
||||
from database.models import Coupon
|
||||
from services.coupons import apply_fixed_coupon
|
||||
from services.errors import LimitExceededError, NotFoundError, ServiceError, ValidationError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
router = generate_crud_router(
|
||||
model=Coupon,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, UTC
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select
|
||||
@@ -10,6 +10,7 @@ from api.depends import get_session, verify_identity_admin
|
||||
from api.v2.schemas.flows import FlowCreate, FlowResponse, FlowUpdate
|
||||
from database.models import WebFlow
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
|
||||
@@ -36,8 +36,10 @@ from database.tariffs import get_tariff_by_id
|
||||
from database.temporary_data import create_temporary_data
|
||||
from services.errors import NotFoundError, ValidationError
|
||||
from services.formatting import get_site_gift_link
|
||||
from services.gifts import create_gift as service_create_gift
|
||||
from services.gifts import redeem_gift as service_redeem_gift
|
||||
from services.gifts import (
|
||||
create_gift as service_create_gift,
|
||||
redeem_gift as service_redeem_gift,
|
||||
)
|
||||
from services.payments.payment_links import PaymentLinkRequest, create_payment_link
|
||||
from services.tariffs import calculate_config_price
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from api.v2.schemas.identities import (
|
||||
)
|
||||
from database import identities as idb
|
||||
|
||||
|
||||
router = APIRouter(tags=["Identities"])
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from ._common import router, user_router
|
||||
from . import admin, user # noqa: F401 — import triggers endpoint registration
|
||||
from ._common import router, user_router
|
||||
|
||||
|
||||
__all__ = ["router", "user_router"]
|
||||
|
||||
@@ -92,7 +92,6 @@ from services.tariffs import calculate_config_price
|
||||
from services.tariffs.tariff_display import GB, get_effective_limits_for_key, get_key_tariff_addons_state
|
||||
|
||||
|
||||
|
||||
router = generate_crud_router(
|
||||
model=Key,
|
||||
schema_response=KeyResponse,
|
||||
@@ -105,8 +104,6 @@ router = generate_crud_router(
|
||||
user_router = APIRouter()
|
||||
|
||||
|
||||
|
||||
|
||||
def _key_actions_config() -> AccountKeyActionsConfigResponse:
|
||||
addons_mode = str(TARIFFS_CONFIG.get("KEY_ADDONS_PACK_MODE", "") or "").strip().lower()
|
||||
if addons_mode not in {"", "traffic", "devices", "all"}:
|
||||
@@ -223,13 +220,9 @@ async def _resolve_available_location_servers(session: AsyncSession, db_key: Key
|
||||
bound_servers = [s for s in available_servers if special in (s.get("special_groups") or [])]
|
||||
if bound_servers:
|
||||
available_servers = bound_servers
|
||||
names = sorted(
|
||||
{
|
||||
str(s.get("server_name") or "").strip()
|
||||
for s in available_servers
|
||||
if str(s.get("server_name") or "").strip()
|
||||
}
|
||||
)
|
||||
names = sorted({
|
||||
str(s.get("server_name") or "").strip() for s in available_servers if str(s.get("server_name") or "").strip()
|
||||
})
|
||||
return names
|
||||
|
||||
|
||||
@@ -275,5 +268,3 @@ def _normalize_expiry_ms(raw_value: int | float | None) -> int:
|
||||
elif value < 10**10:
|
||||
value *= 1000
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from ._common import * # noqa: F401,F403
|
||||
from ._common import router, user_router # noqa: F401
|
||||
|
||||
|
||||
@router.delete("/by_email/{email}", response_model=dict)
|
||||
async def delete_key_by_email(
|
||||
email: str = Path(..., description="Email клиента"),
|
||||
@@ -114,4 +115,3 @@ async def create_key_api(
|
||||
except Exception as e:
|
||||
logger.error(f"[API] Ошибка при создании ключа: {e}")
|
||||
raise HTTPException(status_code=500, detail="Ошибка при создании ключа")
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
from .._common import * # noqa: F401,F403 — подтягиваем все имена для endpoints
|
||||
from .._common import (
|
||||
_key_actions_config,
|
||||
_normalize_expiry_ms,
|
||||
_resolve_available_location_servers,
|
||||
_resolve_billing_user_id,
|
||||
_resolve_default_web_payment_provider,
|
||||
_resolve_public_base_url,
|
||||
_normalize_expiry_ms,
|
||||
router,
|
||||
user_router,
|
||||
)
|
||||
@@ -35,9 +35,7 @@ async def user_key_addons_preview(
|
||||
raise HTTPException(status_code=403, detail="Доп. опции отключены в настройках")
|
||||
billing_user_id = await _resolve_billing_user_id(request, identity, session)
|
||||
db_key = (
|
||||
await session.execute(
|
||||
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
|
||||
)
|
||||
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
|
||||
).scalar_one_or_none()
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
@@ -118,26 +116,32 @@ async def user_key_addons_preview(
|
||||
if not has_device_option and not has_traffic_option:
|
||||
raise HTTPException(status_code=400, detail="Доп. опции для этой подписки недоступны")
|
||||
if pack_mode:
|
||||
include_device_effective = bool(include_device) if include_device is not None else selected_device_limit is not None
|
||||
include_traffic_effective = bool(include_traffic) if include_traffic is not None else selected_traffic_gb is not None
|
||||
selected_device = (
|
||||
selected_device_limit
|
||||
if selected_device_limit is not None
|
||||
else None
|
||||
include_device_effective = (
|
||||
bool(include_device) if include_device is not None else selected_device_limit is not None
|
||||
)
|
||||
selected_traffic = (
|
||||
selected_traffic_gb
|
||||
if selected_traffic_gb is not None
|
||||
else None
|
||||
include_traffic_effective = (
|
||||
bool(include_traffic) if include_traffic is not None else selected_traffic_gb is not None
|
||||
)
|
||||
selected_device = selected_device_limit if selected_device_limit is not None else None
|
||||
selected_traffic = selected_traffic_gb if selected_traffic_gb is not None else None
|
||||
else:
|
||||
include_device_effective = has_device_option
|
||||
include_traffic_effective = has_traffic_option
|
||||
selected_device = selected_device_limit if selected_device_limit is not None else current_device_limit
|
||||
selected_traffic = selected_traffic_gb if selected_traffic_gb is not None else current_traffic_gb
|
||||
if has_device_option and include_device_effective and selected_device is not None and int(selected_device) not in device_options:
|
||||
if (
|
||||
has_device_option
|
||||
and include_device_effective
|
||||
and selected_device is not None
|
||||
and int(selected_device) not in device_options
|
||||
):
|
||||
raise HTTPException(status_code=400, detail="Выбранный пакет устройств недоступен")
|
||||
if has_traffic_option and include_traffic_effective and selected_traffic is not None and int(selected_traffic) not in traffic_options:
|
||||
if (
|
||||
has_traffic_option
|
||||
and include_traffic_effective
|
||||
and selected_traffic is not None
|
||||
and int(selected_traffic) not in traffic_options
|
||||
):
|
||||
raise HTTPException(status_code=400, detail="Выбранный пакет трафика недоступен")
|
||||
current_devices_for_price = int(current_device_limit) if current_device_limit is not None else None
|
||||
current_traffic_for_price = int(current_traffic_gb) if current_traffic_gb is not None else None
|
||||
@@ -154,8 +158,12 @@ async def user_key_addons_preview(
|
||||
tariff=tariff,
|
||||
has_device_option=bool(has_device_option and include_device_effective),
|
||||
has_traffic_option=bool(has_traffic_option and include_traffic_effective),
|
||||
selected_devices=int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
|
||||
selected_traffic_gb=int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
|
||||
selected_devices=int(selected_device)
|
||||
if has_device_option and include_device_effective and selected_device is not None
|
||||
else None,
|
||||
selected_traffic_gb=int(selected_traffic)
|
||||
if has_traffic_option and include_traffic_effective and selected_traffic is not None
|
||||
else None,
|
||||
)
|
||||
)
|
||||
recalc_enabled = bool(
|
||||
@@ -177,8 +185,12 @@ async def user_key_addons_preview(
|
||||
total_price_rub = int(
|
||||
calculate_config_price(
|
||||
tariff=tariff,
|
||||
selected_device_limit=int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
|
||||
selected_traffic_gb=int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
|
||||
selected_device_limit=int(selected_device)
|
||||
if has_device_option and include_device_effective and selected_device is not None
|
||||
else None,
|
||||
selected_traffic_gb=int(selected_traffic)
|
||||
if has_traffic_option and include_traffic_effective and selected_traffic is not None
|
||||
else None,
|
||||
)
|
||||
)
|
||||
extra_price_rub = int(max(0, total_price_rub - base_price_for_current))
|
||||
@@ -196,8 +208,12 @@ async def user_key_addons_preview(
|
||||
has_traffic_option=bool(has_traffic_option),
|
||||
current_device_limit=int(current_device_limit) if current_device_limit is not None else None,
|
||||
current_traffic_gb=int(current_traffic_gb) if current_traffic_gb is not None else None,
|
||||
selected_device_limit=int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
|
||||
selected_traffic_gb=int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
|
||||
selected_device_limit=int(selected_device)
|
||||
if has_device_option and include_device_effective and selected_device is not None
|
||||
else None,
|
||||
selected_traffic_gb=int(selected_traffic)
|
||||
if has_traffic_option and include_traffic_effective and selected_traffic is not None
|
||||
else None,
|
||||
device_options=[
|
||||
AccountKeyAddonOptionResponse(
|
||||
value=int(val),
|
||||
@@ -212,11 +228,7 @@ async def user_key_addons_preview(
|
||||
traffic_options=[
|
||||
AccountKeyAddonOptionResponse(
|
||||
value=int(val),
|
||||
label=(
|
||||
"Безлимит трафика"
|
||||
if int(val) <= 0
|
||||
else (f"+{int(val)} ГБ" if pack_mode else f"{int(val)} ГБ")
|
||||
),
|
||||
label=("Безлимит трафика" if int(val) <= 0 else (f"+{int(val)} ГБ" if pack_mode else f"{int(val)} ГБ")),
|
||||
)
|
||||
for val in traffic_options
|
||||
],
|
||||
@@ -243,9 +255,7 @@ async def user_key_apply_addons(
|
||||
raise HTTPException(status_code=403, detail="Доп. опции отключены в настройках")
|
||||
billing_user_id = await _resolve_billing_user_id(request, identity, session)
|
||||
db_key = (
|
||||
await session.execute(
|
||||
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
|
||||
)
|
||||
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
|
||||
).scalar_one_or_none()
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
@@ -332,24 +342,26 @@ async def user_key_apply_addons(
|
||||
include_traffic_effective = (
|
||||
bool(body.include_traffic) if body.include_traffic is not None else body.selected_traffic_gb is not None
|
||||
)
|
||||
selected_device = (
|
||||
body.selected_device_limit
|
||||
if body.selected_device_limit is not None
|
||||
else None
|
||||
)
|
||||
selected_traffic = (
|
||||
body.selected_traffic_gb
|
||||
if body.selected_traffic_gb is not None
|
||||
else None
|
||||
)
|
||||
selected_device = body.selected_device_limit if body.selected_device_limit is not None else None
|
||||
selected_traffic = body.selected_traffic_gb if body.selected_traffic_gb is not None else None
|
||||
else:
|
||||
include_device_effective = has_device_option
|
||||
include_traffic_effective = has_traffic_option
|
||||
selected_device = body.selected_device_limit if body.selected_device_limit is not None else current_device_limit
|
||||
selected_traffic = body.selected_traffic_gb if body.selected_traffic_gb is not None else current_traffic_gb
|
||||
if has_device_option and include_device_effective and selected_device is not None and int(selected_device) not in device_options:
|
||||
if (
|
||||
has_device_option
|
||||
and include_device_effective
|
||||
and selected_device is not None
|
||||
and int(selected_device) not in device_options
|
||||
):
|
||||
raise HTTPException(status_code=400, detail="Выбранный пакет устройств недоступен")
|
||||
if has_traffic_option and include_traffic_effective and selected_traffic is not None and int(selected_traffic) not in traffic_options:
|
||||
if (
|
||||
has_traffic_option
|
||||
and include_traffic_effective
|
||||
and selected_traffic is not None
|
||||
and int(selected_traffic) not in traffic_options
|
||||
):
|
||||
raise HTTPException(status_code=400, detail="Выбранный пакет трафика недоступен")
|
||||
current_devices_for_price = int(current_device_limit) if current_device_limit is not None else None
|
||||
current_traffic_for_price = int(current_traffic_gb) if current_traffic_gb is not None else None
|
||||
@@ -367,8 +379,12 @@ async def user_key_apply_addons(
|
||||
tariff=tariff,
|
||||
has_device_option=bool(has_device_option and include_device_effective),
|
||||
has_traffic_option=bool(has_traffic_option and include_traffic_effective),
|
||||
selected_devices=int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
|
||||
selected_traffic_gb=int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
|
||||
selected_devices=int(selected_device)
|
||||
if has_device_option and include_device_effective and selected_device is not None
|
||||
else None,
|
||||
selected_traffic_gb=int(selected_traffic)
|
||||
if has_traffic_option and include_traffic_effective and selected_traffic is not None
|
||||
else None,
|
||||
)
|
||||
)
|
||||
recalc_enabled = bool(
|
||||
@@ -390,8 +406,12 @@ async def user_key_apply_addons(
|
||||
selected_total_price = int(
|
||||
calculate_config_price(
|
||||
tariff=tariff,
|
||||
selected_device_limit=int(selected_device) if has_device_option and selected_device is not None else None,
|
||||
selected_traffic_gb=int(selected_traffic) if has_traffic_option and selected_traffic is not None else None,
|
||||
selected_device_limit=int(selected_device)
|
||||
if has_device_option and selected_device is not None
|
||||
else None,
|
||||
selected_traffic_gb=int(selected_traffic)
|
||||
if has_traffic_option and selected_traffic is not None
|
||||
else None,
|
||||
)
|
||||
)
|
||||
extra_price_rub = int(max(0, selected_total_price - base_price_for_current))
|
||||
@@ -458,8 +478,12 @@ async def user_key_apply_addons(
|
||||
"payment_flow": "key_addons",
|
||||
"tariff_id": int(tariff_id),
|
||||
"email": email,
|
||||
"selected_device_limit": int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
|
||||
"selected_traffic_gb": int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
|
||||
"selected_device_limit": int(selected_device)
|
||||
if has_device_option and include_device_effective and selected_device is not None
|
||||
else None,
|
||||
"selected_traffic_gb": int(selected_traffic)
|
||||
if has_traffic_option and include_traffic_effective and selected_traffic is not None
|
||||
else None,
|
||||
"current_device_limit": int(current_device_limit) if current_device_limit is not None else None,
|
||||
"current_traffic_gb": int(current_traffic_gb) if current_traffic_gb is not None else None,
|
||||
"original_price": int(base_price_for_current),
|
||||
@@ -480,8 +504,12 @@ async def user_key_apply_addons(
|
||||
"tariff_id": int(tariff_id),
|
||||
"email": email,
|
||||
"required_amount": int(required_amount),
|
||||
"selected_device_limit": int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
|
||||
"selected_traffic_gb": int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
|
||||
"selected_device_limit": int(selected_device)
|
||||
if has_device_option and include_device_effective and selected_device is not None
|
||||
else None,
|
||||
"selected_traffic_gb": int(selected_traffic)
|
||||
if has_traffic_option and include_traffic_effective and selected_traffic is not None
|
||||
else None,
|
||||
"current_device_limit": int(current_device_limit) if current_device_limit is not None else None,
|
||||
"current_traffic_gb": int(current_traffic_gb) if current_traffic_gb is not None else None,
|
||||
"original_price": int(base_price_for_current),
|
||||
@@ -561,22 +589,34 @@ async def user_key_apply_addons(
|
||||
session=session,
|
||||
email=email,
|
||||
selected_devices=int(new_device_limit_effective) if new_device_limit_effective is not None else None,
|
||||
selected_traffic_gb=int(new_traffic_limit_gb_effective) if new_traffic_limit_gb_effective is not None else None,
|
||||
selected_traffic_gb=int(new_traffic_limit_gb_effective)
|
||||
if new_traffic_limit_gb_effective is not None
|
||||
else None,
|
||||
total_price=int(total_price_after_purchase),
|
||||
has_device_choice=bool(has_device_option and include_device_effective),
|
||||
has_traffic_choice=bool(has_traffic_option and include_traffic_effective),
|
||||
config_mode="pack",
|
||||
)
|
||||
else:
|
||||
selected_device_for_effective = int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None
|
||||
selected_traffic_for_effective = int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else 0
|
||||
selected_device_for_effective = (
|
||||
int(selected_device)
|
||||
if has_device_option and include_device_effective and selected_device is not None
|
||||
else None
|
||||
)
|
||||
selected_traffic_for_effective = (
|
||||
int(selected_traffic)
|
||||
if has_traffic_option and include_traffic_effective and selected_traffic is not None
|
||||
else 0
|
||||
)
|
||||
device_limit_effective_new, traffic_limit_bytes_effective_new = await get_effective_limits_for_key(
|
||||
session=session,
|
||||
tariff_id=int(tariff_id),
|
||||
selected_device_limit=selected_device_for_effective,
|
||||
selected_traffic_gb=selected_traffic_for_effective,
|
||||
)
|
||||
traffic_limit_gb_effective = int(traffic_limit_bytes_effective_new / GB) if traffic_limit_bytes_effective_new else 0
|
||||
traffic_limit_gb_effective = (
|
||||
int(traffic_limit_bytes_effective_new / GB) if traffic_limit_bytes_effective_new else 0
|
||||
)
|
||||
await renew_key_in_cluster(
|
||||
cluster_id=server_id,
|
||||
email=email,
|
||||
@@ -593,8 +633,12 @@ async def user_key_apply_addons(
|
||||
await save_key_config_with_mode(
|
||||
session=session,
|
||||
email=email,
|
||||
selected_devices=int(selected_device) if has_device_option and include_device_effective and selected_device is not None else None,
|
||||
selected_traffic_gb=int(selected_traffic) if has_traffic_option and include_traffic_effective and selected_traffic is not None else None,
|
||||
selected_devices=int(selected_device)
|
||||
if has_device_option and include_device_effective and selected_device is not None
|
||||
else None,
|
||||
selected_traffic_gb=int(selected_traffic)
|
||||
if has_traffic_option and include_traffic_effective and selected_traffic is not None
|
||||
else None,
|
||||
total_price=int(total_price_after_purchase),
|
||||
has_device_choice=bool(has_device_option and include_device_effective),
|
||||
has_traffic_choice=bool(has_traffic_option and include_traffic_effective),
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
from .._common import * # noqa: F401,F403 — подтягиваем все имена для endpoints
|
||||
from .._common import (
|
||||
_key_actions_config,
|
||||
_normalize_expiry_ms,
|
||||
_resolve_available_location_servers,
|
||||
_resolve_billing_user_id,
|
||||
_resolve_default_web_payment_provider,
|
||||
_resolve_public_base_url,
|
||||
_normalize_expiry_ms,
|
||||
router,
|
||||
user_router,
|
||||
)
|
||||
@@ -69,9 +69,7 @@ async def user_key_details(
|
||||
):
|
||||
billing_user_id = await _resolve_billing_user_id(request, identity, session)
|
||||
db_key = (
|
||||
await session.execute(
|
||||
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
|
||||
)
|
||||
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
|
||||
).scalar_one_or_none()
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
@@ -153,9 +151,7 @@ async def user_key_qr(
|
||||
raise HTTPException(status_code=403, detail="QR для подписок отключен в настройках")
|
||||
billing_user_id = await _resolve_billing_user_id(request, identity, session)
|
||||
db_key = (
|
||||
await session.execute(
|
||||
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
|
||||
)
|
||||
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
|
||||
).scalar_one_or_none()
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
@@ -194,9 +190,7 @@ async def user_key_update_alias(
|
||||
raise HTTPException(status_code=400, detail="Alias содержит недопустимые символы")
|
||||
billing_user_id = await _resolve_billing_user_id(request, identity, session)
|
||||
db_key = (
|
||||
await session.execute(
|
||||
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
|
||||
)
|
||||
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
|
||||
).scalar_one_or_none()
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
@@ -229,9 +223,7 @@ async def user_key_delete(
|
||||
raise HTTPException(status_code=403, detail="Удаление подписки отключено в настройках")
|
||||
billing_user_id = await _resolve_billing_user_id(request, identity, session)
|
||||
db_key = (
|
||||
await session.execute(
|
||||
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
|
||||
)
|
||||
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
|
||||
).scalar_one_or_none()
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
from .._common import * # noqa: F401,F403 — подтягиваем все имена для endpoints
|
||||
from .._common import (
|
||||
_key_actions_config,
|
||||
_normalize_expiry_ms,
|
||||
_resolve_available_location_servers,
|
||||
_resolve_billing_user_id,
|
||||
_resolve_default_web_payment_provider,
|
||||
_resolve_public_base_url,
|
||||
_normalize_expiry_ms,
|
||||
router,
|
||||
user_router,
|
||||
)
|
||||
@@ -30,9 +30,7 @@ async def user_key_reset_hwid(
|
||||
raise HTTPException(status_code=403, detail="Сброс устройств отключен в настройках")
|
||||
billing_user_id = await _resolve_billing_user_id(request, identity, session)
|
||||
db_key = (
|
||||
await session.execute(
|
||||
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
|
||||
)
|
||||
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
|
||||
).scalar_one_or_none()
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
from .._common import * # noqa: F401,F403 — подтягиваем все имена для endpoints
|
||||
from .._common import (
|
||||
_key_actions_config,
|
||||
_normalize_expiry_ms,
|
||||
_resolve_available_location_servers,
|
||||
_resolve_billing_user_id,
|
||||
_resolve_default_web_payment_provider,
|
||||
_resolve_public_base_url,
|
||||
_normalize_expiry_ms,
|
||||
router,
|
||||
user_router,
|
||||
)
|
||||
@@ -30,9 +30,7 @@ async def user_key_locations(
|
||||
raise HTTPException(status_code=403, detail="Смена локации отключена в настройках")
|
||||
billing_user_id = await _resolve_billing_user_id(request, identity, session)
|
||||
db_key = (
|
||||
await session.execute(
|
||||
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
|
||||
)
|
||||
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
|
||||
).scalar_one_or_none()
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
@@ -61,9 +59,7 @@ async def user_key_change_location(
|
||||
raise HTTPException(status_code=400, detail="Укажите целевую локацию")
|
||||
billing_user_id = await _resolve_billing_user_id(request, identity, session)
|
||||
db_key = (
|
||||
await session.execute(
|
||||
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
|
||||
)
|
||||
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
|
||||
).scalar_one_or_none()
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
from .._common import * # noqa: F401,F403 — подтягиваем все имена для endpoints
|
||||
from .._common import (
|
||||
_key_actions_config,
|
||||
_normalize_expiry_ms,
|
||||
_resolve_available_location_servers,
|
||||
_resolve_billing_user_id,
|
||||
_resolve_default_web_payment_provider,
|
||||
_resolve_public_base_url,
|
||||
_normalize_expiry_ms,
|
||||
router,
|
||||
user_router,
|
||||
)
|
||||
@@ -39,9 +39,7 @@ async def user_key_renew(
|
||||
raise HTTPException(status_code=403, detail="Продление подписки отключено в настройках")
|
||||
billing_user_id = await _resolve_billing_user_id(request, identity, session)
|
||||
db_key = (
|
||||
await session.execute(
|
||||
select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1)
|
||||
)
|
||||
await session.execute(select(Key).where(Key.user_id == billing_user_id, Key.client_id == client_id).limit(1))
|
||||
).scalar_one_or_none()
|
||||
if db_key is None:
|
||||
raise HTTPException(status_code=404, detail="Подписка не найдена")
|
||||
|
||||
@@ -3,6 +3,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.depends import get_session, verify_identity_admin
|
||||
from api.v2.base_crud import generate_crud_router
|
||||
from api.v2.schemas import (
|
||||
BlockedUserResponse,
|
||||
ManualBanResponse,
|
||||
@@ -11,7 +12,6 @@ from api.v2.schemas import (
|
||||
TemporaryDataResponse,
|
||||
TrackingSourceResponse,
|
||||
)
|
||||
from api.v2.base_crud import generate_crud_router
|
||||
from database import get_tracking_source_stats
|
||||
from database.access.resolution import resolve_user_optional
|
||||
from database.models import (
|
||||
@@ -23,6 +23,7 @@ from database.models import (
|
||||
TrackingSource,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(
|
||||
|
||||
@@ -3,8 +3,9 @@ from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.depends import get_session, verify_identity_token
|
||||
from database.models import Identity
|
||||
from database import web_notifications as wn_db
|
||||
from database.models import Identity
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -55,7 +56,9 @@ async def get_notifications(
|
||||
identity: Identity = Depends(verify_identity_token),
|
||||
):
|
||||
notifications = await wn_db.get_notifications_for_identity(
|
||||
session, identity.id, limit=limit,
|
||||
session,
|
||||
identity.id,
|
||||
limit=limit,
|
||||
)
|
||||
unread_count = await wn_db.count_unread_for_identity(session, identity.id)
|
||||
|
||||
|
||||
+15
-14
@@ -1,13 +1,15 @@
|
||||
import csv
|
||||
import re
|
||||
|
||||
from base64 import b64encode
|
||||
from datetime import datetime
|
||||
from io import BytesIO, StringIO
|
||||
import re
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import qrcode
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
import qrcode
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -16,17 +18,18 @@ from api.v2.schemas.web_public import (
|
||||
PartnerApplyRequest,
|
||||
PartnerApplyResponse,
|
||||
PartnerConditionsResponse,
|
||||
PartnerQrResponse,
|
||||
PartnerTopEntryResponse,
|
||||
PartnerTopResponse,
|
||||
PartnerPayoutEntryResponse,
|
||||
PartnerPayoutHistoryResponse,
|
||||
PartnerPayoutRequestCreate,
|
||||
PartnerPayoutRequestResponse,
|
||||
PartnerQrResponse,
|
||||
PartnerTopEntryResponse,
|
||||
PartnerTopResponse,
|
||||
)
|
||||
from database import identities as idb
|
||||
from utils.referral_codes import decode_partner_code, encode_partner_code
|
||||
|
||||
|
||||
try:
|
||||
from modules.partner_program.settings import PARTNER_BONUS_PERCENTAGES
|
||||
except Exception:
|
||||
@@ -63,10 +66,10 @@ def _row_dt_iso(value) -> str | None:
|
||||
|
||||
def _resolve_public_base_url(request: Request) -> str:
|
||||
origin = str(request.headers.get("origin") or "").strip()
|
||||
if origin.startswith("http://") or origin.startswith("https://"):
|
||||
if origin.startswith(("http://", "https://")):
|
||||
return origin.rstrip("/")
|
||||
referer = str(request.headers.get("referer") or request.headers.get("referrer") or "").strip()
|
||||
if referer.startswith("http://") or referer.startswith("https://"):
|
||||
if referer.startswith(("http://", "https://")):
|
||||
parsed = urlsplit(referer)
|
||||
if parsed.scheme and parsed.netloc:
|
||||
return f"{parsed.scheme}://{parsed.netloc}".rstrip("/")
|
||||
@@ -351,9 +354,9 @@ async def partner_conditions(
|
||||
("ENABLE_PAYOUT_USDT", "USDT"),
|
||||
("ENABLE_PAYOUT_TON", "TON"),
|
||||
]
|
||||
payout_methods = [
|
||||
title for key, title in method_map if bool(getattr(partner_settings, key, False))
|
||||
] if partner_settings else []
|
||||
payout_methods = (
|
||||
[title for key, title in method_map if bool(getattr(partner_settings, key, False))] if partner_settings else []
|
||||
)
|
||||
level_lines: list[str] = []
|
||||
all_levels = sorted({int(k) for k in [*percent_levels_raw.keys(), *flat_levels_raw.keys()] if str(k).isdigit()})
|
||||
for level in all_levels:
|
||||
@@ -422,9 +425,7 @@ async def partner_payouts_me(
|
||||
"""
|
||||
)
|
||||
total = int((await session.scalar(count_sql, {"tg_id": tg_id})) or 0)
|
||||
rows = (
|
||||
await session.execute(rows_sql, {"tg_id": tg_id, "limit": int(limit), "offset": int(offset)})
|
||||
).fetchall()
|
||||
rows = (await session.execute(rows_sql, {"tg_id": tg_id, "limit": int(limit), "offset": int(offset)})).fetchall()
|
||||
items = [
|
||||
PartnerPayoutEntryResponse(
|
||||
id=int(row[0]),
|
||||
@@ -1094,13 +1095,13 @@ async def reset_disabled_payout_methods(
|
||||
):
|
||||
"""Сбрасывает реквизиты для отключённых способов вывода."""
|
||||
try:
|
||||
from modules.partner_program import buttons as B
|
||||
from modules.partner_program.settings import (
|
||||
ENABLE_PAYOUT_CARD,
|
||||
ENABLE_PAYOUT_SBP,
|
||||
ENABLE_PAYOUT_TON,
|
||||
ENABLE_PAYOUT_USDT,
|
||||
)
|
||||
from modules.partner_program import buttons as B
|
||||
except Exception:
|
||||
ENABLE_PAYOUT_CARD = True
|
||||
ENABLE_PAYOUT_USDT = True
|
||||
|
||||
@@ -3,6 +3,7 @@ from io import BytesIO
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import qrcode
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -15,14 +16,25 @@ from api.v2.schemas.web_public import (
|
||||
ReferralTopEntryResponse,
|
||||
ReferralTopResponse,
|
||||
)
|
||||
from config import CHECK_REFERRAL_REWARD_ISSUED, REFERRAL_BONUS_PERCENTAGES, REFERRAL_BUTTON, REFERRAL_QR, TOP_REFERRAL_BUTTON
|
||||
from config import (
|
||||
CHECK_REFERRAL_REWARD_ISSUED,
|
||||
REFERRAL_BONUS_PERCENTAGES,
|
||||
REFERRAL_BUTTON,
|
||||
REFERRAL_QR,
|
||||
TOP_REFERRAL_BUTTON,
|
||||
)
|
||||
from core.bootstrap import BUTTONS_CONFIG
|
||||
from database import add_referral, get_referral_by_referred_id, get_user_referral_count
|
||||
from database.referrals import get_referral_position, get_top_referrals
|
||||
from database import identities as idb
|
||||
from database import (
|
||||
add_referral,
|
||||
get_referral_by_referred_id,
|
||||
get_user_referral_count,
|
||||
identities as idb,
|
||||
)
|
||||
from database.access.resolution import resolve_user_optional
|
||||
from database.referrals import get_referral_position, get_top_referrals
|
||||
from utils.referral_codes import decode_referral_code, encode_referral_code
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -44,10 +56,10 @@ def _normalize_referrer_code(value: str | None, fallback_tg_id: int | None) -> i
|
||||
|
||||
def _resolve_public_base_url(request: Request) -> str:
|
||||
origin = str(request.headers.get("origin") or "").strip()
|
||||
if origin.startswith("http://") or origin.startswith("https://"):
|
||||
if origin.startswith(("http://", "https://")):
|
||||
return origin.rstrip("/")
|
||||
referer = str(request.headers.get("referer") or request.headers.get("referrer") or "").strip()
|
||||
if referer.startswith("http://") or referer.startswith("https://"):
|
||||
if referer.startswith(("http://", "https://")):
|
||||
parsed = urlsplit(referer)
|
||||
if parsed.scheme and parsed.netloc:
|
||||
return f"{parsed.scheme}://{parsed.netloc}".rstrip("/")
|
||||
@@ -170,7 +182,9 @@ async def referral_conditions(
|
||||
level_lines.append(f"{level} уровень: {label}")
|
||||
one_time_mode = bool(CHECK_REFERRAL_REWARD_ISSUED)
|
||||
bonus_mode = "one_time" if one_time_mode else "each_payment"
|
||||
bonus_mode_label = "Бонус за первую успешную оплату реферала" if one_time_mode else "Бонус за каждую успешную оплату реферала"
|
||||
bonus_mode_label = (
|
||||
"Бонус за первую успешную оплату реферала" if one_time_mode else "Бонус за каждую успешную оплату реферала"
|
||||
)
|
||||
rules = [
|
||||
"Бонус начисляется только за реальных приглашённых пользователей.",
|
||||
"Нельзя использовать собственную реферальную ссылку.",
|
||||
|
||||
+11
-13
@@ -4,6 +4,7 @@ import re
|
||||
import time
|
||||
|
||||
import aiohttp
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from config import (
|
||||
@@ -21,18 +22,19 @@ from config import (
|
||||
REFERRAL_QR,
|
||||
REMNAWAVE_WEBAPP,
|
||||
REMNAWAVE_WEBAPP_OPEN_IN_BROWSER,
|
||||
TOP_REFERRAL_BUTTON,
|
||||
TRIAL_TIME_DISABLE,
|
||||
USE_COUNTRY_SELECTION,
|
||||
USERNAME_BOT,
|
||||
TELEGRAM_WEBAPP_DIRECT_LINK,
|
||||
TELEGRAM_WEBAPP_SHORT_NAME,
|
||||
TOP_REFERRAL_BUTTON,
|
||||
TRIAL_TIME_DISABLE,
|
||||
USERNAME_BOT,
|
||||
USE_COUNTRY_SELECTION,
|
||||
)
|
||||
from core.bootstrap import BUTTONS_CONFIG, MODES_CONFIG, MONEY_CONFIG, PAYMENTS_CONFIG
|
||||
from core.settings.web_config import WEB_CONFIG
|
||||
from core.settings.money_config import get_currency_mode
|
||||
from core.settings.web_config import WEB_CONFIG
|
||||
from services.payments.providers import PROVIDERS_BASE, TELEGRAM_ONLY_PROVIDER_IDS, WEB_LINK_PROVIDER_IDS
|
||||
|
||||
|
||||
router = APIRouter(tags=["Root"])
|
||||
|
||||
|
||||
@@ -113,9 +115,7 @@ async def site_config():
|
||||
"email_code_login_enabled": bool(MODES_CONFIG.get("WEB_EMAIL_CODE_LOGIN_ENABLED", True)),
|
||||
},
|
||||
"mobile": {
|
||||
"prefer_mini_app_on_telegram_mobile": bool(
|
||||
MODES_CONFIG.get("PREFER_MINI_APP_ON_TELEGRAM_MOBILE", False)
|
||||
),
|
||||
"prefer_mini_app_on_telegram_mobile": bool(MODES_CONFIG.get("PREFER_MINI_APP_ON_TELEGRAM_MOBILE", False)),
|
||||
},
|
||||
"features": {
|
||||
"channel_enabled": bool(BUTTONS_CONFIG.get("CHANNEL_BUTTON_ENABLE", CHANNEL_EXISTS)),
|
||||
@@ -129,9 +129,7 @@ async def site_config():
|
||||
"coupon_enabled": bool(BUTTONS_CONFIG.get("COUPON_BUTTON_ENABLE", True)),
|
||||
"qr_subscription_enabled": bool(MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK)),
|
||||
"hwid_reset_enabled": bool(BUTTONS_CONFIG.get("HWID_RESET_BUTTON_ENABLE", HWID_RESET_BUTTON)),
|
||||
"country_selection_enabled": bool(
|
||||
MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION)
|
||||
),
|
||||
"country_selection_enabled": bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION)),
|
||||
"captcha_enabled": bool(MODES_CONFIG.get("CAPTCHA_ENABLED", CAPTCHA_ENABLE)),
|
||||
"channel_check_enabled": bool(MODES_CONFIG.get("CHANNEL_CHECK_ENABLED", CHANNEL_REQUIRED)),
|
||||
"trial_enabled": not bool(MODES_CONFIG.get("TRIAL_TIME_DISABLED", TRIAL_TIME_DISABLE)),
|
||||
@@ -201,7 +199,7 @@ def _parse_semver(tag: str) -> tuple[int, int, int, int, tuple[tuple[int, int |
|
||||
|
||||
|
||||
async def _fetch_ghcr_latest_tag(image: str) -> str | None:
|
||||
"""Анонимно тянем список тегов публичного GHCR-пакета и возвращаем максимальный семвер."""
|
||||
"""Возвращает максимальный semver-тег образа в GHCR."""
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
|
||||
token_url = f"https://ghcr.io/token?scope=repository:{image}:pull"
|
||||
async with session.get(token_url) as token_resp:
|
||||
@@ -231,7 +229,7 @@ async def _fetch_ghcr_latest_tag(image: str) -> str | None:
|
||||
|
||||
@router.get("/api/meta/update-check", include_in_schema=True)
|
||||
async def update_check():
|
||||
"""Сравнивает текущую версию Solo-brick с последним тегом публичного GHCR-образа."""
|
||||
"""Сравнивает текущую версию Solo-brick с последним доступным релизом."""
|
||||
current = (os.environ.get("APP_VERSION") or "").strip()
|
||||
image = (os.environ.get("GHCR_IMAGE") or "").strip()
|
||||
now = time.time()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from api.v2.schemas import ServerBase, ServerResponse, ServerUpdate
|
||||
from api.v2.base_crud import generate_crud_router
|
||||
from api.v2.schemas import ServerBase, ServerResponse, ServerUpdate
|
||||
from database.models import Server
|
||||
|
||||
|
||||
router = generate_crud_router(
|
||||
model=Server,
|
||||
schema_response=ServerResponse,
|
||||
|
||||
@@ -7,7 +7,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.depends import get_session, verify_identity_admin
|
||||
from api.v2.schemas import SettingResponse, SettingUpsert
|
||||
from database.settings_cache import settings_cache
|
||||
from core.settings.buttons_config import BUTTONS_CONFIG, update_buttons_config
|
||||
from core.settings.modes_config import MODES_CONFIG, update_modes_config
|
||||
from core.settings.money_config import MONEY_CONFIG, update_money_config
|
||||
@@ -17,6 +16,8 @@ from core.settings.providers_order_config import PROVIDERS_ORDER, update_provide
|
||||
from core.settings.tariffs_config import TARIFFS_CONFIG, update_tariffs_config
|
||||
from database.models import Setting
|
||||
from database.settings import set_setting
|
||||
from database.settings_cache import settings_cache
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ from database.coupons import mark_coupon_used
|
||||
from database.models import Tariff
|
||||
from database.tariffs import get_tariff_by_id
|
||||
from database.temporary_data import create_temporary_data
|
||||
from services.keys import create_vpn_key_headless
|
||||
from logger import logger
|
||||
from services.keys import create_vpn_key_headless
|
||||
from services.payments.payment_links import PaymentLinkRequest, create_payment_link
|
||||
from services.payments.providers import WEB_LINK_PROVIDER_IDS
|
||||
from services.tariffs import calculate_config_price
|
||||
@@ -143,7 +143,11 @@ async def get_tariffs_public(
|
||||
if isinstance(cached, list):
|
||||
return cached
|
||||
|
||||
q = select(Tariff).where(Tariff.is_active.is_(True)).order_by(Tariff.sort_order.asc().nulls_last(), Tariff.price_rub.asc())
|
||||
q = (
|
||||
select(Tariff)
|
||||
.where(Tariff.is_active.is_(True))
|
||||
.order_by(Tariff.sort_order.asc().nulls_last(), Tariff.price_rub.asc())
|
||||
)
|
||||
if tariff_ids:
|
||||
try:
|
||||
ids = [int(x.strip()) for x in tariff_ids.split(",") if x.strip()]
|
||||
|
||||
@@ -5,13 +5,14 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.depends import get_session, verify_identity_admin
|
||||
from api.v2.schemas import UserBase, UserResponse, UserUpdate
|
||||
from api.v2.base_crud import generate_crud_router
|
||||
from api.v2.schemas import UserBase, UserResponse, UserUpdate
|
||||
from database import async_session_maker, delete_user_data, get_servers
|
||||
from database.models import Key, User
|
||||
from database.access.resolution import resolve_user_optional
|
||||
from services.operations import delete_key_from_cluster
|
||||
from database.models import Key, User
|
||||
from logger import logger
|
||||
from services.operations import delete_key_from_cluster
|
||||
|
||||
|
||||
router = generate_crud_router(
|
||||
model=User,
|
||||
|
||||
+10
-13
@@ -2,12 +2,11 @@ import hashlib
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -54,6 +53,7 @@ EXTENSION_CONTENT_TYPES: dict[str, frozenset[str]] = {
|
||||
|
||||
def _sanitize_svg(data: bytes) -> bytes:
|
||||
import re as _re
|
||||
|
||||
text = data.decode("utf-8", errors="replace")
|
||||
text = _re.sub(r"<script[^>]*>.*?</script>", "", text, flags=_re.DOTALL | _re.IGNORECASE)
|
||||
text = _re.sub(r"<style[^>]*>.*?</style>", "", text, flags=_re.DOTALL | _re.IGNORECASE)
|
||||
@@ -535,9 +535,7 @@ async def list_custom_element_builds(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
_identity=Depends(verify_identity_admin),
|
||||
):
|
||||
result = await session.execute(
|
||||
select(WebCustomElementBuild).order_by(WebCustomElementBuild.created_at.desc())
|
||||
)
|
||||
result = await session.execute(select(WebCustomElementBuild).order_by(WebCustomElementBuild.created_at.desc()))
|
||||
builds = result.scalars().all()
|
||||
return [_build_to_dict(b) for b in builds]
|
||||
|
||||
@@ -630,8 +628,9 @@ async def ingest_flow_events(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
from core.redis_cache import cache_incr_checked
|
||||
from api.v2.routes.auth._fallback_limiter import check_and_increment
|
||||
from core.redis_cache import cache_incr_checked
|
||||
|
||||
ip = (request.client.host if request.client else "") or "unknown"
|
||||
count, redis_ok = await cache_incr_checked(f"analytics_rate:{ip}", 60)
|
||||
if not redis_ok:
|
||||
@@ -709,9 +708,7 @@ async def get_flow_funnel(
|
||||
|
||||
for i, node in enumerate(funnel):
|
||||
prev_entered = funnel[i - 1]["entered"] if i > 0 else node["entered"]
|
||||
node["dropOff"] = round(
|
||||
(1 - node["entered"] / prev_entered) * 100, 1
|
||||
) if prev_entered > 0 else 0
|
||||
node["dropOff"] = round((1 - node["entered"] / prev_entered) * 100, 1) if prev_entered > 0 else 0
|
||||
|
||||
return {"flowId": flow_id, "days": days, "funnel": funnel}
|
||||
|
||||
@@ -732,6 +729,7 @@ def _error_signature(name: str, message: str, stack: str | None, url: str | None
|
||||
if url:
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
pathname = urlparse(url).path[:100]
|
||||
except Exception:
|
||||
pass
|
||||
@@ -757,8 +755,9 @@ async def ingest_error_report(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
from core.redis_cache import cache_incr_checked
|
||||
from api.v2.routes.auth._fallback_limiter import check_and_increment
|
||||
from core.redis_cache import cache_incr_checked
|
||||
|
||||
ip = (request.client.host if request.client else "") or "unknown"
|
||||
count, redis_ok = await cache_incr_checked(f"error_report_rate:{ip}", 60)
|
||||
if not redis_ok:
|
||||
@@ -773,9 +772,7 @@ async def ingest_error_report(
|
||||
signature = _error_signature(body.name, body.message, body.stack, body.url)
|
||||
|
||||
existing = (
|
||||
await session.execute(
|
||||
select(WebErrorReport).where(WebErrorReport.signature == signature)
|
||||
)
|
||||
await session.execute(select(WebErrorReport).where(WebErrorReport.signature == signature))
|
||||
).scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from api.v1.schemas import (
|
||||
BlockedUserResponse,
|
||||
CouponBase,
|
||||
CouponResponse,
|
||||
CouponUpdate,
|
||||
@@ -9,6 +10,9 @@ from api.v1.schemas import (
|
||||
GiftUsageResponse,
|
||||
KeyDetailsResponse,
|
||||
KeyResponse,
|
||||
ManualBanResponse,
|
||||
NotificationResponse,
|
||||
PaymentResponse,
|
||||
ReferralResponse,
|
||||
ServerBase,
|
||||
ServerResponse,
|
||||
@@ -16,25 +20,21 @@ from api.v1.schemas import (
|
||||
TariffBase,
|
||||
TariffResponse,
|
||||
TariffUpdate,
|
||||
TemporaryDataResponse,
|
||||
TrackingSourceResponse,
|
||||
UserBase,
|
||||
UserResponse,
|
||||
UserUpdate,
|
||||
BlockedUserResponse,
|
||||
ManualBanResponse,
|
||||
NotificationResponse,
|
||||
PaymentResponse,
|
||||
TemporaryDataResponse,
|
||||
TrackingSourceResponse,
|
||||
)
|
||||
from api.v1.schemas.keys import KeyBase, KeyCreateRequest, KeyUpdate
|
||||
from api.v1.schemas.settings import SettingResponse, SettingUpsert
|
||||
from api.v2.schemas.web import (
|
||||
WebBlockResponse,
|
||||
WebTheme,
|
||||
WebPageResponse,
|
||||
WebPageUpdate,
|
||||
WebPageVariantCreate,
|
||||
WebPageVariantSummary,
|
||||
WebPageVariantUpdate,
|
||||
WebPageVariantsResponse,
|
||||
WebTheme,
|
||||
)
|
||||
|
||||
@@ -3,11 +3,13 @@ from pydantic import BaseModel
|
||||
|
||||
class TariffGroup(BaseModel):
|
||||
"""Группа тарифов (group_code) для выбора в лендинге и др."""
|
||||
|
||||
group_code: str
|
||||
|
||||
|
||||
class TariffPublic(BaseModel):
|
||||
"""Публичный список тарифов (без авторизации)."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
group_code: str
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import json
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
_MAX_BLOCK_DATA_SIZE = 256 * 1024
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user