Skip to content

Техническая спецификация

Commit: ce9317496b

Обзор

Синхронизация аккаунта между устройствами через привязку соцсетей (Google, Apple, Facebook). Игрок привязывает аккаунт к соцсети в настройках, после чего может восстановить прогресс на другом устройстве. При конфликте — выбор между локальной и серверной версией. Одновременная игра на двух устройствах невозможна — активная сессия одна, предыдущая кикается.

1. Флоу работы фичи

Инициализация

  1. LoginAuth.Login()POST /auth/device с Device ID
  2. Сервер ищет Device ID в AuthProviders, создаёт/находит аккаунт
  3. Создаёт сессию в Redis через SessionService.CreateSession()
  4. Генерирует JWT с session claims (sid, scat)
  5. Клиент сохраняет JWT, SessionManager.SetSessionFromToken(jwt) извлекает SessionId

Основной флоу (привязка соцсети)

  1. SocialLinkManager.Link(type, token)POST /auth/linkSocial
  2. Сервер валидирует OAuth-токен через IOAuthService
  3. Ищет AuthProvider с данным ProviderUid
  4. NewLink — соцсеть свободна → создаёт AuthProvider, клиент вызывает MarkAsLinked()
  5. SameLink — уже привязана к текущему → ничего не делает
  6. OtherLink — привязана к другому → клиент загружает данные linked через FetchCloudSave(), показывает диалог выбора
  7. Игрок выбирает: KeepCurrentProgress() (Current) или RestoreProgress() (Linked) → POST /auth/relinkSocial

Жизненный цикл сессии

┌─────────────────────────────────────────────────────────────────┐
│  ЛОГИН (POST /auth/device)                                      │
│  → SessionService.CreateSession(playerId, deviceId)             │
│  → Всегда создаёт новую сессию (перезаписывает старую)          │
│  → Если был другой девайс: SignalR SessionKicked                │
│  → JWT содержит session claims (sid, scat)                      │
├─────────────────────────────────────────────────────────────────┤
│  SAVE (POST /save/save)                                         │
│  → SessionId извлекается из JWT claims                          │
│  → Для клиентов >= 3.0: проверяет владение сессией              │
│  → Новая сессия может перезаписать сейв старой (по scat)        │
│  → При несовпадении: NoSession / SessionMismatch                │
├─────────────────────────────────────────────────────────────────┤
│  КИК СЕССИИ (SignalR)                                           │
│  → Сервер отправляет SessionKicked при логине с другого девайса  │
│  → Клиент показывает диалог и закрывает приложение              │
├─────────────────────────────────────────────────────────────────┤
│  ОБНАРУЖЕНИЕ НЕВАЛИДНОЙ СЕССИИ                                  │
│  → HTTP 401 ответ от сервера → NotifySessionInvalid()           │
│  → SaveStatus.NoSession/SessionMismatch → NotifySessionInvalid()│
│  → SignalR SessionKicked → NotifySessionKicked(reason)          │
└─────────────────────────────────────────────────────────────────┘

Три точки обнаружения невалидной сессии:

  1. HTTP 401 (ServerHttpRequests.cs): при получении 401 → SessionManager.NotifySessionInvalid()
  2. Save rejection (SaveRequests.cs): при SaveStatus.NoSession или SessionMismatchSessionManager.NotifySessionInvalid()
  3. SignalR kick (SignalRequests.cs): при получении SessionKicked с совпадающим SessionId → SessionManager.NotifySessionKicked(reason)

Таблица состояний

Действие Device текущего Соцсети текущего Device linked Соцсети linked
Первый вход Создаётся
Привязка соцсети Без изменений Добавляется
Relink (Linked) → linked → linked Остаётся (multi-device) Остаются
Relink (Current) Остаётся Остаются Остаётся Одна → current
Отвязка соцсети Без изменений Удаляется

Пояснение: - Linked — все провайдеры текущего переносятся на linked, Device linked остаётся (multi-device) - Current — только одна соцсеть (через которую конфликт) переносится на current

Офлайн (pause/resume)

При сворачивании/разворачивании приложения (ApplicationLifecycleHandler.cs): - Pause: PlayerProfileSaver.Save(skipCloudSave: true) — только локальное сохранение - Resume: переподключение SignalR с существующим JWT (сессия не пересоздаётся)


При OtherLink клиент показывает диалог выбора и вызывает POST /auth/relinkSocial.

