mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-12 06:09:00 +08:00
Merge pull request #7743 from Ultimaker/CURA-7427_Add_option_to_sign_in_with_different_account_while_waiting_for_printers
Cura 7427 add option to sign in with different account while waiting for printers
This commit is contained in:
commit
f34e05ac03
@ -163,11 +163,23 @@ class Account(QObject):
|
||||
self._update_timer.stop()
|
||||
|
||||
@pyqtSlot()
|
||||
def login(self) -> None:
|
||||
@pyqtSlot(bool)
|
||||
def login(self, force_logout_before_login: bool = False) -> None:
|
||||
"""
|
||||
Initializes the login process. If the user is logged in already and force_logout_before_login is true, Cura will
|
||||
logout from the account before initiating the authorization flow. If the user is logged in and
|
||||
force_logout_before_login is false, the function will return, as there is nothing to do.
|
||||
|
||||
:param force_logout_before_login: Optional boolean parameter
|
||||
:return: None
|
||||
"""
|
||||
if self._logged_in:
|
||||
if force_logout_before_login:
|
||||
self.logout()
|
||||
else:
|
||||
# Nothing to do, user already logged in.
|
||||
return
|
||||
self._authorization_service.startAuthorizationFlow()
|
||||
self._authorization_service.startAuthorizationFlow(force_logout_before_login)
|
||||
|
||||
@pyqtProperty(str, notify=loginStateChanged)
|
||||
def userName(self):
|
||||
|
@ -3,8 +3,8 @@
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from urllib.parse import urlencode
|
||||
from typing import Optional, TYPE_CHECKING, Dict
|
||||
from urllib.parse import urlencode, quote_plus
|
||||
|
||||
import requests.exceptions
|
||||
from PyQt5.QtCore import QUrl
|
||||
@ -24,6 +24,7 @@ if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import UserProfile, OAuth2Settings
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
MYCLOUD_LOGOFF_URL = "https://mycloud.ultimaker.com/logoff"
|
||||
|
||||
## The authorization service is responsible for handling the login flow,
|
||||
# storing user credentials and providing account information.
|
||||
@ -142,7 +143,7 @@ class AuthorizationService:
|
||||
self.onAuthStateChanged.emit(logged_in = False)
|
||||
|
||||
## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
|
||||
def startAuthorizationFlow(self) -> None:
|
||||
def startAuthorizationFlow(self, force_browser_logout: bool = False) -> None:
|
||||
Logger.log("d", "Starting new OAuth2 flow...")
|
||||
|
||||
# Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
|
||||
@ -153,8 +154,8 @@ class AuthorizationService:
|
||||
|
||||
state = AuthorizationHelpers.generateVerificationCode()
|
||||
|
||||
# Create the query string needed for the OAuth2 flow.
|
||||
query_string = urlencode({
|
||||
# Create the query dict needed for the OAuth2 flow.
|
||||
query_parameters_dict = {
|
||||
"client_id": self._settings.CLIENT_ID,
|
||||
"redirect_uri": self._settings.CALLBACK_URL,
|
||||
"scope": self._settings.CLIENT_SCOPES,
|
||||
@ -162,7 +163,7 @@ class AuthorizationService:
|
||||
"state": state, # Forever in our Hearts, RIP "(.Y.)" (2018-2020)
|
||||
"code_challenge": challenge_code,
|
||||
"code_challenge_method": "S512"
|
||||
})
|
||||
}
|
||||
|
||||
# Start a local web server to receive the callback URL on.
|
||||
try:
|
||||
@ -173,9 +174,28 @@ class AuthorizationService:
|
||||
title=i18n_catalog.i18nc("@info:title", "Warning")).show()
|
||||
return
|
||||
|
||||
auth_url = self._generate_auth_url(query_parameters_dict, force_browser_logout)
|
||||
# Open the authorization page in a new browser window.
|
||||
QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string)))
|
||||
QDesktopServices.openUrl(QUrl(auth_url))
|
||||
|
||||
def _generate_auth_url(self, query_parameters_dict: Dict[str, Optional[str]], force_browser_logout: bool) -> str:
|
||||
"""
|
||||
Generates the authentications url based on the original auth_url and the query_parameters_dict to be included.
|
||||
If there is a request to force logging out of mycloud in the browser, the link to logoff from mycloud is
|
||||
prepended in order to force the browser to logoff from mycloud and then redirect to the authentication url to
|
||||
login again. This case is used to sync the accounts between Cura and the browser.
|
||||
|
||||
:param query_parameters_dict: A dictionary with the query parameters to be url encoded and added to the
|
||||
authentication link
|
||||
:param force_browser_logout: If True, Cura will prepend the MYCLOUD_LOGOFF_URL link before the authentication
|
||||
link to force the a browser logout from mycloud.ultimaker.com
|
||||
:return: The authentication URL, properly formatted and encoded
|
||||
"""
|
||||
auth_url = "{}?{}".format(self._auth_url, urlencode(query_parameters_dict))
|
||||
if force_browser_logout:
|
||||
# The url after '?next=' should be urlencoded
|
||||
auth_url = "{}?next={}".format(MYCLOUD_LOGOFF_URL, quote_plus(auth_url))
|
||||
return auth_url
|
||||
|
||||
## Callback method for the authentication flow.
|
||||
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
|
||||
|
@ -51,11 +51,11 @@ Item
|
||||
}
|
||||
|
||||
// Component that contains a busy indicator and a message, while it waits for Cura to discover a cloud printer
|
||||
Rectangle
|
||||
Item
|
||||
{
|
||||
id: waitingContent
|
||||
width: parent.width
|
||||
height: waitingIndicator.height + waitingLabel.height
|
||||
height: childrenRect.height
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
BusyIndicator
|
||||
@ -74,6 +74,37 @@ Item
|
||||
font: UM.Theme.getFont("large")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
Label
|
||||
{
|
||||
id: noPrintersFoundLabel
|
||||
anchors.top: waitingLabel.bottom
|
||||
anchors.topMargin: 2 * UM.Theme.getSize("wide_margin").height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: catalog.i18nc("@label", "No printers found in your account?")
|
||||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: "Sign in with a different account"
|
||||
anchors.top: noPrintersFoundLabel.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text_link")
|
||||
MouseArea {
|
||||
anchors.fill: parent;
|
||||
onClicked: Cura.API.account.login(true)
|
||||
hoverEnabled: true
|
||||
onEntered:
|
||||
{
|
||||
parent.font.underline = true
|
||||
}
|
||||
onExited:
|
||||
{
|
||||
parent.font.underline = false
|
||||
}
|
||||
}
|
||||
}
|
||||
visible: discoveredCloudPrintersModel.count == 0
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ Item
|
||||
}
|
||||
else
|
||||
{
|
||||
Qt.openUrlExternally("https://mycloud.ultimaker.com/app/manage/printers")
|
||||
Qt.openUrlExternally("https://mycloud.ultimaker.com/")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,16 +19,24 @@ def test_login():
|
||||
account = Account(MagicMock())
|
||||
mocked_auth_service = MagicMock()
|
||||
account._authorization_service = mocked_auth_service
|
||||
account.logout = MagicMock()
|
||||
|
||||
account.login()
|
||||
mocked_auth_service.startAuthorizationFlow.assert_called_once_with()
|
||||
mocked_auth_service.startAuthorizationFlow.assert_called_once_with(False)
|
||||
|
||||
# Fake a sucesfull login
|
||||
# Fake a successful login
|
||||
account._onLoginStateChanged(True)
|
||||
|
||||
# Attempting to log in again shouldn't change anything.
|
||||
account.login()
|
||||
mocked_auth_service.startAuthorizationFlow.assert_called_once_with()
|
||||
mocked_auth_service.startAuthorizationFlow.assert_called_once_with(False)
|
||||
|
||||
# Attempting to log in with force_logout_before_login as True should call the logout before calling the
|
||||
# startAuthorizationFlow(True).
|
||||
account.login(force_logout_before_login=True)
|
||||
account.logout.assert_called_once_with()
|
||||
mocked_auth_service.startAuthorizationFlow.assert_called_with(True)
|
||||
assert mocked_auth_service.startAuthorizationFlow.call_count == 2
|
||||
|
||||
|
||||
def test_initialize():
|
||||
|
@ -7,7 +7,7 @@ from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
from UM.Preferences import Preferences
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
|
||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||
from cura.OAuth2.AuthorizationService import AuthorizationService, MYCLOUD_LOGOFF_URL
|
||||
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
||||
from cura.OAuth2.Models import OAuth2Settings, AuthenticationResponse, UserProfile
|
||||
|
||||
@ -226,3 +226,19 @@ def test_wrongServerResponses() -> None:
|
||||
with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()):
|
||||
authorization_service._onAuthStateChanged(MALFORMED_AUTH_RESPONSE)
|
||||
assert authorization_service.getUserProfile() is None
|
||||
|
||||
|
||||
def test__generate_auth_url() -> None:
|
||||
preferences = Preferences()
|
||||
authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences)
|
||||
query_parameters_dict = {
|
||||
"client_id": "",
|
||||
"redirect_uri": OAUTH_SETTINGS.CALLBACK_URL,
|
||||
"scope": OAUTH_SETTINGS.CLIENT_SCOPES,
|
||||
"response_type": "code"
|
||||
}
|
||||
auth_url = authorization_service._generate_auth_url(query_parameters_dict, force_browser_logout = False)
|
||||
assert MYCLOUD_LOGOFF_URL + "?next=" not in auth_url
|
||||
|
||||
auth_url = authorization_service._generate_auth_url(query_parameters_dict, force_browser_logout = True)
|
||||
assert MYCLOUD_LOGOFF_URL + "?next=" in auth_url
|
||||
|
Loading…
x
Reference in New Issue
Block a user