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.")