ChosenAccount = Linked (переход на привязанный аккаунт): 1. Находит AuthProvider для соцсети → получает linkedPlayerId 2. Проверяет linkedPlayerId != currentPlayerId 3. Переносит все провайдеры текущего аккаунта на linked: - Device: перепривязывается (оба Device остаются — multi-device), устанавливается PreviousPlayerId - Соцсети: переносятся, если у linked нет такого типа 4. Возвращает JWT и PlayerId linked-аккаунта

ChosenAccount = Current (оставить текущий аккаунт): 1. Проверяет linkedPlayerId != currentPlayerId 2. Удаляет AuthProvider соцсети у linked 3. Создаёт AuthProvider соцсети у текущего 4. Возвращает JWT и PlayerId текущего аккаунта

Важно: Linked переносит все провайдеры. Current переносит только одну соцсеть (ту, через которую конфликт).

Действия клиента после Relink: 1. LoginAuth.UpdateAuthData(jwt, playerId) — обновляет credentials 2. FetchCloudSave()ServerHttpRequests.LoadPlayerData() — загружает сейв 3. PlayerProfileSaver.SaveToPlayerPrefs() — сохраняет локально 4. SocialLinkManager.RestartGame() — перезапускает игру

Кик сессии

При логине с другого устройства (POST /auth/device): 1. SessionService.CreateSession() перезаписывает сессию в Redis 2. Если предыдущий DeviceId отличается → NotifySessionKicked() через SignalR 3. SignalRequests на кикнутом устройстве проверяет совпадение SessionId 4. SessionHandler показывает диалог → приложение закрывается

Тестовые сценарии: qa.md


Edge cases

Ситуация Поведение
Два девайса, второй логинится Первый получает SignalR SessionKicked → диалог → выход
Первый девайс офлайн при кике SignalR не доставлен. При следующем запросе получит 401 или SessionMismatch
Save с устаревшей сессией Сервер отклоняет (SessionMismatch), клиент вызывает NotifySessionInvalid()
Save без сессии Сервер отклоняет (NoSession), клиент вызывает NotifySessionInvalid()
Social login JWT без session claims, сохранение в облако невозможно до Device login

API контракт

Эндпоинты

Endpoint Метод Описание Авторизация
/auth/device POST Вход по Device ID, создание сессии Нет
/auth/social POST Вход через соцсеть (Google/Apple/Facebook) Нет
/auth/linkSocial POST Привязка соцсети с автолинком (возвращает OtherLink при конфликте) JWT
/auth/relinkSocial POST Разрешение конфликта при привязке JWT
/auth/unlinkSocial POST Отвязка соцсети от аккаунта JWT
/auth/linkedSocials POST Список привязанных соцсетей (для синхронизации UI) JWT
/auth/checkSocial POST Проверка привязки соцсети (диагностика, требует OAuth токен) JWT
/auth/cleardevice/{deviceId} POST Удаление привязки Device (только для разработки) AdminAuth
/save/save POST Сохранение данных игрока (проверяет сессию для >= 3.0) JWT
/save/load POST Загрузка данных игрока JWT
/save/changeowner POST Смена владельца сейва после relink (синхронизация) JWT
/save/snapshot POST Создание снэпшота профиля JWT
/save/delete POST Удаление данных игрока JWT

Примечание: /auth/social возвращает JWT без session claims. Только /auth/device создаёт сессию и возвращает JWT с SessionId.


Обратная совместимость

Проблема

Старые клиенты (версия < 3.0) не знают о механизме сессий. Если сервер будет требовать сессию для всех клиентов, старые версии не смогут сохранять прогресс.

Решение

Save микросервис проверяет версию клиента и требует сессию только для новых клиентов:

// Server/Save/Controllers/SaveController.cs

private const int ClientSessionMinVersion = 3000;

private static bool IsSessionRequired(string? clientVersion)
{
    if (string.IsNullOrEmpty(clientVersion))
    {
        return false;  // Нет версии → старый клиент → сессия не требуется
    }
    return CompareVersions(clientVersion, ClientSessionMinVersion) >= 0;
}

Как передаётся версия клиента

Клиент передаёт версию приложения в HTTP-заголовке version для всех запросов:

// Assets/Data/Scripts/Meta/Server/ServerHttpRequests.cs

Version = GetAppVersionString();                    // при создании
www.SetRequestHeader("version", Version ?? "");     // при каждом запросе

Сервер читает версию из заголовка:

