mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2025-12-08 11:39:33 +00:00
Formatted with black
This commit is contained in:
@@ -13,192 +13,223 @@ import json
|
||||
from pathlib import Path
|
||||
from flask import Flask, g
|
||||
from jellyfin_accounts.data_store import JSONStorage
|
||||
|
||||
parser = argparse.ArgumentParser(description="jellyfin-accounts")
|
||||
|
||||
parser.add_argument("-c", "--config",
|
||||
help="specifies path to configuration file.")
|
||||
parser.add_argument("-d", "--data",
|
||||
help=("specifies directory to store data in. " +
|
||||
"defaults to ~/.jf-accounts."))
|
||||
parser.add_argument("--host",
|
||||
help="address to host web ui on.")
|
||||
parser.add_argument("-p", "--port",
|
||||
help="port to host web ui on.")
|
||||
parser.add_argument("-g", "--get_defaults",
|
||||
help=("tool to grab a JF users " +
|
||||
"policy (access, perms, etc.) and " +
|
||||
"homescreen layout and " +
|
||||
"output it as json to be used as a user template."),
|
||||
action='store_true')
|
||||
parser.add_argument("-c", "--config", help="specifies path to configuration file.")
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--data",
|
||||
help=("specifies directory to store data in. " + "defaults to ~/.jf-accounts."),
|
||||
)
|
||||
parser.add_argument("--host", help="address to host web ui on.")
|
||||
parser.add_argument("-p", "--port", help="port to host web ui on.")
|
||||
parser.add_argument(
|
||||
"-g",
|
||||
"--get_defaults",
|
||||
help=(
|
||||
"tool to grab a JF users "
|
||||
+ "policy (access, perms, etc.) and "
|
||||
+ "homescreen layout and "
|
||||
+ "output it as json to be used as a user template."
|
||||
),
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
args, leftovers = parser.parse_known_args()
|
||||
|
||||
if args.data is not None:
|
||||
data_dir = Path(args.data)
|
||||
else:
|
||||
data_dir = Path.home() / '.jf-accounts'
|
||||
data_dir = Path.home() / ".jf-accounts"
|
||||
|
||||
local_dir = (Path(__file__).parent / 'data').resolve()
|
||||
local_dir = (Path(__file__).parent / "data").resolve()
|
||||
|
||||
first_run = False
|
||||
if data_dir.exists() is False or (data_dir / 'config.ini').exists() is False:
|
||||
if data_dir.exists() is False or (data_dir / "config.ini").exists() is False:
|
||||
if not data_dir.exists():
|
||||
Path.mkdir(data_dir)
|
||||
print(f'Config dir not found, so created at {str(data_dir)}')
|
||||
print(f"Config dir not found, so created at {str(data_dir)}")
|
||||
if args.config is None:
|
||||
config_path = data_dir / 'config.ini'
|
||||
shutil.copy(str(local_dir / 'config-default.ini'),
|
||||
str(config_path))
|
||||
config_path = data_dir / "config.ini"
|
||||
shutil.copy(str(local_dir / "config-default.ini"), str(config_path))
|
||||
print("Setup through the web UI, or quit and edit the configuration manually.")
|
||||
first_run = True
|
||||
else:
|
||||
config_path = Path(args.config)
|
||||
print(f'config.ini can be found at {str(config_path)}')
|
||||
print(f"config.ini can be found at {str(config_path)}")
|
||||
else:
|
||||
config_path = data_dir / 'config.ini'
|
||||
config_path = data_dir / "config.ini"
|
||||
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(config_path)
|
||||
|
||||
|
||||
def create_log(name):
|
||||
log = logging.getLogger(name)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
if config.getboolean('ui', 'debug'):
|
||||
if config.getboolean("ui", "debug"):
|
||||
log.setLevel(logging.DEBUG)
|
||||
handler.setLevel(logging.DEBUG)
|
||||
else:
|
||||
log.setLevel(logging.INFO)
|
||||
handler.setLevel(logging.INFO)
|
||||
fmt = ' %(name)s - %(levelname)s - %(message)s'
|
||||
fmt = " %(name)s - %(levelname)s - %(message)s"
|
||||
format = logging.Formatter(fmt)
|
||||
handler.setFormatter(format)
|
||||
log.addHandler(handler)
|
||||
log.propagate = False
|
||||
return log
|
||||
|
||||
log = create_log('main')
|
||||
web_log = create_log('waitress')
|
||||
|
||||
log = create_log("main")
|
||||
web_log = create_log("waitress")
|
||||
if not first_run:
|
||||
email_log = create_log('emails')
|
||||
auth_log = create_log('auth')
|
||||
email_log = create_log("emails")
|
||||
auth_log = create_log("auth")
|
||||
|
||||
if args.host is not None:
|
||||
log.debug(f'Using specified host {args.host}')
|
||||
config['ui']['host'] = args.host
|
||||
log.debug(f"Using specified host {args.host}")
|
||||
config["ui"]["host"] = args.host
|
||||
if args.port is not None:
|
||||
log.debug(f'Using specified port {args.port}')
|
||||
config['ui']['port'] = args.port
|
||||
log.debug(f"Using specified port {args.port}")
|
||||
config["ui"]["port"] = args.port
|
||||
|
||||
for key in config['files']:
|
||||
if config['files'][key] == '':
|
||||
if key != 'custom_css':
|
||||
log.debug(f'Using default {key}')
|
||||
config['files'][key] = str(data_dir / (key + '.json'))
|
||||
for key in config["files"]:
|
||||
if config["files"][key] == "":
|
||||
if key != "custom_css":
|
||||
log.debug(f"Using default {key}")
|
||||
config["files"][key] = str(data_dir / (key + ".json"))
|
||||
|
||||
for key in ['user_configuration', 'user_displayprefs']:
|
||||
if key not in config['files']:
|
||||
log.debug(f'Using default {key}')
|
||||
config['files'][key] = str(data_dir / (key + '.json'))
|
||||
for key in ["user_configuration", "user_displayprefs"]:
|
||||
if key not in config["files"]:
|
||||
log.debug(f"Using default {key}")
|
||||
config["files"][key] = str(data_dir / (key + ".json"))
|
||||
|
||||
with open(config['files']['invites'], 'r') as f:
|
||||
with open(config["files"]["invites"], "r") as f:
|
||||
temp_invites = json.load(f)
|
||||
if 'invites' in temp_invites:
|
||||
if "invites" in temp_invites:
|
||||
new_invites = {}
|
||||
log.info('Converting invites.json to new format, temporary.')
|
||||
for el in temp_invites['invites']:
|
||||
i = {'valid_till': el['valid_till']}
|
||||
if 'email' in el:
|
||||
i['email'] = el['email']
|
||||
new_invites[el['code']] = i
|
||||
with open(config['files']['invites'], 'w') as f:
|
||||
log.info("Converting invites.json to new format, temporary.")
|
||||
for el in temp_invites["invites"]:
|
||||
i = {"valid_till": el["valid_till"]}
|
||||
if "email" in el:
|
||||
i["email"] = el["email"]
|
||||
new_invites[el["code"]] = i
|
||||
with open(config["files"]["invites"], "w") as f:
|
||||
f.write(json.dumps(new_invites, indent=4, default=str))
|
||||
|
||||
|
||||
data_store = JSONStorage(config['files']['emails'],
|
||||
config['files']['invites'],
|
||||
config['files']['user_template'],
|
||||
config['files']['user_displayprefs'],
|
||||
config['files']['user_configuration'])
|
||||
data_store = JSONStorage(
|
||||
config["files"]["emails"],
|
||||
config["files"]["invites"],
|
||||
config["files"]["user_template"],
|
||||
config["files"]["user_displayprefs"],
|
||||
config["files"]["user_configuration"],
|
||||
)
|
||||
|
||||
|
||||
def default_css():
|
||||
css = {}
|
||||
css['href'] = "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
|
||||
css['integrity'] = "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
|
||||
css['crossorigin'] = "anonymous"
|
||||
css[
|
||||
"href"
|
||||
] = "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
|
||||
css[
|
||||
"integrity"
|
||||
] = "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
|
||||
css["crossorigin"] = "anonymous"
|
||||
return css
|
||||
|
||||
|
||||
css = {}
|
||||
css = default_css()
|
||||
if 'custom_css' in config['files']:
|
||||
if config['files']['custom_css'] != '':
|
||||
if "custom_css" in config["files"]:
|
||||
if config["files"]["custom_css"] != "":
|
||||
try:
|
||||
shutil.copy(config['files']['custom_css'],
|
||||
(local_dir / 'static' / 'bootstrap.css'))
|
||||
log.debug('Loaded custom CSS')
|
||||
css['href'] = '/bootstrap.css'
|
||||
css['integrity'] = ''
|
||||
css['crossorigin'] = ''
|
||||
shutil.copy(
|
||||
config["files"]["custom_css"], (local_dir / "static" / "bootstrap.css")
|
||||
)
|
||||
log.debug("Loaded custom CSS")
|
||||
css["href"] = "/bootstrap.css"
|
||||
css["integrity"] = ""
|
||||
css["crossorigin"] = ""
|
||||
except FileNotFoundError:
|
||||
log.error(f'Custom CSS {config["files"]["custom_css"]} not found, using default.')
|
||||
log.error(
|
||||
f'Custom CSS {config["files"]["custom_css"]} not found, using default.'
|
||||
)
|
||||
|
||||
|
||||
if ('email_html' not in config['password_resets'] or
|
||||
config['password_resets']['email_html'] == ''):
|
||||
log.debug('Using default password reset email HTML template')
|
||||
config['password_resets']['email_html'] = str(local_dir / 'email.html')
|
||||
if ('email_text' not in config['password_resets'] or
|
||||
config['password_resets']['email_text'] == ''):
|
||||
log.debug('Using default password reset email plaintext template')
|
||||
config['password_resets']['email_text'] = str(local_dir / 'email.txt')
|
||||
if (
|
||||
"email_html" not in config["password_resets"]
|
||||
or config["password_resets"]["email_html"] == ""
|
||||
):
|
||||
log.debug("Using default password reset email HTML template")
|
||||
config["password_resets"]["email_html"] = str(local_dir / "email.html")
|
||||
if (
|
||||
"email_text" not in config["password_resets"]
|
||||
or config["password_resets"]["email_text"] == ""
|
||||
):
|
||||
log.debug("Using default password reset email plaintext template")
|
||||
config["password_resets"]["email_text"] = str(local_dir / "email.txt")
|
||||
|
||||
if ('email_html' not in config['invite_emails'] or
|
||||
config['invite_emails']['email_html'] == ''):
|
||||
log.debug('Using default invite email HTML template')
|
||||
config['invite_emails']['email_html'] = str(local_dir /
|
||||
'invite-email.html')
|
||||
if ('email_text' not in config['invite_emails'] or
|
||||
config['invite_emails']['email_text'] == ''):
|
||||
log.debug('Using default invite email plaintext template')
|
||||
config['invite_emails']['email_text'] = str(local_dir /
|
||||
'invite-email.txt')
|
||||
if ('public_server' not in config['jellyfin'] or
|
||||
config['jellyfin']['public_server'] == ''):
|
||||
config['jellyfin']['public_server'] = config['jellyfin']['server']
|
||||
if (
|
||||
"email_html" not in config["invite_emails"]
|
||||
or config["invite_emails"]["email_html"] == ""
|
||||
):
|
||||
log.debug("Using default invite email HTML template")
|
||||
config["invite_emails"]["email_html"] = str(local_dir / "invite-email.html")
|
||||
if (
|
||||
"email_text" not in config["invite_emails"]
|
||||
or config["invite_emails"]["email_text"] == ""
|
||||
):
|
||||
log.debug("Using default invite email plaintext template")
|
||||
config["invite_emails"]["email_text"] = str(local_dir / "invite-email.txt")
|
||||
if (
|
||||
"public_server" not in config["jellyfin"]
|
||||
or config["jellyfin"]["public_server"] == ""
|
||||
):
|
||||
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
||||
|
||||
|
||||
def main():
|
||||
if args.get_defaults:
|
||||
import json
|
||||
from jellyfin_accounts.jf_api import Jellyfin
|
||||
jf = Jellyfin(config['jellyfin']['server'],
|
||||
config['jellyfin']['client'],
|
||||
config['jellyfin']['version'],
|
||||
config['jellyfin']['device'],
|
||||
config['jellyfin']['device_id'])
|
||||
|
||||
jf = Jellyfin(
|
||||
config["jellyfin"]["server"],
|
||||
config["jellyfin"]["client"],
|
||||
config["jellyfin"]["version"],
|
||||
config["jellyfin"]["device"],
|
||||
config["jellyfin"]["device_id"],
|
||||
)
|
||||
print("NOTE: This can now be done through the web ui.")
|
||||
print("""
|
||||
print(
|
||||
"""
|
||||
This tool lets you grab various settings from a user,
|
||||
so that they can be applied every time a new account is
|
||||
created. """)
|
||||
created. """
|
||||
)
|
||||
print("Step 1: User Policy.")
|
||||
print("""
|
||||
print(
|
||||
"""
|
||||
A user policy stores a users permissions (e.g access rights and
|
||||
most of the other settings in the 'Profile' and 'Access' tabs
|
||||
of a user). """)
|
||||
of a user). """
|
||||
)
|
||||
success = False
|
||||
msg = "Get public users only or all users? (requires auth) [public/all]: "
|
||||
public = False
|
||||
while not success:
|
||||
choice = input(msg)
|
||||
if choice == 'public':
|
||||
if choice == "public":
|
||||
public = True
|
||||
print("Make sure the user is publicly visible!")
|
||||
success = True
|
||||
elif choice == 'all':
|
||||
jf.authenticate(config['jellyfin']['username'],
|
||||
config['jellyfin']['password'])
|
||||
elif choice == "all":
|
||||
jf.authenticate(
|
||||
config["jellyfin"]["username"], config["jellyfin"]["password"]
|
||||
)
|
||||
public = False
|
||||
success = True
|
||||
users = jf.getUsers(public=public)
|
||||
@@ -207,69 +238,78 @@ def main():
|
||||
success = False
|
||||
while not success:
|
||||
try:
|
||||
user_index = int(input(">: "))-1
|
||||
policy = users[user_index]['Policy']
|
||||
user_index = int(input(">: ")) - 1
|
||||
policy = users[user_index]["Policy"]
|
||||
success = True
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
data_store.user_template = policy
|
||||
print(f'Policy written to "{config["files"]["user_template"]}".')
|
||||
print('In future, this policy will be copied to all new users.')
|
||||
print('Step 2: Homescreen Layout')
|
||||
print("""
|
||||
print("In future, this policy will be copied to all new users.")
|
||||
print("Step 2: Homescreen Layout")
|
||||
print(
|
||||
"""
|
||||
You may want to customize the default layout of a new user's
|
||||
home screen. These settings can be applied to an account through
|
||||
the 'Home' section in a user's settings. """)
|
||||
the 'Home' section in a user's settings. """
|
||||
)
|
||||
success = False
|
||||
while not success:
|
||||
choice = input("Grab the chosen user's homescreen layout? [y/n]: ")
|
||||
if choice.lower() == 'y':
|
||||
user_id = users[user_index]['Id']
|
||||
configuration = users[user_index]['Configuration']
|
||||
if choice.lower() == "y":
|
||||
user_id = users[user_index]["Id"]
|
||||
configuration = users[user_index]["Configuration"]
|
||||
display_prefs = jf.getDisplayPreferences(user_id)
|
||||
data_store.user_configuration = configuration
|
||||
print(f'Configuration written to "{config["files"]["user_configuration"]}".')
|
||||
print(
|
||||
f'Configuration written to "{config["files"]["user_configuration"]}".'
|
||||
)
|
||||
data_store.user_displayprefs = display_prefs
|
||||
print(f'Display Prefs written to "{config["files"]["user_displayprefs"]}".')
|
||||
print(
|
||||
f'Display Prefs written to "{config["files"]["user_displayprefs"]}".'
|
||||
)
|
||||
success = True
|
||||
elif choice.lower() == 'n':
|
||||
elif choice.lower() == "n":
|
||||
success = True
|
||||
|
||||
else:
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print('Quitting...')
|
||||
print("Quitting...")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
global app
|
||||
app = Flask(__name__, root_path=str(local_dir))
|
||||
app.config['DEBUG'] = config.getboolean('ui', 'debug')
|
||||
app.config['SECRET_KEY'] = secrets.token_urlsafe(16)
|
||||
app.config["DEBUG"] = config.getboolean("ui", "debug")
|
||||
app.config["SECRET_KEY"] = secrets.token_urlsafe(16)
|
||||
|
||||
from waitress import serve
|
||||
|
||||
if first_run:
|
||||
import jellyfin_accounts.setup
|
||||
host = config['ui']['host']
|
||||
port = config['ui']['port']
|
||||
log.info('Starting web UI for first run setup...')
|
||||
serve(app,
|
||||
host=host,
|
||||
port=port)
|
||||
|
||||
host = config["ui"]["host"]
|
||||
port = config["ui"]["port"]
|
||||
log.info("Starting web UI for first run setup...")
|
||||
serve(app, host=host, port=port)
|
||||
else:
|
||||
import jellyfin_accounts.web_api
|
||||
import jellyfin_accounts.web
|
||||
host = config['ui']['host']
|
||||
port = config['ui']['port']
|
||||
log.info(f'Starting web UI on {host}:{port}')
|
||||
if config.getboolean('password_resets', 'enabled'):
|
||||
|
||||
host = config["ui"]["host"]
|
||||
port = config["ui"]["port"]
|
||||
log.info(f"Starting web UI on {host}:{port}")
|
||||
if config.getboolean("password_resets", "enabled"):
|
||||
|
||||
def start_pwr():
|
||||
import jellyfin_accounts.pw_reset
|
||||
|
||||
jellyfin_accounts.pw_reset.start()
|
||||
|
||||
pwr = threading.Thread(target=start_pwr, daemon=True)
|
||||
log.info('Starting email thread')
|
||||
log.info("Starting email thread")
|
||||
pwr.start()
|
||||
|
||||
serve(app,
|
||||
host=host,
|
||||
port=int(port))
|
||||
serve(app, host=host, port=int(port))
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import json
|
||||
import datetime
|
||||
|
||||
|
||||
class JSONFile(dict):
|
||||
"""
|
||||
Behaves like a dictionary, but automatically
|
||||
reads and writes to a JSON file (most of the time).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def readJSON(path):
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def writeJSON(path, data):
|
||||
with open(path, 'w') as f:
|
||||
with open(path, "w") as f:
|
||||
return f.write(json.dumps(data, indent=4, default=str))
|
||||
|
||||
|
||||
def __init__(self, path, data=None):
|
||||
self.path = path
|
||||
if data is None:
|
||||
@@ -34,14 +36,14 @@ class JSONFile(dict):
|
||||
def __setitem__(self, key, value):
|
||||
data = self.readJSON(self.path)
|
||||
data[key] = value
|
||||
self.writeJSON(self.path, data)
|
||||
self.writeJSON(self.path, data)
|
||||
super(JSONFile, self).__init__(data)
|
||||
|
||||
def __delitem__(self, key):
|
||||
data = self.readJSON(self.path)
|
||||
super(JSONFile, self).__init__(data)
|
||||
del data[key]
|
||||
self.writeJSON(self.path, data)
|
||||
self.writeJSON(self.path, data)
|
||||
super(JSONFile, self).__delitem__(key)
|
||||
|
||||
def __str__(self):
|
||||
@@ -50,18 +52,15 @@ class JSONFile(dict):
|
||||
|
||||
|
||||
class JSONStorage:
|
||||
def __init__(self,
|
||||
emails,
|
||||
invites,
|
||||
user_template,
|
||||
user_displayprefs,
|
||||
user_configuration):
|
||||
def __init__(
|
||||
self, emails, invites, user_template, user_displayprefs, user_configuration
|
||||
):
|
||||
self.emails = JSONFile(path=emails)
|
||||
self.invites = JSONFile(path=invites)
|
||||
self.user_template = JSONFile(path=user_template)
|
||||
self.user_displayprefs = JSONFile(path=user_displayprefs)
|
||||
self.user_configuration = JSONFile(path=user_configuration)
|
||||
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if hasattr(self, name):
|
||||
path = self.__dict__[name].path
|
||||
|
||||
@@ -12,97 +12,109 @@ from jellyfin_accounts import config
|
||||
from jellyfin_accounts import email_log as log
|
||||
|
||||
|
||||
class Email():
|
||||
class Email:
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
log.debug(f'{self.address}: Creating email')
|
||||
log.debug(f"{self.address}: Creating email")
|
||||
self.content = {}
|
||||
self.from_address = config['email']['address']
|
||||
self.from_name = config['email']['from']
|
||||
log.debug((
|
||||
f'{self.address}: Sending from {self.from_address} ' +
|
||||
f'({self.from_name})'))
|
||||
self.from_address = config["email"]["address"]
|
||||
self.from_name = config["email"]["from"]
|
||||
log.debug(
|
||||
(
|
||||
f"{self.address}: Sending from {self.from_address} "
|
||||
+ f"({self.from_name})"
|
||||
)
|
||||
)
|
||||
|
||||
def pretty_time(self, expiry):
|
||||
current_time = datetime.datetime.now()
|
||||
date = expiry.strftime(config['email']['date_format'])
|
||||
if config.getboolean('email', 'use_24h'):
|
||||
log.debug(f'{self.address}: Using 24h time')
|
||||
time = expiry.strftime('%H:%M')
|
||||
date = expiry.strftime(config["email"]["date_format"])
|
||||
if config.getboolean("email", "use_24h"):
|
||||
log.debug(f"{self.address}: Using 24h time")
|
||||
time = expiry.strftime("%H:%M")
|
||||
else:
|
||||
log.debug(f'{self.address}: Using 12h time')
|
||||
time = expiry.strftime('%-I:%M %p')
|
||||
log.debug(f"{self.address}: Using 12h time")
|
||||
time = expiry.strftime("%-I:%M %p")
|
||||
expiry_delta = (expiry - current_time).seconds
|
||||
expires_in = {'hours': expiry_delta//3600,
|
||||
'minutes': (expiry_delta//60) % 60}
|
||||
if expires_in['hours'] == 0:
|
||||
expires_in = {
|
||||
"hours": expiry_delta // 3600,
|
||||
"minutes": (expiry_delta // 60) % 60,
|
||||
}
|
||||
if expires_in["hours"] == 0:
|
||||
expires_in = f'{str(expires_in["minutes"])}m'
|
||||
else:
|
||||
expires_in = (f'{str(expires_in["hours"])}h ' +
|
||||
f'{str(expires_in["minutes"])}m')
|
||||
log.debug(f'{self.address}: Expires in {expires_in}')
|
||||
return {'date': date, 'time': time, 'expires_in': expires_in}
|
||||
expires_in = (
|
||||
f'{str(expires_in["hours"])}h ' + f'{str(expires_in["minutes"])}m'
|
||||
)
|
||||
log.debug(f"{self.address}: Expires in {expires_in}")
|
||||
return {"date": date, "time": time, "expires_in": expires_in}
|
||||
|
||||
def construct_invite(self, invite):
|
||||
self.subject = config['invite_emails']['subject']
|
||||
log.debug(f'{self.address}: Using subject {self.subject}')
|
||||
log.debug(f'{self.address}: Constructing email content')
|
||||
expiry = invite['expiry']
|
||||
self.subject = config["invite_emails"]["subject"]
|
||||
log.debug(f"{self.address}: Using subject {self.subject}")
|
||||
log.debug(f"{self.address}: Constructing email content")
|
||||
expiry = invite["expiry"]
|
||||
expiry.replace(tzinfo=None)
|
||||
pretty = self.pretty_time(expiry)
|
||||
email_message = config['email']['message']
|
||||
invite_link = config['invite_emails']['url_base']
|
||||
invite_link += '/' + invite['code']
|
||||
for key in ['text', 'html']:
|
||||
sp = Path(config['invite_emails']['email_' + key]) / '..'
|
||||
sp = str(sp.resolve()) + '/'
|
||||
email_message = config["email"]["message"]
|
||||
invite_link = config["invite_emails"]["url_base"]
|
||||
invite_link += "/" + invite["code"]
|
||||
for key in ["text", "html"]:
|
||||
sp = Path(config["invite_emails"]["email_" + key]) / ".."
|
||||
sp = str(sp.resolve()) + "/"
|
||||
template_loader = FileSystemLoader(searchpath=sp)
|
||||
template_env = Environment(loader=template_loader)
|
||||
fname = Path(config['invite_emails']['email_' + key]).name
|
||||
fname = Path(config["invite_emails"]["email_" + key]).name
|
||||
template = template_env.get_template(fname)
|
||||
c = template.render(expiry_date=pretty['date'],
|
||||
expiry_time=pretty['time'],
|
||||
expires_in=pretty['expires_in'],
|
||||
invite_link=invite_link,
|
||||
message=email_message)
|
||||
c = template.render(
|
||||
expiry_date=pretty["date"],
|
||||
expiry_time=pretty["time"],
|
||||
expires_in=pretty["expires_in"],
|
||||
invite_link=invite_link,
|
||||
message=email_message,
|
||||
)
|
||||
self.content[key] = c
|
||||
log.info(f'{self.address}: {key} constructed')
|
||||
log.info(f"{self.address}: {key} constructed")
|
||||
|
||||
def construct_reset(self, reset):
|
||||
self.subject = config['password_resets']['subject']
|
||||
log.debug(f'{self.address}: Using subject {self.subject}')
|
||||
log.debug(f'{self.address}: Constructing email content')
|
||||
self.subject = config["password_resets"]["subject"]
|
||||
log.debug(f"{self.address}: Using subject {self.subject}")
|
||||
log.debug(f"{self.address}: Constructing email content")
|
||||
try:
|
||||
expiry = date_parser.parse(reset['ExpirationDate'])
|
||||
expiry = date_parser.parse(reset["ExpirationDate"])
|
||||
expiry = expiry.replace(tzinfo=None)
|
||||
except:
|
||||
log.error(f"{self.address}: Couldn't parse expiry time")
|
||||
return False
|
||||
current_time = datetime.datetime.now()
|
||||
if expiry >= current_time:
|
||||
log.debug(f'{self.address}: Invite valid')
|
||||
log.debug(f"{self.address}: Invite valid")
|
||||
pretty = self.pretty_time(expiry)
|
||||
email_message = config['email']['message']
|
||||
for key in ['text', 'html']:
|
||||
sp = Path(config['password_resets']['email_' + key]) / '..'
|
||||
sp = str(sp.resolve()) + '/'
|
||||
email_message = config["email"]["message"]
|
||||
for key in ["text", "html"]:
|
||||
sp = Path(config["password_resets"]["email_" + key]) / ".."
|
||||
sp = str(sp.resolve()) + "/"
|
||||
template_loader = FileSystemLoader(searchpath=sp)
|
||||
template_env = Environment(loader=template_loader)
|
||||
fname = Path(config['password_resets']['email_' + key]).name
|
||||
fname = Path(config["password_resets"]["email_" + key]).name
|
||||
template = template_env.get_template(fname)
|
||||
c = template.render(username=reset['UserName'],
|
||||
expiry_date=pretty['date'],
|
||||
expiry_time=pretty['time'],
|
||||
expires_in=pretty['expires_in'],
|
||||
pin=reset['Pin'],
|
||||
message=email_message)
|
||||
c = template.render(
|
||||
username=reset["UserName"],
|
||||
expiry_date=pretty["date"],
|
||||
expiry_time=pretty["time"],
|
||||
expires_in=pretty["expires_in"],
|
||||
pin=reset["Pin"],
|
||||
message=email_message,
|
||||
)
|
||||
self.content[key] = c
|
||||
log.info(f'{self.address}: {key} constructed')
|
||||
log.info(f"{self.address}: {key} constructed")
|
||||
return True
|
||||
else:
|
||||
err = ((f"{self.address}: " +
|
||||
"Reset has reportedly already expired. " +
|
||||
"Ensure timezones are correctly configured."))
|
||||
err = (
|
||||
f"{self.address}: "
|
||||
+ "Reset has reportedly already expired. "
|
||||
+ "Ensure timezones are correctly configured."
|
||||
)
|
||||
log.error(err)
|
||||
return False
|
||||
|
||||
@@ -110,71 +122,74 @@ class Email():
|
||||
class Mailgun(Email):
|
||||
def __init__(self, address):
|
||||
super().__init__(address)
|
||||
self.api_url = config['mailgun']['api_url']
|
||||
self.api_key = config['mailgun']['api_key']
|
||||
self.from_mg = f'{self.from_name} <{self.from_address}>'
|
||||
self.api_url = config["mailgun"]["api_url"]
|
||||
self.api_key = config["mailgun"]["api_key"]
|
||||
self.from_mg = f"{self.from_name} <{self.from_address}>"
|
||||
|
||||
def send(self):
|
||||
response = requests.post(self.api_url,
|
||||
auth=("api", self.api_key),
|
||||
data={"from": self.from_mg,
|
||||
"to": [self.address],
|
||||
"subject": self.subject,
|
||||
"text": self.content['text'],
|
||||
"html": self.content['html']})
|
||||
response = requests.post(
|
||||
self.api_url,
|
||||
auth=("api", self.api_key),
|
||||
data={
|
||||
"from": self.from_mg,
|
||||
"to": [self.address],
|
||||
"subject": self.subject,
|
||||
"text": self.content["text"],
|
||||
"html": self.content["html"],
|
||||
},
|
||||
)
|
||||
if response.ok:
|
||||
log.info(f'{self.address}: Sent via mailgun.')
|
||||
log.info(f"{self.address}: Sent via mailgun.")
|
||||
return True
|
||||
log.debug(f'{self.address}: Mailgun: {response.status_code}')
|
||||
log.debug(f"{self.address}: Mailgun: {response.status_code}")
|
||||
return response
|
||||
|
||||
|
||||
class Smtp(Email):
|
||||
def __init__(self, address):
|
||||
super().__init__(address)
|
||||
self.server = config['smtp']['server']
|
||||
self.password = config['smtp']['password']
|
||||
self.server = config["smtp"]["server"]
|
||||
self.password = config["smtp"]["password"]
|
||||
try:
|
||||
self.port = int(config['smtp']['port'])
|
||||
self.port = int(config["smtp"]["port"])
|
||||
except ValueError:
|
||||
self.port = 465
|
||||
log.debug(f'{self.address}: Defaulting to port {self.port}')
|
||||
log.debug(f"{self.address}: Defaulting to port {self.port}")
|
||||
|
||||
def send(self):
|
||||
message = MIMEMultipart("alternative")
|
||||
message["Subject"] = self.subject
|
||||
message["From"] = self.from_address
|
||||
message["To"] = self.address
|
||||
text = MIMEText(self.content['text'], 'plain')
|
||||
html = MIMEText(self.content['html'], 'html')
|
||||
text = MIMEText(self.content["text"], "plain")
|
||||
html = MIMEText(self.content["html"], "html")
|
||||
message.attach(text)
|
||||
message.attach(html)
|
||||
try:
|
||||
if config['smtp']['encryption'] == 'ssl_tls':
|
||||
if config["smtp"]["encryption"] == "ssl_tls":
|
||||
self.context = ssl.create_default_context()
|
||||
with smtplib.SMTP_SSL(self.server,
|
||||
self.port,
|
||||
context=self.context) as server:
|
||||
with smtplib.SMTP_SSL(
|
||||
self.server, self.port, context=self.context
|
||||
) as server:
|
||||
server.ehlo()
|
||||
server.login(self.from_address, self.password)
|
||||
server.sendmail(self.from_address,
|
||||
self.address,
|
||||
message.as_string())
|
||||
log.info(f'{self.address}: Sent via smtp (ssl/tls)')
|
||||
server.sendmail(
|
||||
self.from_address, self.address, message.as_string()
|
||||
)
|
||||
log.info(f"{self.address}: Sent via smtp (ssl/tls)")
|
||||
return True
|
||||
elif config['smtp']['encryption'] == 'starttls':
|
||||
with smtplib.SMTP(self.server,
|
||||
self.port) as server:
|
||||
elif config["smtp"]["encryption"] == "starttls":
|
||||
with smtplib.SMTP(self.server, self.port) as server:
|
||||
server.ehlo()
|
||||
server.starttls()
|
||||
server.login(self.from_address, self.password)
|
||||
server.sendmail(self.from_address,
|
||||
self.address,
|
||||
message.as_string())
|
||||
log.info(f'{self.address}: Sent via smtp (starttls)')
|
||||
server.sendmail(
|
||||
self.from_address, self.address, message.as_string()
|
||||
)
|
||||
log.info(f"{self.address}: Sent via smtp (starttls)")
|
||||
return True
|
||||
except Exception as e:
|
||||
err = f'{self.address}: Failed to send via smtp: '
|
||||
err = f"{self.address}: Failed to send via smtp: "
|
||||
err += type(e).__name__
|
||||
log.error(err)
|
||||
try:
|
||||
|
||||
@@ -2,35 +2,48 @@
|
||||
import requests
|
||||
import time
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Jellyfin:
|
||||
"""
|
||||
Basic Jellyfin API client, providing account related function only.
|
||||
"""
|
||||
|
||||
class UserExistsError(Error):
|
||||
"""
|
||||
Thrown if a user already exists with the same name
|
||||
when creating an account.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class UserNotFoundError(Error):
|
||||
"""Thrown if account with specified user ID/name does not exist."""
|
||||
|
||||
pass
|
||||
|
||||
class AuthenticationError(Error):
|
||||
"""Thrown if authentication with Jellyfin fails."""
|
||||
|
||||
pass
|
||||
|
||||
class AuthenticationRequiredError(Error):
|
||||
"""
|
||||
Thrown if privileged action is attempted without authentication.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class UnknownError(Error):
|
||||
"""
|
||||
Thrown if i've been too lazy to figure out an error's meaning.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def __init__(self, server, client, version, device, deviceId):
|
||||
"""
|
||||
Initializes the Jellyfin object. All parameters except server
|
||||
@@ -58,17 +71,16 @@ class Jellyfin:
|
||||
self.auth += f"DeviceId={self.deviceId}, "
|
||||
self.auth += f"Version={self.version}"
|
||||
self.header = {
|
||||
"Accept": "application/json",
|
||||
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Application": f"{self.client}/{self.version}",
|
||||
"Accept-Charset": "UTF-8,*",
|
||||
"Accept-encoding": "gzip",
|
||||
"User-Agent": self.useragent,
|
||||
"X-Emby-Authorization": self.auth
|
||||
"Accept": "application/json",
|
||||
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Application": f"{self.client}/{self.version}",
|
||||
"Accept-Charset": "UTF-8,*",
|
||||
"Accept-encoding": "gzip",
|
||||
"User-Agent": self.useragent,
|
||||
"X-Emby-Authorization": self.auth,
|
||||
}
|
||||
def getUsers(self, username: str = "all",
|
||||
userId: str = "all",
|
||||
public: bool = True):
|
||||
|
||||
def getUsers(self, username: str = "all", userId: str = "all", public: bool = True):
|
||||
"""
|
||||
Returns details on user(s), such as ID, Name, Policy.
|
||||
|
||||
@@ -81,19 +93,20 @@ class Jellyfin:
|
||||
"""
|
||||
if public is True:
|
||||
if (time.time() - self.userCachePublicAge) >= self.timeout:
|
||||
response = requests.get(self.server+"/emby/Users/Public").json()
|
||||
response = requests.get(self.server + "/emby/Users/Public").json()
|
||||
self.userCachePublic = response
|
||||
self.userCachePublicAge = time.time()
|
||||
else:
|
||||
response = self.userCachePublic
|
||||
elif (public is False and
|
||||
hasattr(self, 'username') and
|
||||
hasattr(self, 'password')):
|
||||
elif (
|
||||
public is False and hasattr(self, "username") and hasattr(self, "password")
|
||||
):
|
||||
if (time.time() - self.userCacheAge) >= self.timeout:
|
||||
response = requests.get(self.server+"/emby/Users",
|
||||
headers=self.header,
|
||||
params={'Username': self.username,
|
||||
'Pw': self.password})
|
||||
response = requests.get(
|
||||
self.server + "/emby/Users",
|
||||
headers=self.header,
|
||||
params={"Username": self.username, "Pw": self.password},
|
||||
)
|
||||
if response.status_code == 200:
|
||||
response = response.json()
|
||||
self.userCache = response
|
||||
@@ -113,7 +126,7 @@ class Jellyfin:
|
||||
elif id == "all":
|
||||
match = False
|
||||
for user in response:
|
||||
if user['Name'] == username:
|
||||
if user["Name"] == username:
|
||||
match = True
|
||||
return user
|
||||
if not match:
|
||||
@@ -121,7 +134,7 @@ class Jellyfin:
|
||||
else:
|
||||
match = False
|
||||
for user in response:
|
||||
if user['Id'] == id:
|
||||
if user["Id"] == id:
|
||||
match = True
|
||||
return user
|
||||
if not match:
|
||||
@@ -136,24 +149,26 @@ class Jellyfin:
|
||||
"""
|
||||
self.username = username
|
||||
self.password = password
|
||||
response = requests.post(self.server+"/emby/Users/AuthenticateByName",
|
||||
headers=self.header,
|
||||
params={'Username': self.username,
|
||||
'Pw': self.password})
|
||||
response = requests.post(
|
||||
self.server + "/emby/Users/AuthenticateByName",
|
||||
headers=self.header,
|
||||
params={"Username": self.username, "Pw": self.password},
|
||||
)
|
||||
if response.status_code == 200:
|
||||
json = response.json()
|
||||
self.userId = json['User']['Id']
|
||||
self.accessToken = json['AccessToken']
|
||||
self.userId = json["User"]["Id"]
|
||||
self.accessToken = json["AccessToken"]
|
||||
self.auth = "MediaBrowser "
|
||||
self.auth += f"Client={self.client}, "
|
||||
self.auth += f"Device={self.device}, "
|
||||
self.auth += f"DeviceId={self.deviceId}, "
|
||||
self.auth += f"Version={self.version}"
|
||||
self.auth += f", Token={self.accessToken}"
|
||||
self.header['X-Emby-Authorization'] = self.auth
|
||||
self.header["X-Emby-Authorization"] = self.auth
|
||||
return True
|
||||
else:
|
||||
raise self.AuthenticationError
|
||||
|
||||
def setPolicy(self, userId: str, policy: dict):
|
||||
"""
|
||||
Sets a user's policy (Admin rights, Library Access, etc.) by user ID.
|
||||
@@ -161,55 +176,64 @@ class Jellyfin:
|
||||
:param userId: ID of the user to modify.
|
||||
:param policy: User policy in dictionary form.
|
||||
"""
|
||||
return requests.post(self.server+"/Users/"+userId+"/Policy",
|
||||
headers=self.header,
|
||||
params=policy)
|
||||
return requests.post(
|
||||
self.server + "/Users/" + userId + "/Policy",
|
||||
headers=self.header,
|
||||
params=policy,
|
||||
)
|
||||
|
||||
def newUser(self, username: str, password: str):
|
||||
for user in self.getUsers():
|
||||
if user['Name'] == username:
|
||||
if user["Name"] == username:
|
||||
raise self.UserExistsError
|
||||
response = requests.post(self.server+"/emby/Users/New",
|
||||
headers=self.header,
|
||||
params={'Name': username,
|
||||
'Password': password})
|
||||
response = requests.post(
|
||||
self.server + "/emby/Users/New",
|
||||
headers=self.header,
|
||||
params={"Name": username, "Password": password},
|
||||
)
|
||||
if response.status_code == 401:
|
||||
if hasattr(self, 'username') and hasattr(self, 'password'):
|
||||
if hasattr(self, "username") and hasattr(self, "password"):
|
||||
self.authenticate(self.username, self.password)
|
||||
return self.newUser(username, password)
|
||||
else:
|
||||
raise self.AuthenticationRequiredError
|
||||
return response
|
||||
|
||||
def getViewOrder(self, userId: str, public: bool = True):
|
||||
if not public:
|
||||
param = '?IncludeHidden=true'
|
||||
param = "?IncludeHidden=true"
|
||||
else:
|
||||
param = ''
|
||||
views = requests.get(self.server+"/Users/"+userId+"/Views"+param,
|
||||
headers=self.header).json()['Items']
|
||||
param = ""
|
||||
views = requests.get(
|
||||
self.server + "/Users/" + userId + "/Views" + param, headers=self.header
|
||||
).json()["Items"]
|
||||
orderedViews = []
|
||||
for library in views:
|
||||
orderedViews.append(library['Id'])
|
||||
orderedViews.append(library["Id"])
|
||||
return orderedViews
|
||||
|
||||
def setConfiguration(self, userId: str, configuration: dict):
|
||||
"""
|
||||
Sets a user's configuration (Settings the user can change themselves).
|
||||
:param userId: ID of the user to modify.
|
||||
:param configuration: Configuration to write in dictionary form.
|
||||
"""
|
||||
resp = requests.post(self.server+"/Users/"+userId+"/Configuration",
|
||||
headers=self.header,
|
||||
params=configuration)
|
||||
if (resp.status_code == 200 or
|
||||
resp.status_code == 204):
|
||||
resp = requests.post(
|
||||
self.server + "/Users/" + userId + "/Configuration",
|
||||
headers=self.header,
|
||||
params=configuration,
|
||||
)
|
||||
if resp.status_code == 200 or resp.status_code == 204:
|
||||
return True
|
||||
elif resp.status_code == 401:
|
||||
if hasattr(self, 'username') and hasattr(self, 'password'):
|
||||
if hasattr(self, "username") and hasattr(self, "password"):
|
||||
self.authenticate(self.username, self.password)
|
||||
return self.setConfiguration(userId, configuration)
|
||||
else:
|
||||
raise self.AuthenticationRequiredError
|
||||
else:
|
||||
raise self.UnknownError
|
||||
|
||||
def getConfiguration(self, username: str = "all", userId: str = "all"):
|
||||
"""
|
||||
Gets a user's Configuration. This can also be found in getUsers if
|
||||
@@ -217,27 +241,36 @@ class Jellyfin:
|
||||
:param username: The user's username.
|
||||
:param userId: The user's ID.
|
||||
"""
|
||||
return self.getUsers(username=username,
|
||||
userId=userId,
|
||||
public=False)['Configuration']
|
||||
return self.getUsers(username=username, userId=userId, public=False)[
|
||||
"Configuration"
|
||||
]
|
||||
|
||||
def getDisplayPreferences(self, userId: str):
|
||||
"""
|
||||
Gets a user's Display Preferences (Home layout).
|
||||
:param userId: The user's ID.
|
||||
"""
|
||||
resp = requests.get((self.server+"/DisplayPreferences/usersettings" +
|
||||
"?userId="+userId+"&client=emby"),
|
||||
headers=self.header)
|
||||
resp = requests.get(
|
||||
(
|
||||
self.server
|
||||
+ "/DisplayPreferences/usersettings"
|
||||
+ "?userId="
|
||||
+ userId
|
||||
+ "&client=emby"
|
||||
),
|
||||
headers=self.header,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
return resp.json()
|
||||
elif resp.status_code == 401:
|
||||
if hasattr(self, 'username') and hasattr(self, 'password'):
|
||||
if hasattr(self, "username") and hasattr(self, "password"):
|
||||
self.authenticate(self.username, self.password)
|
||||
return self.getDisplayPreferences(userId)
|
||||
else:
|
||||
raise self.AuthenticationRequiredError
|
||||
else:
|
||||
raise self.UnknownError
|
||||
|
||||
def setDisplayPreferences(self, userId: str, preferences: dict):
|
||||
"""
|
||||
Sets a user's Display Preferences (Home layout).
|
||||
@@ -245,16 +278,22 @@ class Jellyfin:
|
||||
:param preferences: The preferences to set.
|
||||
"""
|
||||
tempheader = self.header
|
||||
tempheader['Content-type'] = 'application/json'
|
||||
resp = requests.post((self.server+"/DisplayPreferences/usersettings" +
|
||||
"?userId="+userId+"&client=emby"),
|
||||
headers=tempheader,
|
||||
json=preferences)
|
||||
if (resp.status_code == 200 or
|
||||
resp.status_code == 204):
|
||||
tempheader["Content-type"] = "application/json"
|
||||
resp = requests.post(
|
||||
(
|
||||
self.server
|
||||
+ "/DisplayPreferences/usersettings"
|
||||
+ "?userId="
|
||||
+ userId
|
||||
+ "&client=emby"
|
||||
),
|
||||
headers=tempheader,
|
||||
json=preferences,
|
||||
)
|
||||
if resp.status_code == 200 or resp.status_code == 204:
|
||||
return True
|
||||
elif resp.status_code == 401:
|
||||
if hasattr(self, 'username') and hasattr(self, 'password'):
|
||||
if hasattr(self, "username") and hasattr(self, "password"):
|
||||
self.authenticate(self.username, self.password)
|
||||
return self.setDisplayPreferences(userId, preferences)
|
||||
else:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# from flask import g
|
||||
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from itsdangerous import (TimedJSONWebSignatureSerializer
|
||||
as Serializer, BadSignature, SignatureExpired)
|
||||
from itsdangerous import (
|
||||
TimedJSONWebSignatureSerializer as Serializer,
|
||||
BadSignature,
|
||||
SignatureExpired,
|
||||
)
|
||||
from passlib.apps import custom_app_context as pwd_context
|
||||
import uuid
|
||||
from jellyfin_accounts import config, app, g
|
||||
@@ -11,13 +13,16 @@ from jellyfin_accounts import auth_log as log
|
||||
from jellyfin_accounts.jf_api import Jellyfin
|
||||
from jellyfin_accounts.web_api import jf
|
||||
|
||||
auth_jf = Jellyfin(config['jellyfin']['server'],
|
||||
config['jellyfin']['client'],
|
||||
config['jellyfin']['version'],
|
||||
config['jellyfin']['device'],
|
||||
config['jellyfin']['device_id'] + '_authClient')
|
||||
auth_jf = Jellyfin(
|
||||
config["jellyfin"]["server"],
|
||||
config["jellyfin"]["client"],
|
||||
config["jellyfin"]["version"],
|
||||
config["jellyfin"]["device"],
|
||||
config["jellyfin"]["device_id"] + "_authClient",
|
||||
)
|
||||
|
||||
class Account():
|
||||
|
||||
class Account:
|
||||
def __init__(self, username=None, password=None):
|
||||
self.username = username
|
||||
if password is not None:
|
||||
@@ -25,10 +30,12 @@ class Account():
|
||||
self.id = str(uuid.uuid4())
|
||||
self.jf = False
|
||||
elif username is not None:
|
||||
jf.authenticate(config['jellyfin']['username'],
|
||||
config['jellyfin']['password'])
|
||||
self.id = jf.getUsers(self.username, public=False)['Id']
|
||||
jf.authenticate(
|
||||
config["jellyfin"]["username"], config["jellyfin"]["password"]
|
||||
)
|
||||
self.id = jf.getUsers(self.username, public=False)["Id"]
|
||||
self.jf = True
|
||||
|
||||
def verify_password(self, password):
|
||||
if not self.jf:
|
||||
return pwd_context.verify(password, self.password_hash)
|
||||
@@ -37,59 +44,60 @@ class Account():
|
||||
return auth_jf.authenticate(self.username, password)
|
||||
except Jellyfin.AuthenticationError:
|
||||
return False
|
||||
|
||||
def generate_token(self, expiration=1200):
|
||||
s = Serializer(app.config['SECRET_KEY'], expires_in=expiration)
|
||||
s = Serializer(app.config["SECRET_KEY"], expires_in=expiration)
|
||||
log.debug(self.id)
|
||||
return s.dumps({ 'id': self.id })
|
||||
return s.dumps({"id": self.id})
|
||||
|
||||
@staticmethod
|
||||
def verify_token(token, accounts):
|
||||
log.debug(f'verifying token {token}')
|
||||
s = Serializer(app.config['SECRET_KEY'])
|
||||
log.debug(f"verifying token {token}")
|
||||
s = Serializer(app.config["SECRET_KEY"])
|
||||
try:
|
||||
data = s.loads(token)
|
||||
except SignatureExpired:
|
||||
return None
|
||||
except BadSignature:
|
||||
return None
|
||||
if config.getboolean('ui', 'jellyfin_login'):
|
||||
if config.getboolean("ui", "jellyfin_login"):
|
||||
for account in accounts:
|
||||
if data['id'] == accounts[account].id:
|
||||
if data["id"] == accounts[account].id:
|
||||
return account
|
||||
else:
|
||||
return accounts['adminAccount']
|
||||
|
||||
return accounts["adminAccount"]
|
||||
|
||||
|
||||
auth = HTTPBasicAuth()
|
||||
|
||||
accounts = {}
|
||||
|
||||
if config.getboolean('ui', 'jellyfin_login'):
|
||||
log.debug('Using jellyfin for admin authentication')
|
||||
if config.getboolean("ui", "jellyfin_login"):
|
||||
log.debug("Using jellyfin for admin authentication")
|
||||
else:
|
||||
log.debug('Using configured login details for admin authentication')
|
||||
accounts['adminAccount'] = Account(config['ui']['username'],
|
||||
config['ui']['password'])
|
||||
log.debug("Using configured login details for admin authentication")
|
||||
accounts["adminAccount"] = Account(
|
||||
config["ui"]["username"], config["ui"]["password"]
|
||||
)
|
||||
|
||||
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
user = None
|
||||
verified = False
|
||||
log.debug('Verifying auth')
|
||||
if config.getboolean('ui', 'jellyfin_login'):
|
||||
log.debug("Verifying auth")
|
||||
if config.getboolean("ui", "jellyfin_login"):
|
||||
try:
|
||||
jf_user = jf.getUsers(username, public=False)
|
||||
id = jf_user['Id']
|
||||
id = jf_user["Id"]
|
||||
user = accounts[id]
|
||||
except KeyError:
|
||||
if config.getboolean('ui', 'admin_only'):
|
||||
if jf_user['Policy']['IsAdministrator']:
|
||||
if config.getboolean("ui", "admin_only"):
|
||||
if jf_user["Policy"]["IsAdministrator"]:
|
||||
user = Account(username)
|
||||
accounts[id] = user
|
||||
else:
|
||||
log.debug(f'User {username} not admin.')
|
||||
log.debug(f"User {username} not admin.")
|
||||
return False
|
||||
else:
|
||||
user = Account(username)
|
||||
@@ -99,11 +107,11 @@ def verify_password(username, password):
|
||||
if user:
|
||||
verified = True
|
||||
if not user:
|
||||
log.debug(f'User {username} not found on Jellyfin')
|
||||
log.debug(f"User {username} not found on Jellyfin")
|
||||
return False
|
||||
else:
|
||||
user = accounts['adminAccount']
|
||||
verified = Account().verify_token(username, accounts)
|
||||
user = accounts["adminAccount"]
|
||||
verified = Account().verify_token(username, accounts)
|
||||
if not verified:
|
||||
if username == user.username and user.verify_password(password):
|
||||
g.user = user
|
||||
@@ -115,6 +123,3 @@ def verify_password(username, password):
|
||||
g.user = user
|
||||
log.debug("HTTPAuth Allowed")
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from jellyfin_accounts import config, data_store
|
||||
from jellyfin_accounts import email_log as log
|
||||
|
||||
|
||||
|
||||
class Watcher:
|
||||
def __init__(self, dir):
|
||||
self.observer = Observer()
|
||||
@@ -20,13 +19,13 @@ class Watcher:
|
||||
try:
|
||||
self.observer.start()
|
||||
except NotADirectoryError:
|
||||
log.error(f'Directory {self.dir} does not exist')
|
||||
log.error(f"Directory {self.dir} does not exist")
|
||||
try:
|
||||
while True:
|
||||
time.sleep(5)
|
||||
except:
|
||||
self.observer.stop()
|
||||
log.info('Watchdog stopped')
|
||||
log.info("Watchdog stopped")
|
||||
|
||||
|
||||
class Handler(FileSystemEventHandler):
|
||||
@@ -34,33 +33,35 @@ class Handler(FileSystemEventHandler):
|
||||
def on_any_event(event):
|
||||
if event.is_directory:
|
||||
return None
|
||||
elif (event.event_type == 'modified' and
|
||||
'passwordreset' in event.src_path):
|
||||
log.debug(f'Password reset file: {event.src_path}')
|
||||
elif event.event_type == "modified" and "passwordreset" in event.src_path:
|
||||
log.debug(f"Password reset file: {event.src_path}")
|
||||
time.sleep(1)
|
||||
with open(event.src_path, 'r') as f:
|
||||
with open(event.src_path, "r") as f:
|
||||
reset = json.load(f)
|
||||
log.info(f'New password reset for {reset["UserName"]}')
|
||||
try:
|
||||
id = jf.getUsers(reset['UserName'], public=False)['Id']
|
||||
id = jf.getUsers(reset["UserName"], public=False)["Id"]
|
||||
address = data_store.emails[id]
|
||||
if address != '':
|
||||
method = config['email']['method']
|
||||
if method == 'mailgun':
|
||||
if address != "":
|
||||
method = config["email"]["method"]
|
||||
if method == "mailgun":
|
||||
email = Mailgun(address)
|
||||
elif method == 'smtp':
|
||||
elif method == "smtp":
|
||||
email = Smtp(address)
|
||||
if email.construct_reset(reset):
|
||||
email.send()
|
||||
else:
|
||||
raise IndexError
|
||||
except (FileNotFoundError,
|
||||
json.decoder.JSONDecodeError,
|
||||
IndexError) as e:
|
||||
err = f'{address}: Failed: ' + type(e).__name__
|
||||
except (
|
||||
FileNotFoundError,
|
||||
json.decoder.JSONDecodeError,
|
||||
IndexError,
|
||||
) as e:
|
||||
err = f"{address}: Failed: " + type(e).__name__
|
||||
log.error(err)
|
||||
|
||||
|
||||
def start():
|
||||
log.info(f'Monitoring {config["password_resets"]["watch_directory"]}')
|
||||
w = Watcher(config['password_resets']['watch_directory'])
|
||||
w = Watcher(config["password_resets"]["watch_directory"])
|
||||
w.run()
|
||||
|
||||
@@ -7,34 +7,36 @@ from jellyfin_accounts.web_api import resp
|
||||
import os
|
||||
|
||||
if first_run:
|
||||
|
||||
def tempJF(server):
|
||||
return Jellyfin(server,
|
||||
config['jellyfin']['client'],
|
||||
config['jellyfin']['version'],
|
||||
config['jellyfin']['device'] + '_temp',
|
||||
config['jellyfin']['device_id'] + '_temp')
|
||||
return Jellyfin(
|
||||
server,
|
||||
config["jellyfin"]["client"],
|
||||
config["jellyfin"]["version"],
|
||||
config["jellyfin"]["device"] + "_temp",
|
||||
config["jellyfin"]["device_id"] + "_temp",
|
||||
)
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('404.html'), 404
|
||||
return render_template("404.html"), 404
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def setup():
|
||||
return render_template('setup.html')
|
||||
return render_template("setup.html")
|
||||
|
||||
@app.route('/<path:path>')
|
||||
@app.route("/<path:path>")
|
||||
def static_proxy(path):
|
||||
if 'html' not in path:
|
||||
if "html" not in path:
|
||||
return app.send_static_file(path)
|
||||
else:
|
||||
return render_template('404.html'), 404
|
||||
return render_template("404.html"), 404
|
||||
|
||||
@app.route('/modifyConfig', methods=['POST'])
|
||||
@app.route("/modifyConfig", methods=["POST"])
|
||||
def modifyConfig():
|
||||
log.info('Config modification requested')
|
||||
log.info("Config modification requested")
|
||||
data = request.get_json()
|
||||
temp_config = RawConfigParser(comment_prefixes='/',
|
||||
allow_no_value=True)
|
||||
temp_config = RawConfigParser(comment_prefixes="/", allow_no_value=True)
|
||||
temp_config.read(config_path)
|
||||
for section in data:
|
||||
if section in temp_config:
|
||||
@@ -42,24 +44,23 @@ if first_run:
|
||||
if item in temp_config[section]:
|
||||
temp_config[section][item] = data[section][item]
|
||||
data[section][item] = True
|
||||
log.debug(f'{section}/{item} modified')
|
||||
log.debug(f"{section}/{item} modified")
|
||||
else:
|
||||
data[section][item] = False
|
||||
log.debug(f'{section}/{item} does not exist in config')
|
||||
with open(config_path, 'w') as config_file:
|
||||
log.debug(f"{section}/{item} does not exist in config")
|
||||
with open(config_path, "w") as config_file:
|
||||
temp_config.write(config_file)
|
||||
log.debug('Config written')
|
||||
log.debug("Config written")
|
||||
# ugly exit, sorry
|
||||
os._exit(1)
|
||||
return resp()
|
||||
|
||||
@app.route('/testJF', methods=['GET', 'POST'])
|
||||
@app.route("/testJF", methods=["GET", "POST"])
|
||||
def testJF():
|
||||
data = request.get_json()
|
||||
tempjf = tempJF(data['jfHost'])
|
||||
tempjf = tempJF(data["jfHost"])
|
||||
try:
|
||||
tempjf.authenticate(data['jfUser'],
|
||||
data['jfPassword'])
|
||||
tempjf.authenticate(data["jfUser"], data["jfPassword"])
|
||||
tempjf.getUsers(public=False)
|
||||
return resp()
|
||||
except:
|
||||
|
||||
@@ -3,33 +3,39 @@ specials = ['[', '@', '_', '!', '#', '$', '%', '^', '&', '*', '(', ')',
|
||||
|
||||
class PasswordValidator:
|
||||
def __init__(self, min_length, upper, lower, number, special):
|
||||
self.criteria = {'characters': int(min_length),
|
||||
'uppercase characters': int(upper),
|
||||
'lowercase characters': int(lower),
|
||||
'numbers': int(number),
|
||||
'special characters': int(special)}
|
||||
self.criteria = {
|
||||
"characters": int(min_length),
|
||||
"uppercase characters": int(upper),
|
||||
"lowercase characters": int(lower),
|
||||
"numbers": int(number),
|
||||
"special characters": int(special),
|
||||
}
|
||||
|
||||
def validate(self, password):
|
||||
count = {'characters': 0,
|
||||
'uppercase characters': 0,
|
||||
'lowercase characters': 0,
|
||||
'numbers': 0,
|
||||
'special characters': 0}
|
||||
count = {
|
||||
"characters": 0,
|
||||
"uppercase characters": 0,
|
||||
"lowercase characters": 0,
|
||||
"numbers": 0,
|
||||
"special characters": 0,
|
||||
}
|
||||
for c in password:
|
||||
count['characters'] += 1
|
||||
count["characters"] += 1
|
||||
if c.isupper():
|
||||
count['uppercase characters'] += 1
|
||||
count["uppercase characters"] += 1
|
||||
elif c.islower():
|
||||
count['lowercase characters'] += 1
|
||||
count["lowercase characters"] += 1
|
||||
elif c.isnumeric():
|
||||
count['numbers'] += 1
|
||||
count["numbers"] += 1
|
||||
elif c in specials:
|
||||
count['special characters'] += 1
|
||||
count["special characters"] += 1
|
||||
for criterion in count:
|
||||
if count[criterion] < self.criteria[criterion]:
|
||||
count[criterion] = False
|
||||
else:
|
||||
count[criterion] = True
|
||||
return count
|
||||
|
||||
def getCriteria(self):
|
||||
lines = {}
|
||||
for criterion in self.criteria:
|
||||
|
||||
@@ -8,63 +8,76 @@ from jellyfin_accounts.web_api import checkInvite, validator
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('404.html',
|
||||
css_href=css['href'],
|
||||
css_integrity=css['integrity'],
|
||||
css_crossorigin=css['crossorigin'],
|
||||
contactMessage=config['ui']['contact_message']), 404
|
||||
return (
|
||||
render_template(
|
||||
"404.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
),
|
||||
404,
|
||||
)
|
||||
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def admin():
|
||||
# return app.send_static_file('admin.html')
|
||||
return render_template('admin.html',
|
||||
css_href=css['href'],
|
||||
css_integrity=css['integrity'],
|
||||
css_crossorigin=css['crossorigin'],
|
||||
contactMessage='',
|
||||
email_enabled=config.getboolean(
|
||||
'invite_emails', 'enabled'))
|
||||
return render_template(
|
||||
"admin.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
contactMessage="",
|
||||
email_enabled=config.getboolean("invite_emails", "enabled"),
|
||||
)
|
||||
|
||||
|
||||
@app.route('/<path:path>')
|
||||
@app.route("/<path:path>")
|
||||
def static_proxy(path):
|
||||
if 'html' not in path:
|
||||
if "html" not in path:
|
||||
return app.send_static_file(path)
|
||||
return render_template('404.html',
|
||||
css_href=css['href'],
|
||||
css_integrity=css['integrity'],
|
||||
css_crossorigin=css['crossorigin'],
|
||||
contactMessage=config['ui']['contact_message']), 404
|
||||
return (
|
||||
render_template(
|
||||
"404.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
),
|
||||
404,
|
||||
)
|
||||
|
||||
|
||||
@app.route('/invite/<path:path>')
|
||||
@app.route("/invite/<path:path>")
|
||||
def inviteProxy(path):
|
||||
if checkInvite(path):
|
||||
log.info(f'Invite {path} used to request form')
|
||||
log.info(f"Invite {path} used to request form")
|
||||
try:
|
||||
email = data_store.invites[path]['email']
|
||||
email = data_store.invites[path]["email"]
|
||||
except KeyError:
|
||||
email = ''
|
||||
return render_template('form.html',
|
||||
css_href=css['href'],
|
||||
css_integrity=css['integrity'],
|
||||
css_crossorigin=css['crossorigin'],
|
||||
contactMessage=config['ui']['contact_message'],
|
||||
helpMessage=config['ui']['help_message'],
|
||||
successMessage=config['ui']['success_message'],
|
||||
jfLink=config['jellyfin']['public_server'],
|
||||
validate=config.getboolean(
|
||||
'password_validation',
|
||||
'enabled'),
|
||||
requirements=validator.getCriteria(),
|
||||
email=email)
|
||||
elif 'admin.html' not in path and 'admin.html' not in path:
|
||||
email = ""
|
||||
return render_template(
|
||||
"form.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
helpMessage=config["ui"]["help_message"],
|
||||
successMessage=config["ui"]["success_message"],
|
||||
jfLink=config["jellyfin"]["public_server"],
|
||||
validate=config.getboolean("password_validation", "enabled"),
|
||||
requirements=validator.getCriteria(),
|
||||
email=email,
|
||||
)
|
||||
elif "admin.html" not in path and "admin.html" not in path:
|
||||
return app.send_static_file(path)
|
||||
else:
|
||||
log.debug('Attempted use of invalid invite')
|
||||
return render_template('invalidCode.html',
|
||||
css_href=css['href'],
|
||||
css_integrity=css['integrity'],
|
||||
css_crossorigin=css['crossorigin'],
|
||||
contactMessage=config['ui']['contact_message'])
|
||||
log.debug("Attempted use of invalid invite")
|
||||
return render_template(
|
||||
"invalidCode.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
)
|
||||
|
||||
@@ -8,27 +8,30 @@ from jellyfin_accounts import config, config_path, app, g, data_store
|
||||
from jellyfin_accounts import web_log as log
|
||||
from jellyfin_accounts.validate_password import PasswordValidator
|
||||
|
||||
|
||||
def resp(success=True, code=500):
|
||||
if success:
|
||||
r = jsonify({'success': True})
|
||||
r = jsonify({"success": True})
|
||||
if code == 500:
|
||||
r.status_code = 200
|
||||
else:
|
||||
r.status_code = code
|
||||
else:
|
||||
r = jsonify({'success': False})
|
||||
r = jsonify({"success": False})
|
||||
r.status_code = code
|
||||
return r
|
||||
|
||||
|
||||
def checkInvite(code, delete=False):
|
||||
current_time = datetime.datetime.now()
|
||||
invites = dict(data_store.invites)
|
||||
match = False
|
||||
for invite in invites:
|
||||
expiry = datetime.datetime.strptime(invites[invite]['valid_till'],
|
||||
'%Y-%m-%dT%H:%M:%S.%f')
|
||||
expiry = datetime.datetime.strptime(
|
||||
invites[invite]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
||||
)
|
||||
if current_time >= expiry:
|
||||
log.debug(f'Housekeeping: Deleting old invite {invite}')
|
||||
log.debug(f"Housekeeping: Deleting old invite {invite}")
|
||||
del data_store.invites[invite]
|
||||
elif invite == code:
|
||||
match = True
|
||||
@@ -36,34 +39,37 @@ def checkInvite(code, delete=False):
|
||||
del data_store.invites[code]
|
||||
return match
|
||||
|
||||
jf = Jellyfin(config['jellyfin']['server'],
|
||||
config['jellyfin']['client'],
|
||||
config['jellyfin']['version'],
|
||||
config['jellyfin']['device'],
|
||||
config['jellyfin']['device_id'])
|
||||
|
||||
jf = Jellyfin(
|
||||
config["jellyfin"]["server"],
|
||||
config["jellyfin"]["client"],
|
||||
config["jellyfin"]["version"],
|
||||
config["jellyfin"]["device"],
|
||||
config["jellyfin"]["device_id"],
|
||||
)
|
||||
|
||||
from jellyfin_accounts.login import auth
|
||||
|
||||
jf_address = config['jellyfin']['server']
|
||||
jf_address = config["jellyfin"]["server"]
|
||||
success = False
|
||||
for i in range(3):
|
||||
try:
|
||||
jf.authenticate(config['jellyfin']['username'],
|
||||
config['jellyfin']['password'])
|
||||
jf.authenticate(config["jellyfin"]["username"], config["jellyfin"]["password"])
|
||||
success = True
|
||||
log.info(f'Successfully authenticated with {jf_address}')
|
||||
log.info(f"Successfully authenticated with {jf_address}")
|
||||
break
|
||||
except Jellyfin.AuthenticationError:
|
||||
log.error(f'Failed to authenticate with {jf_address}, Retrying...')
|
||||
log.error(f"Failed to authenticate with {jf_address}, Retrying...")
|
||||
time.sleep(5)
|
||||
|
||||
if not success:
|
||||
log.error('Could not authenticate after 3 tries.')
|
||||
log.error("Could not authenticate after 3 tries.")
|
||||
exit()
|
||||
|
||||
|
||||
def switchToIds():
|
||||
try:
|
||||
with open(config['files']['emails'], 'r') as f:
|
||||
with open(config["files"]["emails"], "r") as f:
|
||||
emails = json.load(f)
|
||||
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
||||
emails = {}
|
||||
@@ -72,220 +78,227 @@ def switchToIds():
|
||||
match = False
|
||||
for key in emails:
|
||||
for user in users:
|
||||
if user['Name'] == key:
|
||||
if user["Name"] == key:
|
||||
match = True
|
||||
new_emails[user['Id']] = emails[key]
|
||||
elif user['Id'] == key:
|
||||
new_emails[user['Id']] = emails[key]
|
||||
new_emails[user["Id"]] = emails[key]
|
||||
elif user["Id"] == key:
|
||||
new_emails[user["Id"]] = emails[key]
|
||||
if match:
|
||||
from pathlib import Path
|
||||
email_file = Path(config['files']['emails']).name
|
||||
log.info((f'{email_file} modified to use userID instead of ' +
|
||||
'usernames. These will be used in future.'))
|
||||
|
||||
email_file = Path(config["files"]["emails"]).name
|
||||
log.info(
|
||||
(
|
||||
f"{email_file} modified to use userID instead of "
|
||||
+ "usernames. These will be used in future."
|
||||
)
|
||||
)
|
||||
emails = new_emails
|
||||
with open(config['files']['emails'], 'w') as f:
|
||||
with open(config["files"]["emails"], "w") as f:
|
||||
f.write(json.dumps(emails, indent=4))
|
||||
|
||||
|
||||
# Temporary, switches emails.json over from using Usernames to User IDs.
|
||||
switchToIds()
|
||||
|
||||
if config.getboolean('password_validation', 'enabled'):
|
||||
validator = PasswordValidator(config['password_validation']['min_length'],
|
||||
config['password_validation']['upper'],
|
||||
config['password_validation']['lower'],
|
||||
config['password_validation']['number'],
|
||||
config['password_validation']['special'])
|
||||
if config.getboolean("password_validation", "enabled"):
|
||||
validator = PasswordValidator(
|
||||
config["password_validation"]["min_length"],
|
||||
config["password_validation"]["upper"],
|
||||
config["password_validation"]["lower"],
|
||||
config["password_validation"]["number"],
|
||||
config["password_validation"]["special"],
|
||||
)
|
||||
else:
|
||||
validator = PasswordValidator(0, 0, 0, 0, 0)
|
||||
|
||||
|
||||
@app.route('/newUser', methods=['POST'])
|
||||
@app.route("/newUser", methods=["POST"])
|
||||
def newUser():
|
||||
data = request.get_json()
|
||||
log.debug('Attempted newUser')
|
||||
if checkInvite(data['code']):
|
||||
validation = validator.validate(data['password'])
|
||||
log.debug("Attempted newUser")
|
||||
if checkInvite(data["code"]):
|
||||
validation = validator.validate(data["password"])
|
||||
valid = True
|
||||
for criterion in validation:
|
||||
if validation[criterion] is False:
|
||||
valid = False
|
||||
if valid:
|
||||
log.debug('User password valid')
|
||||
log.debug("User password valid")
|
||||
try:
|
||||
user = jf.newUser(data['username'],
|
||||
data['password'])
|
||||
user = jf.newUser(data["username"], data["password"])
|
||||
except Jellyfin.UserExistsError:
|
||||
error = f'User already exists named {data["username"]}'
|
||||
log.debug(error)
|
||||
return jsonify({'error': error})
|
||||
return jsonify({"error": error})
|
||||
except:
|
||||
return jsonify({'error': 'Unknown error'})
|
||||
checkInvite(data['code'], delete=True)
|
||||
return jsonify({"error": "Unknown error"})
|
||||
checkInvite(data["code"], delete=True)
|
||||
if user.status_code == 200:
|
||||
try:
|
||||
policy = data_store.user_template
|
||||
if policy != {}:
|
||||
jf.setPolicy(user.json()['Id'], policy)
|
||||
jf.setPolicy(user.json()["Id"], policy)
|
||||
else:
|
||||
log.debug('user policy was blank')
|
||||
log.debug("user policy was blank")
|
||||
except:
|
||||
log.error('Failed to set new user policy')
|
||||
log.error("Failed to set new user policy")
|
||||
try:
|
||||
configuration = data_store.user_configuration
|
||||
displayprefs = data_store.user_displayprefs
|
||||
if configuration != {} and displayprefs != {}:
|
||||
if jf.setConfiguration(user.json()['Id'],
|
||||
configuration):
|
||||
jf.setDisplayPreferences(user.json()['Id'],
|
||||
displayprefs)
|
||||
log.debug('Set homescreen layout.')
|
||||
if jf.setConfiguration(user.json()["Id"], configuration):
|
||||
jf.setDisplayPreferences(user.json()["Id"], displayprefs)
|
||||
log.debug("Set homescreen layout.")
|
||||
else:
|
||||
log.debug('user configuration and/or ' +
|
||||
'displayprefs were blank')
|
||||
log.debug(
|
||||
"user configuration and/or " + "displayprefs were blank"
|
||||
)
|
||||
except:
|
||||
log.error('Failed to set new user homescreen layout')
|
||||
if config.getboolean('password_resets', 'enabled'):
|
||||
data_store.emails[user.json()['Id']] = data['email']
|
||||
log.debug('Email address stored')
|
||||
log.info('New user created')
|
||||
log.error("Failed to set new user homescreen layout")
|
||||
if config.getboolean("password_resets", "enabled"):
|
||||
data_store.emails[user.json()["Id"]] = data["email"]
|
||||
log.debug("Email address stored")
|
||||
log.info("New user created")
|
||||
else:
|
||||
log.error(f'New user creation failed: {user.status_code}')
|
||||
log.error(f"New user creation failed: {user.status_code}")
|
||||
return resp(False)
|
||||
else:
|
||||
log.debug('User password invalid')
|
||||
log.debug("User password invalid")
|
||||
return jsonify(validation)
|
||||
else:
|
||||
log.debug('Attempted newUser unauthorized')
|
||||
log.debug("Attempted newUser unauthorized")
|
||||
return resp(False, code=401)
|
||||
|
||||
|
||||
@app.route('/generateInvite', methods=['POST'])
|
||||
@app.route("/generateInvite", methods=["POST"])
|
||||
@auth.login_required
|
||||
def generateInvite():
|
||||
current_time = datetime.datetime.now()
|
||||
data = request.get_json()
|
||||
delta = datetime.timedelta(hours=int(data['hours']),
|
||||
minutes=int(data['minutes']))
|
||||
delta = datetime.timedelta(hours=int(data["hours"]), minutes=int(data["minutes"]))
|
||||
invite_code = secrets.token_urlsafe(16)
|
||||
invite = {}
|
||||
log.debug(f'Creating new invite: {invite_code}')
|
||||
log.debug(f"Creating new invite: {invite_code}")
|
||||
valid_till = current_time + delta
|
||||
invite['valid_till'] = valid_till.strftime('%Y-%m-%dT%H:%M:%S.%f')
|
||||
if 'email' in data and config.getboolean('invite_emails', 'enabled'):
|
||||
address = data['email']
|
||||
invite['email'] = address
|
||||
log.info(f'Sending invite to {address}')
|
||||
method = config['email']['method']
|
||||
if method == 'mailgun':
|
||||
invite["valid_till"] = valid_till.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
||||
if "email" in data and config.getboolean("invite_emails", "enabled"):
|
||||
address = data["email"]
|
||||
invite["email"] = address
|
||||
log.info(f"Sending invite to {address}")
|
||||
method = config["email"]["method"]
|
||||
if method == "mailgun":
|
||||
from jellyfin_accounts.email import Mailgun
|
||||
|
||||
email = Mailgun(address)
|
||||
elif method == 'smtp':
|
||||
elif method == "smtp":
|
||||
from jellyfin_accounts.email import Smtp
|
||||
|
||||
email = Smtp(address)
|
||||
email.construct_invite({'expiry': valid_till,
|
||||
'code': invite_code})
|
||||
email.construct_invite({"expiry": valid_till, "code": invite_code})
|
||||
response = email.send()
|
||||
if response is False or type(response) != bool:
|
||||
invite['email'] = f'Failed to send to {address}'
|
||||
invite["email"] = f"Failed to send to {address}"
|
||||
data_store.invites[invite_code] = invite
|
||||
log.info(f'New invite created: {invite_code}')
|
||||
log.info(f"New invite created: {invite_code}")
|
||||
return resp()
|
||||
|
||||
|
||||
@app.route('/getInvites', methods=['GET'])
|
||||
@app.route("/getInvites", methods=["GET"])
|
||||
@auth.login_required
|
||||
def getInvites():
|
||||
log.debug('Invites requested')
|
||||
log.debug("Invites requested")
|
||||
current_time = datetime.datetime.now()
|
||||
invites = dict(data_store.invites)
|
||||
for code in invites:
|
||||
checkInvite(code)
|
||||
invites = dict(data_store.invites)
|
||||
response = {'invites': []}
|
||||
response = {"invites": []}
|
||||
for code in invites:
|
||||
expiry = datetime.datetime.strptime(invites[code]['valid_till'],
|
||||
'%Y-%m-%dT%H:%M:%S.%f')
|
||||
expiry = datetime.datetime.strptime(
|
||||
invites[code]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
||||
)
|
||||
valid_for = expiry - current_time
|
||||
invite = {'code': code,
|
||||
'hours': valid_for.seconds//3600,
|
||||
'minutes': (valid_for.seconds//60) % 60}
|
||||
if 'email' in invites[code]:
|
||||
invite['email'] = invites[code]['email']
|
||||
response['invites'].append(invite)
|
||||
invite = {
|
||||
"code": code,
|
||||
"hours": valid_for.seconds // 3600,
|
||||
"minutes": (valid_for.seconds // 60) % 60,
|
||||
}
|
||||
if "email" in invites[code]:
|
||||
invite["email"] = invites[code]["email"]
|
||||
response["invites"].append(invite)
|
||||
return jsonify(response)
|
||||
|
||||
@app.route('/deleteInvite', methods=['POST'])
|
||||
|
||||
@app.route("/deleteInvite", methods=["POST"])
|
||||
@auth.login_required
|
||||
def deleteInvite():
|
||||
code = request.get_json()['code']
|
||||
code = request.get_json()["code"]
|
||||
invites = dict(data_store.invites)
|
||||
if code in invites:
|
||||
del data_store.invites[code]
|
||||
log.info(f'Invite deleted: {code}')
|
||||
log.info(f"Invite deleted: {code}")
|
||||
return resp()
|
||||
|
||||
|
||||
@app.route('/getToken')
|
||||
@app.route("/getToken")
|
||||
@auth.login_required
|
||||
def get_token():
|
||||
token = g.user.generate_token()
|
||||
return jsonify({'token': token.decode('ascii')})
|
||||
return jsonify({"token": token.decode("ascii")})
|
||||
|
||||
|
||||
@app.route('/getUsers', methods=['GET'])
|
||||
@app.route("/getUsers", methods=["GET"])
|
||||
@auth.login_required
|
||||
def getUsers():
|
||||
log.debug('User and email list requested')
|
||||
response = {'users': []}
|
||||
log.debug("User and email list requested")
|
||||
response = {"users": []}
|
||||
users = jf.getUsers(public=False)
|
||||
emails = data_store.emails
|
||||
for user in users:
|
||||
entry = {'name': user['Name']}
|
||||
if user['Id'] in emails:
|
||||
entry['email'] = emails[user['Id']]
|
||||
response['users'].append(entry)
|
||||
entry = {"name": user["Name"]}
|
||||
if user["Id"] in emails:
|
||||
entry["email"] = emails[user["Id"]]
|
||||
response["users"].append(entry)
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@app.route('/modifyUsers', methods=['POST'])
|
||||
@app.route("/modifyUsers", methods=["POST"])
|
||||
@auth.login_required
|
||||
def modifyUsers():
|
||||
data = request.get_json()
|
||||
log.debug('Email list modification requested')
|
||||
log.debug("Email list modification requested")
|
||||
for key in data:
|
||||
uid = jf.getUsers(key, public=False)['Id']
|
||||
uid = jf.getUsers(key, public=False)["Id"]
|
||||
data_store.emails[uid] = data[key]
|
||||
log.debug(f'Email for user "{key}" modified')
|
||||
return resp()
|
||||
|
||||
|
||||
@app.route('/setDefaults', methods=['POST'])
|
||||
@app.route("/setDefaults", methods=["POST"])
|
||||
@auth.login_required
|
||||
def setDefaults():
|
||||
data = request.get_json()
|
||||
username = data['username']
|
||||
log.debug(f'Storing default settings from user {username}')
|
||||
username = data["username"]
|
||||
log.debug(f"Storing default settings from user {username}")
|
||||
try:
|
||||
user = jf.getUsers(username=username,
|
||||
public=False)
|
||||
user = jf.getUsers(username=username, public=False)
|
||||
except Jellyfin.UserNotFoundError:
|
||||
log.error(f'Storing defaults failed: Couldn\'t find user {username}')
|
||||
log.error(f"Storing defaults failed: Couldn't find user {username}")
|
||||
return resp(False)
|
||||
uid = user['Id']
|
||||
policy = user['Policy']
|
||||
uid = user["Id"]
|
||||
policy = user["Policy"]
|
||||
data_store.user_template = policy
|
||||
if data['homescreen']:
|
||||
configuration = user['Configuration']
|
||||
if data["homescreen"]:
|
||||
configuration = user["Configuration"]
|
||||
try:
|
||||
displayprefs = jf.getDisplayPreferences(uid)
|
||||
data_store.user_configuration = configuration
|
||||
data_store.user_displayprefs = displayprefs
|
||||
except:
|
||||
log.error('Storing defaults failed: ' +
|
||||
'couldn\'t store homescreen layout')
|
||||
log.error("Storing defaults failed: " + "couldn't store homescreen layout")
|
||||
return resp(False)
|
||||
return resp()
|
||||
|
||||
import jellyfin_accounts.setup
|
||||
|
||||
|
||||
import jellyfin_accounts.setup
|
||||
|
||||
Reference in New Issue
Block a user