Skip to content
This repository was archived by the owner on Oct 13, 2024. It is now read-only.

feat: add Plex authentification #229

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ cython_debug/
Contents/Info.plist

# Remove plexhints files
plexhints-temp
plexhints/
plexhints-temp/
*cache.sqlite

# Remove python modules
Expand Down
1 change: 1 addition & 0 deletions Contents/Code/default_prefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
enum_webapp_locale='en',
str_webapp_http_host='0.0.0.0',
int_webapp_http_port='9494',
bool_webapp_require_login='False',
bool_webapp_log_werkzeug_messages='False',
bool_migrate_locked_themes='False',
bool_migrate_locked_collection_fields='False',
Expand Down
53 changes: 53 additions & 0 deletions Contents/Code/plex_api_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from plexapi.base import PlexPartialObject
from plexapi.exceptions import BadRequest
import plexapi.server
from plexapi.myplex import MyPlexAccount # must be imported after plexapi.server otherwise it breaks
from plexapi.utils import reverseSearchType

# local imports
Expand Down Expand Up @@ -597,6 +598,58 @@ def get_plex_item(rating_key):
return item


def get_user_info(token):
# type: (str) -> Optional[MyPlexAccount]
"""
Get the Plex user info.

Parameters
----------
token : str
The Plex token.

Returns
-------
Optional[MyPlexAccount]
The Plex user info.

Examples
--------
>>> get_user_info(token='...')
...
"""
try:
return MyPlexAccount(token=token)
except Exception as e:
Log.Error('Error getting user info: {}'.format(e))
return None


def is_server_owner(user):
# type: (MyPlexAccount) -> bool
"""
Check if the user is the owner of the Plex server.

Parameters
----------
user : MyPlexAccount
The Plex user info.

Returns
-------
py:class:`bool`
True if the user is the owner of the Plex server, False otherwise.

Examples
--------
>>> is_server_owner(user=...)
...
"""
plex = setup_plexapi()

return plex.account().username in {user.email, user.username}


def process_queue():
# type: () -> None
"""
Expand Down
165 changes: 163 additions & 2 deletions Contents/Code/webapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import logging
import os
from threading import Lock, Thread
from typing import Optional
import uuid

# plex debugging
try:
Expand All @@ -23,7 +25,7 @@

# lib imports
import flask
from flask import Flask, Response, render_template, send_from_directory
from flask import Flask, Response, render_template as flask_render_template, send_from_directory, session
from flask_babel import Babel
import polib
from six.moves.urllib.parse import quote_plus
Expand All @@ -32,10 +34,39 @@
# local imports
from constants import contributes_to, issue_urls, plugin_directory, plugin_identifier, themerr_data_directory
import general_helper
from plex_api_helper import get_database_info, setup_plexapi
from plex_api_helper import get_database_info, get_user_info, is_server_owner, setup_plexapi
import themerr_db_helper
import tmdb_helper


def render_template(*args, **kwargs):
# type: (str, str) -> flask.render_template
"""
Render a template.

This function is a wrapper for flask.render_template that adds various useful globals to the template context.

Parameters
----------
*args : str
The template name.
**kwargs : str
The template context.

Returns
-------
flask.render_template
The rendered template.

Examples
--------
>>> render_template('home.html', title='Home', items=items)
"""
kwargs['Prefs'] = Prefs
kwargs['is_logged_in'] = is_logged_in()
return flask_render_template(*args, **kwargs)

Check warning on line 67 in Contents/Code/webapp.py

View check run for this annotation

Codecov / codecov/patch

Contents/Code/webapp.py#L65-L67

Added lines #L65 - L67 were not covered by tests


# setup flask app
app = Flask(
import_name=__name__,
Expand Down Expand Up @@ -107,6 +138,32 @@
database_cache_lock = Lock()


def create_secret():
"""
Create secret file with random uuid.