var clientVersion = Request.Headers["version"].FirstOrDefault();
if (IsSessionRequired(clientVersion))
{
    // Извлечь SessionId из JWT claims и проверить...
}

Как передаётся SessionId

SessionId не передаётся отдельным полем — он встроен в JWT токен как claim sid. Save сервер извлекает его из JWT middleware:

var sessionId = GetSessionId();        // из JWT claim "sid"
var sessionCreatedAt = GetSessionCreatedAt(); // из JWT claim "scat"

Таблица совместимости

Версия клиента Заголовок version Требуется сессия Поведение
< 3.0 "2.12.0" Нет Сохранение работает без SessionId
>= 3.0 "3.0.0" Да Требуется валидный SessionId в JWT
Отсутствует пустой / null Нет Legacy режим, сессия не требуется

Миграция

При выходе версии 3.0: - Существующие игроки продолжают играть на старых клиентах без проблем - Новые клиенты получают полную защиту сессий - После полного обновления базы игроков константу ClientSessionMinVersion можно удалить


2. Архитектура

graph TD
    subgraph client-social["Клиент — Social"]
        SocialLinkManager -->|Link/Relink/Unlink| ServerHttpRequests
        SocialLinkManager --> SocialLinksComponent
        SocialLinkManager --> SocialNetworks
        SocialNetworks --> ISocialNetwork
        ISocialNetwork -.-> AppleSocialNetwork
        ISocialNetwork -.-> GoogleSocialNetwork
        ISocialNetwork -.-> FacebookSocialNetwork
    end

    subgraph client-auth["Клиент — Auth"]
        LoginAuth -->|POST /auth/device| ServerHttpRequests
        LoginAuth -->|POST /auth/social| ServerHttpRequests
        LoginAuth --> SessionManager
        SessionManager --> TokenService
    end


    subgraph client-session["Клиент — Session"]
        SessionHandler --> SessionManager
        SignalRequests -->|SessionKicked| SessionManager
        SaveRequests -->|NoSession/Mismatch| SessionManager
        ServerHttpRequests -->|401| SessionManager
    end

    subgraph server-main["Сервер — Main :5000"]
        AuthController --> SessionService
        AuthController --> IOAuthService
        AuthController --> JwtService
        SessionService --> Redis
        AuthController --> DB[(PostgreSQL)]
    end

    subgraph server-save["Сервер — Save :5001"]
        SaveController --> JwtService
        SaveController --> SessionService
        SaveController --> DB
    end

    ServerHttpRequests -->|HTTP + JWT| AuthController
    ServerHttpRequests -->|HTTP + JWT| SaveController
    SignalRequests -->|SignalR| AuthController

Точка входа — LoginAuth (аутентификация) и SocialLinkManager (привязка соцсетей). SocialNetworks управляет SDK-реализациями провайдеров. Сессии контролируются через SessionManager на клиенте и SessionService (Redis) на сервере. Все HTTP-запросы проходят через ServerHttpRequests, кик сессий — через SignalR (SignalRequests).

Базовые правила

  1. Один аккаунт = несколько устройств, одна активная сессия
  2. Соцсеть = ключ для переноса между устройствами
  3. При переносе соцсеть перепривязывается к выбранному аккаунту
  4. Каждый запрос к серверу проверяет JWT токен
  5. Multi-Device: несколько устройств привязываются к одному аккаунту через Relink, Device не удаляется — оба работают
  6. Сессии и кик: при логине создаётся сессия в Redis, предыдущая перезаписывается — активное устройство получает SignalR SessionKicked и закрывается
  7. Версионирование: клиенты >= 3.0 требуют сессию для облачных сохранений, старые (< 3.0) работают без сессий

Разделение Main / Save

Сессии реализованы по принципу session-in-JWT — SessionId и время создания сессии встраиваются в JWT токен при логине. Отдельных HTTP-эндпоинтов для управления сессиями нет.

MAIN (5000) — аутентификация и сессии:
├── POST /auth/device     — логин + создание сессии в Redis + JWT с session claims
└── SignalR SessionKicked  — уведомление кикнутого устройства

SAVE (5001) — только данные:
├── POST /save/save       — извлекает SessionId из JWT, проверяет, сохраняет
└── POST /save/load       — загружает

Хранение сессий (Redis)

Сессии хранятся в Redis Hash, не в PostgreSQL:

Ключ: session:{playerId}
Поля:
  sid — SessionId (GUID)
  did — DeviceId
  cat — CreatedAt (Unix timestamp)

