diff --git a/config-default.ini b/config-default.ini index 4daf69f..93cb739 100644 --- a/config-default.ini +++ b/config-default.ini @@ -9,9 +9,9 @@ server = http://jellyfin.local:8096 public_server = https://jellyf.in:443 ; this and below settings will show on the jellyfin dashboard when the program connects. you may as well leave them alone. client = jf-accounts -version = 0.3.2 +version = 0.3.6 device = jf-accounts -device_id = jf-accounts-0.3.2 +device_id = jf-accounts-0.3.6 [ui] ; settings related to the ui and program functionality. @@ -31,7 +31,7 @@ password = your password debug = false ; displayed at bottom of all pages except admin contact_message = Need help? contact me. -; display at top of invite form. +; displayed at top of invite form. help_message = Enter your details to create an account. ; displayed when a user creates an account success_message = Your account has been created. Click below to continue to Jellyfin. diff --git a/jellyfin_accounts/__init__.py b/jellyfin_accounts/__init__.py index 3e0b451..cb3d76a 100755 --- a/jellyfin_accounts/__init__.py +++ b/jellyfin_accounts/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +# Runs it! __version__ = "0.3.6" import secrets @@ -13,6 +13,7 @@ import json from pathlib import Path from flask import Flask, jsonify, g from jellyfin_accounts.data_store import JSONStorage +from jellyfin_accounts.config import Config parser = argparse.ArgumentParser(description="jellyfin-accounts") @@ -69,7 +70,7 @@ if data_dir.exists() is False or (data_dir / "config.ini").exists() is False: else: config_path = data_dir / "config.ini" - +# Temp config so logger knows whether to use debug mode or not temp_config = configparser.RawConfigParser() temp_config.read(config_path) @@ -93,61 +94,7 @@ def create_log(name): log = create_log("main") - -def load_config(config_path, data_dir): - config = configparser.RawConfigParser() - config.read(config_path) - global log - 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")) - - if "no_username" not in config["email"]: - config["email"]["no_username"] = "false" - log.debug("Set no_username to false") - 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 "bs5" not in config["ui"] or config["ui"]["bs5"] == "": - config["ui"]["bs5"] = "false" - return config - - -config = load_config(config_path, data_dir) +config = Config(config_path, secrets.token_urlsafe(16), data_dir, local_dir, log) web_log = create_log("waitress") if not first_run: diff --git a/jellyfin_accounts/config.py b/jellyfin_accounts/config.py new file mode 100644 index 0000000..203c30c --- /dev/null +++ b/jellyfin_accounts/config.py @@ -0,0 +1,95 @@ +import os +import configparser +import secrets +from pathlib import Path + + +class Config: + """ + Configuration object that can automatically reload modified settings. + Behaves mostly like a dictionary. + :param file: Path to config.ini, where parameters are set. + :param instance: Used to identify specific jf-accounts instances in environment variables. + :param data_dir: Path to directory with config, invites, templates, etc. + :param local_dir: Path to internally stored config base, emails, etc. + """ + + @staticmethod + def load_config(config_path, data_dir, local_dir, log): + config = configparser.RawConfigParser() + config.read(config_path) + 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")) + + if "no_username" not in config["email"]: + config["email"]["no_username"] = "false" + log.debug("Set no_username to false") + 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 "bs5" not in config["ui"] or config["ui"]["bs5"] == "": + config["ui"]["bs5"] = "false" + return config + + def __init__(self, file, instance, data_dir, local_dir, log): + self.config_path = Path(file) + self.data_dir = data_dir + self.local_dir = local_dir + self.instance = instance + self.log = log + self.varname = f"JFA_{self.instance}_RELOADCONFIG" + os.environ[self.varname] = "true" + + def __getitem__(self, key): + if os.environ[self.varname] == "true": + self.config = Config.load_config( + self.config_path, self.data_dir, self.local_dir, self.log + ) + os.environ[self.varname] = "false" + return self.config.__getitem__(key) + + def getboolean(self, sect, key): + if os.environ[self.varname] == "true": + self.config = Config.load_config( + self.config_path, self.data_dir, self.local_dir, self.log + ) + os.environ[self.varname] = "false" + return self.config.getboolean(sect, key) + + def trigger_reload(self): + os.environ[self.varname] = "true" diff --git a/jellyfin_accounts/data/config-base.json b/jellyfin_accounts/data/config-base.json index af11e87..a782f7b 100644 --- a/jellyfin_accounts/data/config-base.json +++ b/jellyfin_accounts/data/config-base.json @@ -143,7 +143,7 @@ "contact_message": { "name": "Contact message", "required": false, - "requires_restart": true, + "requires_restart": false, "type": "text", "value": "Need help? contact me.", "description": "Displayed at bottom of all pages except admin" @@ -151,15 +151,15 @@ "help_message": { "name": "Help message", "required": false, - "requires_restart": true, + "requires_restart": false, "type": "text", "value": "Enter your details to create an account.", - "description": "Display at top of invite form." + "description": "Displayed at top of invite form." }, "success_message": { "name": "Success message", "required": false, - "requires_restart": true, + "requires_restart": false, "type": "text", "value": "Your account has been created. Click below to continue to Jellyfin.", "description": "Displayed when a user creates an account" @@ -167,7 +167,7 @@ "bs5": { "name": "Use Bootstrap 5", "required": false, - "requires_restart": true, + "requires_restart": false, "type": "bool", "value": false, "description": "Use Bootstrap 5 (currently in alpha). This also removes the need for jQuery, so the page should load faster." @@ -181,41 +181,41 @@ "enabled": { "name": "Enabled", "required": false, - "requires_restart": true, + "requires_restart": false, "type": "bool", "value": true }, "min_length": { "name": "Minimum Length", - "requires_restart": true, + "requires_restart": false, "depends_true": "enabled", "type": "text", "value": "8" }, "upper": { "name": "Minimum uppercase characters", - "requires_restart": true, + "requires_restart": false, "depends_true": "enabled", "type": "text", "value": "1" }, "lower": { "name": "Minimum lowercase characters", - "requires_restart": true, + "requires_restart": false, "depends_true": "enabled", "type": "text", "value": "0" }, "number": { "name": "Minimum number count", - "requires_restart": true, + "requires_restart": false, "depends_true": "enabled", "type": "text", "value": "1" }, "special": { "name": "Minimum number of special characters", - "requires_restart": true, + "requires_restart": false, "depends_true": "enabled", "type": "text", "value": "0" @@ -229,7 +229,7 @@ "no_username": { "name": "Use email addresses as username", "required": false, - "requires_restart": true, + "requires_restart": false, "depends_true": "method", "type": "bool", "value": false, @@ -350,7 +350,7 @@ "enabled": { "name": "Enabled", "required": false, - "requires_restart": true, + "requires_restart": false, "type": "bool", "value": true }, diff --git a/jellyfin_accounts/data/templates/admin.html b/jellyfin_accounts/data/templates/admin.html index 13ca279..ae75ba3 100644 --- a/jellyfin_accounts/data/templates/admin.html +++ b/jellyfin_accounts/data/templates/admin.html @@ -35,19 +35,15 @@ {% else %} const bsVersion = 4; {% endif %} - console.log('create'); var css = document.createElement('link'); css.setAttribute('rel', 'stylesheet'); css.setAttribute('type', 'text/css'); var cssCookie = getCookie("css"); if (cssCookie.includes('bs' + bsVersion)) { - console.log('href'); css.setAttribute('href', cssCookie); } else { - console.log('href'); css.setAttribute('href', '{{ css_file }}'); }; - console.log('append'); document.head.appendChild(css); {% if not bs5 %} diff --git a/jellyfin_accounts/data_store.py b/jellyfin_accounts/data_store.py index 9b8f7ef..bcbd925 100644 --- a/jellyfin_accounts/data_store.py +++ b/jellyfin_accounts/data_store.py @@ -1,3 +1,4 @@ +# Automatic storage of everything except the config import json import datetime diff --git a/jellyfin_accounts/email.py b/jellyfin_accounts/email.py index baf7536..9093194 100644 --- a/jellyfin_accounts/email.py +++ b/jellyfin_accounts/email.py @@ -1,3 +1,4 @@ +# Handles everything related to emails import datetime import pytz import requests @@ -43,9 +44,7 @@ class Email: 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' - ) + expires_in = f'{str(expires_in["hours"])}h {str(expires_in["minutes"])}m' log.debug(f"{self.address}: Expires in {expires_in}") return {"date": date, "time": time, "expires_in": expires_in} diff --git a/jellyfin_accounts/generate_ini.py b/jellyfin_accounts/generate_ini.py index 5a77308..f4bcfbb 100644 --- a/jellyfin_accounts/generate_ini.py +++ b/jellyfin_accounts/generate_ini.py @@ -1,3 +1,4 @@ +# Generates config file import configparser import json from pathlib import Path diff --git a/jellyfin_accounts/jf_api.py b/jellyfin_accounts/jf_api.py index be458e1..f1c9b28 100644 --- a/jellyfin_accounts/jf_api.py +++ b/jellyfin_accounts/jf_api.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +# Jellyfin API client import requests import time diff --git a/jellyfin_accounts/login.py b/jellyfin_accounts/login.py index 9df81b3..a17c913 100644 --- a/jellyfin_accounts/login.py +++ b/jellyfin_accounts/login.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +# Handles authentication from flask_httpauth import HTTPBasicAuth from itsdangerous import ( diff --git a/jellyfin_accounts/pw_reset.py b/jellyfin_accounts/pw_reset.py index 293f25a..06b7fe1 100755 --- a/jellyfin_accounts/pw_reset.py +++ b/jellyfin_accounts/pw_reset.py @@ -1,3 +1,4 @@ +# Watches Jellyfin for password resets and sends emails. import time import json from watchdog.observers import Observer diff --git a/jellyfin_accounts/setup.py b/jellyfin_accounts/setup.py index 34ea48f..b0181de 100644 --- a/jellyfin_accounts/setup.py +++ b/jellyfin_accounts/setup.py @@ -1,3 +1,4 @@ +# Views and endpoints for the initial setup from flask import request, jsonify, render_template from configparser import RawConfigParser from jellyfin_accounts.jf_api import Jellyfin diff --git a/jellyfin_accounts/validate_password.py b/jellyfin_accounts/validate_password.py index 8837d2a..5ab675f 100644 --- a/jellyfin_accounts/validate_password.py +++ b/jellyfin_accounts/validate_password.py @@ -1,3 +1,4 @@ +# Password validation specials = ['[', '@', '_', '!', '#', '$', '%', '^', '&', '*', '(', ')', '<', '>', '?', '/', '\\', '|', '}', '{', '~', ':', ']'] diff --git a/jellyfin_accounts/web.py b/jellyfin_accounts/web.py index a972a0d..51d5c21 100644 --- a/jellyfin_accounts/web.py +++ b/jellyfin_accounts/web.py @@ -1,15 +1,16 @@ +# Web views from pathlib import Path from flask import Flask, send_from_directory, render_template -from jellyfin_accounts import app, g, css_file, data_store +from jellyfin_accounts import config, app, g, css_file, data_store from jellyfin_accounts import web_log as log -from jellyfin_accounts.web_api import config, checkInvite, validator +from jellyfin_accounts.web_api import checkInvite, validator -if config.getboolean("ui", "bs5"): - bsVersion = 5 -else: - bsVersion = 4 +def bsVersion(): + if config.getboolean("ui", "bs5"): + return 5 + return 4 @app.errorhandler(404) @@ -42,7 +43,7 @@ def static_proxy(path): if "html" not in path: if "admin.js" in path: return ( - render_template("admin.js", bsVersion=bsVersion, css_file=css_file), + render_template("admin.js", bsVersion=bsVersion(), css_file=css_file), 200, {"Content-Type": "text/javascript"}, ) @@ -75,7 +76,7 @@ def inviteProxy(path): successMessage=config["ui"]["success_message"], jfLink=config["jellyfin"]["public_server"], validate=config.getboolean("password_validation", "enabled"), - requirements=validator.getCriteria(), + requirements=validator().getCriteria(), email=email, username=(not config.getboolean("email", "no_username")), ) diff --git a/jellyfin_accounts/web_api.py b/jellyfin_accounts/web_api.py index 769451f..9041a31 100644 --- a/jellyfin_accounts/web_api.py +++ b/jellyfin_accounts/web_api.py @@ -1,3 +1,4 @@ +# A bit of a mess, but mostly does API endpoints and a couple compatability fixes from flask import request, jsonify from jellyfin_accounts.jf_api import Jellyfin import json @@ -7,8 +8,6 @@ import time from jellyfin_accounts import ( config, config_path, - load_config, - data_dir, app, g, data_store, @@ -136,11 +135,11 @@ if ( version.parse(jf.info["Version"]) >= version.parse("10.6.0") and bool(data_store.user_template) is not False ): - log.info("Updating user_template for Jellyfin >= 10.6.0") if ( data_store.user_template["AuthenticationProviderId"] == "Emby.Server.Implementations.Library.DefaultAuthenticationProvider" ): + log.info("Updating user_template for Jellyfin >= 10.6.0") data_store.user_template[ "AuthenticationProviderId" ] = "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider" @@ -153,16 +152,16 @@ if ( ] = "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider" -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) +def validator(): + if config.getboolean("password_validation", "enabled"): + return PasswordValidator( + config["password_validation"]["min_length"], + config["password_validation"]["upper"], + config["password_validation"]["lower"], + config["password_validation"]["number"], + config["password_validation"]["special"], + ) + return PasswordValidator(0, 0, 0, 0, 0) @app.route("/newUser", methods=["POST"]) @@ -170,7 +169,7 @@ def newUser(): data = request.get_json() log.debug("Attempted newUser") if checkInvite(data["code"]): - validation = validator.validate(data["password"]) + validation = validator().validate(data["password"]) valid = True for criterion in validation: if validation[criterion] is False: @@ -203,9 +202,7 @@ def newUser(): 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"): @@ -392,18 +389,11 @@ def modifyConfig(): log.debug(f"{section}/{item} modified") with open(config_path, "w") as config_file: temp_config.write(config_file) - config = load_config(config_path, data_dir) + config.trigger_reload() log.info("Config written. Restart may be needed to load settings.") return resp() -# @app.route('/getConfig', methods=["GET"]) -# @auth.login_required -# def getConfig(): -# log.debug('Config requested') -# return jsonify(config._sections), 200 - - @app.route("/getConfig", methods=["GET"]) @auth.login_required def getConfig():