Newer
Older
gnexus-tasks / backend / app / services / users.py
"""Сервис пользователей: upsert профиля из SSO и claim бесхозных данных (ТЗ 1.2)."""

from typing import Any, cast

from sqlalchemy import select
from sqlalchemy.engine import CursorResult
from sqlalchemy.orm import Session

from app.models import AppSetting, AppSettingGlobal, User

# Таблицы с user_id, подлежащим claim'у (user_id IS NULL → новый пользователь).
# mcp_tokens и app_settings не входят: там user_id NOT NULL, они per-user с рождения.
CLAIM_TABLES = (
    "projects",
    "tasks",
    "tags",
    "documents",
    "xp_events",
    "coin_events",
    "garden_items",
)


def upsert_user(
    db: Session,
    user_id: str,
    email: str,
    avatar_url: str | None,
    locale: str | None,
    name: str | None = None,
) -> User:
    """Создать или обновить профиль по данным SSO (email/имя/аватар/локаль меняются)."""
    user = db.get(User, user_id)
    if user is None:
        user = User(id=user_id, email=email, name=name, avatar_url=avatar_url, locale=locale)
        db.add(user)
    else:
        user.email = email
        if name:
            user.name = name
        user.avatar_url = avatar_url
        user.locale = locale
    # flush сразу: claim_* (Core UPDATE) не триггерит autoflush, и FK
    # projects.user_id → users без этого падает на первом логине
    db.flush()
    return user


def claim_orphan_data(db: Session, user_id: str) -> int:
    """Приписать все строки без владельца вошедшему пользователю.

    Claim-on-first-login: миграция оставила существующие данные с user_id NULL;
    первый вошедший забирает их (идемпотентно — NULL после этого не остаётся).
    """
    claimed = 0
    for table in CLAIM_TABLES:
        model = User.metadata.tables[table]
        result = cast(
            CursorResult[Any],
            db.execute(model.update().where(model.c.user_id.is_(None)).values(user_id=user_id)),
        )
        claimed += result.rowcount or 0
    return claimed


def claim_global_settings(db: Session, user_id: str) -> None:
    """Скопировать глобальные настройки прежней эпохи пользователю (идемпотентно)."""
    existing = {s.key for s in db.scalars(select(AppSetting).where(AppSetting.user_id == user_id))}
    for row in db.scalars(select(AppSettingGlobal)):
        if row.key not in existing:
            db.add(AppSetting(user_id=user_id, key=row.key, value=row.value))