3. Сцены и ассеты

[нет данных]


4. Конфиги

[нет данных]


5. Модели данных

DB модели

AuthPlayer (Server/Main/Model/DB/AuthPlayer.cs):

Поле Тип Описание
Id string PlayerId, 16 hex chars (GeneratePlayerId)
CreatedAt DateTime Время создания
AuthProviders List<AuthProvider> Навигационное свойство

AuthProvider (Server/Main/Model/DB/AuthProvider.cs):

Поле Тип Описание
Id int PK
PlayerId string FK → AuthPlayer
ProviderType AuthProviderType Device, Google, Apple, Facebook
ProviderUid string Внешний ID провайдера
CreatedAt DateTime Время создания
PreviousPlayerId string? Для отслеживания Relink (Device)

Примечание: AuthPlayer не содержит полей сессии. Вся информация о сессиях хранится в Redis.


API модели — запросы

DeviceAuthRequest:

DeviceId: string

SocialAuthRequest (для /auth/social):

ProviderType: AuthProviderType (Google, Apple, Facebook)
Token: string (OAuth токен от провайдера)

LinkSocialRequest (для /auth/linkSocial):

ProviderType: AuthProviderType (Google, Apple, Facebook)
Token: string (OAuth токен от провайдера)

RelinkSocialRequest:

ProviderType: AuthProviderType
Token: string
ChosenAccount: ChosenAccountType (Current = 0, Linked = 1)
DeviceId: string

UnlinkSocialRequest:

ProviderType: AuthProviderType

CheckSocialRequest:

ProviderType: AuthProviderType
Token: string

API модели — ответы

AuthResponse:

Token: string (JWT, при Device-логине содержит session claims)
PlayerId: string
RelinkFromPlayerId: string (ID предыдущего аккаунта, если Device был перепривязан через Relink)

LinkSocialResponse (для /auth/linkSocial):

Result: LinkSocialResult (NewLink | SameLink | OtherLink | Error)
Jwt: string
PlayerId: string
ProviderUid: string (ID пользователя у провайдера)
LinkedJwt: string (JWT привязанного аккаунта, только при OtherLink)
LinkedPlayerId: string (ID привязанного аккаунта, только при OtherLink)
Message: string

Примечание: При OtherLink сервер возвращает LinkedJwt и LinkedPlayerId. Клиент загружает полные данные профиля через FetchCloudSave() из Save микросервиса.

RelinkSocialResponse:

Success: bool
Jwt: string
PlayerId: string
ProviderUid: string (ID пользователя у провайдера)
Message: string

UnlinkSocialResponse:

Success: bool
Message: string

CheckSocialResponse:

Result: LinkSocialResult (NewLink | SameLink | OtherLink | Error)
LinkedJwt: string (JWT привязанного аккаунта, при OtherLink)
LinkedPlayerId: string (ID привязанного аккаунта, при OtherLink)

LinkedSocialsResponse:

Providers: List<AuthProviderType> (список привязанных соцсетей)

ChangeOwnerResponse (для /save/changeowner):

Success: bool
Error: string
OwnerChanged: bool
PreviousSessionId: string
HasSave: bool
Version: int
Timestamp: long
Name: string
Level: int
Gems: int

Модели сессий — клиент

Assets/Data/Scripts/Meta/Server/ServerHttpRequestModels.cs:

public class SessionInfo
{
    public string SessionId;
    public string DeviceId;
}

public class SessionKickedMessage
{
    public string PlayerId;
    public string SessionId;
    public string Reason;
    public long Timestamp;
}

Модели сессий — сервер

Server/Main/Services/SessionService.cs:

public class SessionInfo
{
    public string? SessionId { get; set; }
    public string? DeviceId { get; set; }
    public long CreatedAtUtcTimestamp { get; set; }
}

public class CreateSessionResult
{
    public bool Success { get; set; }
    public SessionInfo? CreatedSession { get; set; }
    public SessionInfo? PreviousSession { get; set; }
    public string? KickedSessionId { get; set; }
    public string? Message { get; set; }
}

TokenInfo (JWT)

Server/Shared/JwtService.cs:

public class TokenInfo
{
    public string PlayerId { get; set; }
    public string? SessionId { get; set; }
    public long? SessionCreatedAt { get; set; }
    public bool HasSession => !string.IsNullOrEmpty(SessionId) && SessionCreatedAt.HasValue;
}

JWT Claims

При Device-логине JWT содержит дополнительные claims:

