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.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.BooleanSupplier;
@ -700,7 +701,7 @@ public final class FXUtils {
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 {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
@ -719,6 +720,10 @@ public final class FXUtils {
JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent());
hyperlink.setOnAction(e -> hyperlinkAction.accept(href));
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())) {
texts.add(new Text("\n"));
} else {
@ -728,12 +733,17 @@ public final class FXUtils {
texts.add(new Text(node.getTextContent()));
}
}
final TextFlow tf = new TextFlow(texts.toArray(new javafx.scene.Node[0]));
return tf;
return texts;
} catch (SAXException | ParserConfigurationException | IOException 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.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
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.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
@ -88,6 +91,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
private final Pane detailsContainer;
private final BooleanProperty logging = new SimpleBooleanProperty();
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
private TaskExecutor loginTask;
@ -217,6 +222,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
logging.set(true);
deviceCode.set(null);
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
.whenComplete(Schedulers.javafx(), account -> {
@ -262,15 +268,22 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
if (factory == Accounts.FACTORY_MICROSOFT) {
VBox vbox = new VBox(8);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
logging
? i18n("account.methods.microsoft.manual")
: i18n("account.methods.microsoft.hint")));
hintPane.setOnMouseClicked(e -> {
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode()));
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
}
});
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);
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.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
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 java.util.logging.Level;
@ -43,10 +45,13 @@ public class OAuthAccountLoginDialog extends DialogPane {
Label usernameLabel = new Label(account.getUsername());
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.textProperty().bind(BindingMapping.of(deviceCode).map(deviceCode ->
deviceCode != null
? i18n("account.methods.microsoft.manual", deviceCode.getUserCode())
: i18n("account.methods.microsoft.hint")));
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode()));
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
}
});
hintPane.setOnMouseClicked(e -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getVerificationUri());
@ -70,7 +75,9 @@ public class OAuthAccountLoginDialog extends DialogPane {
}
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
deviceCode.set(event);
FXUtils.runInFX(() -> {
deviceCode.set(event);
});
}
@Override

View File

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

View File

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

View File

@ -98,8 +98,9 @@ public class OAuth {
.form(pair("client_id", options.callback.getClientId()), pair("scope", options.scope))
.ignoreHttpCode()
.getJson(DeviceTokenResponse.class);
handleErrorResponse(deviceTokenResponse);
options.callback.grantDeviceCode(deviceTokenResponse.deviceCode, deviceTokenResponse.verificationURI);
options.callback.grantDeviceCode(deviceTokenResponse.userCode, deviceTokenResponse.verificationURI);
// Microsoft OAuth Flow
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.
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();
}
@ -121,6 +122,7 @@ public class OAuth {
pair("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
pair("code", deviceTokenResponse.deviceCode),
pair("client_id", options.callback.getClientId()))
.ignoreHttpCode()
.getJson(TokenResponse.class);
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")
public String userCode;