diff --git a/data/interfaces/default/css/tautulli.css b/data/interfaces/default/css/tautulli.css index ec34f782..419070d8 100644 --- a/data/interfaces/default/css/tautulli.css +++ b/data/interfaces/default/css/tautulli.css @@ -3470,6 +3470,9 @@ a.no-highlight:hover { max-width: 1170px; } } +.login-body-container { + margin: 50px 0; +} .login-container { margin-right: auto; margin-left: auto; @@ -3512,6 +3515,30 @@ a.no-highlight:hover { font-weight: 400; cursor: pointer; } +.login-divider { + text-align: center; + border-bottom: 1px solid #555; + line-height: 0.1em; + margin: 50px auto; + max-width: 400px; + text-transform: uppercase; +} +.login-divider span { + background: #1f1f1f; + padding: 0 15px; + color: #999; +} +.login-button-plex { + text-align: center; +} +.login-button-plex button#sign-in-plex { + float: none; +} +.login-alert { + text-align: center; + padding: 8px; + display: none; +} #admin-login-modal .form-group label { font-weight: 400; color: #999; diff --git a/data/interfaces/default/login.html b/data/interfaces/default/login.html index 5dec164f..dc651939 100644 --- a/data/interfaces/default/login.html +++ b/data/interfaces/default/login.html @@ -32,20 +32,22 @@ - -
+ +
@@ -78,25 +93,91 @@ diff --git a/plexpy/plextv.py b/plexpy/plextv.py index 47704236..73604485 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -226,6 +226,45 @@ class PlexTV(object): return server_token + def get_plextv_pin(self, pin='', output_format=''): + if pin: + uri = '/api/v2/pins/' + pin + request = self.request_handler.make_request(uri=uri, + request_type='GET', + output_format=output_format, + no_token=True) + else: + uri = '/api/v2/pins?strong=true' + request = self.request_handler.make_request(uri=uri, + request_type='POST', + output_format=output_format, + no_token=True) + return request + + def get_pin(self, pin=''): + plextv_response = self.get_plextv_pin(pin=pin, + output_format='xml') + + if plextv_response: + try: + xml_head = plextv_response.getElementsByTagName('pin') + if xml_head: + pin = {'id': xml_head[0].getAttribute('id'), + 'code': xml_head[0].getAttribute('code'), + 'token': xml_head[0].getAttribute('authToken') + } + return pin + else: + logger.warn(u"Tautulli PlexTV :: Could not get Plex authentication pin.") + return None + + except Exception as e: + logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_pin: %s." % e) + return None + + else: + return None + def get_plextv_user_data(self): plextv_response = self.get_plex_auth(output_format='dict') @@ -819,3 +858,28 @@ class PlexTV(object): return True else: return False + + def get_plex_account_details(self): + account_data = self.get_plextv_user_details(output_format='xml') + + try: + xml_head = account_data.getElementsByTagName('user') + except Exception as e: + logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_plex_account_details: %s." % e) + return None + + for a in xml_head: + account_details = {"user_id": helpers.get_xml_attr(a, 'id'), + "username": helpers.get_xml_attr(a, 'username'), + "thumb": helpers.get_xml_attr(a, 'thumb'), + "email": helpers.get_xml_attr(a, 'email'), + "is_home_user": helpers.get_xml_attr(a, 'home'), + "is_restricted": helpers.get_xml_attr(a, 'restricted'), + "filter_all": helpers.get_xml_attr(a, 'filterAll'), + "filter_movies": helpers.get_xml_attr(a, 'filterMovies'), + "filter_tv": helpers.get_xml_attr(a, 'filterTelevision'), + "filter_music": helpers.get_xml_attr(a, 'filterMusic'), + "filter_photos": helpers.get_xml_attr(a, 'filterPhotos'), + "user_token": helpers.get_xml_attr(a, 'authToken') + } + return account_details diff --git a/plexpy/webauth.py b/plexpy/webauth.py index 856caa93..1fee7323 100644 --- a/plexpy/webauth.py +++ b/plexpy/webauth.py @@ -19,7 +19,6 @@ # Session tool to be loaded. from datetime import datetime, timedelta -import re from urllib import quote, unquote import cherrypy @@ -37,17 +36,27 @@ JWT_ALGORITHM = 'HS256' JWT_COOKIE_NAME = 'tautulli_token_' -def user_login(username=None, password=None): - if not username or not password: - return None +def user_login(username=None, password=None, token=None): + user_token = None + user_id = None # Try to login to Plex.tv to check if the user has a vaild account - plex_tv = PlexTV(username=username, password=password) - plex_user = plex_tv.get_token() - if plex_user: - user_token = plex_user['auth_token'] - user_id = plex_user['user_id'] + if username and password: + plex_tv = PlexTV(username=username, password=password) + plex_user = plex_tv.get_token() + if plex_user: + user_token = plex_user['auth_token'] + user_id = plex_user['user_id'] + elif token: + plex_tv = PlexTV(token=token) + plex_user = plex_tv.get_plex_account_details() + if plex_user: + user_token = token + user_id = plex_user['user_id'] + else: + return None + if user_token and user_id: # Try to retrieve the user from the database. # Also make sure guest access is enabled for the user and the user is not deleted. user_data = Users() @@ -57,7 +66,7 @@ def user_login(username=None, password=None): return None elif plexpy.CONFIG.HTTP_PLEX_ADMIN and user_details['is_admin']: # Plex admin login - return 'admin' + return user_details, 'admin' elif not user_details['allow_guest'] or user_details['deleted_user']: # Guest access is disabled or the user is deleted. return None @@ -75,49 +84,64 @@ def user_login(username=None, password=None): # Register the new user / update the access tokens. monitor_db = MonitorDatabase() try: - logger.debug(u"Tautulli WebAuth :: Regestering tokens for user '%s' in the database." % username) - result = monitor_db.action('UPDATE users SET user_token = ?, server_token = ? WHERE user_id = ?', - [user_token, server_token, user_id]) + logger.debug(u"Tautulli WebAuth :: Regestering tokens for user '%s' in the database." + % user_details['username']) + result = monitor_db.action('UPDATE users SET server_token = ? WHERE user_id = ?', + [server_token, user_details['user_id']]) if result: # Refresh the users list to make sure we have all the correct permissions. refresh_users() # Successful login - return 'guest' + return user_details, 'guest' else: - logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database." % username) + logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database." + % user_details['username']) return None except Exception as e: - logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database: %s." % (username, e)) + logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database: %s." + % (user_details['username'], e)) return None else: - logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv server token for user '%s'." % username) + logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv server token for user '%s'." + % user_details['username']) return None - else: + elif username: logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv user token for user '%s'." % username) return None - return None + elif token: + logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv user token for Plex OAuth.") + return None -def check_credentials(username, password, admin_login='0'): +def check_credentials(username=None, password=None, token=None, admin_login='0'): """Verifies credentials for username and password. Returns True and the user group on success or False and no user group""" - if plexpy.CONFIG.HTTP_PASSWORD: - if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \ - username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD): - return True, 'tautulli admin' - elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \ - username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD: - return True, 'tautulli admin' + if username and password: + if plexpy.CONFIG.HTTP_PASSWORD: + user_details = {'user_id': None, 'username': username} - if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS): - plex_login = user_login(username, password) - if plex_login is not None: - return True, plex_login + if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \ + username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD): + return True, user_details, 'admin' + elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \ + username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD: + return True, user_details, 'admin' - return False, None + if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS): + plex_login = user_login(username=username, password=password) + if plex_login is not None: + return True, plex_login[0], plex_login[1] + + elif token: + if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS): + plex_login = user_login(token=token) + if plex_login is not None: + return True, plex_login[0], plex_login[1] + + return False, None, None def check_jwt_token(): @@ -279,41 +303,33 @@ class AuthController(object): @cherrypy.expose @cherrypy.tools.json_out() - def signin(self, username=None, password=None, remember_me='0', admin_login='0', *args, **kwargs): + def signin(self, username=None, password=None, token=None, remember_me='0', admin_login='0', *args, **kwargs): if cherrypy.request.method != 'POST': cherrypy.response.status = 405 return {'status': 'error', 'message': 'Sign in using POST.'} - error_message = {'status': 'error', 'message': 'Incorrect username or password.'} + error_message = {'status': 'error', 'message': 'Invalid credentials.'} - valid_login, user_group = check_credentials(username, password, admin_login) + valid_login, user_details, user_group = check_credentials(username=username, + password=password, + token=token, + admin_login=admin_login) if valid_login: - if user_group == 'tautulli admin': - user_group = 'admin' - user_id = None - else: - if re.match(r"[^@]+@[^@]+\.[^@]+", username): - user_details = Users().get_details(email=username) - else: - user_details = Users().get_details(user=username) - - user_id = user_details['user_id'] - time_delta = timedelta(days=30) if remember_me == '1' else timedelta(minutes=60) expiry = datetime.utcnow() + time_delta payload = { - 'user_id': user_id, - 'user': username, + 'user_id': user_details['user_id'], + 'user': user_details['username'], 'user_group': user_group, 'exp': expiry } jwt_token = jwt.encode(payload, plexpy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM) - self.on_login(username=username, - user_id=user_id, + self.on_login(username=user_details['username'], + user_id=user_details['user_id'], user_group=user_group, success=1) @@ -326,14 +342,36 @@ class AuthController(object): cherrypy.response.status = 200 return {'status': 'success', 'token': jwt_token.decode('utf-8'), 'uuid': plexpy.CONFIG.PMS_UUID} - elif admin_login == '1': + elif admin_login == '1' and username: self.on_login(username=username) logger.debug(u"Tautulli WebAuth :: Invalid admin login attempt from '%s'." % username) cherrypy.response.status = 401 return error_message - else: + elif username: self.on_login(username=username) - logger.debug(u"Tautulli WebAuth :: Invalid login attempt from '%s'." % username) + logger.debug(u"Tautulli WebAuth :: Invalid user login attempt from '%s'." % username) cherrypy.response.status = 401 return error_message + + elif token: + logger.debug(u"Tautulli WebAuth :: Invalid Plex OAuth login attempt.") + cherrypy.response.status = 401 + return error_message + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_plex_oauth_url(self, *args, **kwargs): + pin = PlexTV().get_pin() + oauth_url = 'https://app.plex.tv/auth/#!?clientID={}&code={}'.format( + plexpy.CONFIG.PMS_UUID, pin['code']) + return {'pin': pin['id'], 'url': oauth_url} + + @cherrypy.expose + @cherrypy.tools.json_out() + def pin(self, pin=None, *args, **kwargs): + pin = PlexTV().get_pin(pin=pin) + if pin['token']: + return {'result': 'success', 'token': pin['token']} + else: + return {'result': 'polling'}