feat(microsoft): use device code to login.

This commit is contained in:
huanghongxun
2021-10-23 02:22:04 +08:00
parent a36b0ebed2
commit 651aedaa50
6 changed files with 63 additions and 21 deletions

View File

@@ -77,6 +77,7 @@ import java.lang.reflect.Method;
import java.net.URI; import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
@@ -700,7 +701,7 @@ public final class FXUtils {
Controllers.showToast(i18n("message.copied")); Controllers.showToast(i18n("message.copied"));
} }
public static TextFlow segmentToTextFlow(final String segment, Consumer<String> hyperlinkAction) { public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
try { try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder(); DocumentBuilder builder = factory.newDocumentBuilder();
@@ -719,6 +720,10 @@ public final class FXUtils {
JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent()); JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent());
hyperlink.setOnAction(e -> hyperlinkAction.accept(href)); hyperlink.setOnAction(e -> hyperlinkAction.accept(href));
texts.add(hyperlink); texts.add(hyperlink);
} else if ("b".equals(element.getTagName())) {
Text text = new Text(element.getTextContent());
text.getStyleClass().add("bold");
texts.add(text);
} else if ("br".equals(element.getTagName())) { } else if ("br".equals(element.getTagName())) {
texts.add(new Text("\n")); texts.add(new Text("\n"));
} else { } else {
@@ -728,12 +733,17 @@ public final class FXUtils {
texts.add(new Text(node.getTextContent())); texts.add(new Text(node.getTextContent()));
} }
} }
final TextFlow tf = new TextFlow(texts.toArray(new javafx.scene.Node[0])); return texts;
return tf;
} catch (SAXException | ParserConfigurationException | IOException e) { } catch (SAXException | ParserConfigurationException | IOException e) {
LOG.log(Level.WARNING, "Failed to parse xml", e); LOG.log(Level.WARNING, "Failed to parse xml", e);
return new TextFlow(new Text(segment)); return Collections.singletonList(new Text(segment));
} }
} }
public static TextFlow segmentToTextFlow(final String segment, Consumer<String> hyperlinkAction) {
TextFlow tf = new TextFlow();
tf.getChildren().setAll(parseSegment(segment, hyperlinkAction));
return tf;
}
} }

View File

@@ -23,7 +23,9 @@ 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;
@@ -53,6 +55,7 @@ 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.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
@@ -88,6 +91,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
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;
@@ -217,6 +222,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
} }
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 -> {
@@ -262,15 +268,22 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
if (factory == Accounts.FACTORY_MICROSOFT) { if (factory == Accounts.FACTORY_MICROSOFT) {
VBox vbox = new VBox(8); VBox vbox = new VBox(8);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging -> FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
logging if (deviceCode != null) {
? i18n("account.methods.microsoft.manual") hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode()));
: i18n("account.methods.microsoft.hint"))); } else {
hintPane.setOnMouseClicked(e -> { hintPane.setSegment(i18n("account.methods.microsoft.hint"));
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
} }
}); });
hintPane.setOnMouseClicked(e -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getVerificationUri());
}
});
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value -> {
runInFX(() -> deviceCode.set(value));
}));
HBox box = new HBox(8); HBox box = new HBox(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth")); JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));

View File

@@ -15,8 +15,10 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.util.javafx.BindingMapping; 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 java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.Level;
@@ -43,10 +45,13 @@ public class OAuthAccountLoginDialog extends DialogPane {
Label usernameLabel = new Label(account.getUsername()); Label usernameLabel = new Label(account.getUsername());
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.textProperty().bind(BindingMapping.of(deviceCode).map(deviceCode -> FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
deviceCode != null if (deviceCode != null) {
? i18n("account.methods.microsoft.manual", deviceCode.getUserCode()) hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode()));
: i18n("account.methods.microsoft.hint"))); } else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
}
});
hintPane.setOnMouseClicked(e -> { hintPane.setOnMouseClicked(e -> {
if (deviceCode.get() != null) { if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getVerificationUri()); FXUtils.copyText(deviceCode.get().getVerificationUri());
@@ -70,7 +75,9 @@ public class OAuthAccountLoginDialog extends DialogPane {
} }
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) { private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
deviceCode.set(event); FXUtils.runInFX(() -> {
deviceCode.set(event);
});
} }
@Override @Override

View File

@@ -27,6 +27,8 @@ import javafx.scene.layout.VBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.SVG;
public class HintPane extends VBox { public class HintPane extends VBox {
@@ -84,6 +86,10 @@ public class HintPane extends VBox {
this.text.set(text); this.text.set(text);
} }
public void setSegment(String segment) {
flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction));
}
public void setChildren(Node... children) { public void setChildren(Node... children) {
flow.getChildren().setAll(children); flow.getChildren().setAll(children);
} }

View File

@@ -95,6 +95,10 @@
-fx-pref-width: 200; -fx-pref-width: 200;
} }
.bold {
-fx-font-weight: bold;
}
.memory-label { .memory-label {
} }

View File

@@ -98,8 +98,9 @@ public class OAuth {
.form(pair("client_id", options.callback.getClientId()), pair("scope", options.scope)) .form(pair("client_id", options.callback.getClientId()), pair("scope", options.scope))
.ignoreHttpCode() .ignoreHttpCode()
.getJson(DeviceTokenResponse.class); .getJson(DeviceTokenResponse.class);
handleErrorResponse(deviceTokenResponse);
options.callback.grantDeviceCode(deviceTokenResponse.deviceCode, deviceTokenResponse.verificationURI); options.callback.grantDeviceCode(deviceTokenResponse.userCode, deviceTokenResponse.verificationURI);
// Microsoft OAuth Flow // Microsoft OAuth Flow
options.callback.openBrowser(deviceTokenResponse.verificationURI); options.callback.openBrowser(deviceTokenResponse.verificationURI);
@@ -112,7 +113,7 @@ public class OAuth {
// We stop waiting if user does not respond our authentication request in 15 minutes. // We stop waiting if user does not respond our authentication request in 15 minutes.
long estimatedTime = System.nanoTime() - startTime; long estimatedTime = System.nanoTime() - startTime;
if (TimeUnit.MINUTES.convert(estimatedTime, TimeUnit.SECONDS) >= Math.min(deviceTokenResponse.expiresIn, 900)) { if (TimeUnit.SECONDS.convert(estimatedTime, TimeUnit.NANOSECONDS) >= Math.min(deviceTokenResponse.expiresIn, 900)) {
throw new NoSelectedCharacterException(); throw new NoSelectedCharacterException();
} }
@@ -121,6 +122,7 @@ public class OAuth {
pair("grant_type", "urn:ietf:params:oauth:grant-type:device_code"), pair("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
pair("code", deviceTokenResponse.deviceCode), pair("code", deviceTokenResponse.deviceCode),
pair("client_id", options.callback.getClientId())) pair("client_id", options.callback.getClientId()))
.ignoreHttpCode()
.getJson(TokenResponse.class); .getJson(TokenResponse.class);
if ("authorization_pending".equals(tokenResponse.error)) { if ("authorization_pending".equals(tokenResponse.error)) {
@@ -256,7 +258,7 @@ public class OAuth {
} }
} }
private static class DeviceTokenResponse { private static class DeviceTokenResponse extends ErrorResponse {
@SerializedName("user_code") @SerializedName("user_code")
public String userCode; public String userCode;