197 lines
7.5 KiB
Python
197 lines
7.5 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from api.depends import (
|
|
bind_identity_actor,
|
|
clear_auth_cookie,
|
|
get_request_actor,
|
|
get_session,
|
|
verify_identity_token,
|
|
)
|
|
from api.v2.routes.auth._common import _resolve_partner_snapshot
|
|
from api.v2.schemas.identities import (
|
|
ChangePasswordRequest,
|
|
IdentityResponse,
|
|
SetPasswordRequest,
|
|
)
|
|
from api.v2.schemas.web_public import AccountSummaryResponse
|
|
from database import (
|
|
get_balance,
|
|
get_keys,
|
|
get_trial,
|
|
identities as idb,
|
|
)
|
|
from database.models import CouponUsage, Gift, GiftUsage
|
|
from database.referrals import get_referral_stats
|
|
from database.web_notifications import count_unread_for_identity
|
|
from utils.referral_codes import encode_referral_code
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/me", response_model=IdentityResponse)
|
|
async def me(
|
|
identity=Depends(verify_identity_token),
|
|
):
|
|
"""Текущая идентичность по HttpOnly cookie `auth_token`."""
|
|
return IdentityResponse.model_validate(identity)
|
|
|
|
|
|
@router.post("/logout")
|
|
async def logout(
|
|
request: Request,
|
|
response: Response,
|
|
):
|
|
"""Очищает auth cookie. Не требует валидной сессии — всегда возвращает ok."""
|
|
clear_auth_cookie(response, request)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/me/onboarding/complete", response_model=IdentityResponse)
|
|
async def onboarding_complete(
|
|
session: AsyncSession = Depends(get_session),
|
|
identity=Depends(verify_identity_token),
|
|
):
|
|
"""Отмечает, что админ прошёл/скипнул онбординг-тур."""
|
|
from datetime import datetime as _dt
|
|
|
|
if identity.onboarding_completed_at is None:
|
|
identity.onboarding_completed_at = _dt.utcnow()
|
|
return IdentityResponse.model_validate(identity)
|
|
|
|
|
|
@router.post("/me/onboarding/reset", response_model=IdentityResponse)
|
|
async def onboarding_reset(
|
|
session: AsyncSession = Depends(get_session),
|
|
identity=Depends(verify_identity_token),
|
|
):
|
|
"""Сбрасывает флаг онбординга — туториал запустится снова."""
|
|
identity.onboarding_completed_at = None
|
|
identity.onboarding_stage = "landing"
|
|
return IdentityResponse.model_validate(identity)
|
|
|
|
|
|
_ONBOARDING_STAGES = {"landing", "header", "cabinet", "flow", "elements", "done"}
|
|
|
|
|
|
@router.post("/me/onboarding/stage", response_model=IdentityResponse)
|
|
async def onboarding_set_stage(
|
|
body: dict,
|
|
session: AsyncSession = Depends(get_session),
|
|
identity=Depends(verify_identity_token),
|
|
):
|
|
"""Переводит админа на указанный этап онбординга."""
|
|
from datetime import datetime as _dt
|
|
|
|
stage = str(body.get("stage") or "").strip()
|
|
if stage not in _ONBOARDING_STAGES:
|
|
raise HTTPException(status_code=400, detail="Неизвестный этап онбординга")
|
|
identity.onboarding_stage = stage
|
|
if stage == "done" and identity.onboarding_completed_at is None:
|
|
identity.onboarding_completed_at = _dt.utcnow()
|
|
return IdentityResponse.model_validate(identity)
|
|
|
|
|
|
@router.get("/summary", response_model=AccountSummaryResponse)
|
|
async def auth_summary(
|
|
request: Request,
|
|
session: AsyncSession = Depends(get_session),
|
|
identity=Depends(verify_identity_token),
|
|
):
|
|
actor = get_request_actor(request)
|
|
billing_user_id = actor.billing_user_id if actor and actor.billing_user_id is not None else None
|
|
if billing_user_id is None:
|
|
billing_user_id = await idb.ensure_billing_user_for_identity(session, identity)
|
|
balance = float(await get_balance(session, billing_user_id))
|
|
trial_status = await get_trial(session, billing_user_id)
|
|
keys = await get_keys(session, billing_user_id)
|
|
keys_total = len(keys) if keys else 0
|
|
gifts_sent_r = await session.execute(
|
|
select(func.count()).select_from(Gift).where(Gift.sender_user_id == billing_user_id)
|
|
)
|
|
gifts_sent = gifts_sent_r.scalar_one() or 0
|
|
gifts_claimed_r = await session.execute(
|
|
select(func.count()).select_from(GiftUsage).where(GiftUsage.user_id == billing_user_id)
|
|
)
|
|
gifts_claimed = gifts_claimed_r.scalar_one() or 0
|
|
coupons_r = await session.execute(
|
|
select(func.count()).select_from(CouponUsage).where(CouponUsage.user_id == billing_user_id)
|
|
)
|
|
coupons_used = coupons_r.scalar_one() or 0
|
|
ref = await get_referral_stats(session, billing_user_id)
|
|
partner = await _resolve_partner_snapshot(session, int(billing_user_id))
|
|
unread_notifications = await count_unread_for_identity(session, identity.id)
|
|
return AccountSummaryResponse(
|
|
identity_id=identity.id,
|
|
email=identity.email,
|
|
tg_id=identity.tg_id,
|
|
linked_telegram=identity.tg_id is not None,
|
|
referral_code=encode_referral_code(int(billing_user_id)),
|
|
balance=balance,
|
|
trial_status=int(trial_status),
|
|
keys_total=keys_total,
|
|
referrals_total=int(ref.get("total_referrals") or 0),
|
|
referrals_active=int(ref.get("active_referrals") or 0),
|
|
referral_bonus_total=float(ref.get("total_referral_bonus") or 0),
|
|
gifts_sent=int(gifts_sent),
|
|
gifts_claimed=int(gifts_claimed),
|
|
coupons_used=int(coupons_used),
|
|
partner_enabled=bool(partner.get("partner_enabled", False)),
|
|
partner_code=str(partner.get("partner_code") or ""),
|
|
partner_balance=float(partner.get("partner_balance") or 0.0),
|
|
partner_percent=float(partner.get("partner_percent") or 0.0),
|
|
partner_percent_custom=bool(partner.get("partner_percent_custom", False)),
|
|
partner_referred_total=int(partner.get("partner_referred_total") or 0),
|
|
partner_payout_method=partner.get("partner_payout_method"),
|
|
unread_notifications=int(unread_notifications),
|
|
)
|
|
|
|
|
|
@router.post("/set-password")
|
|
async def set_password(
|
|
body: SetPasswordRequest,
|
|
request: Request,
|
|
session: AsyncSession = Depends(get_session),
|
|
identity=Depends(verify_identity_token),
|
|
):
|
|
if body.password != body.password_confirm:
|
|
raise HTTPException(status_code=400, detail="Пароли не совпадают")
|
|
updated = await idb.set_initial_password(session, identity.id, body.password)
|
|
if not updated:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="Пароль уже установлен или аккаунт недоступен",
|
|
)
|
|
await bind_identity_actor(request, session, updated)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/change-password")
|
|
async def change_password(
|
|
body: ChangePasswordRequest,
|
|
request: Request,
|
|
session: AsyncSession = Depends(get_session),
|
|
identity=Depends(verify_identity_token),
|
|
):
|
|
if body.password != body.password_confirm:
|
|
raise HTTPException(status_code=400, detail="Новые пароли не совпадают")
|
|
err = await idb.change_identity_password(
|
|
session,
|
|
identity.id,
|
|
body.current_password,
|
|
body.password,
|
|
)
|
|
if err == "no_password":
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="Пароль ещё не установлен. Сначала задайте пароль в кабинете.",
|
|
)
|
|
if err == "wrong_password":
|
|
raise HTTPException(status_code=401, detail="Неверный текущий пароль")
|
|
refreshed = await idb.get_identity_by_id(session, identity.id)
|
|
if refreshed:
|
|
await bind_identity_actor(request, session, refreshed)
|
|
return {"ok": True}
|