sid  — SessionId (GUID)
scat — SessionCreatedAt (Unix timestamp)

Клиент извлекает SessionId из JWT через TokenService.Parse(token) и SessionManager.SetSessionFromToken(token).


Enum-ы

AuthProviderType: | Значение | Код | |----------|-----| | Device | 0 | | Google | 1 | | Apple | 2 | | Facebook | 3 |

LinkSocialResult: | Значение | Код | Описание | |----------|-----|----------| | NewLink | 0 | Соцсеть успешно привязана (была свободна) | | SameLink | 1 | Соцсеть уже привязана к текущему аккаунту | | OtherLink | 2 | Соцсеть привязана к другому аккаунту (конфликт) | | Error | 3 | Ошибка валидации токена |

ChosenAccountType: | Значение | Код | |----------|-----| | Current | 0 | | Linked | 1 |

SaveStatus: | Значение | Код | |----------|-----| | Saved | 0 | | Conflict | 1 | | NoSession | 2 | | SessionMismatch | 3 |


6. Префабы

[нет данных]


7. Скрипты

Client — Auth

LoginAuth.cs

Assets/Data/Scripts/Meta/Server/LoginAuth.cs

  • Основной класс аутентификации. Поддерживает вход по Device ID и через соцсеть. Управляет JWT-токеном и PlayerId, предоставляет доступ к SessionManager. Включает миграцию v3.0 для принудительного перелогина при отсутствии SessionId.
Login(callback)                              → POST /auth/device
LoginBySocial(providerType, token, callback) → POST /auth/social
UpdateAuthData(jwt, playerId)                — обновление credentials после relink
ClearAuthData()                              — очистка JWT и PlayerId
TokenClearIfOldVersion()                     — миграция v3.0: очистка токена без SessionId
Session                                      — свойство: SessionManager
LoggedIn                                     — bool, есть ли валидный токен
LastLoginTokenUpdated                        — bool, обновился ли токен при последнем логине
event Action OnLoginSuccessNotify;  // успешный логин

ServerHttpRequests.cs

Assets/Data/Scripts/Meta/Server/ServerHttpRequests.cs

  • HTTP-клиент для всех серверных запросов. Устанавливает JWT в заголовок Authorization, версию в version. Поддерживает кэширование, дедупликацию запросов, отмену. При 401 вызывает SessionManager.NotifySessionInvalid().
LoginByDevice(deviceId, callback)            → POST /auth/device
LoginBySocial(providerType, token, callback) → POST /auth/social
LinkSocial(request, callback)                → POST /auth/linkSocial
RelinkSocial(request, callback)              → POST /auth/relinkSocial
UnlinkSocial(type, callback)                 → POST /auth/unlinkSocial
GetLinkedSocials(callback)                   → POST /auth/linkedSocials
CheckSocial(request, callback)               → POST /auth/checkSocial
LoadPlayerData(callback)                     → POST /save/load (Save микросервис)
SavePlayerData(version, data, callback)      → POST /save/save
ChangeOwner(callback)                        → POST /save/changeowner
MakeSnapshot(data, callback)                 → POST /save/snapshot
DeletePlayerData(callback)                   → POST /save/delete

Примечание: ClearDevice доступен только в ServerHttpRequestsEditor (editor-only).

ServerHttpRequestModels.cs

Assets/Data/Scripts/Meta/Server/ServerHttpRequestModels.cs

  • Все сериализуемые модели запросов и ответов для HTTP API. Содержит enum-ы AuthProviderType, LinkSocialResult, SaveStatus, ChosenAccountType и все request/response классы.

SaveRequests.cs

Assets/Data/Scripts/Meta/Server/SaveRequests.cs

  • Обёртка над ServerHttpRequests для облачных сохранений. Обрабатывает SaveStatus.NoSession и SessionMismatch через SessionManager.NotifySessionInvalid(). Поддерживает конфликт-резолвер (IConflictResolver), снэпшоты, удаление и смену владельца.

Client — Social

SocialLinkManager.cs

Assets/Data/Scripts/Meta/Social/SocialLinkManager.cs

  • Основной API для привязки/отвязки соцсетей с обработкой конфликтов. Синглтон. Кэширует список привязок (60 сек). При OtherLink загружает данные linked-аккаунта и показывает диалог выбора. Отправляет аналитику через LinkSocialAnalytics.
