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()
|
self._update_timer.stop()
|
||||||
|
|
||||||
@pyqtSlot()
|
@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 self._logged_in:
|
||||||
# Nothing to do, user already logged in.
|
if force_logout_before_login:
|
||||||
return
|
self.logout()
|
||||||
self._authorization_service.startAuthorizationFlow()
|
else:
|
||||||
|
# Nothing to do, user already logged in.
|
||||||
|
return
|
||||||
|
self._authorization_service.startAuthorizationFlow(force_logout_before_login)
|
||||||
|
|
||||||
@pyqtProperty(str, notify=loginStateChanged)
|
@pyqtProperty(str, notify=loginStateChanged)
|
||||||
def userName(self):
|
def userName(self):
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING, Dict
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode, quote_plus
|
||||||
|
|
||||||
import requests.exceptions
|
import requests.exceptions
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
@ -24,6 +24,7 @@ if TYPE_CHECKING:
|
|||||||
from cura.OAuth2.Models import UserProfile, OAuth2Settings
|
from cura.OAuth2.Models import UserProfile, OAuth2Settings
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
|
|
||||||
|
MYCLOUD_LOGOFF_URL = "https://mycloud.ultimaker.com/logoff"
|
||||||
|
|
||||||
## The authorization service is responsible for handling the login flow,
|
## The authorization service is responsible for handling the login flow,
|
||||||
# storing user credentials and providing account information.
|
# storing user credentials and providing account information.
|
||||||
@ -142,7 +143,7 @@ class AuthorizationService:
|
|||||||
self.onAuthStateChanged.emit(logged_in = False)
|
self.onAuthStateChanged.emit(logged_in = False)
|
||||||
|
|
||||||
## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
|
## 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...")
|
Logger.log("d", "Starting new OAuth2 flow...")
|
||||||
|
|
||||||
# Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
|
# Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
|
||||||
@ -153,8 +154,8 @@ class AuthorizationService:
|
|||||||
|
|
||||||
state = AuthorizationHelpers.generateVerificationCode()
|
state = AuthorizationHelpers.generateVerificationCode()
|
||||||
|
|
||||||
# Create the query string needed for the OAuth2 flow.
|
# Create the query dict needed for the OAuth2 flow.
|
||||||
query_string = urlencode({
|
query_parameters_dict = {
|
||||||
"client_id": self._settings.CLIENT_ID,
|
"client_id": self._settings.CLIENT_ID,
|
||||||
"redirect_uri": self._settings.CALLBACK_URL,
|
"redirect_uri": self._settings.CALLBACK_URL,
|
||||||
"scope": self._settings.CLIENT_SCOPES,
|
"scope": self._settings.CLIENT_SCOPES,
|
||||||
@ -162,7 +163,7 @@ class AuthorizationService:
|
|||||||
"state": state, # Forever in our Hearts, RIP "(.Y.)" (2018-2020)
|
"state": state, # Forever in our Hearts, RIP "(.Y.)" (2018-2020)
|
||||||
"code_challenge": challenge_code,
|
"code_challenge": challenge_code,
|
||||||
"code_challenge_method": "S512"
|
"code_challenge_method": "S512"
|
||||||
})
|
}
|
||||||
|
|
||||||
# Start a local web server to receive the callback URL on.
|
# Start a local web server to receive the callback URL on.
|
||||||
try:
|
try:
|
||||||
@ -173,9 +174,28 @@ class AuthorizationService:
|
|||||||
title=i18n_catalog.i18nc("@info:title", "Warning")).show()
|
title=i18n_catalog.i18nc("@info:title", "Warning")).show()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
auth_url = self._generate_auth_url(query_parameters_dict, force_browser_logout)
|
||||||
# Open the authorization page in a new browser window.
|
# 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.
|
## Callback method for the authentication flow.
|
||||||
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
|
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
|
// Component that contains a busy indicator and a message, while it waits for Cura to discover a cloud printer
|
||||||
Rectangle
|
Item
|
||||||
{
|
{
|
||||||
id: waitingContent
|
id: waitingContent
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: waitingIndicator.height + waitingLabel.height
|
height: childrenRect.height
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
BusyIndicator
|
BusyIndicator
|
||||||
@ -74,6 +74,37 @@ Item
|
|||||||
font: UM.Theme.getFont("large")
|
font: UM.Theme.getFont("large")
|
||||||
renderType: Text.NativeRendering
|
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
|
visible: discoveredCloudPrintersModel.count == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ Item
|
|||||||
}
|
}
|
||||||
else
|
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())
|
account = Account(MagicMock())
|
||||||
mocked_auth_service = MagicMock()
|
mocked_auth_service = MagicMock()
|
||||||
account._authorization_service = mocked_auth_service
|
account._authorization_service = mocked_auth_service
|
||||||
|
account.logout = MagicMock()
|
||||||
|
|
||||||
account.login()
|
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)
|
account._onLoginStateChanged(True)
|
||||||
|
|
||||||
# Attempting to log in again shouldn't change anything.
|
# Attempting to log in again shouldn't change anything.
|
||||||
account.login()
|
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():
|
def test_initialize():
|
||||||
|
@ -7,7 +7,7 @@ from PyQt5.QtGui import QDesktopServices
|
|||||||
|
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
|
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.LocalAuthorizationServer import LocalAuthorizationServer
|
||||||
from cura.OAuth2.Models import OAuth2Settings, AuthenticationResponse, UserProfile
|
from cura.OAuth2.Models import OAuth2Settings, AuthenticationResponse, UserProfile
|
||||||
|
|
||||||
@ -226,3 +226,19 @@ def test_wrongServerResponses() -> None:
|
|||||||
with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()):
|
with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()):
|
||||||
authorization_service._onAuthStateChanged(MALFORMED_AUTH_RESPONSE)
|
authorization_service._onAuthStateChanged(MALFORMED_AUTH_RESPONSE)
|
||||||
assert authorization_service.getUserProfile() is None
|
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