From 4606415a388999ecb9e8ec8da53d09d9af9e807e Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 29 Jun 2020 22:05:40 +0100 Subject: [PATCH 1/7] added config-base file and config.ini generator --- .gitignore | 2 + jellyfin_accounts/__init__.py | 41 ++- jellyfin_accounts/jf_api.py | 5 +- jellyfin_accounts/setup.py | 3 +- jellyfin_accounts/web_api.py | 47 ++-- tools/config-base.json | 486 ++++++++++++++++++++++++++++++++++ tools/config.ini | 115 ++++++++ tools/generate-config.py | 33 +++ 8 files changed, 701 insertions(+), 31 deletions(-) create mode 100644 tools/config-base.json create mode 100644 tools/config.ini create mode 100644 tools/generate-config.py diff --git a/.gitignore b/.gitignore index c375fd4..ad134ed 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ colors.txt theme.css data/static/bootstrap-jf.css old/ +.jf-accounts/ +tools/__pycache__/ diff --git a/jellyfin_accounts/__init__.py b/jellyfin_accounts/__init__.py index f35352d..697f117 100755 --- a/jellyfin_accounts/__init__.py +++ b/jellyfin_accounts/__init__.py @@ -11,7 +11,7 @@ import signal import sys import json from pathlib import Path -from flask import Flask, g +from flask import Flask, jsonify, g from jellyfin_accounts.data_store import JSONStorage parser = argparse.ArgumentParser(description="jellyfin-accounts") @@ -110,18 +110,21 @@ if "no_username" not in config["email"]: config["email"]["no_username"] = "false" log.debug("Set no_username to false") -with open(config["files"]["invites"], "r") as f: - temp_invites = json.load(f) -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: - f.write(json.dumps(new_invites, indent=4, default=str)) +try: + with open(config["files"]["invites"], "r") as f: + temp_invites = json.load(f) + 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: + f.write(json.dumps(new_invites, indent=4, default=str)) +except FileNotFoundError: + pass data_store = JSONStorage( @@ -195,6 +198,18 @@ if ( config["jellyfin"]["public_server"] = config["jellyfin"]["server"] +def resp(success=True, code=500): + if success: + r = jsonify({"success": True}) + if code == 500: + r.status_code = 200 + else: + r.status_code = code + else: + r = jsonify({"success": False}) + r.status_code = code + return r + def main(): if args.get_defaults: import json diff --git a/jellyfin_accounts/jf_api.py b/jellyfin_accounts/jf_api.py index 8a59e73..33cdf13 100644 --- a/jellyfin_accounts/jf_api.py +++ b/jellyfin_accounts/jf_api.py @@ -79,7 +79,10 @@ class Jellyfin: "User-Agent": self.useragent, "X-Emby-Authorization": self.auth, } - self.info = requests.get(self.server + "/System/Info/Public").json() + try: + self.info = requests.get(self.server + "/System/Info/Public").json() + except: + pass def getUsers(self, username: str = "all", userId: str = "all", public: bool = True): """ diff --git a/jellyfin_accounts/setup.py b/jellyfin_accounts/setup.py index 560c582..34ea48f 100644 --- a/jellyfin_accounts/setup.py +++ b/jellyfin_accounts/setup.py @@ -1,9 +1,8 @@ from flask import request, jsonify, render_template from configparser import RawConfigParser from jellyfin_accounts.jf_api import Jellyfin -from jellyfin_accounts import config, config_path, app, first_run +from jellyfin_accounts import config, config_path, app, first_run, resp from jellyfin_accounts import web_log as log -from jellyfin_accounts.web_api import resp import os if first_run: diff --git a/jellyfin_accounts/web_api.py b/jellyfin_accounts/web_api.py index f7606e5..5742903 100644 --- a/jellyfin_accounts/web_api.py +++ b/jellyfin_accounts/web_api.py @@ -4,24 +4,11 @@ import json import datetime import secrets import time -from jellyfin_accounts import config, config_path, app, g, data_store +from jellyfin_accounts import config, config_path, app, g, data_store, resp, configparser 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}) - if code == 500: - r.status_code = 200 - else: - r.status_code = code - else: - 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) @@ -327,4 +314,34 @@ def setDefaults(): return resp() -import jellyfin_accounts.setup + +@app.route("/modifyConfig", methods=["POST"]) +@auth.login_required +def modifyConfig(): + log.info("Config modification requested") + data = request.get_json() + temp_config = configparser.RawConfigParser(comment_prefixes="/", allow_no_value=True) + temp_config.read(config_path) + for section in data: + if section in temp_config: + for item in data[section]: + if item in temp_config[section]: + temp_config[section][item] = data[section][item] + data[section][item] = True + 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: + temp_config.write(config_file) + log.info("Config written, reloading") + config.read(config_path) + log.info("Config reloaded.") + return resp() + + +@app.route('/getConfig', methods=["GET"]) +@auth.login_required +def getConfig(): + log.debug('Config requested') + return jsonify(config._sections), 200 diff --git a/tools/config-base.json b/tools/config-base.json new file mode 100644 index 0000000..a54cdc4 --- /dev/null +++ b/tools/config-base.json @@ -0,0 +1,486 @@ +{ + "jellyfin": { + "meta": { + "name": "Jellyfin", + "description": "Settings for connecting to Jellyfin" + }, + "username": { + "name": "Jellyfin Username", + "required": true, + "requires_restart": true, + "type": "text", + "value": "username", + "description": "It is recommended to create a limited admin account for this program." + }, + "password": { + "name": "Jellyfin Password", + "required": true, + "requires_restart": true, + "type": "password", + "value": "password" + }, + "server": { + "name": "Server address", + "required": true, + "requires_restart": true, + "type": "text", + "value": "http://jellyfin.local:8096", + "description": "Jellyfin server address. Can be public, or local for security purposes." + }, + "public_server": { + "name": "Public address", + "required": false, + "requires_restart": false, + "type": "text", + "value": "https://jellyf.in:443", + "description": "Publicly accessible Jellyfin address for invite form. Leave blank to reuse the above address." + }, + "client": { + "name": "Client Name", + "required": true, + "requires_restart": true, + "type": "text", + "value": "jf-accounts", + "description": "This and below settings will show on the Jellyfin dashboard when the program connects. You may as well leave them alone." + }, + "version": { + "name": "Version Number", + "required": true, + "requires_restart": true, + "type": "text", + "value": "0.2.4" + }, + "device": { + "name": "Device Name", + "required": true, + "requires_restart": true, + "type": "text", + "value": "jf-accounts" + }, + "device_id": { + "name": "Device ID", + "required": true, + "requires_restart": true, + "type": "text", + "value": "jf-accounts-0.2.4" + } + }, + "ui": { + "meta": { + "name": "General", + "description": "Settings related to the UI and program functionality." + }, + "host": { + "name": "Address", + "required": true, + "requires_restart": true, + "type": "text", + "value": "0.0.0.0", + "description": "Set 0.0.0.0 to run on localhost" + }, + "port": { + "name": "Port", + "required": true, + "requires_restart": true, + "type": "int", + "value": 8056 + }, + "jellyfin_login": { + "name": "Use Jellyfin for authentication", + "required": true, + "requires_restart": true, + "type": "bool", + "value": true, + "description": "Enable this to use Jellyfin users instead of the below username and pw." + }, + "admin_only": { + "name": "Allow admin users only", + "required": false, + "requires_restart": true, + "depends_true": "jellyfin_login", + "type": "bool", + "value": true, + "description": "Allows only admin users on Jellyfin to access the admin page." + }, + "username": { + "name": "Web Username", + "required": true, + "requires_restart": true, + "depends_false": "jellyfin_login", + "type": "text", + "value": "your username", + "description": "Username for admin page (Leave blank if using jellyfin_login)" + }, + "password": { + "name": "Web Password", + "required": true, + "requires_restart": true, + "depends_false": "jellyfin_login", + "type": "password", + "value": "your password", + "description": "Password for admin page (Leave blank if using jellyfin_login)" + }, + "debug": { + "name": "Debug logging", + "required": true, + "requires_restart": true, + "type": "bool", + "value": false + }, + "contact_message": { + "name": "Contact message", + "required": false, + "requires_restart": false, + "type": "text", + "value": "Need help? contact me.", + "description": "Displayed at bottom of all pages except admin" + }, + "help_message": { + "name": "Help message", + "required": false, + "requires_restart": false, + "type": "text", + "value": "Enter your details to create an account.", + "description": "Display at top of invite form." + }, + "success_message": { + "name": "Success message", + "required": false, + "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" + } + }, + "password_validation": { + "meta": { + "name": "Password Validation", + "description": "Password validation (minimum length, etc.)" + }, + "enabled": { + "name": "Enabled", + "required": true, + "requires_restart": true, + "type": "bool", + "value": true + }, + "min_length": { + "name": "Minimum Length", + "requires_restart": true, + "depends_true": "enabled", + "type": "text", + "value": "8" + }, + "upper": { + "name": "Minimum uppercase characters", + "requires_restart": true, + "depends_true": "enabled", + "type": "text", + "value": "1" + }, + "lower": { + "name": "Minimum lowercase characters", + "requires_restart": true, + "depends_true": "enabled", + "type": "text", + "value": "0" + }, + "number": { + "name": "Minimum number count", + "requires_restart": true, + "depends_true": "enabled", + "type": "text", + "value": "1" + }, + "special": { + "name": "Minimum number of special characters", + "requires_restart": true, + "type": "text", + "value": "0" + } + }, + "email": { + "meta": { + "name": "Email", + "description": "General email settings. Ignore if not using email features." + }, + "no_username": { + "name": "Use email addresses as username", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "bool", + "value": false, + "description": "Use email address from invite form as username on Jellyfin." + }, + "use_24h": { + "name": "Use 24h time", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "bool", + "value": true + }, + "date_format": { + "name": "Date format", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "text", + "value": "%d/%m/%y", + "description": "Date format used in emails. Follows datetime.strftime format." + }, + "message": { + "name": "Help message", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "text", + "value": "Need help? contact me.", + "description": "Message displayed at bottom of emails." + }, + "method": { + "name": "Email method", + "required": false, + "requires_restart": false, + "type": "select", + "options": [ + "smtp", + "mailgun" + ], + "value": "smtp", + "description": "Method of sending email to use." + }, + "address": { + "name": "Sent from (address)", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "email", + "value": "jellyfin@jellyf.in", + "description": "Address to send emails from" + }, + "from": { + "Name": "Sent from (name)", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "text", + "value": "Jellyfin", + "description": "The name of the sender" + } + }, + "password_resets": { + "meta": { + "name": "Password Resets", + "description": "Settings for the password reset handler." + }, + "enabled": { + "name": "Enabled", + "required": true, + "requires_restart": true, + "type": "bool", + "value": true, + "description": "Enable to store provided email addresses, monitor Jellyfin directory for pw-resets, and send reset pins" + }, + "watch_directory": { + "name": "Jellyfin directory", + "required": true, + "requires_restart": true, + "depends_true": "enabled", + "type": "text", + "value": "/path/to/jellyfin", + "description": "Path to the folder Jellyfin puts password-reset files." + }, + "email_html": { + "name": "Custom email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email html" + }, + "email_text": { + "name": "Custom email (plaintext)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email in plain text" + }, + "subject": { + "name": "Email subject", + "required": true, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "Password Reset - Jellyfin", + "description": "Subject of password reset emails." + } + }, + "invite_emails": { + "meta": { + "name": "Invite emails", + "description": "Settings for sending invites directly to users." + }, + "enabled": { + "name": "Enabled", + "required": true, + "requires_restart": true, + "type": "bool", + "value": true + }, + "email_html": { + "name": "Custom email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email HTML" + }, + "email_text": { + "name": "Custom email (plaintext)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email in plain text" + }, + "subject": { + "name": "Email subject", + "required": true, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "Invite - Jellyfin", + "description": "Subject of invite emails." + }, + "url_base": { + "name": "URL Base", + "required": true, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "http://accounts.jellyf.in:8056/invite", + "description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself." + } + }, + "mailgun": { + "meta": { + "name": "Mailgun (Email)", + "description": "Mailgun API connection settings" + }, + "api_url": { + "name": "API URL", + "required": false, + "requires_restart": false, + "type": "text", + "value": "https://api.mailgun.net..." + }, + "api_key": { + "name": "API Key", + "required": false, + "requires_restart": false, + "type": "text", + "value": "your api key" + } + }, + "smtp": { + "meta": { + "name": "SMTP (Email)", + "description": "SMTP Server connection settings." + }, + "encryption": { + "name": "Encryption Method", + "required": false, + "requires_restart": false, + "type": "select", + "options": [ + "ssl_tls", + "starttls" + ], + "value": "starttls", + "description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls." + }, + "server": { + "name": "Server address", + "required": false, + "requires_restart": false, + "type": "text", + "value": "smtp.jellyf.in", + "description": "SMTP Server address." + }, + "port": { + "name": "Port", + "required": false, + "requires_restart": false, + "type": "int", + "value": 465 + }, + "password": { + "name": "Password", + "required": false, + "requires_restart": false, + "type": "password", + "value": "smtp password" + } + }, + "files": { + "meta": { + "name": "File Storage", + "description": "Optional settings for changing storage locations." + }, + "invites": { + "name": "Invite Storage", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored invites (json)." + }, + "emails": { + "name": "Email Addresses", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored email addresses (json)." + }, + "user_template": { + "name": "User Template", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored user policy template (json)." + }, + "user_configuration": { + "name": "userConfiguration", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored user configuration template (used for setting homescreen layout) (json)" + }, + "user_displayprefs": { + "name": "displayPreferences", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored displayPreferences template (also used for homescreen layout) (json)" + }, + "custom_css": { + "name": "Custom CSS", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of custom bootstrap CSS." + } + } +} diff --git a/tools/config.ini b/tools/config.ini new file mode 100644 index 0000000..31631d2 --- /dev/null +++ b/tools/config.ini @@ -0,0 +1,115 @@ +[jellyfin] +; settings for connecting to jellyfin +; it is recommended to create a limited admin account for this program. +username = username +password = password +; jellyfin server address. can be public, or local for security purposes. +server = http://jellyfin.local:8096 +; publicly accessible jellyfin address for invite form. leave blank to reuse the above address. +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.2.4 +device = jf-accounts +device_id = jf-accounts-0.2.4 + +[ui] +; settings related to the ui and program functionality. +; set 0.0.0.0 to run on localhost +host = 0.0.0.0 +port = 8056 +; enable this to use jellyfin users instead of the below username and pw. +jellyfin_login = true +; allows only admin users on jellyfin to access the admin page. +admin_only = true +; username for admin page (leave blank if using jellyfin_login) +username = your username +; password for admin page (leave blank if using jellyfin_login) +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. +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. + +[password_validation] +; password validation (minimum length, etc.) +enabled = true +min_length = 8 +upper = 1 +lower = 0 +number = 1 +special = 0 + +[email] +; general email settings. ignore if not using email features. +; use email address from invite form as username on jellyfin. +no_username = false +use_24h = true +; date format used in emails. follows datetime.strftime format. +date_format = %d/%m/%y +; message displayed at bottom of emails. +message = Need help? contact me. +; method of sending email to use. +method = smtp +; address to send emails from +address = jellyfin@jellyf.in +; the name of the sender +from = Jellyfin + +[password_resets] +; settings for the password reset handler. +; enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send reset pins +enabled = true +; path to the folder jellyfin puts password-reset files. +watch_directory = /path/to/jellyfin +; path to custom email html +email_html = +; path to custom email in plain text +email_text = +; subject of password reset emails. +subject = Password Reset - Jellyfin + +[invite_emails] +; settings for sending invites directly to users. +enabled = true +; path to custom email html +email_html = +; path to custom email in plain text +email_text = +; subject of invite emails. +subject = Invite - Jellyfin +; base url for jf-accounts. this is necessary because using a reverse proxy means the program has no way of knowing the url itself. +url_base = http://accounts.jellyf.in:8056/invite + +[mailgun] +; mailgun api connection settings +api_url = https://api.mailgun.net... +api_key = your api key + +[smtp] +; smtp server connection settings. +; your email provider should provide different ports for each encryption method. generally 465 for ssl_tls, 587 for starttls. +encryption = starttls +; smtp server address. +server = smtp.jellyf.in +port = 465 +password = smtp password + +[files] +; optional settings for changing storage locations. +; location of stored invites (json). +invites = +; location of stored email addresses (json). +emails = +; location of stored user policy template (json). +user_template = +; location of stored user configuration template (used for setting homescreen layout) (json) +user_configuration = +; location of stored displaypreferences template (also used for homescreen layout) (json) +user_displayprefs = +; location of custom bootstrap css. +custom_css = + diff --git a/tools/generate-config.py b/tools/generate-config.py new file mode 100644 index 0000000..1d35ab9 --- /dev/null +++ b/tools/generate-config.py @@ -0,0 +1,33 @@ +import configparser, json +from pathlib import Path + +print("This tool generates a config.ini from the base JSON config format.") + +# path = Path(input("Path to config-base.json: ")) +path = 'config-base.json' + +with open(path, 'r') as f: + config_base = json.load(f) + +ini = configparser.RawConfigParser(allow_no_value=True) + +for section in config_base: + ini.add_section(section) + for entry in config_base[section]: + if 'description' in config_base[section][entry]: + ini.set(section, + '; ' + config_base[section][entry]['description']) + if entry != 'meta': + value = config_base[section][entry]['value'] + print(f'{entry} : {type(value)} : should be {config_base[section][entry]["type"]}') + if isinstance(value, bool): + value = str(value).lower() + else: + value = str(value) + ini.set(section, + entry, + value) + +with open('config.ini', 'w') as config_file: + ini.write(config_file) +print("written.") From 55d26b541ac67a5bfb7263b9a518953944280e19 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 29 Jun 2020 23:06:58 +0100 Subject: [PATCH 2/7] dynamically generate default config on first run --- .gitignore | 6 +- jellyfin_accounts/__init__.py | 7 +- .../data}/config-base.json | 0 jellyfin_accounts/data/config-default.ini | 109 ++++++++--------- jellyfin_accounts/generate_ini.py | 32 +++++ pyproject.toml | 2 +- tools/config.ini | 115 ------------------ tools/generate-config.py | 33 ----- 8 files changed, 94 insertions(+), 210 deletions(-) rename {tools => jellyfin_accounts/data}/config-base.json (100%) create mode 100644 jellyfin_accounts/generate_ini.py delete mode 100644 tools/config.ini delete mode 100644 tools/generate-config.py diff --git a/.gitignore b/.gitignore index ad134ed..6096704 100644 --- a/.gitignore +++ b/.gitignore @@ -4,13 +4,13 @@ MANIFEST.in dist/ build/ test.txt -data/node_modules/ +jellyfin_accounts/data/node_modules/ +jellyfin_accounts/data/config-default.ini *.egg-info/ pw-reset/ jfa/ colors.txt theme.css -data/static/bootstrap-jf.css +jellyfin_accounts/data/static/bootstrap-jf.css old/ .jf-accounts/ -tools/__pycache__/ diff --git a/jellyfin_accounts/__init__.py b/jellyfin_accounts/__init__.py index 697f117..f07edb4 100755 --- a/jellyfin_accounts/__init__.py +++ b/jellyfin_accounts/__init__.py @@ -49,10 +49,13 @@ first_run = 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 generating 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)) + from jellyfin_accounts.generate_ini import generate_ini + default_path = local_dir / "config-default.ini" + generate_ini(local_dir / "config-base.json", default_path) + shutil.copy(str(default_path), str(config_path)) print("Setup through the web UI, or quit and edit the configuration manually.") first_run = True else: diff --git a/tools/config-base.json b/jellyfin_accounts/data/config-base.json similarity index 100% rename from tools/config-base.json rename to jellyfin_accounts/data/config-base.json diff --git a/jellyfin_accounts/data/config-default.ini b/jellyfin_accounts/data/config-default.ini index 3c1541d..31631d2 100644 --- a/jellyfin_accounts/data/config-default.ini +++ b/jellyfin_accounts/data/config-default.ini @@ -1,118 +1,115 @@ [jellyfin] -; It is reccommended to create a limited admin account for this program. +; settings for connecting to jellyfin +; it is recommended to create a limited admin account for this program. username = username password = password -; Jellyfin server address. Can be public, or local for security purposes. +; jellyfin server address. can be public, or local for security purposes. server = http://jellyfin.local:8096 -; Publicly accessible Jellyfin address, used on invite form. -; Leave blank to use the same address as above. +; publicly accessible jellyfin address for invite form. leave blank to reuse the above address. 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.1 +version = 0.2.4 device = jf-accounts -device_id = jf-accounts-0.1 +device_id = jf-accounts-0.2.4 [ui] -; Set 0.0.0.0 to run localhost +; settings related to the ui and program functionality. +; set 0.0.0.0 to run on localhost host = 0.0.0.0 port = 8056 -; Enable this to use Jellyfin users instead of the below username and pw. +; enable this to use jellyfin users instead of the below username and pw. jellyfin_login = true -; Allows only admin users on Jellyfin to access admin page. +; allows only admin users on jellyfin to access the admin page. admin_only = true -; Username to use on admin page... (leave blank if using jellyfin_login) +; username for admin page (leave blank if using jellyfin_login) username = your username -; ..and its corresponding password (leave blank if using jellyfin_login) +; password for admin page (leave blank if using jellyfin_login) password = your password - debug = false - -; Displayed at the bottom of all pages except admin +; displayed at bottom of all pages except admin contact_message = Need help? contact me. -; Displayed at top of form page. -help_message = Enter your details to create an account. -; Displayed when an account is created. +; display 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. [password_validation] -; Enables password validation. -enabled = true -; Min. password length +; password validation (minimum length, etc.) +enabled = true min_length = 8 -; Min. number of uppercase characters upper = 1 -; Min. number of lowercase characters lower = 0 -; Min. number of numbers number = 1 -; Min. number of special characters special = 0 [email] -; When true, disables username input on invite form and sets the Jellyfin username to the email address +; general email settings. ignore if not using email features. +; use email address from invite form as username on jellyfin. no_username = false -; Leave the rest of this section if you aren't using any email-related features. use_24h = true -; Date format follows datetime's strftime. +; date format used in emails. follows datetime.strftime format. date_format = %d/%m/%y -; Displayed at bottom of emails +; message displayed at bottom of emails. message = Need help? contact me. -; Mail methods: mailgun, smtp +; method of sending email to use. method = smtp -; Address to send from +; address to send emails from address = jellyfin@jellyf.in -; The name of the sender +; the name of the sender from = Jellyfin [password_resets] -; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin +; settings for the password reset handler. +; enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send reset pins enabled = true -; Directory to monitor for passwordReset*.json files. Usually the jellyfin config directory +; path to the folder jellyfin puts password-reset files. watch_directory = /path/to/jellyfin -; Path to custom email html. If blank, uses the internal template. -email_html = -; Path to alternate plaintext email. If blank, uses the internal template. +; path to custom email html +email_html = +; path to custom email in plain text email_text = -; Subject of emails +; subject of password reset emails. subject = Password Reset - Jellyfin [invite_emails] -; If enabled, allows one to send an invite directly to an email address. +; settings for sending invites directly to users. enabled = true -; Path to custom email html. If blank, uses the internal template. -email_html = -; Path to alternate plaintext email. If blank, uses the internal template. +; path to custom email html +email_html = +; path to custom email in plain text email_text = +; subject of invite emails. subject = Invite - Jellyfin -; Base url for jf-accounts. This necessary because most will use a reverse proxy, so the program has no other way of knowing what URL to send. +; base url for jf-accounts. this is necessary because using a reverse proxy means the program has no way of knowing the url itself. url_base = http://accounts.jellyf.in:8056/invite [mailgun] - +; mailgun api connection settings api_url = https://api.mailgun.net... api_key = your api key [smtp] -; Choose between ssl_tls and starttls. Your provider should tell you which to use, but generally SSL/TLS is 465, STARTTLS 587 +; smtp server connection settings. +; your email provider should provide different ports for each encryption method. generally 465 for ssl_tls, 587 for starttls. encryption = starttls +; smtp server address. server = smtp.jellyf.in -; Uses SMTP_SSL, so make sure the port is for this, not starttls. port = 465 password = smtp password [files] -; When the below paths are left blank, files are stored in ~/.jf-accounts/. - -; Path to store valid invites. +; optional settings for changing storage locations. +; location of stored invites (json). invites = -; Path to store emails addresses in JSON +; location of stored email addresses (json). emails = -; Path to the user policy template. Can be acquired with get-defaults (jf-accounts -g). -user_template = -; Path to the user configuration template (part of homescreen layout). Can be acquired with get-defaults (jf-accounts -g). -user_configuration = -; Path to the user display preferences template (part of homescreen layout). Can be acquired with get-defaults (jf-accounts -g). -user_displayprefs = -; Path to custom bootstrap.css +; location of stored user policy template (json). +user_template = +; location of stored user configuration template (used for setting homescreen layout) (json) +user_configuration = +; location of stored displaypreferences template (also used for homescreen layout) (json) +user_displayprefs = +; location of custom bootstrap css. custom_css = diff --git a/jellyfin_accounts/generate_ini.py b/jellyfin_accounts/generate_ini.py new file mode 100644 index 0000000..1c78bfd --- /dev/null +++ b/jellyfin_accounts/generate_ini.py @@ -0,0 +1,32 @@ +import configparser +import json +from pathlib import Path + +def generate_ini(base_file, ini_file): + """ + Generates .ini file from config-base file. + """ + with open(Path(base_file), 'r') as f: + config_base = json.load(f) + + ini = configparser.RawConfigParser(allow_no_value=True) + + for section in config_base: + ini.add_section(section) + for entry in config_base[section]: + if 'description' in config_base[section][entry]: + ini.set(section, + '; ' + config_base[section][entry]['description']) + if entry != 'meta': + value = config_base[section][entry]['value'] + if isinstance(value, bool): + value = str(value).lower() + else: + value = str(value) + ini.set(section, + entry, + value) + + with open(Path(ini_file), 'w') as config_file: + ini.write(config_file) + return True diff --git a/pyproject.toml b/pyproject.toml index dfbe439..eae05d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jellyfin-accounts" -version = "0.2.3" +version = "0.2.4" readme = "README.md" description = "A simple account management system for Jellyfin" authors = ["Harvey Tindall "] diff --git a/tools/config.ini b/tools/config.ini deleted file mode 100644 index 31631d2..0000000 --- a/tools/config.ini +++ /dev/null @@ -1,115 +0,0 @@ -[jellyfin] -; settings for connecting to jellyfin -; it is recommended to create a limited admin account for this program. -username = username -password = password -; jellyfin server address. can be public, or local for security purposes. -server = http://jellyfin.local:8096 -; publicly accessible jellyfin address for invite form. leave blank to reuse the above address. -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.2.4 -device = jf-accounts -device_id = jf-accounts-0.2.4 - -[ui] -; settings related to the ui and program functionality. -; set 0.0.0.0 to run on localhost -host = 0.0.0.0 -port = 8056 -; enable this to use jellyfin users instead of the below username and pw. -jellyfin_login = true -; allows only admin users on jellyfin to access the admin page. -admin_only = true -; username for admin page (leave blank if using jellyfin_login) -username = your username -; password for admin page (leave blank if using jellyfin_login) -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. -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. - -[password_validation] -; password validation (minimum length, etc.) -enabled = true -min_length = 8 -upper = 1 -lower = 0 -number = 1 -special = 0 - -[email] -; general email settings. ignore if not using email features. -; use email address from invite form as username on jellyfin. -no_username = false -use_24h = true -; date format used in emails. follows datetime.strftime format. -date_format = %d/%m/%y -; message displayed at bottom of emails. -message = Need help? contact me. -; method of sending email to use. -method = smtp -; address to send emails from -address = jellyfin@jellyf.in -; the name of the sender -from = Jellyfin - -[password_resets] -; settings for the password reset handler. -; enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send reset pins -enabled = true -; path to the folder jellyfin puts password-reset files. -watch_directory = /path/to/jellyfin -; path to custom email html -email_html = -; path to custom email in plain text -email_text = -; subject of password reset emails. -subject = Password Reset - Jellyfin - -[invite_emails] -; settings for sending invites directly to users. -enabled = true -; path to custom email html -email_html = -; path to custom email in plain text -email_text = -; subject of invite emails. -subject = Invite - Jellyfin -; base url for jf-accounts. this is necessary because using a reverse proxy means the program has no way of knowing the url itself. -url_base = http://accounts.jellyf.in:8056/invite - -[mailgun] -; mailgun api connection settings -api_url = https://api.mailgun.net... -api_key = your api key - -[smtp] -; smtp server connection settings. -; your email provider should provide different ports for each encryption method. generally 465 for ssl_tls, 587 for starttls. -encryption = starttls -; smtp server address. -server = smtp.jellyf.in -port = 465 -password = smtp password - -[files] -; optional settings for changing storage locations. -; location of stored invites (json). -invites = -; location of stored email addresses (json). -emails = -; location of stored user policy template (json). -user_template = -; location of stored user configuration template (used for setting homescreen layout) (json) -user_configuration = -; location of stored displaypreferences template (also used for homescreen layout) (json) -user_displayprefs = -; location of custom bootstrap css. -custom_css = - diff --git a/tools/generate-config.py b/tools/generate-config.py deleted file mode 100644 index 1d35ab9..0000000 --- a/tools/generate-config.py +++ /dev/null @@ -1,33 +0,0 @@ -import configparser, json -from pathlib import Path - -print("This tool generates a config.ini from the base JSON config format.") - -# path = Path(input("Path to config-base.json: ")) -path = 'config-base.json' - -with open(path, 'r') as f: - config_base = json.load(f) - -ini = configparser.RawConfigParser(allow_no_value=True) - -for section in config_base: - ini.add_section(section) - for entry in config_base[section]: - if 'description' in config_base[section][entry]: - ini.set(section, - '; ' + config_base[section][entry]['description']) - if entry != 'meta': - value = config_base[section][entry]['value'] - print(f'{entry} : {type(value)} : should be {config_base[section][entry]["type"]}') - if isinstance(value, bool): - value = str(value).lower() - else: - value = str(value) - ini.set(section, - entry, - value) - -with open('config.ini', 'w') as config_file: - ini.write(config_file) -print("written.") From 52f9b5c9639764303484508f282b70d4bd6940a9 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 29 Jun 2020 23:23:43 +0100 Subject: [PATCH 3/7] added new /getConfig --- jellyfin_accounts/__init__.py | 6 +++++- jellyfin_accounts/generate_ini.py | 20 ++++++++--------- jellyfin_accounts/web_api.py | 36 +++++++++++++++++++++++++------ 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/jellyfin_accounts/__init__.py b/jellyfin_accounts/__init__.py index f07edb4..86181f6 100755 --- a/jellyfin_accounts/__init__.py +++ b/jellyfin_accounts/__init__.py @@ -44,6 +44,7 @@ else: data_dir = Path.home() / ".jf-accounts" local_dir = (Path(__file__).parent / "data").resolve() +config_base_path = local_dir / "config-base.json" first_run = False if data_dir.exists() is False or (data_dir / "config.ini").exists() is False: @@ -53,8 +54,9 @@ if data_dir.exists() is False or (data_dir / "config.ini").exists() is False: if args.config is None: config_path = data_dir / "config.ini" from jellyfin_accounts.generate_ini import generate_ini + default_path = local_dir / "config-default.ini" - generate_ini(local_dir / "config-base.json", default_path) + generate_ini(config_base_path, default_path) shutil.copy(str(default_path), str(config_path)) print("Setup through the web UI, or quit and edit the configuration manually.") first_run = True @@ -213,6 +215,7 @@ def resp(success=True, code=500): r.status_code = code return r + def main(): if args.get_defaults: import json @@ -306,6 +309,7 @@ def main(): 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["JSON_SORT_KEYS"] = False from waitress import serve diff --git a/jellyfin_accounts/generate_ini.py b/jellyfin_accounts/generate_ini.py index 1c78bfd..31b34d7 100644 --- a/jellyfin_accounts/generate_ini.py +++ b/jellyfin_accounts/generate_ini.py @@ -2,31 +2,29 @@ import configparser import json from pathlib import Path + def generate_ini(base_file, ini_file): """ Generates .ini file from config-base file. """ - with open(Path(base_file), 'r') as f: + with open(Path(base_file), "r") as f: config_base = json.load(f) - + ini = configparser.RawConfigParser(allow_no_value=True) for section in config_base: ini.add_section(section) for entry in config_base[section]: - if 'description' in config_base[section][entry]: - ini.set(section, - '; ' + config_base[section][entry]['description']) - if entry != 'meta': - value = config_base[section][entry]['value'] + if "description" in config_base[section][entry]: + ini.set(section, "; " + config_base[section][entry]["description"]) + if entry != "meta": + value = config_base[section][entry]["value"] if isinstance(value, bool): value = str(value).lower() else: value = str(value) - ini.set(section, - entry, - value) + ini.set(section, entry, value) - with open(Path(ini_file), 'w') as config_file: + with open(Path(ini_file), "w") as config_file: ini.write(config_file) return True diff --git a/jellyfin_accounts/web_api.py b/jellyfin_accounts/web_api.py index 5742903..f53412f 100644 --- a/jellyfin_accounts/web_api.py +++ b/jellyfin_accounts/web_api.py @@ -4,7 +4,16 @@ import json import datetime import secrets import time -from jellyfin_accounts import config, config_path, app, g, data_store, resp, configparser +from jellyfin_accounts import ( + config, + config_path, + app, + g, + data_store, + resp, + configparser, + config_base_path, +) from jellyfin_accounts import web_log as log from jellyfin_accounts.validate_password import PasswordValidator @@ -314,13 +323,14 @@ def setDefaults(): return resp() - @app.route("/modifyConfig", methods=["POST"]) @auth.login_required def modifyConfig(): log.info("Config modification requested") data = request.get_json() - temp_config = configparser.RawConfigParser(comment_prefixes="/", allow_no_value=True) + temp_config = configparser.RawConfigParser( + comment_prefixes="/", allow_no_value=True + ) temp_config.read(config_path) for section in data: if section in temp_config: @@ -340,8 +350,22 @@ def modifyConfig(): return resp() -@app.route('/getConfig', methods=["GET"]) -@auth.login_required +# @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(): log.debug('Config requested') - return jsonify(config._sections), 200 + with open(config_base_path, "r") as f: + config_base = json.load(f) + response_config = config_base + for section in config_base: + for entry in config_base[section]: + if entry in config[section]: + response_config[section][entry]["value"] = config[section][entry] + return jsonify(response_config), 200 From 52a11c3905794e77e770b1abe0a1afa62294c87c Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 29 Jun 2020 23:24:54 +0100 Subject: [PATCH 4/7] auth fix --- jellyfin_accounts/web_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jellyfin_accounts/web_api.py b/jellyfin_accounts/web_api.py index f53412f..cad8ac8 100644 --- a/jellyfin_accounts/web_api.py +++ b/jellyfin_accounts/web_api.py @@ -358,7 +358,7 @@ def modifyConfig(): @app.route("/getConfig", methods=["GET"]) -# @auth.login_required +@auth.login_required def getConfig(): log.debug('Config requested') with open(config_base_path, "r") as f: From eb8e04d5a25eeab1a05636d9a9daf7d2550d506c Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 30 Jun 2020 16:17:40 +0100 Subject: [PATCH 5/7] Added settings menu to UI Currently all setting changes require a restart to apply, so there's a bit of commented out code that i implemented before i realized. Still needs tooltips for each setting. --- jellyfin_accounts/data/config-base.json | 10 +- jellyfin_accounts/data/static/admin.js | 211 +++++++++++++++++++- jellyfin_accounts/data/templates/admin.html | 36 +++- jellyfin_accounts/web_api.py | 5 +- 4 files changed, 241 insertions(+), 21 deletions(-) diff --git a/jellyfin_accounts/data/config-base.json b/jellyfin_accounts/data/config-base.json index a54cdc4..16227fe 100644 --- a/jellyfin_accounts/data/config-base.json +++ b/jellyfin_accounts/data/config-base.json @@ -82,7 +82,7 @@ "name": "Port", "required": true, "requires_restart": true, - "type": "int", + "type": "number", "value": 8056 }, "jellyfin_login": { @@ -261,7 +261,7 @@ "description": "Address to send emails from" }, "from": { - "Name": "Sent from (name)", + "name": "Sent from (name)", "required": false, "requires_restart": false, "depends_true": "method", @@ -285,7 +285,7 @@ }, "watch_directory": { "name": "Jellyfin directory", - "required": true, + "required": false, "requires_restart": true, "depends_true": "enabled", "type": "text", @@ -312,7 +312,7 @@ }, "subject": { "name": "Email subject", - "required": true, + "required": false, "requires_restart": false, "depends_true": "enabled", "type": "text", @@ -418,7 +418,7 @@ "name": "Port", "required": false, "requires_restart": false, - "type": "int", + "type": "number", "value": 465 }, "password": { diff --git a/jellyfin_accounts/data/static/admin.js b/jellyfin_accounts/data/static/admin.js index 8e99840..79f7433 100644 --- a/jellyfin_accounts/data/static/admin.js +++ b/jellyfin_accounts/data/static/admin.js @@ -241,9 +241,6 @@ $("form#loginForm").submit(function() { }); return false; }); -document.getElementById('openSettings').onclick = function () { - $('#settingsMenu').modal('show'); -} document.getElementById('openDefaultsWizard').onclick = function () { this.disabled = true; this.innerHTML = @@ -336,6 +333,7 @@ document.getElementById('storeDefaults').onclick = function () { }, error: function() { button.textContent = 'Failed'; + config_base_path = local_dir / "config-base.json" button.classList.remove('btn-primary'); button.classList.add('btn-danger'); setTimeout(function(){ @@ -449,3 +447,210 @@ document.getElementById('openUsers').onclick = function () { }; generateInvites(empty = true); $("#login").modal('show'); + +var config = {}; +var modifiedConfig = {}; + +document.getElementById('openSettings').onclick = function () { + restart_setting_changed = false; + $.ajax('getConfig', { + type : 'GET', + dataType : 'json', + contentType : 'json', + xhrFields : { + withCredentials: true + }, + beforeSend : function (xhr) { + xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); + }, + complete : function(data) { + if (data['status'] == 200) { + var settingsList = document.getElementById('settingsList'); + settingsList.textContent = ''; + config = data['responseJSON']; + for (var section of Object.keys(config)) { + var sectionCollapse = document.createElement('div'); + sectionCollapse.classList.add('collapse'); + sectionCollapse.id = section; + + var sectionTitle = config[section]['meta']['name']; + var sectionDescription = config[section]['meta']['description']; + var entryListID = section + '_entryList'; + var sectionFooter = section + '_footer'; + + var innerCollapse = ` +
+ ${sectionDescription} +
+
+
+ `; + + sectionCollapse.innerHTML = innerCollapse; + + for (var entry of Object.keys(config[section])) { + if (entry != 'meta') { + var entryName = config[section][entry]['name']; + var required = false; + if (config[section][entry]['required']) { + entryName += ' *'; + required = true; + }; + // if (config[section][entry]['requires_restart']) { + // entryName += ' R'; + // }; + var entryValue = config[section][entry]['value']; + var entryType = config[section][entry]['type']; + var entryGroup = document.createElement('div'); + if (entryType == 'bool') { + entryGroup.classList.add('form-check'); + if (entryValue) { + var checked = true; + } else { + var checked = false; + }; + entryGroup.innerHTML = ` + + + `; + entryGroup.getElementsByClassName('form-check-input')[0].required = required; + entryGroup.getElementsByClassName('form-check-input')[0].checked = checked; + entryGroup.getElementsByClassName('form-check-input')[0].onclick = function() { + var state = this.checked; + for (var sect of Object.keys(config)) { + for (var ent of Object.keys(config[sect])) { + if ((sect + '_' + config[sect][ent]['depends_true']) == this.id) { + document.getElementById(sect + '_' + ent).disabled = !state; + } else if ((sect + '_' + config[sect][ent]['depends_false']) == this.id) { + document.getElementById(sect + '_' + ent).disabled = state; + }; + }; + }; + }; + } else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) { + entryGroup.classList.add('form-group'); + entryGroup.innerHTML = ` + + + `; + entryGroup.getElementsByClassName('form-control')[0].required = required; + } else if (entryType == 'select') { + entryGroup.classList.add('form-group'); + var entryOptions = config[section][entry]['options']; + var innerGroup = ` + + '; + entryGroup.innerHTML = innerGroup; + entryGroup.getElementsByClassName('form-control')[0].required = required; + + }; + sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup); + }; + }; + var sectionButton = document.createElement('button'); + sectionButton.setAttribute('type', 'button'); + sectionButton.classList.add('list-group-item', 'list-group-item-action'); + sectionButton.appendChild(document.createTextNode(sectionTitle)); + sectionButton.id = section + '_button'; + sectionButton.setAttribute('data-toggle', 'collapse'); + sectionButton.setAttribute('data-target', '#' + section); + settingsList.appendChild(sectionButton); + settingsList.appendChild(sectionCollapse); + }; + }; + }, + }); + $('#settingsMenu').modal('show'); +}; + +function sendConfig(modalId) { + var modal = document.getElementById(modalId); + var send = JSON.stringify(modifiedConfig); + $.ajax('/modifyConfig', { + data : send, + contentType : 'application/json', + type : 'POST', + xhrFields : { + withCredentials: true + }, + beforeSend : function (xhr) { + xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":")); + }, + success: function() { + if (modalId != 'settingsMenu') { + $('#' + modalId).modal('hide'); + $('#settingsMenu').modal('hide'); + }; + }, + fail: function(xhr, textStatus, errorThrown) { + var footer = modal.getElementsByClassName('modal-dialog')[0].getElementsByClassName('modal-content')[0].getElementsByClassName('modal-footer')[0]; + var alert = document.createElement('div'); + alert.classList.add('alert', 'alert-danger'); + alert.setAttribute('role', 'alert'); + alert.appendChild(document.createTextNode('Error: ' + errorThrown)); + footer.appendChild(alert); + }, + }); + // placeholder +}; + +document.getElementById('settingsSave').onclick = function() { + modifiedConfig = {}; + // Live config changes have not yet been implemented, so restart always required. + // var restart_setting_changed = false; + var settings_changed = false; + + for (var section of Object.keys(config)) { + for (var entry of Object.keys(config[section])) { + if (entry != 'meta') { + var entryID = section + '_' + entry; + var el = document.getElementById(entryID); + if (el.type == 'checkbox') { + var value = el.checked.toString(); + } else { + var value = el.value.toString(); + }; + if (value != config[section][entry]['value'].toString()) { + if (!modifiedConfig.hasOwnProperty(section)) { + modifiedConfig[section] = {}; + }; + modifiedConfig[section][entry] = value; + settings_changed = true; + // if (config[section][entry]['requires_restart']) { + // restart_setting_changed = true; + // }; + }; + }; + }; + }; + // if (restart_setting_changed) { + if (settings_changed) { + document.getElementById('applyRestarts').onclick = function(){sendConfig('restartModal');}; + $('#settingsMenu').modal('hide'); + $('#restartModal').modal({ + backdrop: 'static', + show: true + }); + } else { + // sendConfig('settingsMenu'); + $('#settingsMenu').modal('hide'); + }; +}; + + + + + + diff --git a/jellyfin_accounts/data/templates/admin.html b/jellyfin_accounts/data/templates/admin.html index 4cf3464..2b2ce52 100644 --- a/jellyfin_accounts/data/templates/admin.html +++ b/jellyfin_accounts/data/templates/admin.html @@ -93,20 +93,20 @@ @@ -153,6 +153,22 @@ +

Accounts admin diff --git a/jellyfin_accounts/web_api.py b/jellyfin_accounts/web_api.py index cad8ac8..cc83060 100644 --- a/jellyfin_accounts/web_api.py +++ b/jellyfin_accounts/web_api.py @@ -344,9 +344,7 @@ def modifyConfig(): log.debug(f"{section}/{item} does not exist in config") with open(config_path, "w") as config_file: temp_config.write(config_file) - log.info("Config written, reloading") - config.read(config_path) - log.info("Config reloaded.") + log.info("Config written. Restart is needed to load settings.") return resp() @@ -363,6 +361,7 @@ def getConfig(): log.debug('Config requested') with open(config_base_path, "r") as f: config_base = json.load(f) + config.read(config_path) response_config = config_base for section in config_base: for entry in config_base[section]: From 8e94f04d5a098cb0c96e06711d2d21c84fe91893 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 30 Jun 2020 18:57:04 +0100 Subject: [PATCH 6/7] Add tooltips; cleanup --- jellyfin_accounts/data/static/admin.js | 22 ++++++++++++++++------ jellyfin_accounts/web_api.py | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/jellyfin_accounts/data/static/admin.js b/jellyfin_accounts/data/static/admin.js index 79f7433..44dd2cf 100644 --- a/jellyfin_accounts/data/static/admin.js +++ b/jellyfin_accounts/data/static/admin.js @@ -291,7 +291,8 @@ document.getElementById('openDefaultsWizard').onclick = function () { } else if (submitButton.classList.contains('btn-danger')) { submitButton.classList.remove('btn-danger'); submitButton.classList.add('btn-primary'); - } + }; + $('#settingsMenu').modal('hide'); $('#userDefaults').modal('show'); } } @@ -440,6 +441,7 @@ document.getElementById('openUsers').onclick = function () { var button = document.getElementById('openUsers'); button.disabled = false; button.innerHTML = 'Users '; + $('#settingsMenu').modal('hide'); $('#users').modal('show'); }; } @@ -496,6 +498,13 @@ document.getElementById('openSettings').onclick = function () { entryName += ' *'; required = true; }; + if (config[section][entry].hasOwnProperty('description')) { + var tooltip = ` + + `; + entryName += ' '; + entryName += tooltip; + }; // if (config[section][entry]['requires_restart']) { // entryName += ' R'; // }; @@ -575,6 +584,12 @@ document.getElementById('openSettings').onclick = function () { $('#settingsMenu').modal('show'); }; +$('#settingsMenu').on('shown.bs.modal', function() { + $("a[data-toggle='tooltip']").each(function (i, obj) { + $(obj).tooltip(); + }); +}); + function sendConfig(modalId) { var modal = document.getElementById(modalId); var send = JSON.stringify(modifiedConfig); @@ -649,8 +664,3 @@ document.getElementById('settingsSave').onclick = function() { }; }; - - - - - diff --git a/jellyfin_accounts/web_api.py b/jellyfin_accounts/web_api.py index cc83060..5d4c687 100644 --- a/jellyfin_accounts/web_api.py +++ b/jellyfin_accounts/web_api.py @@ -358,7 +358,7 @@ def modifyConfig(): @app.route("/getConfig", methods=["GET"]) @auth.login_required def getConfig(): - log.debug('Config requested') + log.debug("Config requested") with open(config_base_path, "r") as f: config_base = json.load(f) config.read(config_path) From 0bb54d1c4561d001603b4412407e70b411dcef94 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 30 Jun 2020 19:58:06 +0100 Subject: [PATCH 7/7] Remove config from readme, bump to 0.2.5 --- README.md | 133 +----------------- .../config-default.ini => config-default.ini | 4 +- jellyfin_accounts/__init__.py | 4 +- jellyfin_accounts/data/config-base.json | 4 +- jellyfin_accounts/generate_ini.py | 7 +- pyproject.toml | 2 +- 6 files changed, 18 insertions(+), 136 deletions(-) rename jellyfin_accounts/data/config-default.ini => config-default.ini (98%) diff --git a/README.md b/README.md index dfa1e7c..10ef966 100644 --- a/README.md +++ b/README.md @@ -68,146 +68,23 @@ optional arguments: ## Setup #### New user template * You may want to restrict a user from accessing certain libraries (e.g 4K Movies), display their account on the login screen by default, or set a default homecrseen layout. Jellyfin stores these settings in the user's policy, configuration and displayPreferences. -* Make a temporary account and change its settings, then run `jf-accounts --get_defaults`. Choose your user, and this data will be stored at the location you set in `user_template`, `user_configuration` and `user_displayprefs` (or their default locations), and used for all subsequent new accounts. +* Make a temporary account and configure it, then in the web UI, go into "Settings => Set new account defaults". Choose the account, and its configuration will be stored for future use. #### Emails/Password Resets * When someone initiates forget password on Jellyfin, a file named `passwordreset*.json` is created in its configuration directory. This directory is monitored and when created, the program reads the username, expiry time and PIN, puts it into a template and sends it to whatever address is specified in `emails.json`. * **The default forget password popup references the `passwordreset*.json` file created. This is confusing for users, so a quick fix is to edit the `MessageForgotPasswordFileCreated` string in Jellyfin's language folder.** * Currently, jellyfin-accounts supports generic SSL/TLS or STARTTLS secured SMTP, and the [mailgun](https://mailgun.com) REST API. * Email html is created using [mjml](https://mjml.io), and [jinja](https://github.com/pallets/jinja) templating is used. If you wish to create your own, ensure you use the same jinja expressions (`{{ pin }}`, etc.) as used in `data/email.mjml` or `invite-email.mjml`, and also create plain text versions for legacy email clients. -### Donations -I strongly suggest you send your money to [Jellyfin](https://opencollective.com/jellyfin) or a good charity, but for those who want to help me out, a Paypal link is below. - -[Donate](https://www.paypal.me/hrfee) - ### Configuration * Note: Make sure to put this behind a reverse proxy with HTTPS. On first run, access the setup wizard at `0.0.0.0:8056`. When finished, restart the program. -The configuration is stored at `~/.jf-accounts/config.ini`. +The configuration is stored at `~/.jf-accounts/config.ini`. Settings can be changed through the web UI, or by manually editing the file. For detailed descriptions of each setting, see [setup](https://github.com/hrfee/jellyfin-accounts/wiki/Setup). +### Donations +I strongly suggest you send your money to [Jellyfin](https://opencollective.com/jellyfin) or a good charity, but for those who want to help me out, a Paypal link is below. -``` -[jellyfin] -; It is reccommended to create a limited admin account for this program. -username = username -password = password -; Jellyfin server address. Can be public, or local for security purposes. -server = http://jellyfin.local:8096 -; Publicly accessible Jellyfin address, used on invite form. -; Leave blank to use the same address as above. -public_server = https://jellyf.in:443 -client = jf-accounts -version = 0.1 -device = jf-accounts -device_id = jf-accounts-0.1 - -[ui] -; Set 0.0.0.0 to run localhost -host = 0.0.0.0 -port = 8056 -; Enable this to use Jellyfin users instead of the below username and pw. -jellyfin_login = true -; Allows only admin users on Jellyfin to access admin page. -admin_only = true -; Username to use on admin page... (leave blank if using jellyfin_login) -username = your username -; ..and its corresponding password (leave blank if using jellyfin_login) -password = your password - -debug = false - -; Displayed at the bottom of all pages except admin -contact_message = Need help? contact me. -; Displayed at top of form page. -help_message = Enter your details to create an account. -; Displayed when an account is created. -success_message = Your account has been created. Click below to continue to Jellyfin. - -[password_validation] -; Enables password validation. -enabled = true -; Min. password length -min_length = 8 -; Min. number of uppercase characters -upper = 1 -; Min. number of lowercase characters -lower = 0 -; Min. number of numbers -number = 1 -; Min. number of special characters -special = 0 - -[email] -; When true, disables username input on invite form and sets the Jellyfin username to the email address -no_username = false -; Leave the rest of this section if you aren't using any email-related features. -use_24h = true -; Date format follows datetime's strftime. -date_format = %d/%m/%y -; Displayed at bottom of emails -message = Need help? contact me. -; Mail methods: mailgun, smtp -method = smtp -; Address to send from -address = jellyfin@jellyf.in -; The name of the sender -from = Jellyfin - -[password_resets] -; Enable to store provided email addresses, monitor jellyfin directory for pw-resets, and send pin -enabled = true -; Directory to monitor for passwordReset*.json files. Usually the jellyfin config directory -watch_directory = /path/to/jellyfin -; Path to custom email html. If blank, uses the internal template. -email_html = -; Path to alternate plaintext email. If blank, uses the internal template. -email_text = -; Subject of emails -subject = Password Reset - Jellyfin - -[invite_emails] -; If enabled, allows one to send an invite directly to an email address. -enabled = true -; Path to custom email html. If blank, uses the internal template. -email_html = -; Path to alternate plaintext email. If blank, uses the internal template. -email_text = -subject = Invite - Jellyfin -; Base url for jf-accounts. This necessary because most will use a reverse proxy, so the program has no other way of knowing what URL to send. -url_base = http://accounts.jellyf.in:8056/invite - -[mailgun] - -api_url = https://api.mailgun.net... -api_key = your api key - -[smtp] -; Choose between ssl_tls and starttls. Your provider should tell you which to use, but generally SSL/TLS is 465, STARTTLS 587 -encryption = starttls -server = smtp.jellyf.in -; Uses SMTP_SSL, so make sure the port is for this, not starttls. -port = 465 -password = smtp password - -[files] -; When the below paths are left blank, files are stored in ~/.jf-accounts/. - -; Path to store valid invites. -invites = -; Path to store emails addresses in JSON -emails = -; Path to the user policy template. Can be acquired with get-defaults (jf-accounts -g). -user_template = -; Path to the user configuration template (part of homescreen layout). Can be acquired with get-defaults (jf-accounts -g). -user_configuration = -; Path to the user display preferences template (part of homescreen layout). Can be acquired with get-defaults (jf-accounts -g). -user_displayprefs = -; Path to custom bootstrap.css -custom_css = -``` - - +[Donate](https://www.paypal.me/hrfee) diff --git a/jellyfin_accounts/data/config-default.ini b/config-default.ini similarity index 98% rename from jellyfin_accounts/data/config-default.ini rename to config-default.ini index 31631d2..cdbdbc9 100644 --- a/jellyfin_accounts/data/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.2.4 +version = 0.2.5 device = jf-accounts -device_id = jf-accounts-0.2.4 +device_id = jf-accounts-0.2.5 [ui] ; settings related to the ui and program functionality. diff --git a/jellyfin_accounts/__init__.py b/jellyfin_accounts/__init__.py index 86181f6..2ca3187 100755 --- a/jellyfin_accounts/__init__.py +++ b/jellyfin_accounts/__init__.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -__version__ = "0.2.2" +__version__ = "0.2.5" import secrets import configparser @@ -56,7 +56,7 @@ if data_dir.exists() is False or (data_dir / "config.ini").exists() is False: from jellyfin_accounts.generate_ini import generate_ini default_path = local_dir / "config-default.ini" - generate_ini(config_base_path, default_path) + generate_ini(config_base_path, default_path, __version__) shutil.copy(str(default_path), str(config_path)) print("Setup through the web UI, or quit and edit the configuration manually.") first_run = True diff --git a/jellyfin_accounts/data/config-base.json b/jellyfin_accounts/data/config-base.json index 16227fe..5b83fa7 100644 --- a/jellyfin_accounts/data/config-base.json +++ b/jellyfin_accounts/data/config-base.json @@ -48,7 +48,7 @@ "required": true, "requires_restart": true, "type": "text", - "value": "0.2.4" + "value": "{version}" }, "device": { "name": "Device Name", @@ -62,7 +62,7 @@ "required": true, "requires_restart": true, "type": "text", - "value": "jf-accounts-0.2.4" + "value": "jf-accounts-{version}" } }, "ui": { diff --git a/jellyfin_accounts/generate_ini.py b/jellyfin_accounts/generate_ini.py index 31b34d7..5a77308 100644 --- a/jellyfin_accounts/generate_ini.py +++ b/jellyfin_accounts/generate_ini.py @@ -3,7 +3,7 @@ import json from pathlib import Path -def generate_ini(base_file, ini_file): +def generate_ini(base_file, ini_file, version): """ Generates .ini file from config-base file. """ @@ -25,6 +25,11 @@ def generate_ini(base_file, ini_file): value = str(value) ini.set(section, entry, value) + ini["jellyfin"]["version"] = version + ini["jellyfin"]["device_id"] = ini["jellyfin"]["device_id"].replace( + "{version}", version + ) + with open(Path(ini_file), "w") as config_file: ini.write(config_file) return True diff --git a/pyproject.toml b/pyproject.toml index eae05d7..795bb84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jellyfin-accounts" -version = "0.2.4" +version = "0.2.5" readme = "README.md" description = "A simple account management system for Jellyfin" authors = ["Harvey Tindall "]