Link(type, token, callback)             → POST /auth/linkSocial
KeepCurrentProgress(type, token, cb)    → POST /auth/relinkSocial (Current)
RestoreProgress(type, token, cb)        → POST /auth/relinkSocial (Linked)
Unlink(type, callback)                  → POST /auth/unlinkSocial
RefreshLinkedSocials(callback)          → POST /auth/linkedSocials (кэш 60 сек)
IsLinked(type)                          — проверка привязки из локального кэша
static event Action<AuthProviderType> OnSocialLinked;
static event Action<AuthProviderType> OnSocialUnlinked;
static event Action OnLinkedSocialsRefreshed;  // после обновления списка с сервера

SocialLinksComponent.cs

Assets/Data/Scripts/Meta/Social/SocialLinksComponent.cs

  • Компонент профиля (наследует ProfileComponentBase) для хранения состояния привязанных провайдеров. Хранит список LinkedProvider (Type, Token, ProviderId). Поддерживает отложенное сохранение через параметр save.
IsLinked(type)                                   — проверка привязки
GetLinkedProvider(type)                          — данные провайдера
MarkAsLinked(type, token, providerId, save=true) — отметить как привязанный
MarkAsUnlinked(type, save=true)                  — отметить как отвязанный
GetLinkedProviders()                             — список всех привязанных
LinkedCount                                      — количество привязанных

ISocialNetwork.cs

Assets/Data/Scripts/Meta/Social/ISocialNetwork.cs

  • Интерфейс для SDK социальных сетей. Определяет контракт: инициализация, открытие/закрытие сессии, получение токена и UserId. Наследует ILoginAuthProvider.

SocialNetworks.cs

Assets/Data/Scripts/Meta/Social/SocialNetworks.cs

  • Реестр и менеджер всех зарегистрированных социальных сетей. Синглтон. Позволяет регистрировать, получать по типу и проверять доступность SDK-реализаций.
static SocialNetworks Instance
RegisterSocialNetwork(ISocialNetwork)         — регистрация
Get(AuthProviderType type)                    — получить по типу
IsAvailable(AuthProviderType type)            — доступна и инициализирована
OpenSession(AuthProviderType, entryPoint, cb) — открыть сессию

SocialNetworksDefaultRegistrator.cs

Assets/Data/Scripts/Meta/Social/SocialNetworksDefaultRegistrator.cs

  • Платформенная регистрация SDK. iOS: Apple + Google + Facebook. Android: Google + Facebook. В Editor/эмуляции подставляет EmulatedSocialNetwork.

SocialNetworkBase.cs

Assets/Data/Scripts/Meta/Social/SocialNetworkBase.cs

  • Абстрактный базовый класс для реализаций ISocialNetwork. Делегирует в InitCore/OpenSessionCore/CloseSessionCore. Автоматически отправляет аналитику через LinkSocialAnalytics.

EmulatedSocialStorage.cs

Assets/Data/Scripts/Meta/Social/EmulatedSocialStorage.cs

  • Хранилище данных эмулированных соцсетей в PlayerPrefs по ключу EmulatedSocial_{playerId}. Поддерживает кэширование и CRUD-операции для ProviderData (UserId, Token).

LinkSocialAnalytics.cs

Assets/Data/Scripts/Meta/Social/LinkSocialAnalytics.cs

  • Статический класс аналитики для всех операций привязки соцсетей. Отправляет события в Amplitude через Analytics.SendEvent().
SendLinkSocial(provider, result, time)                           — результат привязки
SendLinkSocialVersionMismatch(provider, localVer, linkedVer)     — несовпадение версий сейвов
SendLinkConflictShown(provider, currentProfile, linkedProfile)   — показан диалог конфликта
SendLinkConflictResolved(provider, keepCurrent, current, linked) — выбор в диалоге конфликта
SendLinkConflictAutoResolved(provider, reason)                   — автоматическое разрешение
SendRelinkSocial(provider, choice, success, time)                — результат relink
SendRestoreProgress(provider, oldPlayerId, newPlayerId)          — восстановление прогресса
SendUnlinkSocial(provider, success, message)                     — отвязка
SendSocialOpenSession(provider, entryPoint, isSuccess, isAuto)   — открытие сессии SDK
SendSocialCloseSession(provider)                                 — закрытие сессии SDK
SendSyncError(errorCode, operation, provider?, message?)         — ошибка синхронизации

Client — Social/Networks

AppleSocialNetwork.cs

