diff --git a/.gitignore b/.gitignore index c375fd4..6096704 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +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/ 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/config-default.ini b/config-default.ini new file mode 100644 index 0000000..cdbdbc9 --- /dev/null +++ b/config-default.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.5 +device = jf-accounts +device_id = jf-accounts-0.2.5 + +[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/jellyfin_accounts/__init__.py b/jellyfin_accounts/__init__.py index f35352d..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 @@ -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") @@ -44,15 +44,20 @@ 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: 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(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 else: @@ -110,18 +115,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 +203,19 @@ 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 @@ -288,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/data/config-base.json b/jellyfin_accounts/data/config-base.json new file mode 100644 index 0000000..5b83fa7 --- /dev/null +++ b/jellyfin_accounts/data/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": "{version}" + }, + "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-{version}" + } + }, + "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": "number", + "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": false, + "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": false, + "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": "number", + "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/jellyfin_accounts/data/config-default.ini b/jellyfin_accounts/data/config-default.ini deleted file mode 100644 index 3c1541d..0000000 --- a/jellyfin_accounts/data/config-default.ini +++ /dev/null @@ -1,118 +0,0 @@ -[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 = - diff --git a/jellyfin_accounts/data/static/admin.js b/jellyfin_accounts/data/static/admin.js index 8e99840..44dd2cf 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 = @@ -294,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'); } } @@ -336,6 +334,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(){ @@ -442,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'); }; } @@ -449,3 +449,218 @@ 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].hasOwnProperty('description')) { + var tooltip = ` + + `; + entryName += ' '; + entryName += tooltip; + }; + // 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'); +}; + +$('#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); + $.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/generate_ini.py b/jellyfin_accounts/generate_ini.py new file mode 100644 index 0000000..5a77308 --- /dev/null +++ b/jellyfin_accounts/generate_ini.py @@ -0,0 +1,35 @@ +import configparser +import json +from pathlib import Path + + +def generate_ini(base_file, ini_file, version): + """ + 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) + + 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/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..5d4c687 100644 --- a/jellyfin_accounts/web_api.py +++ b/jellyfin_accounts/web_api.py @@ -4,24 +4,20 @@ 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, + config_base_path, +) 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 +323,48 @@ 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. Restart is 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(): + 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]: + if entry in config[section]: + response_config[section][entry]["value"] = config[section][entry] + return jsonify(response_config), 200 diff --git a/pyproject.toml b/pyproject.toml index dfbe439..795bb84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jellyfin-accounts" -version = "0.2.3" +version = "0.2.5" readme = "README.md" description = "A simple account management system for Jellyfin" authors = ["Harvey Tindall "]