feat(microsoft): use device code to login.
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,10 @@
|
|||||||
-fx-pref-width: 200;
|
-fx-pref-width: 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.memory-label {
|
.memory-label {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user