# MsaAuthService ## Обзор класса `MsaAuthService` — авторизация через учётную запись Microsoft, то есть вход с лицензионной копией игры. Это та же цепочка, что и в официальном лаунчере: OAuth2 → Xbox Live → XSTS → Minecraft Services → проверка лицензии. Результат отдаётся тем же `AuthResult`, что и [AuthService](AuthService.md) для Ely.by и офлайна, поэтому запуск игры дальше идёт по общему пути и ничего не знает о способе входа. Класс не показывает окно входа сам: страницу Microsoft открывает [MicrosoftLoginDialog](../qml/MicrosoftLoginDialog.md) на стороне QML, а сервис даёт ему адрес страницы и разбирает адрес возврата. ## Место в проекте и зависимости Подключает `authservice.h` — ради общей структуры `AuthResult`. Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Требования сборки: `Qt6::Core` (`QJsonObject`, `QString`, `QUrl`) и `Qt6::Network` (`QNetworkAccessManager`). Сам класс собирается всегда и не зависит от Qt WebEngine — от наличия WebEngine зависит только окно, в котором показывается страница входа. Поэтому в сборке без WebEngine сервис существует, но воспользоваться им нельзя: показать страницу нечем. ## Иерархия и роль Наследует `QObject`: мета-объектная система, сигнал `progress`, владение по родителю. Виртуальных методов базового класса не переопределяет. ## Псевдонимы типов `MsaAuthService::Callback` — `std::function`. Колбэк вызывается ровно один раз и в потоке GUI. ## Публичные методы #### explicit MsaAuthService(QObject \*parent = nullptr) Создаёт сервис и его `QNetworkAccessManager`. Конструктор помечен `explicit`. #### static QString clientId() Идентификатор приложения лаунчера в Microsoft. Игра ждёт его в `${clientid}`: официальный лаунчер подставляет туда именно идентификатор приложения, а не случайный токен сессии. #### static QUrl authorizationUrl() Адрес страницы входа для встроенного окна браузера. В адресе уже собраны идентификатор клиента, `redirect_uri` и параметр выбора аккаунта — вызывающему коду достаточно открыть эту ссылку. #### static bool matchRedirect(const QUrl &url, QString \*code, QString \*error) Отличает адрес, на который Microsoft возвращает управление после входа, от остальной навигации внутри окна. Возвращает `true` только для адреса возврата. При совпадении заполняется ровно одно из двух: `code` — код авторизации при успешном входе, либо `error` — текст отказа. Разбор адреса живёт здесь, а не в QML, потому что правила совпадения обязаны совпадать с теми, по которым сервис сам строит `redirect_uri`. #### void loginWithCode(const QString &code, Callback callback) Полный вход по коду, полученному из окна браузера. Проходит всю цепочку: обмен кода на токен Microsoft, аутентификация в Xbox Live, авторизация XSTS, вход в Minecraft Services, проверка лицензии и получение профиля. По ходу испускает `progress` с описанием текущего шага — цепочка длинная, и без обратной связи вход выглядел бы зависанием. Результат приходит в `callback` один раз. Если вход прошёл, но копии игры на аккаунте нет, в результате выставлен `licenseMissing`: этот случай чинится только покупкой, поэтому обрабатывается отдельно от прочих ошибок. #### void loginWithRefreshToken(const QString &refreshToken, Callback callback) Продление сессии без участия пользователя. Refresh-токен Microsoft живёт куда дольше суточного токена Minecraft, так что при повторном запуске лаунчера обычно хватает его, и окно входа показывать не приходится. Проходит ту же цепочку, начиная с обмена refresh-токена. Неудача означает, что токен окончательно протух и нужен полноценный вход через окно. ## Сигналы #### progress(const QString &message) Описание текущего шага цепочки авторизации. Обработчик показывает сообщение пользователю. Сигнал особенно важен для этого класса: шагов пять, каждый — отдельный сетевой запрос, и между ними проходит заметное время. ## Владение и время жизни Класс наследует `QObject` и принимает `parent` — родитель его и удалит. `QNetworkAccessManager` создаётся в конструкторе с сервисом в роли родителя. Каждый шаг цепочки вызывается из колбэка предыдущего, поэтому незавершённый вход держит цепочку захваченных колбэков до своего конца. Уничтожение сервиса посреди цепочки обрывает её вместе с менеджером сети, и колбэк не вызывается. ## Потокобезопасность Только поток GUI. Все методы асинхронные, колбэки и сигналы приходят в поток, где создан сервис. ## Взаимодействие с другими классами `LauncherBackend` создаёт сервис, отдаёт в QML адрес страницы входа, принимает от окна код авторизации и вызывает `loginWithCode()`. Полученные `refreshToken` и `expiresAt` он сохраняет в профиле, чтобы при следующем запуске обойтись `loginWithRefreshToken()`. Заполненный `AuthResult` дальше раскладывается по полям `LaunchOptions` для [GameLauncher](GameLauncher.md) — ровно так же, как результат от [AuthService](AuthService.md). Со стороны QML вход выглядит так: `LauncherBackend` испускает сигнал с адресом страницы, главное окно открывает [MicrosoftLoginDialog](../qml/MicrosoftLoginDialog.md), тот следит за навигацией и возвращает код обратно в бэкенд. ## Внешнее взаимодействие **Сеть, исходящие запросы.** Класс последовательно обращается к пяти внешним службам: конечной точке OAuth2 Microsoft, Xbox Live, XSTS, Minecraft Services и профильной конечной точке Minecraft. Формат — JSON поверх HTTPS, кроме первого шага, где тело запроса отправляется как форма (`postForm()`); дальше используются `postJson()` и `getJson()` с токеном в заголовке авторизации. Все запросы инициирует лаунчер. Ответ каждого шага разбирается тремя исходами: успех, ошибка с кодом состояния и транспортная ошибка — последняя приходит отдельным параметром, чтобы отличить недоступную сеть от отказа службы. Повторных попыток класс не делает. Все сигналы и колбэки приходят в поток GUI. ## Пример использования ```cpp auto *msa = new MsaAuthService(this); connect(msa, &MsaAuthService::progress, this, &Backend::showStatus); // 1. открыть окно браузера на этом адресе emit microsoftLoginUrlReady(MsaAuthService::authorizationUrl()); // 2. когда окно поймало адрес возврата QString code, error; if (MsaAuthService::matchRedirect(url, &code, &error) && !code.isEmpty()) { msa->loginWithCode(code, [this](const AuthResult &result) { if (result.licenseMissing) { emit loginFailed(tr("На аккаунте нет копии Minecraft")); return; } if (!result.ok) { emit loginFailed(result.error); return; } storeSession(result); }); } ``` --- При создании этого документа использовался ИИ.