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:
Nino van Hooff 2020-05-12 17:39:07 +02:00 committed by GitHub
commit f34e05ac03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 18 deletions

View File

@ -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):

View File

@ -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:

View File

@ -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
}

View File

@ -75,7 +75,7 @@ Item
}
else
{
Qt.openUrlExternally("https://mycloud.ultimaker.com/app/manage/printers")
Qt.openUrlExternally("https://mycloud.ultimaker.com/")
}
}

View File

@ -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():

View File

@ -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