feat: 使用授权代码流登录微软帐户 (#5330)
This commit is contained in:
@@ -22,7 +22,7 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
|||||||
import org.jackhuang.hmcl.auth.OAuth;
|
import org.jackhuang.hmcl.auth.OAuth;
|
||||||
import org.jackhuang.hmcl.event.Event;
|
import org.jackhuang.hmcl.event.Event;
|
||||||
import org.jackhuang.hmcl.event.EventManager;
|
import org.jackhuang.hmcl.event.EventManager;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.theme.Themes;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
@@ -37,8 +37,8 @@ import java.util.concurrent.ExecutionException;
|
|||||||
|
|
||||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||||
private final int port;
|
private final int port;
|
||||||
@@ -104,8 +104,11 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
String html;
|
String html;
|
||||||
try {
|
try {
|
||||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||||
|
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
|
||||||
.replace("%lang%", Locale.getDefault().toLanguageTag())
|
.replace("%lang%", Locale.getDefault().toLanguageTag())
|
||||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
.replace("%success%", i18n("message.success"))
|
||||||
|
.replace("%ok%", i18n("button.ok"))
|
||||||
|
.replace("%close_page%", i18n("account.methods.microsoft.close_page"));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.error("Failed to load html", e);
|
LOG.error("Failed to load html", e);
|
||||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||||
@@ -123,7 +126,8 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
|
|
||||||
public static class Factory implements OAuth.Callback {
|
public static class Factory implements OAuth.Callback {
|
||||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||||
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
|
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
|
||||||
|
public final EventManager<OpenBrowserEvent> onOpenBrowserDevice = new EventManager<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
||||||
@@ -150,17 +154,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void openBrowser(String url) throws IOException {
|
public void openBrowser(OAuth.GrantFlow grantFlow, String url) throws IOException {
|
||||||
lastlyOpenedURL = url;
|
lastlyOpenedURL = url;
|
||||||
|
|
||||||
try {
|
switch (grantFlow) {
|
||||||
Thread.sleep(1500);
|
case AUTHORIZATION_CODE -> onOpenBrowserAuthorizationCode.fireEvent(new OpenBrowserEvent(this, url));
|
||||||
} catch (InterruptedException ignored) {
|
case DEVICE -> onOpenBrowserDevice.fireEvent(new OpenBrowserEvent(this, url));
|
||||||
}
|
}
|
||||||
|
|
||||||
FXUtils.openLink(url);
|
|
||||||
|
|
||||||
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui;
|
|||||||
|
|
||||||
import org.jackhuang.hmcl.auth.*;
|
import org.jackhuang.hmcl.auth.*;
|
||||||
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
|
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
|
||||||
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
|
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginPane;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
@@ -49,10 +49,10 @@ public final class DialogController {
|
|||||||
CountDownLatch latch = new CountDownLatch(1);
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
|
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
|
||||||
runInFX(() -> {
|
runInFX(() -> {
|
||||||
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
|
MicrosoftAccountLoginPane pane = new MicrosoftAccountLoginPane(account, it -> {
|
||||||
res.set(it);
|
res.set(it);
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}, latch::countDown);
|
}, latch::countDown, false);
|
||||||
Controllers.dialog(pane);
|
Controllers.dialog(pane);
|
||||||
});
|
});
|
||||||
latch.await();
|
latch.await();
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
|
|||||||
microsoftItem.getStyleClass().add("navigation-drawer-item");
|
microsoftItem.getStyleClass().add("navigation-drawer-item");
|
||||||
microsoftItem.setTitle(i18n("account.methods.microsoft"));
|
microsoftItem.setTitle(i18n("account.methods.microsoft"));
|
||||||
microsoftItem.setLeftIcon(SVG.MICROSOFT);
|
microsoftItem.setLeftIcon(SVG.MICROSOFT);
|
||||||
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
|
microsoftItem.setOnAction(e -> Controllers.dialog(new MicrosoftAccountLoginPane()));
|
||||||
|
|
||||||
AdvancedListItem offlineItem = new AdvancedListItem();
|
AdvancedListItem offlineItem = new AdvancedListItem();
|
||||||
offlineItem.getStyleClass().add("navigation-drawer-item");
|
offlineItem.getStyleClass().add("navigation-drawer-item");
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ import javafx.application.Platform;
|
|||||||
import javafx.beans.NamedArg;
|
import javafx.beans.NamedArg;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanBinding;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
@@ -45,7 +43,6 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
|
|||||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||||
import org.jackhuang.hmcl.game.OAuthServer;
|
|
||||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||||
import org.jackhuang.hmcl.setting.Accounts;
|
import org.jackhuang.hmcl.setting.Accounts;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
@@ -54,7 +51,6 @@ import org.jackhuang.hmcl.task.TaskExecutor;
|
|||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.*;
|
import org.jackhuang.hmcl.ui.construct.*;
|
||||||
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
@@ -85,13 +81,11 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
|||||||
private final JFXButton btnAccept;
|
private final JFXButton btnAccept;
|
||||||
private final SpinnerPane spinner;
|
private final SpinnerPane spinner;
|
||||||
private final Node body;
|
private final Node body;
|
||||||
|
private final HBox actions;
|
||||||
private Node detailsPane; // AccountDetailsInputPane for Offline / Mojang / authlib-injector, Label for Microsoft
|
private Node detailsPane; // AccountDetailsInputPane for Offline / Mojang / authlib-injector, Label for Microsoft
|
||||||
private final Pane detailsContainer;
|
private final Pane detailsContainer;
|
||||||
|
|
||||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
|
||||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
|
||||||
|
|
||||||
private TaskExecutor loginTask;
|
private TaskExecutor loginTask;
|
||||||
|
|
||||||
@@ -146,10 +140,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
|||||||
btnCancel.setOnAction(e -> onCancel());
|
btnCancel.setOnAction(e -> onCancel());
|
||||||
onEscPressed(this, btnCancel::fire);
|
onEscPressed(this, btnCancel::fire);
|
||||||
|
|
||||||
HBox hbox = new HBox(spinner, btnCancel);
|
actions = new HBox(spinner, btnCancel);
|
||||||
hbox.setAlignment(Pos.CENTER_RIGHT);
|
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
setActions(lblErrorMessage, hbox);
|
setActions(lblErrorMessage, actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showMethodSwitcher) {
|
if (showMethodSwitcher) {
|
||||||
@@ -226,7 +220,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
|||||||
|
|
||||||
Runnable doCreate = () -> {
|
Runnable doCreate = () -> {
|
||||||
logging.set(true);
|
logging.set(true);
|
||||||
deviceCode.set(null);
|
|
||||||
|
|
||||||
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
|
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
|
||||||
.whenComplete(Schedulers.javafx(), account -> {
|
.whenComplete(Schedulers.javafx(), account -> {
|
||||||
@@ -281,88 +274,22 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
|||||||
detailsContainer.getChildren().remove(detailsPane);
|
detailsContainer.getChildren().remove(detailsPane);
|
||||||
lblErrorMessage.setText("");
|
lblErrorMessage.setText("");
|
||||||
lblErrorMessage.setVisible(true);
|
lblErrorMessage.setVisible(true);
|
||||||
|
actions.setVisible(true);
|
||||||
|
actions.setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (factory == Accounts.FACTORY_MICROSOFT) {
|
if (factory == Accounts.FACTORY_MICROSOFT) {
|
||||||
VBox vbox = new VBox(8);
|
VBox vbox = new VBox(8);
|
||||||
detailsPane = vbox;
|
detailsPane = vbox;
|
||||||
|
|
||||||
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
|
||||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
|
||||||
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
|
|
||||||
vbox.getChildren().add(hintPane);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IntegrityChecker.isOfficial()) {
|
|
||||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
|
||||||
hintPane.setSegment(i18n("unofficial.hint"));
|
|
||||||
vbox.getChildren().add(hintPane);
|
|
||||||
}
|
|
||||||
|
|
||||||
VBox codeBox = new VBox(8);
|
|
||||||
Label hint = new Label(i18n("account.methods.microsoft.code"));
|
|
||||||
Label code = new Label();
|
|
||||||
code.setMouseTransparent(true);
|
|
||||||
code.setStyle("-fx-font-size: 24");
|
|
||||||
codeBox.getChildren().addAll(hint, code);
|
|
||||||
codeBox.setAlignment(Pos.CENTER);
|
|
||||||
vbox.getChildren().add(codeBox);
|
|
||||||
|
|
||||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||||
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
|
hintPane.setText(i18n("account.methods.microsoft.hint"));
|
||||||
errHintPane.setVisible(false);
|
vbox.getChildren().addAll(new MicrosoftAccountLoginPane(true));
|
||||||
errHintPane.setManaged(false);
|
btnAccept.setOnAction(e -> {
|
||||||
|
fireEvent(new DialogCloseEvent());
|
||||||
codeBox.setVisible(false);
|
Controllers.dialog(new MicrosoftAccountLoginPane());
|
||||||
codeBox.setManaged(false);
|
|
||||||
|
|
||||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
|
||||||
if (deviceCode != null) {
|
|
||||||
FXUtils.copyText(deviceCode.getUserCode());
|
|
||||||
code.setText(deviceCode.getUserCode());
|
|
||||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getVerificationUri()));
|
|
||||||
codeBox.setVisible(true);
|
|
||||||
codeBox.setManaged(true);
|
|
||||||
} else {
|
|
||||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
|
||||||
codeBox.setVisible(false);
|
|
||||||
codeBox.setManaged(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
actions.setManaged(false);
|
||||||
lblErrorMessage.setVisible(false);
|
actions.setVisible(false);
|
||||||
lblErrorMessage.textProperty().addListener((obs, oldVal, newVal) -> {
|
|
||||||
boolean hasError = !newVal.isEmpty();
|
|
||||||
errHintPane.setSegment(newVal);
|
|
||||||
errHintPane.setVisible(hasError);
|
|
||||||
errHintPane.setManaged(hasError);
|
|
||||||
hintPane.setVisible(!hasError);
|
|
||||||
hintPane.setManaged(!hasError);
|
|
||||||
codeBox.setVisible(!hasError && deviceCode.get() != null);
|
|
||||||
codeBox.setManaged(!hasError && deviceCode.get() != null);
|
|
||||||
});
|
|
||||||
|
|
||||||
FXUtils.onClicked(codeBox, () -> {
|
|
||||||
if (deviceCode.get() != null) FXUtils.copyText(deviceCode.get().getUserCode());
|
|
||||||
});
|
|
||||||
|
|
||||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value ->
|
|
||||||
runInFX(() -> deviceCode.set(value))
|
|
||||||
));
|
|
||||||
|
|
||||||
HBox linkBox = new HBox();
|
|
||||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
|
||||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
|
||||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
|
||||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
|
||||||
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
|
|
||||||
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
|
|
||||||
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
|
|
||||||
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
|
|
||||||
linkBox.getChildren().setAll(profileLink, purchaseLink, deauthorizeLink, forgotpasswordLink);
|
|
||||||
|
|
||||||
vbox.getChildren().addAll(hintPane, errHintPane, linkBox);
|
|
||||||
btnAccept.setDisable(false);
|
btnAccept.setDisable(false);
|
||||||
} else {
|
} else {
|
||||||
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
|
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,109 +0,0 @@
|
|||||||
package org.jackhuang.hmcl.ui.account;
|
|
||||||
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
|
||||||
import org.jackhuang.hmcl.auth.OAuthAccount;
|
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
|
||||||
import org.jackhuang.hmcl.game.OAuthServer;
|
|
||||||
import org.jackhuang.hmcl.setting.Accounts;
|
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
|
||||||
import org.jackhuang.hmcl.task.Task;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|
||||||
|
|
||||||
public class OAuthAccountLoginDialog extends DialogPane {
|
|
||||||
private final OAuthAccount account;
|
|
||||||
private final Consumer<AuthInfo> success;
|
|
||||||
private final Runnable failed;
|
|
||||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
|
||||||
|
|
||||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
|
||||||
|
|
||||||
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
|
|
||||||
this.account = account;
|
|
||||||
this.success = success;
|
|
||||||
this.failed = failed;
|
|
||||||
|
|
||||||
setTitle(i18n("account.login.refresh"));
|
|
||||||
|
|
||||||
VBox vbox = new VBox(8);
|
|
||||||
Label usernameLabel = new Label(account.getUsername());
|
|
||||||
|
|
||||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
|
||||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
|
||||||
if (deviceCode != null) {
|
|
||||||
FXUtils.copyText(deviceCode.getUserCode());
|
|
||||||
hintPane.setSegment(
|
|
||||||
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
|
|
||||||
+ i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri())
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
hintPane.setSegment(
|
|
||||||
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
|
|
||||||
+ i18n("account.methods.microsoft.hint")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
FXUtils.onClicked(hintPane, () -> {
|
|
||||||
if (deviceCode.get() != null) {
|
|
||||||
FXUtils.copyText(deviceCode.get().getUserCode());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
HBox box = new HBox(8);
|
|
||||||
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
|
|
||||||
birthLink.setExternalLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-837badbc-999e-54d2-2617-d19206b9540a");
|
|
||||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
|
||||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
|
||||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
|
||||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
|
||||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
|
|
||||||
GridPane.setColumnSpan(box, 2);
|
|
||||||
|
|
||||||
vbox.getChildren().setAll(usernameLabel, hintPane, box);
|
|
||||||
setBody(vbox);
|
|
||||||
|
|
||||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
|
|
||||||
FXUtils.runInFX(() -> {
|
|
||||||
deviceCode.set(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAccept() {
|
|
||||||
setLoading();
|
|
||||||
Task.supplyAsync(account::logInWhenCredentialsExpired)
|
|
||||||
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
|
|
||||||
if (exception == null) {
|
|
||||||
success.accept(authInfo);
|
|
||||||
onSuccess();
|
|
||||||
} else {
|
|
||||||
LOG.info("Failed to login when credentials expired: " + account, exception);
|
|
||||||
onFailure(Accounts.localizeErrorMessage(exception));
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCancel() {
|
|
||||||
failed.run();
|
|
||||||
super.onCancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -572,6 +572,36 @@
|
|||||||
-fx-text-fill: -monet-on-surface;
|
-fx-text-fill: -monet-on-surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* Microsoft Login Dialog *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.method-title {
|
||||||
|
-fx-text-fill: -monet-on-surface;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-desc {
|
||||||
|
-fx-text-fill: -monet-outline;
|
||||||
|
-fx-font-size: 0.9em;
|
||||||
|
-fx-line-spacing: 2;
|
||||||
|
-fx-wrap-text: true;
|
||||||
|
-fx-text-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box {
|
||||||
|
-fx-background-color: -monet-surface-variant;
|
||||||
|
-fx-background-radius: 6;
|
||||||
|
-fx-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box .code-label {
|
||||||
|
-fx-font-size: 22px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-text-fill: -monet-primary;
|
||||||
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* *
|
* *
|
||||||
|
|||||||
@@ -118,11 +118,14 @@ account.methods.microsoft.error.wrong_verify_method=Failed to log in. Please try
|
|||||||
account.methods.microsoft.logging_in=Logging in...
|
account.methods.microsoft.logging_in=Logging in...
|
||||||
account.methods.microsoft.makegameidsettings=Create Profile / Edit Profile Name
|
account.methods.microsoft.makegameidsettings=Create Profile / Edit Profile Name
|
||||||
account.methods.microsoft.hint=Click the "Log in" button to start adding your Microsoft account.
|
account.methods.microsoft.hint=Click the "Log in" button to start adding your Microsoft account.
|
||||||
account.methods.microsoft.manual=Please enter the code shown above on the pop-up webpage to complete the login.\n\
|
account.methods.microsoft.methods.or=Or
|
||||||
\n\
|
account.methods.microsoft.methods.device=Log in on another device
|
||||||
If the website fails to load, please open %s manually in your browser.\n\
|
account.methods.microsoft.methods.device.hint=Scan the QR code on another device, or visit \n %s \n to complete log in
|
||||||
\n\
|
account.methods.microsoft.methods.device.copy=Copy code
|
||||||
<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether. You may try again later or switch to a different internet connection.</b>
|
account.methods.microsoft.methods.browser=Log in via browser (Recommended)
|
||||||
|
account.methods.microsoft.methods.browser.copy_open=Copy link and visit in browser
|
||||||
|
account.methods.microsoft.methods.browser.hint=Click the following button to copy and open the browser to log in
|
||||||
|
account.methods.microsoft.manual=<b>If your internet connection is bad, it may cause web pages to load slowly or fail to load altogether.\nYou may try again later or switch to a different internet connection.</b>
|
||||||
account.methods.microsoft.profile=Account Profile
|
account.methods.microsoft.profile=Account Profile
|
||||||
account.methods.microsoft.purchase=Buy Minecraft
|
account.methods.microsoft.purchase=Buy Minecraft
|
||||||
account.methods.microsoft.snapshot=You are using an unofficial build of HMCL. Please download the <a href="https://hmcl.huangyuhui.net/download">official build</a> to log in.
|
account.methods.microsoft.snapshot=You are using an unofficial build of HMCL. Please download the <a href="https://hmcl.huangyuhui.net/download">official build</a> to log in.
|
||||||
|
|||||||
@@ -117,9 +117,14 @@ account.methods.microsoft.error.wrong_verify_method=登入失敗。請在 Micros
|
|||||||
account.methods.microsoft.logging_in=登入中……
|
account.methods.microsoft.logging_in=登入中……
|
||||||
account.methods.microsoft.makegameidsettings=建立檔案 / 編輯檔案名稱
|
account.methods.microsoft.makegameidsettings=建立檔案 / 編輯檔案名稱
|
||||||
account.methods.microsoft.hint=點擊「登入」按鈕開始新增 Microsoft 帳戶。
|
account.methods.microsoft.hint=點擊「登入」按鈕開始新增 Microsoft 帳戶。
|
||||||
account.methods.microsoft.manual=請在彈出的網頁中輸入上方顯示的代碼以完成登入。\n\
|
account.methods.microsoft.methods.or=或
|
||||||
若網站未能顯示,請手動在瀏覽器中開啟:%s\n\
|
account.methods.microsoft.methods.device=在其他裝置登入
|
||||||
<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>\n
|
account.methods.microsoft.methods.device.hint=在其他裝置掃描 QR Code,或開啟 \n %s \n 完成登入
|
||||||
|
account.methods.microsoft.methods.device.copy=複製代碼
|
||||||
|
account.methods.microsoft.methods.browser=在瀏覽器登入 (推薦)
|
||||||
|
account.methods.microsoft.methods.browser.copy_open=複製連結並在瀏覽器中開啟
|
||||||
|
account.methods.microsoft.methods.browser.hint=點擊下方按鈕複製連結並開啟瀏覽器以登入
|
||||||
|
account.methods.microsoft.manual=<b>若網路環境不佳,可能會導致網頁載入緩慢甚至無法載入,請稍後再試或更換網路環境後再試。</b>
|
||||||
account.methods.microsoft.profile=編輯帳戶個人資訊
|
account.methods.microsoft.profile=編輯帳戶個人資訊
|
||||||
account.methods.microsoft.purchase=購買 Minecraft
|
account.methods.microsoft.purchase=購買 Minecraft
|
||||||
account.methods.forgot_password=忘記密碼
|
account.methods.forgot_password=忘記密碼
|
||||||
|
|||||||
@@ -118,9 +118,14 @@ account.methods.microsoft.error.wrong_verify_method=登录失败。请在微软
|
|||||||
account.methods.microsoft.logging_in=登录中……
|
account.methods.microsoft.logging_in=登录中……
|
||||||
account.methods.microsoft.makegameidsettings=创建档案 / 编辑档案名称
|
account.methods.microsoft.makegameidsettings=创建档案 / 编辑档案名称
|
||||||
account.methods.microsoft.hint=点击“登录”按钮开始添加微软账户。
|
account.methods.microsoft.hint=点击“登录”按钮开始添加微软账户。
|
||||||
account.methods.microsoft.manual=请在弹出的网页中输入上方显示的代码以完成登录。\n\
|
account.methods.microsoft.methods.or=或
|
||||||
若网站未能显示,请手动在浏览器中打开:%s\n\
|
account.methods.microsoft.methods.device=在其他设备登录
|
||||||
<b>若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。</b>\n\
|
account.methods.microsoft.methods.device.hint=在其他设备扫描二维码,或打开 \n %s \n 完成登录
|
||||||
|
account.methods.microsoft.methods.device.copy=复制代码
|
||||||
|
account.methods.microsoft.methods.browser=在浏览器登录 (推荐)
|
||||||
|
account.methods.microsoft.methods.browser.copy_open=复制链接并打开浏览器
|
||||||
|
account.methods.microsoft.methods.browser.hint=点击下方按钮复制链接并打开浏览器登录
|
||||||
|
account.methods.microsoft.manual=<b>若网络环境不佳,可能会导致网页加载缓慢甚至无法加载,请使用网络代理并重试。</b>\n\
|
||||||
如遇到问题,你可以点击右上角帮助按钮进行求助。
|
如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||||
account.methods.microsoft.profile=编辑账户个人信息
|
account.methods.microsoft.profile=编辑账户个人信息
|
||||||
account.methods.microsoft.purchase=购买 Minecraft
|
account.methods.microsoft.purchase=购买 Minecraft
|
||||||
|
|||||||
@@ -19,20 +19,134 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
<html lang="%lang%">
|
<html lang="%lang%">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Hello Minecraft! Launcher</title>
|
<title>Hello Minecraft! Launcher</title>
|
||||||
|
<style>
|
||||||
|
%style%
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--monet-surface);
|
||||||
|
color: var(--monet-on-surface);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-card {
|
||||||
|
background-color: var(--monet-surface-container-high);
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.02);
|
||||||
|
max-width: 560px;
|
||||||
|
width: 90%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-column {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-column svg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-column {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--monet-on-surface);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--monet-on-surface-variant);
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--monet-primary);
|
||||||
|
border: none;
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
outline: none;
|
||||||
|
min-width: 64px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm:hover {
|
||||||
|
background-color: rgba(128, 128, 128, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm:active {
|
||||||
|
background-color: rgba(128, 128, 128, 0.12);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div class="dialog-card">
|
||||||
%close-page%
|
<div class="icon-column">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="m10.6 16.6l7.05-7.05l-1.4-1.4l-5.65 5.65l-2.85-2.85l-1.4 1.4zM12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-column">
|
||||||
|
<div class="title">%success%</div>
|
||||||
|
<div class="subtitle">%close_page%</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn-confirm" onclick="closeWindow()">%ok%</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
setTimeout(function() {
|
function closeWindow() {
|
||||||
open("about:blank","_self").close();
|
window.close();
|
||||||
}, 5000);
|
window.open("about:blank", "_self").close();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(closeWindow, 3000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -77,7 +77,7 @@ public class OAuth {
|
|||||||
|
|
||||||
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
|
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
|
||||||
Session session = options.callback.startServer();
|
Session session = options.callback.startServer();
|
||||||
options.callback.openBrowser(NetworkUtils.withQuery(authorizationURL,
|
options.callback.openBrowser(GrantFlow.AUTHORIZATION_CODE, NetworkUtils.withQuery(authorizationURL,
|
||||||
mapOf(pair("client_id", options.callback.getClientId()), pair("response_type", "code"),
|
mapOf(pair("client_id", options.callback.getClientId()), pair("response_type", "code"),
|
||||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope),
|
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope),
|
||||||
pair("prompt", "select_account"))));
|
pair("prompt", "select_account"))));
|
||||||
@@ -106,7 +106,7 @@ public class OAuth {
|
|||||||
options.callback.grantDeviceCode(deviceTokenResponse.userCode, deviceTokenResponse.verificationURI);
|
options.callback.grantDeviceCode(deviceTokenResponse.userCode, deviceTokenResponse.verificationURI);
|
||||||
|
|
||||||
// Microsoft OAuth Flow
|
// Microsoft OAuth Flow
|
||||||
options.callback.openBrowser(deviceTokenResponse.verificationURI);
|
options.callback.openBrowser(GrantFlow.DEVICE, deviceTokenResponse.verificationURI);
|
||||||
|
|
||||||
long startTime = System.nanoTime();
|
long startTime = System.nanoTime();
|
||||||
long interval = TimeUnit.MILLISECONDS.convert(deviceTokenResponse.interval, TimeUnit.SECONDS);
|
long interval = TimeUnit.MILLISECONDS.convert(deviceTokenResponse.interval, TimeUnit.SECONDS);
|
||||||
@@ -237,9 +237,10 @@ public class OAuth {
|
|||||||
/**
|
/**
|
||||||
* Open browser
|
* Open browser
|
||||||
*
|
*
|
||||||
|
* @param grantFlow the grant flow.
|
||||||
* @param url OAuth url.
|
* @param url OAuth url.
|
||||||
*/
|
*/
|
||||||
void openBrowser(String url) throws IOException;
|
void openBrowser(GrantFlow grantFlow, String url) throws IOException;
|
||||||
|
|
||||||
String getClientId();
|
String getClientId();
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ public final class MicrosoftAccount extends OAuthAccount {
|
|||||||
this.characterUUID = requireNonNull(session.getProfile().getId());
|
this.characterUUID = requireNonNull(session.getProfile().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected MicrosoftAccount(MicrosoftService service, CharacterSelector characterSelector) throws AuthenticationException {
|
protected MicrosoftAccount(MicrosoftService service, OAuth.GrantFlow flow) throws AuthenticationException {
|
||||||
this.service = requireNonNull(service);
|
this.service = requireNonNull(service);
|
||||||
|
|
||||||
MicrosoftSession acquiredSession = service.authenticate();
|
MicrosoftSession acquiredSession = service.authenticate(flow);
|
||||||
if (acquiredSession.getProfile() == null) {
|
if (acquiredSession.getProfile() == null) {
|
||||||
session = service.refresh(acquiredSession);
|
session = service.refresh(acquiredSession);
|
||||||
} else {
|
} else {
|
||||||
@@ -105,7 +105,7 @@ public final class MicrosoftAccount extends OAuthAccount {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthInfo logInWhenCredentialsExpired() throws AuthenticationException {
|
public AuthInfo logInWhenCredentialsExpired() throws AuthenticationException {
|
||||||
MicrosoftSession acquiredSession = service.authenticate();
|
MicrosoftSession acquiredSession = service.authenticate(OAuth.GrantFlow.DEVICE);
|
||||||
if (!Objects.equals(characterUUID, acquiredSession.getProfile().getId())) {
|
if (!Objects.equals(characterUUID, acquiredSession.getProfile().getId())) {
|
||||||
throw new WrongAccountException(characterUUID, acquiredSession.getProfile().getId());
|
throw new WrongAccountException(characterUUID, acquiredSession.getProfile().getId());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.auth.microsoft;
|
|||||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||||
|
import org.jackhuang.hmcl.auth.OAuth;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -39,9 +40,7 @@ public class MicrosoftAccountFactory extends AccountFactory<MicrosoftAccount> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MicrosoftAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException {
|
public MicrosoftAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException {
|
||||||
Objects.requireNonNull(selector);
|
return new MicrosoftAccount(service, (OAuth.GrantFlow) additionalData);
|
||||||
|
|
||||||
return new MicrosoftAccount(service, selector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ public class MicrosoftService {
|
|||||||
return profileRepository;
|
return profileRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MicrosoftSession authenticate() throws AuthenticationException {
|
public MicrosoftSession authenticate(OAuth.GrantFlow flow) throws AuthenticationException {
|
||||||
try {
|
try {
|
||||||
OAuth.Result result = OAuth.MICROSOFT.authenticate(OAuth.GrantFlow.DEVICE, new OAuth.Options(SCOPE, callback));
|
OAuth.Result result = OAuth.MICROSOFT.authenticate(flow, new OAuth.Options(SCOPE, callback));
|
||||||
return authenticateViaLiveAccessToken(result.getAccessToken(), result.getRefreshToken());
|
return authenticateViaLiveAccessToken(result.getAccessToken(), result.getRefreshToken());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ServerDisconnectException(e);
|
throw new ServerDisconnectException(e);
|
||||||
|
|||||||
Reference in New Issue
Block a user