Assets/Data/Scripts/Meta/Social/Networks/AppleSocialNetwork.cs

  • Реализация Sign in with Apple (iOS). Использует AppleAuthManager. Поддерживает тихий логин при инициализации через GetCredentialState(). Возвращает identity token.

GoogleSocialNetwork.cs

Assets/Data/Scripts/Meta/Social/Networks/GoogleSocialNetwork.cs

  • Реализация Google Sign-In (iOS, Android). Использует Google Play Services и Firebase. Поддерживает тихий логин через SignInSilently(). Возвращает ID token.

FacebookSocialNetwork.cs

Assets/Data/Scripts/Meta/Social/Networks/FacebookSocialNetwork.cs

  • Реализация Facebook Login (iOS, Android). Использует Facebook Unity SDK. Запрашивает только public_profile. Возвращает access token.

EmulatedSocialNetwork.cs

Assets/Data/Scripts/Meta/Social/Networks/EmulatedSocialNetwork.cs

  • Эмуляция для Editor и тестов. Показывает popup для ввода UserId. Генерирует токен формата emulated_{type}_{userId}. Хранит данные в EmulatedSocialStorage.

BlankSocialNetwork.cs

Assets/Data/Scripts/Meta/Social/Networks/BlankSocialNetwork.cs

  • Заглушка для недоступных сетей. Всегда IsInitialized = false, IsSessionOpened = false. Предотвращает null reference errors.

Client — Session

SessionManager.cs

Assets/Data/Scripts/Meta/Server/SessionManager.cs

  • Хранение и управление состоянием сессии. Синглтон, доступен через LoginAuth.Session и SessionManager.Instance. Извлекает SessionId из JWT через Base64Url-декодирование payload.
public class SessionManager
{
    public static SessionManager Instance { get; private set; }

    public SessionInfo SessionInfo { get; private set; }
    public string SessionId => SessionInfo?.SessionId;
    public string DeviceId => SessionInfo?.DeviceId;
    public bool HasSession => !string.IsNullOrEmpty(SessionId);

    public event Action<bool> OnSessionInvalid;    // bool hadSession
    public event Action<string> OnSessionKicked;    // string reason

    public void Init(LoginAuth auth);
    public void SetSession(SessionInfo sessionInfo);
    public void SetSessionFromToken(string token);  // извлекает SessionId из JWT
    public void ClearSession();
    public void NotifySessionInvalid();
    public void NotifySessionKicked(string reason);
}

SessionHandler.cs

Assets/Data/Scripts/Meta/Server/SessionHandler.cs

  • Обрабатывает UI-события сессии. При OnSessionInvalid без активной сессии — тихий перелогин. При активной сессии или OnSessionKicked — диалог с локализованным текстом (session_kicked.*) → выход из приложения.

TokenService.cs

Assets/Data/Scripts/Meta/Server/TokenService.cs

  • Статический парсер JWT на клиенте. Извлекает PlayerId (nameidentifier claim) и SessionId (sid claim) без внешних библиотек. Manual Base64Url decoding.
public static class TokenService
{
    public static TokenInfo Parse(string token);
    public static bool HasSession(string token);
    public static string GetSessionId(string token);
    public static string GetPlayerId(string token);
    public static bool IsValidForPlayer(string token, string playerId);
}

SignalRequests.cs

Assets/Data/Scripts/Meta/Server/SignalRequests.cs

  • Управление SignalR-соединением. Обрабатывает сигнал SessionKicked — при совпадении SessionId вызывает SessionManager.NotifySessionKicked(). Поддерживает экспоненциальный backoff при переподключении.

Client — Прочее

PlayerProfileSaver.cs

Assets/Data/Scripts/Meta/PlayerProfile/PlayerProfileSaver.cs

  • Сохранение профиля игрока локально и в облако. При кике сессии вызывается с skipCloudSave: true для сохранения только локально.

ApplicationLifecycleHandler.cs

Assets/Data/Scripts/Application/Services/ApplicationLifecycleHandler.cs

  • Обработка lifecycle приложения (pause/resume). При pause — локальное сохранение. При resume — переподключение SignalR (сессия не пересоздаётся).

Server — Main

AuthController.cs

Server/Main/Controllers/AuthController.cs

  • Все эндпоинты аутентификации и привязки соцсетей. Зависимости: PlayerContext, IConfiguration, IOAuthService, SessionService.