Examples
--------
>>> create_secret()
"""
secret_file = os.path.join(themerr_data_directory, 'secret.json')
try:
with open(secret_file, 'r') as f:
app.secret_key = json.load(f)['secret']
except Exception:
# create random secret
Log.Info('Creating random secret')
app.secret_key = uuid.uuid4().hex
try:
with open(secret_file, 'w') as f:
json.dump({'secret': app.secret_key}, f)
except Exception as e:
Log.Error('Error saving secret: {}'.format(e))

Check warning on line 161 in Contents/Code/webapp.py

View check run for this annotation

Codecov / codecov/patch

Contents/Code/webapp.py#L160-L161

Added lines #L160 - L161 were not covered by tests


create_secret()


responses = {
500: Response(response='Internal Server Error', status=500, mimetype='text/plain')
}
Expand Down Expand Up @@ -495,3 +552,107 @@
return Response(response=json.dumps(data),
status=200,
mimetype='application/json')


def is_logged_in():
# type: () -> bool
"""
Check if the user is logged in.

Returns
-------
py:class:`bool`
True if the user is logged in, otherwise False.

Examples
--------
>>> is_logged_in()
"""
if not Prefs['bool_webapp_require_login']:
return True

Check warning on line 572 in Contents/Code/webapp.py

View check run for this annotation

Codecov / codecov/patch

Contents/Code/webapp.py#L572

Added line #L572 was not covered by tests

if "token" not in session:
return False

token = session["token"]
user = get_user_info(token=token)
logged_in = user and is_server_owner(user=user)
return logged_in

Check warning on line 580 in Contents/Code/webapp.py

View check run for this annotation

Codecov / codecov/patch

Contents/Code/webapp.py#L577-L580

Added lines #L577 - L580 were not covered by tests


@app.route("/logout", methods=["GET"])
def logout():
# type: () -> Response
"""
Logout the user.

Returns
-------
Response
The logout response.

Examples
--------
>>> logout()
"""
session.pop("token", None)
return flask.redirect(flask.url_for('home'))

Check warning on line 599 in Contents/Code/webapp.py

View check run for this annotation

Codecov / codecov/patch

Contents/Code/webapp.py#L598-L599

Added lines #L598 - L599 were not covered by tests


@app.before_request
def check_login_status():
# type: () -> Optional[flask.redirect]
"""
Check if the user is logged in.

If the user is not logged in, redirect to the login page.

Examples
--------
>>> check_login_status()
"""
if not Prefs['bool_webapp_require_login']:
return

Check warning on line 615 in Contents/Code/webapp.py

View check run for this annotation

Codecov / codecov/patch

Contents/Code/webapp.py#L615

Added line #L615 was not covered by tests

if flask.request.path.startswith('/web'):
return

Check warning on line 618 in Contents/Code/webapp.py

View check run for this annotation

Codecov / codecov/patch

Contents/Code/webapp.py#L618

Added line #L618 was not covered by tests

if flask.request.path in {'/login', '/logout'}:
return

Check warning on line 621 in Contents/Code/webapp.py

View check run for this annotation

Codecov / codecov/patch

Contents/Code/webapp.py#L621

Added line #L621 was not covered by tests

if not is_logged_in():
# not logged in, redirect to login page
return flask.redirect(flask.url_for('login', redirect_uri=flask.request.path))


@app.route("/login", methods=["GET"])
def login(redirect_uri="/"):
# type: (str) -> render_template
"""
Serve the login page.

Returns
-------
render_template
The rendered page.

Notes
-----
The following routes trigger this function.

- `/login`

Examples
--------
>>> login()
"""
return render_template('login.html', title='Login', redirect_uri=redirect_uri)

Check warning on line 649 in Contents/Code/webapp.py

View check run for this annotation

Codecov / codecov/patch

Contents/Code/webapp.py#L649

Added line #L649 was not covered by tests


@app.route("/login", methods=["POST"])
def login_post():
session.permanent = True
session["token"] = flask.request.form["token"]
if not is_logged_in():
return flask.Response(status=401)
return flask.Response(status=200)

Check warning on line 658 in Contents/Code/webapp.py

View check run for this annotation

Codecov / codecov/patch

Contents/Code/webapp.py#L654-L658

Added lines #L654 - L658 were not covered by tests
8 changes: 8 additions & 0 deletions Contents/DefaultPrefs.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@
"default": "9494",
"secure": "false"
},
{
"id": "bool_webapp_require_login",
"type": "bool",
"label": "Require login to access Web UI",
"default": "True",
"secure": "false",
"hidden": "true"
},
{
"id": "bool_webapp_log_werkzeug_messages",
"type": "bool",
Expand Down
Loading
Loading