mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-08 22:35:03 -06:00
Initial move of the code of CuraPluginOAuth2Module
CURA-5744
This commit is contained in:
parent
dbe0d6d82a
commit
3830fa0fd9
7 changed files with 537 additions and 0 deletions
127
cura/OAuth2/AuthorizationHelpers.py
Normal file
127
cura/OAuth2/AuthorizationHelpers.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
import random
|
||||
from _sha512 import sha512
|
||||
from base64 import b64encode
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
# As this module is specific for Cura plugins, we can rely on these imports.
|
||||
from UM.Logger import Logger
|
||||
|
||||
# Plugin imports need to be relative to work in final builds.
|
||||
from .models import AuthenticationResponse, UserProfile, OAuth2Settings
|
||||
|
||||
|
||||
class AuthorizationHelpers:
|
||||
"""Class containing several helpers to deal with the authorization flow."""
|
||||
|
||||
def __init__(self, settings: "OAuth2Settings"):
|
||||
self._settings = settings
|
||||
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
|
||||
|
||||
@property
|
||||
def settings(self) -> "OAuth2Settings":
|
||||
"""Get the OAuth2 settings object."""
|
||||
return self._settings
|
||||
|
||||
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str)->\
|
||||
Optional["AuthenticationResponse"]:
|
||||
"""
|
||||
Request the access token from the authorization server.
|
||||
:param authorization_code: The authorization code from the 1st step.
|
||||
:param verification_code: The verification code needed for the PKCE extension.
|
||||
:return: An AuthenticationResponse object.
|
||||
"""
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data={
|
||||
"client_id": self._settings.CLIENT_ID,
|
||||
"redirect_uri": self._settings.CALLBACK_URL,
|
||||
"grant_type": "authorization_code",
|
||||
"code": authorization_code,
|
||||
"code_verifier": verification_code,
|
||||
"scope": self._settings.CLIENT_SCOPES
|
||||
}))
|
||||
|
||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> Optional["AuthenticationResponse"]:
|
||||
"""
|
||||
Request the access token from the authorization server using a refresh token.
|
||||
:param refresh_token:
|
||||
:return: An AuthenticationResponse object.
|
||||
"""
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data={
|
||||
"client_id": self._settings.CLIENT_ID,
|
||||
"redirect_uri": self._settings.CALLBACK_URL,
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refresh_token,
|
||||
"scope": self._settings.CLIENT_SCOPES
|
||||
}))
|
||||
|
||||
@staticmethod
|
||||
def parseTokenResponse(token_response: "requests.request") -> Optional["AuthenticationResponse"]:
|
||||
"""
|
||||
Parse the token response from the authorization server into an AuthenticationResponse object.
|
||||
:param token_response: The JSON string data response from the authorization server.
|
||||
:return: An AuthenticationResponse object.
|
||||
"""
|
||||
token_data = None
|
||||
|
||||
try:
|
||||
token_data = json.loads(token_response.text)
|
||||
except ValueError:
|
||||
Logger.log("w", "Could not parse token response data: %s", token_response.text)
|
||||
|
||||
if not token_data:
|
||||
return AuthenticationResponse(success=False, err_message="Could not read response.")
|
||||
|
||||
if token_response.status_code not in (200, 201):
|
||||
return AuthenticationResponse(success=False, err_message=token_data["error_description"])
|
||||
|
||||
return AuthenticationResponse(success=True,
|
||||
token_type=token_data["token_type"],
|
||||
access_token=token_data["access_token"],
|
||||
refresh_token=token_data["refresh_token"],
|
||||
expires_in=token_data["expires_in"],
|
||||
scope=token_data["scope"])
|
||||
|
||||
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
|
||||
"""
|
||||
Calls the authentication API endpoint to get the token data.
|
||||
:param access_token: The encoded JWT token.
|
||||
:return: Dict containing some profile data.
|
||||
"""
|
||||
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
if token_request.status_code not in (200, 201):
|
||||
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
||||
return None
|
||||
user_data = token_request.json().get("data")
|
||||
if not user_data or not isinstance(user_data, dict):
|
||||
Logger.log("w", "Could not parse user data from token: %s", user_data)
|
||||
return None
|
||||
return UserProfile(
|
||||
user_id = user_data["user_id"],
|
||||
username = user_data["username"],
|
||||
profile_image_url = user_data.get("profile_image_url", "")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def generateVerificationCode(code_length: int = 16) -> str:
|
||||
"""
|
||||
Generate a 16-character verification code.
|
||||
:param code_length:
|
||||
:return:
|
||||
"""
|
||||
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
|
||||
|
||||
@staticmethod
|
||||
def generateVerificationCodeChallenge(verification_code: str) -> str:
|
||||
"""
|
||||
Generates a base64 encoded sha512 encrypted version of a given string.
|
||||
:param verification_code:
|
||||
:return: The encrypted code in base64 format.
|
||||
"""
|
||||
encoded = sha512(verification_code.encode()).digest()
|
||||
return b64encode(encoded, altchars = b"_-").decode()
|
105
cura/OAuth2/AuthorizationRequestHandler.py
Normal file
105
cura/OAuth2/AuthorizationRequestHandler.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, Callable
|
||||
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
# Plugin imports need to be relative to work in final builds.
|
||||
from .AuthorizationHelpers import AuthorizationHelpers
|
||||
from .models import AuthenticationResponse, ResponseData, HTTP_STATUS, ResponseStatus
|
||||
|
||||
|
||||
class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||
"""
|
||||
This handler handles all HTTP requests on the local web server.
|
||||
It also requests the access token for the 2nd stage of the OAuth flow.
|
||||
"""
|
||||
|
||||
def __init__(self, request, client_address, server):
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
# These values will be injected by the HTTPServer that this handler belongs to.
|
||||
self.authorization_helpers = None # type: AuthorizationHelpers
|
||||
self.authorization_callback = None # type: Callable[[AuthenticationResponse], None]
|
||||
self.verification_code = None # type: str
|
||||
|
||||
def do_GET(self):
|
||||
"""Entry point for GET requests"""
|
||||
|
||||
# Extract values from the query string.
|
||||
parsed_url = urlparse(self.path)
|
||||
query = parse_qs(parsed_url.query)
|
||||
|
||||
# Handle the possible requests
|
||||
if parsed_url.path == "/callback":
|
||||
server_response, token_response = self._handleCallback(query)
|
||||
else:
|
||||
server_response = self._handleNotFound()
|
||||
token_response = None
|
||||
|
||||
# Send the data to the browser.
|
||||
self._sendHeaders(server_response.status, server_response.content_type, server_response.redirect_uri)
|
||||
|
||||
if server_response.data_stream:
|
||||
# If there is data in the response, we send it.
|
||||
self._sendData(server_response.data_stream)
|
||||
|
||||
if token_response:
|
||||
# Trigger the callback if we got a response.
|
||||
# This will cause the server to shut down, so we do it at the very end of the request handling.
|
||||
self.authorization_callback(token_response)
|
||||
|
||||
def _handleCallback(self, query: dict) -> ("ResponseData", Optional["AuthenticationResponse"]):
|
||||
"""
|
||||
Handler for the callback URL redirect.
|
||||
:param query: Dict containing the HTTP query parameters.
|
||||
:return: HTTP ResponseData containing a success page to show to the user.
|
||||
"""
|
||||
if self._queryGet(query, "code"):
|
||||
# If the code was returned we get the access token.
|
||||
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode(
|
||||
self._queryGet(query, "code"), self.verification_code)
|
||||
|
||||
elif self._queryGet(query, "error_code") == "user_denied":
|
||||
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).
|
||||
token_response = AuthenticationResponse(
|
||||
success=False,
|
||||
err_message="Please give the required permissions when authorizing this application."
|
||||
)
|
||||
|
||||
else:
|
||||
# We don't know what went wrong here, so instruct the user to check the logs.
|
||||
token_response = AuthenticationResponse(
|
||||
success=False,
|
||||
error_message="Something unexpected happened when trying to log in, please try again."
|
||||
)
|
||||
|
||||
return ResponseData(
|
||||
status=HTTP_STATUS["REDIRECT"],
|
||||
data_stream=b"Redirecting...",
|
||||
redirect_uri=self.authorization_helpers.settings.AUTH_SUCCESS_REDIRECT if token_response.success else
|
||||
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
|
||||
), token_response
|
||||
|
||||
@staticmethod
|
||||
def _handleNotFound() -> "ResponseData":
|
||||
"""Handle all other non-existing server calls."""
|
||||
return ResponseData(status=HTTP_STATUS["NOT_FOUND"], content_type="text/html", data_stream=b"Not found.")
|
||||
|
||||
def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None:
|
||||
"""Send out the headers"""
|
||||
self.send_response(status.code, status.message)
|
||||
self.send_header("Content-type", content_type)
|
||||
if redirect_uri:
|
||||
self.send_header("Location", redirect_uri)
|
||||
self.end_headers()
|
||||
|
||||
def _sendData(self, data: bytes) -> None:
|
||||
"""Send out the data"""
|
||||
self.wfile.write(data)
|
||||
|
||||
@staticmethod
|
||||
def _queryGet(query_data: dict, key: str, default=None) -> Optional[str]:
|
||||
"""Helper for getting values from a pre-parsed query string"""
|
||||
return query_data.get(key, [default])[0]
|
25
cura/OAuth2/AuthorizationRequestServer.py
Normal file
25
cura/OAuth2/AuthorizationRequestServer.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from http.server import HTTPServer
|
||||
|
||||
from .AuthorizationHelpers import AuthorizationHelpers
|
||||
|
||||
|
||||
class AuthorizationRequestServer(HTTPServer):
|
||||
"""
|
||||
The authorization request callback handler server.
|
||||
This subclass is needed to be able to pass some data to the request handler.
|
||||
This cannot be done on the request handler directly as the HTTPServer creates an instance of the handler after init.
|
||||
"""
|
||||
|
||||
def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None:
|
||||
"""Set the authorization helpers instance on the request handler."""
|
||||
self.RequestHandlerClass.authorization_helpers = authorization_helpers
|
||||
|
||||
def setAuthorizationCallback(self, authorization_callback) -> None:
|
||||
"""Set the authorization callback on the request handler."""
|
||||
self.RequestHandlerClass.authorization_callback = authorization_callback
|
||||
|
||||
def setVerificationCode(self, verification_code: str) -> None:
|
||||
"""Set the verification code on the request handler."""
|
||||
self.RequestHandlerClass.verification_code = verification_code
|
151
cura/OAuth2/AuthorizationService.py
Normal file
151
cura/OAuth2/AuthorizationService.py
Normal file
|
@ -0,0 +1,151 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
import webbrowser
|
||||
from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
# As this module is specific for Cura plugins, we can rely on these imports.
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import Signal
|
||||
|
||||
# Plugin imports need to be relative to work in final builds.
|
||||
from .LocalAuthorizationServer import LocalAuthorizationServer
|
||||
from .AuthorizationHelpers import AuthorizationHelpers
|
||||
from .models import OAuth2Settings, AuthenticationResponse, UserProfile
|
||||
|
||||
|
||||
class AuthorizationService:
|
||||
"""
|
||||
The authorization service is responsible for handling the login flow,
|
||||
storing user credentials and providing account information.
|
||||
"""
|
||||
|
||||
# Emit signal when authentication is completed.
|
||||
onAuthStateChanged = Signal()
|
||||
|
||||
# Emit signal when authentication failed.
|
||||
onAuthenticationError = Signal()
|
||||
|
||||
def __init__(self, preferences, settings: "OAuth2Settings"):
|
||||
self._settings = settings
|
||||
self._auth_helpers = AuthorizationHelpers(settings)
|
||||
self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL)
|
||||
self._auth_data = None # type: Optional[AuthenticationResponse]
|
||||
self._user_profile = None # type: Optional[UserProfile]
|
||||
self._cura_preferences = preferences
|
||||
self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True)
|
||||
self._loadAuthData()
|
||||
|
||||
def getUserProfile(self) -> Optional["UserProfile"]:
|
||||
"""
|
||||
Get the user data that is stored in the JWT token.
|
||||
:return: Dict containing some user data.
|
||||
"""
|
||||
if not self._user_profile:
|
||||
# If no user profile was stored locally, we try to get it from JWT.
|
||||
self._user_profile = self._parseJWT()
|
||||
if not self._user_profile:
|
||||
# If there is still no user profile from the JWT, we have to log in again.
|
||||
return None
|
||||
return self._user_profile
|
||||
|
||||
def _parseJWT(self) -> Optional["UserProfile"]:
|
||||
"""
|
||||
Tries to parse the JWT if all the needed data exists.
|
||||
:return: UserProfile if found, otherwise None.
|
||||
"""
|
||||
if not self._auth_data:
|
||||
# If no auth data exists, we should always log in again.
|
||||
return None
|
||||
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
if user_data:
|
||||
# If the profile was found, we return it immediately.
|
||||
return user_data
|
||||
# The JWT was expired or invalid and we should request a new one.
|
||||
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
||||
if not self._auth_data:
|
||||
# The token could not be refreshed using the refresh token. We should login again.
|
||||
return None
|
||||
return self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
|
||||
def getAccessToken(self) -> Optional[str]:
|
||||
"""
|
||||
Get the access token response data.
|
||||
:return: Dict containing token data.
|
||||
"""
|
||||
if not self.getUserProfile():
|
||||
# We check if we can get the user profile.
|
||||
# If we can't get it, that means the access token (JWT) was invalid or expired.
|
||||
return None
|
||||
return self._auth_data.access_token
|
||||
|
||||
def refreshAccessToken(self) -> None:
|
||||
"""
|
||||
Refresh the access token when it expired.
|
||||
"""
|
||||
self._storeAuthData(self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token))
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
|
||||
def deleteAuthData(self):
|
||||
"""Delete authentication data from preferences and locally."""
|
||||
self._storeAuthData()
|
||||
self.onAuthStateChanged.emit(logged_in=False)
|
||||
|
||||
def startAuthorizationFlow(self) -> None:
|
||||
"""Start a new OAuth2 authorization flow."""
|
||||
|
||||
Logger.log("d", "Starting new OAuth2 flow...")
|
||||
|
||||
# Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
|
||||
# This is needed because the CuraDrivePlugin is a untrusted (open source) client.
|
||||
# More details can be found at https://tools.ietf.org/html/rfc7636.
|
||||
verification_code = self._auth_helpers.generateVerificationCode()
|
||||
challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code)
|
||||
|
||||
# Create the query string needed for the OAuth2 flow.
|
||||
query_string = urlencode({
|
||||
"client_id": self._settings.CLIENT_ID,
|
||||
"redirect_uri": self._settings.CALLBACK_URL,
|
||||
"scope": self._settings.CLIENT_SCOPES,
|
||||
"response_type": "code",
|
||||
"state": "CuraDriveIsAwesome",
|
||||
"code_challenge": challenge_code,
|
||||
"code_challenge_method": "S512"
|
||||
})
|
||||
|
||||
# Open the authorization page in a new browser window.
|
||||
webbrowser.open_new("{}?{}".format(self._auth_url, query_string))
|
||||
|
||||
# Start a local web server to receive the callback URL on.
|
||||
self._server.start(verification_code)
|
||||
|
||||
def _onAuthStateChanged(self, auth_response: "AuthenticationResponse") -> None:
|
||||
"""Callback method for an authentication flow."""
|
||||
if auth_response.success:
|
||||
self._storeAuthData(auth_response)
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
else:
|
||||
self.onAuthenticationError.emit(logged_in=False, error_message=auth_response.err_message)
|
||||
self._server.stop() # Stop the web server at all times.
|
||||
|
||||
def _loadAuthData(self) -> None:
|
||||
"""Load authentication data from preferences if available."""
|
||||
self._cura_preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
|
||||
try:
|
||||
preferences_data = json.loads(self._cura_preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
|
||||
if preferences_data:
|
||||
self._auth_data = AuthenticationResponse(**preferences_data)
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
except ValueError as err:
|
||||
Logger.log("w", "Could not load auth data from preferences: %s", err)
|
||||
|
||||
def _storeAuthData(self, auth_data: Optional["AuthenticationResponse"] = None) -> None:
|
||||
"""Store authentication data in preferences and locally."""
|
||||
self._auth_data = auth_data
|
||||
if auth_data:
|
||||
self._user_profile = self.getUserProfile()
|
||||
self._cura_preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(vars(auth_data)))
|
||||
else:
|
||||
self._user_profile = None
|
||||
self._cura_preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
67
cura/OAuth2/LocalAuthorizationServer.py
Normal file
67
cura/OAuth2/LocalAuthorizationServer.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import threading
|
||||
from http.server import HTTPServer
|
||||
from typing import Optional, Callable
|
||||
|
||||
# As this module is specific for Cura plugins, we can rely on these imports.
|
||||
from UM.Logger import Logger
|
||||
|
||||
# Plugin imports need to be relative to work in final builds.
|
||||
from .AuthorizationHelpers import AuthorizationHelpers
|
||||
from .AuthorizationRequestServer import AuthorizationRequestServer
|
||||
from .AuthorizationRequestHandler import AuthorizationRequestHandler
|
||||
from .models import AuthenticationResponse
|
||||
|
||||
|
||||
class LocalAuthorizationServer:
|
||||
def __init__(self, auth_helpers: "AuthorizationHelpers",
|
||||
auth_state_changed_callback: "Callable[[AuthenticationResponse], any]",
|
||||
daemon: bool):
|
||||
"""
|
||||
:param auth_helpers: An instance of the authorization helpers class.
|
||||
:param auth_state_changed_callback: A callback function to be called when the authorization state changes.
|
||||
:param daemon: Whether the server thread should be run in daemon mode. Note: Daemon threads are abruptly stopped
|
||||
at shutdown. Their resources (e.g. open files) may never be released.
|
||||
"""
|
||||
self._web_server = None # type: Optional[HTTPServer]
|
||||
self._web_server_thread = None # type: Optional[threading.Thread]
|
||||
self._web_server_port = auth_helpers.settings.CALLBACK_PORT
|
||||
self._auth_helpers = auth_helpers
|
||||
self._auth_state_changed_callback = auth_state_changed_callback
|
||||
self._daemon = daemon
|
||||
|
||||
def start(self, verification_code: "str") -> None:
|
||||
"""
|
||||
Starts the local web server to handle the authorization callback.
|
||||
:param verification_code: The verification code part of the OAuth2 client identification.
|
||||
"""
|
||||
if self._web_server:
|
||||
# If the server is already running (because of a previously aborted auth flow), we don't have to start it.
|
||||
# We still inject the new verification code though.
|
||||
self._web_server.setVerificationCode(verification_code)
|
||||
return
|
||||
|
||||
Logger.log("d", "Starting local web server to handle authorization callback on port %s",
|
||||
self._web_server_port)
|
||||
|
||||
# Create the server and inject the callback and code.
|
||||
self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port),
|
||||
AuthorizationRequestHandler)
|
||||
self._web_server.setAuthorizationHelpers(self._auth_helpers)
|
||||
self._web_server.setAuthorizationCallback(self._auth_state_changed_callback)
|
||||
self._web_server.setVerificationCode(verification_code)
|
||||
|
||||
# Start the server on a new thread.
|
||||
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
|
||||
self._web_server_thread.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
""" Stops the web server if it was running. Also deletes the objects. """
|
||||
|
||||
Logger.log("d", "Stopping local web server...")
|
||||
|
||||
if self._web_server:
|
||||
self._web_server.server_close()
|
||||
self._web_server = None
|
||||
self._web_server_thread = None
|
60
cura/OAuth2/Models.py
Normal file
60
cura/OAuth2/Models.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class BaseModel:
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
||||
# OAuth OAuth2Settings data template.
|
||||
class OAuth2Settings(BaseModel):
|
||||
CALLBACK_PORT = None # type: Optional[str]
|
||||
OAUTH_SERVER_URL = None # type: Optional[str]
|
||||
CLIENT_ID = None # type: Optional[str]
|
||||
CLIENT_SCOPES = None # type: Optional[str]
|
||||
CALLBACK_URL = None # type: Optional[str]
|
||||
AUTH_DATA_PREFERENCE_KEY = None # type: Optional[str]
|
||||
AUTH_SUCCESS_REDIRECT = "https://ultimaker.com" # type: str
|
||||
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
|
||||
|
||||
|
||||
# User profile data template.
|
||||
class UserProfile(BaseModel):
|
||||
user_id = None # type: Optional[str]
|
||||
username = None # type: Optional[str]
|
||||
profile_image_url = None # type: Optional[str]
|
||||
|
||||
|
||||
# Authentication data template.
|
||||
class AuthenticationResponse(BaseModel):
|
||||
"""Data comes from the token response with success flag and error message added."""
|
||||
success = True # type: bool
|
||||
token_type = None # type: Optional[str]
|
||||
access_token = None # type: Optional[str]
|
||||
refresh_token = None # type: Optional[str]
|
||||
expires_in = None # type: Optional[str]
|
||||
scope = None # type: Optional[str]
|
||||
err_message = None # type: Optional[str]
|
||||
|
||||
|
||||
# Response status template.
|
||||
class ResponseStatus(BaseModel):
|
||||
code = 200 # type: int
|
||||
message = "" # type str
|
||||
|
||||
|
||||
# Response data template.
|
||||
class ResponseData(BaseModel):
|
||||
status = None # type: Optional[ResponseStatus]
|
||||
data_stream = None # type: Optional[bytes]
|
||||
redirect_uri = None # type: Optional[str]
|
||||
content_type = "text/html" # type: str
|
||||
|
||||
|
||||
# Possible HTTP responses.
|
||||
HTTP_STATUS = {
|
||||
"OK": ResponseStatus(code=200, message="OK"),
|
||||
"NOT_FOUND": ResponseStatus(code=404, message="NOT FOUND"),
|
||||
"REDIRECT": ResponseStatus(code=302, message="REDIRECT")
|
||||
}
|
2
cura/OAuth2/__init__.py
Normal file
2
cura/OAuth2/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
Loading…
Add table
Add a link
Reference in a new issue