Device(request)          — аутентификация по Device ID + создание сессии в Redis
Social(request)          — аутентификация через соцсеть (Google/Apple/Facebook), без сессии
LinkSocial(request)      — привязка с автолинком (OtherLink при конфликте)
RelinkSocial(request)    — разрешение конфликта:
                           • Current → переносит одну соцсеть на текущий
                           • Linked → переносит ВСЕ провайдеры на linked
UnlinkSocial(request)    — отвязка соцсети
LinkedSocials()          — список привязанных соцсетей (без OAuth токена)
CheckSocial(request)     — проверка привязки (требует OAuth токен)
ClearDevice(deviceId)    — удаление привязки Device (требует AdminAuth)

Вспомогательные:

GetOrCreatePlayer(type, uid, legacyId)           — создание/получение игрока
ValidateSocialToken(type, token)                 — валидация токена соцсети через IOAuthService
                                                   (Google API, Apple OpenID, Facebook debug_token или /me)
GenerateToken(playerId)                          — генерация JWT без сессии (для соцсетей)
GenerateTokenWithSession(playerId, sid, scat)    — генерация JWT с session claims (для Device)
ValidateAuthHeader(authorization)                — извлечение PlayerId из JWT
NotifySessionKicked(playerId, kickedSid, reason) — отправка SignalR уведомления о кике

SessionService.cs

Server/Main/Services/SessionService.cs

  • Управление сессиями в Redis. Создаёт новые сессии (всегда перезаписывает), определяет нужен ли кик (другой DeviceId). Ключ: session:{playerId}, поля: sid, did, cat.
CreateSession(playerId, deviceId) → CreateSessionResult
GetSessionInfo(playerId)          → SessionInfo?

OAuthService.cs

Server/Main/Services/OAuthService.cs

  • Продакшн-валидация OAuth-токенов. Google: через Google.Apis.Auth. Apple: JWT-верификация через OpenID Connect. Facebook: debug_token endpoint или /me endpoint.

IOAuthService.cs

Server/Main/Services/IOAuthService.cs

  • Интерфейс валидации OAuth-токенов.
Task<string?> ValidateGoogleToken(string token)
Task<string?> ValidateAppleToken(string token)
Task<string?> ValidateFacebookToken(string token)
Task<string?> ValidateFacebookTokenViaMe(string token)
Реализация Назначение
OAuthService Продакшн — валидация через внешние API
EmulatedOAuthService Эмуляция — принимает токены формата emulated_*
DummyOAuthService Заглушка — всегда возвращает null

JwtAuthMiddleware.cs

Server/Main/Middleware/JwtAuthMiddleware.cs

  • Извлекает JWT из Authorization: Bearer {token}, валидирует через JwtService.ValidateAndGetInfo(), устанавливает HttpContext.Items["TokenInfo"] для контроллеров.

AdminAuthAttribute.cs

Server/Main/Middleware/AdminAuthAttribute.cs

  • Атрибут [AdminAuth] для защиты dev-эндпоинтов (например, /auth/cleardevice). Проверяет заголовок admin-token.

AuthPlayer.cs

Server/Main/Model/DB/AuthPlayer.cs

  • EF Core модель игрока. PlayerId — 16-character hex ID. Содержит навигационное свойство AuthProviders.

AuthProvider.cs

Server/Main/Model/DB/AuthProvider.cs

  • EF Core модель провайдера аутентификации. Связывает игрока с Device ID или соцсетью. Поле PreviousPlayerId используется для отслеживания Relink.

Server — Save

SaveController.cs

Server/Save/Controllers/SaveController.cs

  • Контроллер сохранений. Проверяет сессию для клиентов >= 3.0 (IsSessionRequired). Извлекает SessionId из JWT claims. Поддерживает save, load, snapshot, delete, changeowner.

JwtAuthMiddleware.cs

Server/Save/Middleware/JwtAuthMiddleware.cs

  • JWT middleware для Save-микросервиса. Аналогичен Main-версии.

Server — Shared

JwtService.cs

Server/Shared/JwtService.cs

  • Генерация и валидация JWT-токенов. Issuer/Audience: "Zombusters". Алгоритм: HmacSha256. Токены не имеют срока действия.
GenerateToken(playerId, secret)                          — JWT без сессии
GenerateToken(playerId, sessionId, sessionCreatedAt, secret) — JWT с session claims
ValidateToken(token, secret) → string?                   — валидация, возвращает PlayerId
ValidateAndGetInfo(token, secret) → TokenInfo?           — валидация + полная информация
GeneratePlayerId()                                       — уникальный ID (16 hex chars, криптографический random)