mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2025-12-19 16:51:13 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5af2e7f9d | |||
| dea613fa85 | |||
| b8fdb64f68 | |||
| e80b233af2 | |||
| 3e53bcab27 | |||
| 2551307877 | |||
| 290e6b3dca | |||
| a49b4d9027 | |||
| d615b21c7d | |||
| 9afbd31faa | |||
| 27169e4e0d | |||
| db3b992857 | |||
| 89c132e92e | |||
| 7bda2f4141 | |||
| 71f05f2348 | |||
| 94e69ad090 | |||
| a3d3d97b3b | |||
| 781306f1ef | |||
| a62eab9565 | |||
| a2a2abc7f2 | |||
| fa0527c6a7 | |||
| b33922059c |
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Template for bug reports.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
Describe the problem, and what you would expect if it isn't clear already.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
What to do to reproduce the problem.
|
||||
|
||||
**Logs**
|
||||
|
||||
When you notice the problem, check the output of `jf-accounts`. If the problem is not obvious (e.g an expection or 'ERROR' log), try enabling `debug` in your configuration's `[ui]` section, restarting and reproducing the problem. You should then take a screenshot of the output, or paste it here, preferably between \`\`\` tags (e.g \`\`\``Log here`\`\`\`).
|
||||
If nothing catches your eye in the log, access the admin page via your browser, go into the console (Right click > Inspect Element > Console), refresh, then paste the output here in the same way as above.
|
||||
|
||||
**Platform**
|
||||
|
||||
Include the platform jf-accounts is running on (e.g Windows, Linux, Docker), the python version, and if necessary the browser version and platform.
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -12,8 +12,12 @@ jfa/
|
||||
colors.txt
|
||||
theme.css
|
||||
jellyfin_accounts/__pycache__/
|
||||
jellyfin_accounts/data/static/*.css
|
||||
old/
|
||||
.jf-accounts/
|
||||
requirements.txt
|
||||
package-lock.json
|
||||
video/
|
||||
scss/bs5/*.css*
|
||||
scss/bs4/*.css*
|
||||
mail/*.html
|
||||
jellyfin_accounts/data/*.html
|
||||
|
||||
@@ -9,12 +9,12 @@ A basic account management system for [Jellyfin](https://github.com/jellyfin/jel
|
||||
* Password resets are handled using smtplib, requests, and [jinja](https://github.com/pallets/jinja)
|
||||
## Interface
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/jfa.gif" width="100%"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/jfa.gif" width="100%"></img>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/admin.png" width="48%" style="margin-right: 1.5%;" alt="Admin page"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/create.png" width="48%" style="margin-left: 1.5%;" alt="Account creation page"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/admin.png" width="48%" style="margin-right: 1.5%;" alt="Admin page"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/create.png" width="48%" style="margin-left: 1.5%;" alt="Account creation page"></img>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ server = http://jellyfin.local:8096
|
||||
public_server = https://jellyf.in:443
|
||||
; this and below settings will show on the jellyfin dashboard when the program connects. you may as well leave them alone.
|
||||
client = jf-accounts
|
||||
version = 0.3.0
|
||||
version = 0.3.7
|
||||
device = jf-accounts
|
||||
device_id = jf-accounts-0.3.0
|
||||
device_id = jf-accounts-0.3.7
|
||||
|
||||
[ui]
|
||||
; settings related to the ui and program functionality.
|
||||
; choose the look of jellyfin-accounts.
|
||||
; default appearance for all users.
|
||||
theme = Jellyfin (Dark)
|
||||
; set 0.0.0.0 to run on localhost
|
||||
host = 0.0.0.0
|
||||
@@ -28,10 +28,12 @@ admin_only = true
|
||||
username = your username
|
||||
; password for admin page (leave blank if using jellyfin_login)
|
||||
password = your password
|
||||
; address to send notifications to (leave blank if using jellyfin_login)
|
||||
email = example@example.com
|
||||
debug = false
|
||||
; displayed at bottom of all pages except admin
|
||||
contact_message = Need help? contact me.
|
||||
; display at top of invite form.
|
||||
; displayed at top of invite form.
|
||||
help_message = Enter your details to create an account.
|
||||
; displayed when a user creates an account
|
||||
success_message = Your account has been created. Click below to continue to Jellyfin.
|
||||
@@ -88,6 +90,19 @@ 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
|
||||
|
||||
[notifications]
|
||||
; notification related settings.
|
||||
; enabling adds optional toggles to invites to notify on expiry and user creation.
|
||||
enabled = true
|
||||
; path to expiry notification email html.
|
||||
expiry_html =
|
||||
; path to expiry notification email in plaintext.
|
||||
expiry_text =
|
||||
; path to user creation notification email html.
|
||||
created_html =
|
||||
; path to user creation notification email in plaintext.
|
||||
created_text =
|
||||
|
||||
[mailgun]
|
||||
; mailgun api connection settings
|
||||
api_url = https://api.mailgun.net...
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
__version__ = "0.3.0"
|
||||
# Runs it!
|
||||
__version__ = "0.3.7"
|
||||
|
||||
import secrets
|
||||
import configparser
|
||||
@@ -13,6 +13,7 @@ import json
|
||||
from pathlib import Path
|
||||
from flask import Flask, jsonify, g
|
||||
from jellyfin_accounts.data_store import JSONStorage
|
||||
from jellyfin_accounts.config import Config
|
||||
|
||||
parser = argparse.ArgumentParser(description="jellyfin-accounts")
|
||||
|
||||
@@ -69,7 +70,7 @@ if data_dir.exists() is False or (data_dir / "config.ini").exists() is False:
|
||||
else:
|
||||
config_path = data_dir / "config.ini"
|
||||
|
||||
|
||||
# Temp config so logger knows whether to use debug mode or not
|
||||
temp_config = configparser.RawConfigParser()
|
||||
temp_config.read(config_path)
|
||||
|
||||
@@ -93,61 +94,7 @@ def create_log(name):
|
||||
|
||||
log = create_log("main")
|
||||
|
||||
|
||||
def load_config(config_path, data_dir):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(config_path)
|
||||
global log
|
||||
for key in config["files"]:
|
||||
if config["files"][key] == "":
|
||||
if key != "custom_css":
|
||||
log.debug(f"Using default {key}")
|
||||
config["files"][key] = str(data_dir / (key + ".json"))
|
||||
|
||||
for key in ["user_configuration", "user_displayprefs"]:
|
||||
if key not in config["files"]:
|
||||
log.debug(f"Using default {key}")
|
||||
config["files"][key] = str(data_dir / (key + ".json"))
|
||||
|
||||
if "no_username" not in config["email"]:
|
||||
config["email"]["no_username"] = "false"
|
||||
log.debug("Set no_username to false")
|
||||
if (
|
||||
"email_html" not in config["password_resets"]
|
||||
or config["password_resets"]["email_html"] == ""
|
||||
):
|
||||
log.debug("Using default password reset email HTML template")
|
||||
config["password_resets"]["email_html"] = str(local_dir / "email.html")
|
||||
if (
|
||||
"email_text" not in config["password_resets"]
|
||||
or config["password_resets"]["email_text"] == ""
|
||||
):
|
||||
log.debug("Using default password reset email plaintext template")
|
||||
config["password_resets"]["email_text"] = str(local_dir / "email.txt")
|
||||
|
||||
if (
|
||||
"email_html" not in config["invite_emails"]
|
||||
or config["invite_emails"]["email_html"] == ""
|
||||
):
|
||||
log.debug("Using default invite email HTML template")
|
||||
config["invite_emails"]["email_html"] = str(local_dir / "invite-email.html")
|
||||
if (
|
||||
"email_text" not in config["invite_emails"]
|
||||
or config["invite_emails"]["email_text"] == ""
|
||||
):
|
||||
log.debug("Using default invite email plaintext template")
|
||||
config["invite_emails"]["email_text"] = str(local_dir / "invite-email.txt")
|
||||
if (
|
||||
"public_server" not in config["jellyfin"]
|
||||
or config["jellyfin"]["public_server"] == ""
|
||||
):
|
||||
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
||||
if "bs5" not in config["ui"] or config["ui"]["bs5"] == "":
|
||||
config["ui"]["bs5"] = "false"
|
||||
return config
|
||||
|
||||
|
||||
config = load_config(config_path, data_dir)
|
||||
config = Config(config_path, secrets.token_urlsafe(16), data_dir, local_dir, log)
|
||||
|
||||
web_log = create_log("waitress")
|
||||
if not first_run:
|
||||
@@ -218,7 +165,7 @@ elif "Custom" in current_theme and "custom_css" in config["files"]:
|
||||
try:
|
||||
css_path = Path(config["files"]["custom_css"])
|
||||
shutil.copy(css_path, (local_dir / "static" / css_path.name))
|
||||
log.debug('Loaded custom CSS "{css_path.name}"')
|
||||
log.debug(f'Loaded custom CSS "{css_path.name}"')
|
||||
css_file = css_path.name
|
||||
except FileNotFoundError:
|
||||
log.error(
|
||||
@@ -343,13 +290,6 @@ def main():
|
||||
success = True
|
||||
|
||||
else:
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print("Quitting...")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
global app
|
||||
app = Flask(__name__, root_path=str(local_dir))
|
||||
app.config["DEBUG"] = config.getboolean("ui", "debug")
|
||||
@@ -359,6 +299,13 @@ def main():
|
||||
from waitress import serve
|
||||
|
||||
if first_run:
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print("Quitting...")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
import jellyfin_accounts.setup
|
||||
|
||||
host = config["ui"]["host"]
|
||||
@@ -368,6 +315,7 @@ def main():
|
||||
else:
|
||||
import jellyfin_accounts.web_api
|
||||
import jellyfin_accounts.web
|
||||
import jellyfin_accounts.invite_daemon
|
||||
|
||||
host = config["ui"]["host"]
|
||||
port = config["ui"]["port"]
|
||||
@@ -383,4 +331,12 @@ def main():
|
||||
log.info("Starting email thread")
|
||||
pwr.start()
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print("Quitting...")
|
||||
if config.getboolean("notifications", "enabled"):
|
||||
jellyfin_accounts.invite_daemon.inviteDaemon.stop()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
serve(app, host=host, port=int(port))
|
||||
|
||||
121
jellyfin_accounts/config.py
Normal file
121
jellyfin_accounts/config.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import os
|
||||
import configparser
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
Configuration object that can automatically reload modified settings.
|
||||
Behaves mostly like a dictionary.
|
||||
:param file: Path to config.ini, where parameters are set.
|
||||
:param instance: Used to identify specific jf-accounts instances in environment variables.
|
||||
:param data_dir: Path to directory with config, invites, templates, etc.
|
||||
:param local_dir: Path to internally stored config base, emails, etc.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def load_config(config_path, data_dir, local_dir, log):
|
||||
# Lord forgive me for this mess
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(config_path)
|
||||
for key in config["files"]:
|
||||
if config["files"][key] == "":
|
||||
if key != "custom_css":
|
||||
log.debug(f"Using default {key}")
|
||||
config["files"][key] = str(data_dir / (key + ".json"))
|
||||
|
||||
for key in ["user_configuration", "user_displayprefs"]:
|
||||
if key not in config["files"]:
|
||||
log.debug(f"Using default {key}")
|
||||
config["files"][key] = str(data_dir / (key + ".json"))
|
||||
|
||||
if "no_username" not in config["email"]:
|
||||
config["email"]["no_username"] = "false"
|
||||
log.debug("Set no_username to false")
|
||||
if (
|
||||
"email_html" not in config["password_resets"]
|
||||
or config["password_resets"]["email_html"] == ""
|
||||
):
|
||||
log.debug("Using default password reset email HTML template")
|
||||
config["password_resets"]["email_html"] = str(local_dir / "email.html")
|
||||
if (
|
||||
"email_text" not in config["password_resets"]
|
||||
or config["password_resets"]["email_text"] == ""
|
||||
):
|
||||
log.debug("Using default password reset email plaintext template")
|
||||
config["password_resets"]["email_text"] = str(local_dir / "email.txt")
|
||||
|
||||
if (
|
||||
"email_html" not in config["invite_emails"]
|
||||
or config["invite_emails"]["email_html"] == ""
|
||||
):
|
||||
log.debug("Using default invite email HTML template")
|
||||
config["invite_emails"]["email_html"] = str(local_dir / "invite-email.html")
|
||||
if (
|
||||
"email_text" not in config["invite_emails"]
|
||||
or config["invite_emails"]["email_text"] == ""
|
||||
):
|
||||
log.debug("Using default invite email plaintext template")
|
||||
config["invite_emails"]["email_text"] = str(local_dir / "invite-email.txt")
|
||||
if (
|
||||
"public_server" not in config["jellyfin"]
|
||||
or config["jellyfin"]["public_server"] == ""
|
||||
):
|
||||
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
||||
if "bs5" not in config["ui"] or config["ui"]["bs5"] == "":
|
||||
config["ui"]["bs5"] = "false"
|
||||
if (
|
||||
"expiry_html" not in config["notifications"]
|
||||
or config["notifications"]["expiry_html"] == ""
|
||||
):
|
||||
log.debug("Using default expiry notification HTML template")
|
||||
config["notifications"]["expiry_html"] = str(local_dir / "expired.html")
|
||||
if (
|
||||
"expiry_text" not in config["notifications"]
|
||||
or config["notifications"]["expiry_text"] == ""
|
||||
):
|
||||
log.debug("Using default expiry notification plaintext template")
|
||||
config["notifications"]["expiry_text"] = str(local_dir / "expired.txt")
|
||||
if (
|
||||
"created_html" not in config["notifications"]
|
||||
or config["notifications"]["created_html"] == ""
|
||||
):
|
||||
log.debug("Using default user creation notification HTML template")
|
||||
config["notifications"]["created_html"] = str(local_dir / "created.html")
|
||||
if (
|
||||
"created_text" not in config["notifications"]
|
||||
or config["notifications"]["created_text"] == ""
|
||||
):
|
||||
log.debug("Using default user creation notification plaintext template")
|
||||
config["notifications"]["created_text"] = str(local_dir / "created.txt")
|
||||
|
||||
return config
|
||||
|
||||
def __init__(self, file, instance, data_dir, local_dir, log):
|
||||
self.config_path = Path(file)
|
||||
self.data_dir = data_dir
|
||||
self.local_dir = local_dir
|
||||
self.instance = instance
|
||||
self.log = log
|
||||
self.varname = f"JFA_{self.instance}_RELOADCONFIG"
|
||||
os.environ[self.varname] = "true"
|
||||
|
||||
def __getitem__(self, key):
|
||||
if os.environ[self.varname] == "true":
|
||||
self.config = Config.load_config(
|
||||
self.config_path, self.data_dir, self.local_dir, self.log
|
||||
)
|
||||
os.environ[self.varname] = "false"
|
||||
return self.config.__getitem__(key)
|
||||
|
||||
def getboolean(self, sect, key):
|
||||
if os.environ[self.varname] == "true":
|
||||
self.config = Config.load_config(
|
||||
self.config_path, self.data_dir, self.local_dir, self.log
|
||||
)
|
||||
os.environ[self.varname] = "false"
|
||||
return self.config.getboolean(sect, key)
|
||||
|
||||
def trigger_reload(self):
|
||||
os.environ[self.varname] = "true"
|
||||
@@ -71,7 +71,7 @@
|
||||
"description": "Settings related to the UI and program functionality."
|
||||
},
|
||||
"theme": {
|
||||
"name": "Look",
|
||||
"name": "Default Look",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "select",
|
||||
@@ -81,7 +81,7 @@
|
||||
"Custom CSS"
|
||||
],
|
||||
"value": "Jellyfin (Dark)",
|
||||
"description": "Choose the look of jellyfin-accounts."
|
||||
"description": "Default appearance for all users."
|
||||
},
|
||||
"host": {
|
||||
"name": "Address",
|
||||
@@ -133,6 +133,15 @@
|
||||
"value": "your password",
|
||||
"description": "Password for admin page (Leave blank if using jellyfin_login)"
|
||||
},
|
||||
"email": {
|
||||
"name": "Admin email address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_false": "jellyfin_login",
|
||||
"type": "text",
|
||||
"value": "example@example.com",
|
||||
"description": "Address to send notifications to (Leave blank if using jellyfin_login)"
|
||||
},
|
||||
"debug": {
|
||||
"name": "Debug logging",
|
||||
"required": false,
|
||||
@@ -143,7 +152,7 @@
|
||||
"contact_message": {
|
||||
"name": "Contact message",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Need help? contact me.",
|
||||
"description": "Displayed at bottom of all pages except admin"
|
||||
@@ -151,15 +160,15 @@
|
||||
"help_message": {
|
||||
"name": "Help message",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Enter your details to create an account.",
|
||||
"description": "Display at top of invite form."
|
||||
"description": "Displayed at top of invite form."
|
||||
},
|
||||
"success_message": {
|
||||
"name": "Success message",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
||||
"description": "Displayed when a user creates an account"
|
||||
@@ -167,7 +176,7 @@
|
||||
"bs5": {
|
||||
"name": "Use Bootstrap 5",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use Bootstrap 5 (currently in alpha). This also removes the need for jQuery, so the page should load faster."
|
||||
@@ -181,41 +190,41 @@
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
"min_length": {
|
||||
"name": "Minimum Length",
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "8"
|
||||
},
|
||||
"upper": {
|
||||
"name": "Minimum uppercase characters",
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "1"
|
||||
},
|
||||
"lower": {
|
||||
"name": "Minimum lowercase characters",
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "0"
|
||||
},
|
||||
"number": {
|
||||
"name": "Minimum number count",
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "1"
|
||||
},
|
||||
"special": {
|
||||
"name": "Minimum number of special characters",
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "0"
|
||||
@@ -229,7 +238,7 @@
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
@@ -350,7 +359,7 @@
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
@@ -391,6 +400,56 @@
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"meta": {
|
||||
"name": "Notifications",
|
||||
"description": "Notification related settings."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": "false",
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"description": "Enabling adds optional toggles to invites to notify on expiry and user creation."
|
||||
},
|
||||
"expiry_html": {
|
||||
"name": "Expiry email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to expiry notification email HTML."
|
||||
},
|
||||
"expiry_text": {
|
||||
"name": "Expiry email (Plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": "false",
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to expiry notification email in plaintext."
|
||||
},
|
||||
"created_html": {
|
||||
"name": "User created email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to user creation notification email HTML."
|
||||
},
|
||||
"created_text": {
|
||||
"name": "User created email (Plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to user creation notification email in plaintext."
|
||||
}
|
||||
},
|
||||
"mailgun": {
|
||||
"meta": {
|
||||
"name": "Mailgun (Email)",
|
||||
|
||||
7
jellyfin_accounts/data/created.txt
Normal file
7
jellyfin_accounts/data/created.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
A user was created using code {{ code }}.
|
||||
|
||||
Name: {{ username }}
|
||||
Address: {{ address }}
|
||||
Time: {{ time }}
|
||||
|
||||
Note: Notification emails can be toggled on the admin dashboard.
|
||||
@@ -1,242 +0,0 @@
|
||||
<!-- FILE: email.mjml -->
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||
@import url(https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#f0f0f0;background-color:#f0f0f0;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f0f0f0;background-color:#f0f0f0;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;font-style:bold;line-height:1;text-align:left;color:#000000;">Jellyfin</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:left;color:#000000;">
|
||||
<p>Hi {{ username }},</p>
|
||||
<p> Someone has recently requested a password reset on Jellyfin.</p>
|
||||
<p>If this was you, enter the below pin into the prompt.</p>
|
||||
<p>The code will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}.</p>
|
||||
<p>If this wasn't you, please ignore this email.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#414141;" valign="middle">
|
||||
<p style="display:inline-block;background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
|
||||
{{ pin }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#f0f0f0;background-color:#f0f0f0;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f0f0f0;background-color:#f0f0f0;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:10px;font-style:italic;line-height:1;text-align:left;color:#000000;">{{ message }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,34 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section background-color="#f0f0f0">
|
||||
<mj-column>
|
||||
<mj-text font-style="bold" font-size="20px">
|
||||
Jellyfin
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text font-size="15px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<p>Hi {{ username }},</p>
|
||||
<p> Someone has recently requested a password reset on Jellyfin.</p>
|
||||
<p>If this was you, enter the below pin into the prompt.</p>
|
||||
<p>The code will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}.</p>
|
||||
<p>If this wasn't you, please ignore this email.</p>
|
||||
</mj-text>
|
||||
<mj-button>{{ pin }}</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section background-color="#f0f0f0">
|
||||
<mj-column>
|
||||
<mj-text font-style="italic" font-size="10px">
|
||||
{{ message }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</body>
|
||||
</mjml>
|
||||
|
||||
5
jellyfin_accounts/data/expired.txt
Normal file
5
jellyfin_accounts/data/expired.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Invite expired.
|
||||
|
||||
Code {{ code }} expired at {{ expiry }}.
|
||||
|
||||
Note: Notification emails can be toggled on the admin dashboard.
|
||||
@@ -1,239 +0,0 @@
|
||||
<!-- FILE: invite-email.mjml -->
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||
@import url(https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#f0f0f0;background-color:#f0f0f0;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f0f0f0;background-color:#f0f0f0;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;font-style:bold;line-height:1;text-align:left;color:#000000;">Jellyfin</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Noto Sans, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:left;color:#000000;">
|
||||
<p>Hi,</p>
|
||||
<h2>You've been invited to Jellyfin.</h2>
|
||||
<p>To join, click the button below.</p>
|
||||
<p>This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#414141;" valign="middle">
|
||||
<a href="{{ invite_link }}" style="display:inline-block;background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Setup your account </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#f0f0f0;background-color:#f0f0f0;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f0f0f0;background-color:#f0f0f0;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:10px;font-style:italic;line-height:1;text-align:left;color:#000000;">{{ message }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,33 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section background-color="#f0f0f0">
|
||||
<mj-column>
|
||||
<mj-text font-style="bold" font-size="20px">
|
||||
Jellyfin
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text font-size="15px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<p>Hi,</p>
|
||||
<h2>You've been invited to Jellyfin.</h2>
|
||||
<p>To join, click the button below.</p>
|
||||
<p>This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.</p>
|
||||
</mj-text>
|
||||
<mj-button href="{{ invite_link }}">Setup your account</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section background-color="#f0f0f0">
|
||||
<mj-column>
|
||||
<mj-text font-style="italic" font-size="10px">
|
||||
{{ message }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</body>
|
||||
</mjml>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,6 +16,7 @@ function serializeForm(id) {
|
||||
case 'password':
|
||||
case 'select-one':
|
||||
case 'email':
|
||||
case 'number':
|
||||
formData[name] = el.value;
|
||||
break;
|
||||
};
|
||||
|
||||
@@ -13,16 +13,20 @@ for (var i = 0; i < authRadios.length; i++) {
|
||||
checkAuthRadio();
|
||||
});
|
||||
};
|
||||
|
||||
function checkEmailRadio() {
|
||||
document.getElementById('emailNextButton').href = '#page-5';
|
||||
document.getElementById('valBackButton').href = '#page-7';
|
||||
if (document.getElementById('emailSMTPRadio').checked) {
|
||||
document.getElementById('emailCommonArea').style.display = '';
|
||||
document.getElementById('emailSMTPArea').style.display = '';
|
||||
document.getElementById('emailMailgunArea').style.display = 'none';
|
||||
} else if (document.getElementById('emailMailgunRadio').checked) {
|
||||
document.getElementById('emailCommonArea').style.display = '';
|
||||
document.getElementById('emailSMTPArea').style.display = 'none';
|
||||
document.getElementById('emailMailgunArea').style.display = '';
|
||||
} else if (document.getElementById('emailDisabledRadio').checked) {
|
||||
document.getElementById('emailCommonArea').style.display = 'none';
|
||||
document.getElementById('emailSMTPArea').style.display = 'none';
|
||||
document.getElementById('emailMailgunArea').style.display = 'none';
|
||||
document.getElementById('emailNextButton').href = '#page-8';
|
||||
@@ -35,6 +39,7 @@ for (var i = 0; i < emailRadios.length; i++) {
|
||||
checkEmailRadio();
|
||||
});
|
||||
};
|
||||
|
||||
function checkSSL() {
|
||||
var label = document.getElementById('emailSSL_TLSLabel');
|
||||
if (document.getElementById('emailSSL_TLS').checked) {
|
||||
@@ -101,16 +106,15 @@ document.getElementById('jfTestButton').onclick = function() {
|
||||
jfData['jfHost'] = document.getElementById('jfHost').value;
|
||||
jfData['jfUser'] = document.getElementById('jfUser').value;
|
||||
jfData['jfPassword'] = document.getElementById('jfPassword').value;
|
||||
$.ajax('/testJF', {
|
||||
type : 'POST',
|
||||
dataType : 'json',
|
||||
contentType : 'application/json',
|
||||
data : JSON.stringify(jfData),
|
||||
complete: function(response) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/testJF", true);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
testButton.disabled = false;
|
||||
testButton.className = '';
|
||||
var success = response['responseJSON']['success'];
|
||||
if (success == true) {
|
||||
if (this.response['success'] == true) {
|
||||
testButton.classList.add('btn', 'btn-success');
|
||||
testButton.textContent = 'Success';
|
||||
nextButton.classList.remove('disabled');
|
||||
@@ -118,9 +122,10 @@ document.getElementById('jfTestButton').onclick = function() {
|
||||
} else {
|
||||
testButton.classList.add('btn', 'btn-danger');
|
||||
testButton.textContent = 'Failed';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send(JSON.stringify(jfData));
|
||||
};
|
||||
|
||||
document.getElementById('submitButton').onclick = function() {
|
||||
@@ -138,6 +143,7 @@ document.getElementById('submitButton').onclick = function() {
|
||||
config['invite_emails'] = {};
|
||||
config['mailgun'] = {};
|
||||
config['smtp'] = {};
|
||||
config['notifications'] = {};
|
||||
// Page 2: Auth
|
||||
if (document.getElementById('jfAuthRadio').checked) {
|
||||
config['ui']['jellyfin_login'] = 'true';
|
||||
@@ -149,6 +155,7 @@ document.getElementById('submitButton').onclick = function() {
|
||||
} else {
|
||||
config['ui']['username'] = document.getElementById('manualAuthUsername').value;
|
||||
config['ui']['password'] = document.getElementById('manualAuthPassword').value;
|
||||
config['ui']['email'] = document.getElementById('manualAuthEmail').value;
|
||||
};
|
||||
// Page 3: Connect to jellyfin
|
||||
config['jellyfin']['server'] = document.getElementById('jfHost').value;
|
||||
@@ -158,7 +165,7 @@ document.getElementById('submitButton').onclick = function() {
|
||||
if (document.getElementById('emailDisabledRadio').checked) {
|
||||
config['password_resets']['enabled'] = 'false';
|
||||
config['invite_emails']['enabled'] = 'false';
|
||||
} else {
|
||||
} else {
|
||||
if (document.getElementById('emailSMTPRadio').checked) {
|
||||
if (document.getElementById('emailSSL_TLS').checked) {
|
||||
config['smtp']['encryption'] = 'ssl_tls';
|
||||
@@ -176,6 +183,7 @@ document.getElementById('submitButton').onclick = function() {
|
||||
config['mailgun']['api_key'] = document.getElementById('emailMailgunKey').value;
|
||||
config['email']['address'] = document.getElementById('emailMailgunAddress').value;
|
||||
};
|
||||
config['notifications']['enabled'] = document.getElementById('notificationsEnabled').checked.toString();
|
||||
// Page 5: Email formatting
|
||||
config['email']['from'] = document.getElementById('emailSender').value;
|
||||
config['email']['date_format'] = document.getElementById('emailDateFormat').value;
|
||||
@@ -211,24 +219,24 @@ document.getElementById('submitButton').onclick = function() {
|
||||
config['password_validation']['number'] = document.getElementById('valNumber').value;
|
||||
config['password_validation']['special'] = document.getElementById('valSpecial').value;
|
||||
} else {
|
||||
config['password_validation']['enabled'] = 'false';
|
||||
config['password_validation']['enabled'] = 'false';
|
||||
};
|
||||
// Page 9: Messages
|
||||
config['ui']['contact_message'] = document.getElementById('msgContact').value;
|
||||
config['ui']['help_message'] = document.getElementById('msgHelp').value;
|
||||
config['ui']['success_message'] = document.getElementById('msgSuccess').value;
|
||||
console.log(config);
|
||||
$.ajax('/modifyConfig', {
|
||||
type : 'POST',
|
||||
dataType : 'json',
|
||||
contentType : 'application/json',
|
||||
data : JSON.stringify(config),
|
||||
complete: function(response) {
|
||||
// Send it
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyConfig", true);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
submitButton.disabled = false;
|
||||
submitButton.className = '';
|
||||
submitButton.classList.add('btn', 'btn-success');
|
||||
submitButton.textContent = 'Success';
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
req.send(JSON.stringify(config));
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,38 @@
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<!-- Bootstrap CSS -->
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
<script>
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var decodedCookie = decodeURIComponent(document.cookie);
|
||||
var ca = decodedCookie.split(';');
|
||||
for(var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
{% if bs5 %}
|
||||
const bsVersion = 5;
|
||||
{% else %}
|
||||
const bsVersion = 4;
|
||||
{% endif %}
|
||||
var css = document.createElement('link');
|
||||
css.setAttribute('rel', 'stylesheet');
|
||||
css.setAttribute('type', 'text/css');
|
||||
var cssCookie = getCookie("css");
|
||||
if (cssCookie.includes('bs' + bsVersion)) {
|
||||
css.setAttribute('href', cssCookie);
|
||||
} else {
|
||||
css.setAttribute('href', '{{ css_file }}');
|
||||
};
|
||||
document.head.appendChild(css);
|
||||
</script>
|
||||
{% if not bs5 %}
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
@@ -53,10 +84,42 @@
|
||||
margin-top: 5%;
|
||||
color: grey;
|
||||
}
|
||||
.circle {
|
||||
/*margin-left: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
z-index: 5000;*/
|
||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeInCubic */
|
||||
}
|
||||
.smooth-transition {
|
||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeincubic */
|
||||
}
|
||||
.rotated {
|
||||
transform: rotate(180deg);
|
||||
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||
}
|
||||
|
||||
.not-rotated {
|
||||
transform: rotate(0deg);
|
||||
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||
}
|
||||
</style>
|
||||
<title>Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<body class="smooth-transition">
|
||||
<div class="modal fade" id="login" role="dialog" aria-labelledby="login" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
@@ -171,9 +234,11 @@
|
||||
<h1>
|
||||
Accounts admin
|
||||
</h1>
|
||||
<button type="button" class="btn btn-secondary" id="openSettings">
|
||||
Settings <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
<div class="btn-group" role="group" id="headerButtons">
|
||||
<button type="button" class="btn btn-primary" id="openSettings">
|
||||
Settings <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card mb-3 linkGroup">
|
||||
<div class="card-header">Current Invites</div>
|
||||
<ul class="list-group list-group-flush" id="invites">
|
||||
@@ -183,29 +248,66 @@
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Generate Invite</div>
|
||||
<div class="card-body">
|
||||
<form action="#" method="POST" id="inviteForm">
|
||||
<div class="form-group">
|
||||
<label for="hours">Hours</label>
|
||||
<select class="form-control" id="hours" name="hours">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="minutes">Minutes</label>
|
||||
<select class="form-control" id="minutes" name="minutes">
|
||||
</select>
|
||||
</div>
|
||||
{% if email_enabled %}
|
||||
<div class="form-group">
|
||||
<label for="send_to_address">Send invite to address</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
|
||||
</div>
|
||||
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
||||
<form action="#" method="POST" id="inviteForm" class="container">
|
||||
<div class="row align-items-start">
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="days">Days</label>
|
||||
<select class="form-control form-select" id="days" name="days">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hours">Hours</label>
|
||||
<select class="form-control form-select" id="hours" name="hours">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="minutes">Minutes</label>
|
||||
<select class="form-control form-select" id="minutes" name="minutes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">Generate</button>
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="multiUseCount">
|
||||
Multiple uses
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('multiUseCount').disabled = !this.checked; document.getElementById('noUseLimit').disabled = !this.checked" aria-label="Checkbox to allow choice of invite usage limit" name="multiple-uses" id="multiUseEnabled">
|
||||
</div>
|
||||
<input type="number" class="form-control" name="remaining-uses" id="multiUseCount">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check" style="margin-top: 1rem; margin-bottom: 1rem;">
|
||||
<input class="form-check-input" type="checkbox" value="" name="no-limit" id="noUseLimit" onchange="document.getElementById('multiUseCount').disabled = this.checked; if (this.checked) { document.getElementById('noLimitWarning').style = 'display: block;' } else { document.getElementById('noLimitWarning').style = 'display: none;'; }">
|
||||
<label class="form-check-label" for="noUseLimit">
|
||||
No use limit
|
||||
</label>
|
||||
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
|
||||
</div>
|
||||
{% if email_enabled %}
|
||||
<div class="form-group">
|
||||
<label for="send_to_address">Send invite to address</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
|
||||
</div>
|
||||
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-group d-flex float-right">
|
||||
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -156,6 +156,9 @@
|
||||
submitButton.replaceChild(newSpan, oldSpan);
|
||||
};
|
||||
document.getElementById('accountForm').onsubmit = function() {
|
||||
if (document.getElementById('errorMessage')) {
|
||||
document.getElementById('errorMessage').remove();
|
||||
}
|
||||
toggleSpinner();
|
||||
var send = serializeForm('accountForm');
|
||||
send['code'] = code;
|
||||
@@ -171,12 +174,18 @@
|
||||
if (this.readyState == 4) {
|
||||
toggleSpinner();
|
||||
var data = this.response;
|
||||
if ('error' in data) {
|
||||
var text = document.createTextNode(data['error']);
|
||||
if ('error' in data || data['success'] == false) {
|
||||
if (typeof(data['error']) != 'undefined') {
|
||||
var errorMessage = data['error'];
|
||||
} else {
|
||||
var errorMessage = 'Unknown Error';
|
||||
}
|
||||
var text = document.createTextNode(errorMessage);
|
||||
var error = document.createElement('button');
|
||||
error.classList.add('btn', 'btn-outline-danger');
|
||||
error.setAttribute('disabled', '');
|
||||
error.appendChild(text);
|
||||
error.id = 'errorMessage';
|
||||
document.getElementById('errorBox').appendChild(error);
|
||||
} else {
|
||||
var valid = true
|
||||
|
||||
@@ -89,6 +89,11 @@
|
||||
<label for="manualAuthPassword">Password</label>
|
||||
<input type="password" class="form-control" id="manualAuthPassword" placeholder="Password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manualAuthEmail">Email (Optional)</label>
|
||||
<input type="email" class="form-control" id="manualAuthEmail" placeholder="example@example.com">
|
||||
<small class="form-text text-muted">Your email address is only required if you want to recieve activity notifications.</small>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
@@ -180,6 +185,15 @@
|
||||
<input type="email" class="form-control" id="emailMailgunAddress" placeholder="jellyfin@jellyf.in">
|
||||
</div>
|
||||
</div>
|
||||
<div id="emailCommonArea">
|
||||
<h5 class="card-title">Notifications</h5>
|
||||
<p class="card-text">Enabling notifications will allow you to choose (per-invite) to recieve emails when an invite expires, or when a new user is created. If you chose to use Manual auth instead of Jellyfin auth previously, make sure you provided an email address.</p>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="notificationsEnabled">
|
||||
<label for="notificationsEnabled" class="form-check-label">Enabled</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-3">Back</a>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Automatic storage of everything except the config
|
||||
import json
|
||||
import datetime
|
||||
|
||||
@@ -42,7 +43,10 @@ class JSONFile(dict):
|
||||
def __delitem__(self, key):
|
||||
data = self.readJSON(self.path)
|
||||
super(JSONFile, self).__init__(data)
|
||||
del data[key]
|
||||
try:
|
||||
del data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
self.writeJSON(self.path, data)
|
||||
super(JSONFile, self).__delitem__(key)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Handles everything related to emails
|
||||
import datetime
|
||||
import pytz
|
||||
import requests
|
||||
@@ -12,6 +13,15 @@ from jellyfin_accounts import config
|
||||
from jellyfin_accounts import email_log as log
|
||||
|
||||
|
||||
def format_datetime(dt):
|
||||
result = dt.strftime(config["email"]["date_format"])
|
||||
if config.getboolean("email", "use_24h"):
|
||||
result += f' {dt.strftime("%H:%M")}'
|
||||
else:
|
||||
result += f' {dt.strftime("%I:%M %p")}'
|
||||
return result
|
||||
|
||||
|
||||
class Email:
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
@@ -43,9 +53,7 @@ class Email:
|
||||
if expires_in["hours"] == 0:
|
||||
expires_in = f'{str(expires_in["minutes"])}m'
|
||||
else:
|
||||
expires_in = (
|
||||
f'{str(expires_in["hours"])}h ' + f'{str(expires_in["minutes"])}m'
|
||||
)
|
||||
expires_in = f'{str(expires_in["hours"])}h {str(expires_in["minutes"])}m'
|
||||
log.debug(f"{self.address}: Expires in {expires_in}")
|
||||
return {"date": date, "time": time, "expires_in": expires_in}
|
||||
|
||||
@@ -76,6 +84,47 @@ class Email:
|
||||
self.content[key] = c
|
||||
log.info(f"{self.address}: {key} constructed")
|
||||
|
||||
def construct_expiry(self, invite):
|
||||
self.subject = "Notice: Invite expired"
|
||||
log.debug(f'Constructing expiry notification for {invite["code"]}')
|
||||
expiry = format_datetime(invite["expiry"])
|
||||
for key in ["text", "html"]:
|
||||
sp = Path(config["notifications"]["expiry_" + key]) / ".."
|
||||
sp = str(sp.resolve()) + "/"
|
||||
template_loader = FileSystemLoader(searchpath=sp)
|
||||
template_env = Environment(loader=template_loader)
|
||||
fname = Path(config["notifications"]["expiry_" + key]).name
|
||||
template = template_env.get_template(fname)
|
||||
c = template.render(code=invite["code"], expiry=expiry)
|
||||
self.content[key] = c
|
||||
log.info(f"{self.address}: {key} constructed")
|
||||
return True
|
||||
|
||||
def construct_created(self, invite):
|
||||
self.subject = "Notice: User created"
|
||||
log.debug(f'Constructing expiry notification for {invite["code"]}')
|
||||
created = format_datetime(invite["created"])
|
||||
if config.getboolean("email", "no_username"):
|
||||
email = "n/a"
|
||||
else:
|
||||
email = invite["address"]
|
||||
for key in ["text", "html"]:
|
||||
sp = Path(config["notifications"]["created_" + key]) / ".."
|
||||
sp = str(sp.resolve()) + "/"
|
||||
template_loader = FileSystemLoader(searchpath=sp)
|
||||
template_env = Environment(loader=template_loader)
|
||||
fname = Path(config["notifications"]["created_" + key]).name
|
||||
template = template_env.get_template(fname)
|
||||
c = template.render(
|
||||
code=invite["code"],
|
||||
username=invite["username"],
|
||||
address=email,
|
||||
time=created,
|
||||
)
|
||||
self.content[key] = c
|
||||
log.info(f"{self.address}: {key} constructed")
|
||||
return True
|
||||
|
||||
def construct_reset(self, reset):
|
||||
self.subject = config["password_resets"]["subject"]
|
||||
log.debug(f"{self.address}: Using subject {self.subject}")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Generates config file
|
||||
import configparser
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
42
jellyfin_accounts/invite_daemon.py
Normal file
42
jellyfin_accounts/invite_daemon.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from threading import Timer
|
||||
import time
|
||||
from jellyfin_accounts import config, data_store
|
||||
from jellyfin_accounts.web_api import checkInvite
|
||||
|
||||
|
||||
class Repeat:
|
||||
def __init__(self, interval, function, *args, **kwargs):
|
||||
self._timer = None
|
||||
self.interval = interval
|
||||
self.function = function
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.is_running = False
|
||||
self.next_call = time.time()
|
||||
self.start()
|
||||
|
||||
def _run(self):
|
||||
self.is_running = False
|
||||
self.start()
|
||||
self.function(*self.args, **self.kwargs)
|
||||
|
||||
def start(self):
|
||||
if not self.is_running:
|
||||
self.next_call += self.interval
|
||||
self._timer = Timer(self.next_call - time.time(), self._run)
|
||||
self._timer.start()
|
||||
self.is_running = True
|
||||
|
||||
def stop(self):
|
||||
self._timer.cancel()
|
||||
self.is_running = False
|
||||
|
||||
|
||||
def checkInvites():
|
||||
invites = dict(data_store.invites)
|
||||
# checkInvite already loops over everything, no point running it multiple times.
|
||||
checkInvite(list(invites.keys())[0])
|
||||
|
||||
|
||||
if config.getboolean("notifications", "enabled"):
|
||||
inviteDaemon = Repeat(60, checkInvites)
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Jellyfin API client
|
||||
import requests
|
||||
import time
|
||||
|
||||
@@ -190,7 +190,7 @@ class Jellyfin:
|
||||
)
|
||||
|
||||
def newUser(self, username: str, password: str):
|
||||
for user in self.getUsers():
|
||||
for user in self.getUsers(public=False):
|
||||
if user["Name"] == username:
|
||||
raise self.UserExistsError
|
||||
response = requests.post(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Handles authentication
|
||||
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from itsdangerous import (
|
||||
@@ -106,6 +106,8 @@ def verify_password(username, password):
|
||||
user = Account().verify_token(username, accounts)
|
||||
if user:
|
||||
verified = True
|
||||
if user in accounts:
|
||||
user = accounts[user]
|
||||
if not user:
|
||||
log.debug(f"User {username} not found on Jellyfin")
|
||||
return False
|
||||
@@ -116,10 +118,10 @@ def verify_password(username, password):
|
||||
if username == user.username and user.verify_password(password):
|
||||
g.user = user
|
||||
log.debug("HTTPAuth Allowed")
|
||||
return True
|
||||
return user
|
||||
else:
|
||||
log.debug("HTTPAuth Denied")
|
||||
return False
|
||||
g.user = user
|
||||
log.debug("HTTPAuth Allowed")
|
||||
return True
|
||||
return user
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Watches Jellyfin for password resets and sends emails.
|
||||
import time
|
||||
import json
|
||||
from watchdog.observers import Observer
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Views and endpoints for the initial setup
|
||||
from flask import request, jsonify, render_template
|
||||
from configparser import RawConfigParser
|
||||
from jellyfin_accounts.jf_api import Jellyfin
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Password validation
|
||||
specials = ['[', '@', '_', '!', '#', '$', '%', '^', '&', '*', '(', ')',
|
||||
'<', '>', '?', '/', '\\', '|', '}', '{', '~', ':', ']']
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
# Web views
|
||||
from pathlib import Path
|
||||
from flask import Flask, send_from_directory, render_template
|
||||
|
||||
from jellyfin_accounts import app, g, css_file, data_store
|
||||
from jellyfin_accounts import config, app, g, css_file, data_store
|
||||
from jellyfin_accounts import web_log as log
|
||||
from jellyfin_accounts.web_api import config, checkInvite, validator
|
||||
from jellyfin_accounts.web_api import checkInvite, validator
|
||||
|
||||
|
||||
def bsVersion():
|
||||
if config.getboolean("ui", "bs5"):
|
||||
return 5
|
||||
return 4
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
@@ -35,11 +42,16 @@ def admin():
|
||||
def static_proxy(path):
|
||||
if "html" not in path:
|
||||
if "admin.js" in path:
|
||||
if config.getboolean("ui", "bs5"):
|
||||
bsVersion = 5
|
||||
else:
|
||||
bsVersion = 4
|
||||
return render_template("admin.js", bsVersion=bsVersion)
|
||||
return (
|
||||
render_template(
|
||||
"admin.js",
|
||||
bsVersion=bsVersion(),
|
||||
css_file=css_file,
|
||||
notifications=config.getboolean("notifications", "enabled"),
|
||||
),
|
||||
200,
|
||||
{"Content-Type": "text/javascript"},
|
||||
)
|
||||
return app.send_static_file(path)
|
||||
return (
|
||||
render_template(
|
||||
@@ -69,7 +81,7 @@ def inviteProxy(path):
|
||||
successMessage=config["ui"]["success_message"],
|
||||
jfLink=config["jellyfin"]["public_server"],
|
||||
validate=config.getboolean("password_validation", "enabled"),
|
||||
requirements=validator.getCriteria(),
|
||||
requirements=validator().getCriteria(),
|
||||
email=email,
|
||||
username=(not config.getboolean("email", "no_username")),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# A bit of a mess, but mostly does API endpoints and a couple compatability fixes
|
||||
from flask import request, jsonify
|
||||
from jellyfin_accounts.jf_api import Jellyfin
|
||||
import json
|
||||
@@ -7,8 +8,6 @@ import time
|
||||
from jellyfin_accounts import (
|
||||
config,
|
||||
config_path,
|
||||
load_config,
|
||||
data_dir,
|
||||
app,
|
||||
g,
|
||||
data_store,
|
||||
@@ -16,25 +15,70 @@ from jellyfin_accounts import (
|
||||
configparser,
|
||||
config_base_path,
|
||||
)
|
||||
from jellyfin_accounts.email import Mailgun, Smtp
|
||||
from jellyfin_accounts import web_log as log
|
||||
from jellyfin_accounts.validate_password import PasswordValidator
|
||||
|
||||
|
||||
def checkInvite(code, delete=False):
|
||||
def format_datetime(dt):
|
||||
result = dt.strftime(config["email"]["date_format"])
|
||||
if config.getboolean("email", "use_24h"):
|
||||
result += f' {dt.strftime("%H:%M")}'
|
||||
else:
|
||||
result += f' {dt.strftime("%I:%M %p")}'
|
||||
return result
|
||||
|
||||
|
||||
def checkInvite(code, used=False, username=None):
|
||||
current_time = datetime.datetime.now()
|
||||
invites = dict(data_store.invites)
|
||||
match = False
|
||||
for invite in invites:
|
||||
if (
|
||||
"remaining-uses" not in invites[invite]
|
||||
and "no-limit" not in invites[invite]
|
||||
):
|
||||
invites[invite]["remaining-uses"] = 1
|
||||
expiry = datetime.datetime.strptime(
|
||||
invites[invite]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
||||
)
|
||||
if current_time >= expiry:
|
||||
log.debug(f"Housekeeping: Deleting old invite {invite}")
|
||||
if current_time >= expiry or (
|
||||
"no-limit" not in invites[invite] and invites[invite]["remaining-uses"] < 1
|
||||
):
|
||||
log.debug(f"Housekeeping: Deleting expired invite {invite}")
|
||||
if (
|
||||
config.getboolean("notifications", "enabled")
|
||||
and "notify" in invites[invite]
|
||||
):
|
||||
for address in invites[invite]["notify"]:
|
||||
if "notify-expiry" in invites[invite]["notify"][address]:
|
||||
if invites[invite]["notify"][address]["notify-expiry"]:
|
||||
method = config["email"]["method"]
|
||||
if method == "mailgun":
|
||||
email = Mailgun(address)
|
||||
elif method == "smtp":
|
||||
email = Smtp(address)
|
||||
if email.construct_expiry(
|
||||
{"code": invite, "expiry": expiry}
|
||||
):
|
||||
email.send()
|
||||
del data_store.invites[invite]
|
||||
elif invite == code:
|
||||
match = True
|
||||
if delete:
|
||||
del data_store.invites[code]
|
||||
if used:
|
||||
delete = False
|
||||
inv = dict(data_store.invites[code])
|
||||
if "used-by" not in inv:
|
||||
inv["used-by"] = []
|
||||
if "remaining-uses" in inv:
|
||||
if inv["remaining-uses"] == 1:
|
||||
delete = True
|
||||
del data_store.invites[code]
|
||||
elif "no-limit" not in invites[invite]:
|
||||
inv["remaining-uses"] -= 1
|
||||
inv["used-by"].append([username, format_datetime(current_time)])
|
||||
if not delete:
|
||||
data_store.invites[code] = inv
|
||||
return match
|
||||
|
||||
|
||||
@@ -108,11 +152,11 @@ if (
|
||||
version.parse(jf.info["Version"]) >= version.parse("10.6.0")
|
||||
and bool(data_store.user_template) is not False
|
||||
):
|
||||
log.info("Updating user_template for Jellyfin >= 10.6.0")
|
||||
if (
|
||||
data_store.user_template["AuthenticationProviderId"]
|
||||
== "Emby.Server.Implementations.Library.DefaultAuthenticationProvider"
|
||||
):
|
||||
log.info("Updating user_template for Jellyfin >= 10.6.0")
|
||||
data_store.user_template[
|
||||
"AuthenticationProviderId"
|
||||
] = "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider"
|
||||
@@ -125,16 +169,16 @@ if (
|
||||
] = "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider"
|
||||
|
||||
|
||||
if config.getboolean("password_validation", "enabled"):
|
||||
validator = PasswordValidator(
|
||||
config["password_validation"]["min_length"],
|
||||
config["password_validation"]["upper"],
|
||||
config["password_validation"]["lower"],
|
||||
config["password_validation"]["number"],
|
||||
config["password_validation"]["special"],
|
||||
)
|
||||
else:
|
||||
validator = PasswordValidator(0, 0, 0, 0, 0)
|
||||
def validator():
|
||||
if config.getboolean("password_validation", "enabled"):
|
||||
return PasswordValidator(
|
||||
config["password_validation"]["min_length"],
|
||||
config["password_validation"]["upper"],
|
||||
config["password_validation"]["lower"],
|
||||
config["password_validation"]["number"],
|
||||
config["password_validation"]["special"],
|
||||
)
|
||||
return PasswordValidator(0, 0, 0, 0, 0)
|
||||
|
||||
|
||||
@app.route("/newUser", methods=["POST"])
|
||||
@@ -142,7 +186,7 @@ def newUser():
|
||||
data = request.get_json()
|
||||
log.debug("Attempted newUser")
|
||||
if checkInvite(data["code"]):
|
||||
validation = validator.validate(data["password"])
|
||||
validation = validator().validate(data["password"])
|
||||
valid = True
|
||||
for criterion in validation:
|
||||
if validation[criterion] is False:
|
||||
@@ -157,7 +201,28 @@ def newUser():
|
||||
return jsonify({"error": error})
|
||||
except:
|
||||
return jsonify({"error": "Unknown error"})
|
||||
checkInvite(data["code"], delete=True)
|
||||
invites = dict(data_store.invites)
|
||||
checkInvite(data["code"], used=True, username=data["username"])
|
||||
if (
|
||||
config.getboolean("notifications", "enabled")
|
||||
and "notify" in invites[data["code"]]
|
||||
):
|
||||
for address in invites[data["code"]]["notify"]:
|
||||
if "notify-creation" in invites[data["code"]]["notify"][address]:
|
||||
if invites[data["code"]]["notify"][address]["notify-creation"]:
|
||||
method = config["email"]["method"]
|
||||
if method == "mailgun":
|
||||
email = Mailgun(address)
|
||||
elif method == "smtp":
|
||||
email = Smtp(address)
|
||||
if email.construct_created(
|
||||
{
|
||||
"code": data["code"],
|
||||
"username": data["username"],
|
||||
"created": datetime.datetime.now(),
|
||||
}
|
||||
):
|
||||
email.send()
|
||||
if user.status_code == 200:
|
||||
try:
|
||||
policy = data_store.user_template
|
||||
@@ -175,9 +240,7 @@ def newUser():
|
||||
jf.setDisplayPreferences(user.json()["Id"], displayprefs)
|
||||
log.debug("Set homescreen layout.")
|
||||
else:
|
||||
log.debug(
|
||||
"user configuration and/or " + "displayprefs were blank"
|
||||
)
|
||||
log.debug("user configuration and/or displayprefs were blank")
|
||||
except:
|
||||
log.error("Failed to set new user homescreen layout")
|
||||
if config.getboolean("password_resets", "enabled"):
|
||||
@@ -200,9 +263,19 @@ def newUser():
|
||||
def generateInvite():
|
||||
current_time = datetime.datetime.now()
|
||||
data = request.get_json()
|
||||
delta = datetime.timedelta(hours=int(data["hours"]), minutes=int(data["minutes"]))
|
||||
delta = datetime.timedelta(
|
||||
days=int(data["days"]), hours=int(data["hours"]), minutes=int(data["minutes"])
|
||||
)
|
||||
invite_code = secrets.token_urlsafe(16)
|
||||
invite = {}
|
||||
invite["created"] = format_datetime(current_time)
|
||||
if data["multiple-uses"]:
|
||||
if data["no-limit"]:
|
||||
invite["no-limit"] = True
|
||||
else:
|
||||
invite["remaining-uses"] = int(data["remaining-uses"])
|
||||
else:
|
||||
invite["remaining-uses"] = 1
|
||||
log.debug(f"Creating new invite: {invite_code}")
|
||||
valid_till = current_time + delta
|
||||
invite["valid_till"] = valid_till.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
||||
@@ -223,6 +296,11 @@ def generateInvite():
|
||||
response = email.send()
|
||||
if response is False or type(response) != bool:
|
||||
invite["email"] = f"Failed to send to {address}"
|
||||
if config.getboolean("notifications", "enabled"):
|
||||
if "notify-creation" in data:
|
||||
invite["notify-creation"] = data["notify-creation"]
|
||||
if "notify-expiry" in data:
|
||||
invite["notify-expiry"] = data["notify-expiry"]
|
||||
data_store.invites[invite_code] = invite
|
||||
log.info(f"New invite created: {invite_code}")
|
||||
return resp()
|
||||
@@ -245,11 +323,36 @@ def getInvites():
|
||||
valid_for = expiry - current_time
|
||||
invite = {
|
||||
"code": code,
|
||||
"days": valid_for.days,
|
||||
"hours": valid_for.seconds // 3600,
|
||||
"minutes": (valid_for.seconds // 60) % 60,
|
||||
}
|
||||
if "created" in invites[code]:
|
||||
invite["created"] = invites[code]["created"]
|
||||
if "used-by" in invites[code]:
|
||||
invite["used-by"] = invites[code]["used-by"]
|
||||
if "no-limit" in invites[code]:
|
||||
invite["no-limit"] = invites[code]["no-limit"]
|
||||
if "remaining-uses" in invites[code]:
|
||||
invite["remaining-uses"] = invites[code]["remaining-uses"]
|
||||
else:
|
||||
invite["remaining-uses"] = 1
|
||||
if "email" in invites[code]:
|
||||
invite["email"] = invites[code]["email"]
|
||||
if "notify" in invites[code]:
|
||||
if config.getboolean("ui", "jellyfin_login"):
|
||||
address = data_store.emails[g.user.id]
|
||||
else:
|
||||
address = config["ui"]["email"]
|
||||
if address in invites[code]["notify"]:
|
||||
if "notify-expiry" in invites[code]["notify"][address]:
|
||||
invite["notify-expiry"] = invites[code]["notify"][address][
|
||||
"notify-expiry"
|
||||
]
|
||||
if "notify-creation" in invites[code]["notify"][address]:
|
||||
invite["notify-creation"] = invites[code]["notify"][address][
|
||||
"notify-creation"
|
||||
]
|
||||
response["invites"].append(invite)
|
||||
return jsonify(response)
|
||||
|
||||
@@ -343,18 +446,11 @@ def modifyConfig():
|
||||
log.debug(f"{section}/{item} modified")
|
||||
with open(config_path, "w") as config_file:
|
||||
temp_config.write(config_file)
|
||||
config = load_config(config_path, data_dir)
|
||||
config.trigger_reload()
|
||||
log.info("Config written. Restart may be needed to load settings.")
|
||||
return resp()
|
||||
|
||||
|
||||
# @app.route('/getConfig', methods=["GET"])
|
||||
# @auth.login_required
|
||||
# def getConfig():
|
||||
# log.debug('Config requested')
|
||||
# return jsonify(config._sections), 200
|
||||
|
||||
|
||||
@app.route("/getConfig", methods=["GET"])
|
||||
@auth.login_required
|
||||
def getConfig():
|
||||
@@ -368,3 +464,29 @@ def getConfig():
|
||||
if entry in config[section]:
|
||||
response_config[section][entry]["value"] = config[section][entry]
|
||||
return jsonify(response_config), 200
|
||||
|
||||
|
||||
@app.route("/setNotify", methods=["POST"])
|
||||
@auth.login_required
|
||||
def setNotify():
|
||||
data = request.get_json()
|
||||
change = False
|
||||
for code in data:
|
||||
for key in data[code]:
|
||||
if key in ["notify-expiry", "notify-creation"]:
|
||||
inv = data_store.invites[code]
|
||||
if config.getboolean("ui", "jellyfin_login"):
|
||||
address = data_store.emails[g.user.id]
|
||||
else:
|
||||
address = config["ui"]["email"]
|
||||
if "notify" not in inv:
|
||||
inv["notify"] = {}
|
||||
if address not in inv["notify"]:
|
||||
inv["notify"][address] = {}
|
||||
inv["notify"][address][key] = data[code][key]
|
||||
log.debug(f"{code}: Notification settings changed")
|
||||
change = True
|
||||
if change:
|
||||
data_store.invites[code] = inv
|
||||
return resp()
|
||||
return resp(success=False)
|
||||
|
||||
47
mail/created.mjml
Normal file
47
mail/created.mjml
Normal file
@@ -0,0 +1,47 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
</mj-attributes>
|
||||
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> jellyfin-accounts </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<h3>User Created</h3>
|
||||
<p>A user was created using code {{ code }}.</p>
|
||||
</mj-text>
|
||||
<mj-table mj-class="text" container-background-color="#242424">
|
||||
<tr style="text-align: left;">
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
<tr style="font-style: italic; text-align: left; color: rgb(153,153,153);">
|
||||
<th>{{ username }}</th>
|
||||
<th>{{ address }}</th>
|
||||
<th>{{ time }}</th>
|
||||
</mj-table>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||
Notification emails can be toggled on the admin dashboard.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</body>
|
||||
</mjml>
|
||||
7
mail/created.txt
Normal file
7
mail/created.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
A user was created using code {{ code }}.
|
||||
|
||||
Name: {{ username }}
|
||||
Address: {{ address }}
|
||||
Time: {{ time }}
|
||||
|
||||
Note: Notification emails can be toggled on the admin dashboard.
|
||||
40
mail/email.mjml
Normal file
40
mail/email.mjml
Normal file
@@ -0,0 +1,40 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
</mj-attributes>
|
||||
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> Jellyfin </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<p>Hi {{ username }},</p>
|
||||
<p> Someone has recently requested a password reset on Jellyfin.</p>
|
||||
<p>If this was you, enter the below pin into the prompt.</p>
|
||||
<p>The code will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}.</p>
|
||||
<p>If this wasn't you, please ignore this email.</p>
|
||||
</mj-text>
|
||||
<mj-button mj-class="blue bold">{{ pin }}</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||
{{ message }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</body>
|
||||
</mjml>
|
||||
10
mail/email.txt
Normal file
10
mail/email.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Hi {{ username }},
|
||||
|
||||
Someone has recently requests a password reset on Jellyfin.
|
||||
If this was you, enter the below pin into the prompt.
|
||||
This code will expire on {{ expiry_date }}, at {{ expiry_time }} , which is in {{ expires_in }}.
|
||||
If this wasn't you, please ignore this email.
|
||||
|
||||
PIN: {{ pin }}
|
||||
|
||||
{{ message }}
|
||||
36
mail/expired.mjml
Normal file
36
mail/expired.mjml
Normal file
@@ -0,0 +1,36 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
</mj-attributes>
|
||||
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> jellyfin-accounts </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<h3>Invite Expired.</h3>
|
||||
<p>Code {{ code }} expired at {{ expiry }}.</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||
Notification emails can be toggled on the admin dashboard.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</body>
|
||||
</mjml>
|
||||
5
mail/expired.txt
Normal file
5
mail/expired.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Invite expired.
|
||||
|
||||
Code {{ code }} expired at {{ expiry }}.
|
||||
|
||||
Note: Notification emails can be toggled on the admin dashboard.
|
||||
34
mail/generate.py
Executable file
34
mail/generate.py
Executable file
@@ -0,0 +1,34 @@
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
def runcmd(cmd):
|
||||
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
|
||||
return proc.communicate()
|
||||
|
||||
local_path = Path(__file__).resolve().parent
|
||||
node_bin = local_path.parent / 'node_modules' / '.bin'
|
||||
|
||||
for mjml in [f for f in local_path.iterdir() if f.is_file() and 'mjml' in f.suffix]:
|
||||
print(f'Compiling {mjml.name}')
|
||||
fname = mjml.with_suffix(".html")
|
||||
runcmd(f'{str(node_bin / "mjml")} {str(mjml)} -o {str(fname)}')
|
||||
if fname.is_file():
|
||||
print('Done.')
|
||||
|
||||
html = [f for f in local_path.iterdir() if f.is_file() and 'html' in f.suffix]
|
||||
|
||||
output = local_path.parent / 'jellyfin_accounts' / 'data'
|
||||
|
||||
for f in html:
|
||||
shutil.copy(str(f),
|
||||
str(output / f.name))
|
||||
print(f'Copied {f.name} to {str(output / f.name)}')
|
||||
txtfile = f.with_suffix('.txt')
|
||||
if txtfile.is_file():
|
||||
shutil.copy(str(txtfile),
|
||||
str(output / txtfile.name))
|
||||
print(f'Copied {txtfile.name} to {str(output / txtfile.name)}')
|
||||
else:
|
||||
print(f'Warning: {txtfile.name} does not exist. Text versions of emails should be supplied.')
|
||||
|
||||
39
mail/invite-email.mjml
Normal file
39
mail/invite-email.mjml
Normal file
@@ -0,0 +1,39 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
</mj-attributes>
|
||||
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> Jellyfin </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<p>Hi,</p>
|
||||
<h3>You've been invited to Jellyfin.</h3>
|
||||
<p>To join, click the button below.</p>
|
||||
<p>This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.</p>
|
||||
</mj-text>
|
||||
<mj-button mj-class="blue bold" href="{{ invite_link }}">Setup your account</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||
{{ message }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</body>
|
||||
</mjml>
|
||||
8
mail/invite-email.txt
Normal file
8
mail/invite-email.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Hi,
|
||||
You've been invited to Jellyfin.
|
||||
To join, follow the below link.
|
||||
This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.
|
||||
|
||||
{{ invite_link }}
|
||||
|
||||
{{ message }}
|
||||
1474
package-lock.json
generated
1474
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "jellyfin-accounts",
|
||||
"version": "1.0.0",
|
||||
"description": "This is only used for grabbing scss build dependencies, and isn't a real package.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hrfee/jellyfin-accounts.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/hrfee/jellyfin-accounts/issues"
|
||||
},
|
||||
"homepage": "https://github.com/hrfee/jellyfin-accounts#readme",
|
||||
"dependencies": {
|
||||
"autoprefixer": "^9.8.5",
|
||||
"bootstrap": "^5.0.0-alpha1",
|
||||
"bootstrap4": "npm:bootstrap@^4.5.0",
|
||||
"clean-css-cli": "^4.3.0",
|
||||
"lodash": "^4.17.19",
|
||||
"mjml": "^4.6.3",
|
||||
"postcss-cli": "^7.1.1"
|
||||
}
|
||||
}
|
||||
43
poetry.lock
generated
43
poetry.lock
generated
@@ -162,6 +162,17 @@ MarkupSafe = ">=0.23"
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=0.8)"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Sass for Python: A straightforward binding of libsass for Python."
|
||||
name = "libsass"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.20.0"
|
||||
|
||||
[package.dependencies]
|
||||
six = "*"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
@@ -332,6 +343,17 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "1.15.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "tasks runner for python projects"
|
||||
name = "taskipy"
|
||||
optional = false
|
||||
python-versions = ">=3.6,<4.0"
|
||||
version = "1.2.1"
|
||||
|
||||
[package.dependencies]
|
||||
toml = ">=0.10.0,<0.11.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
@@ -400,7 +422,7 @@ dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "847ce2a6a3927efdfb3b78935b348e9b4dc63d7e60959af6cc8b9fbc5a24567b"
|
||||
content-hash = "fa8b5fb1ded41b673b8062a2bfc6467e6a484ff62b578147bec001d7d9d8ca16"
|
||||
python-versions = "^3.6"
|
||||
|
||||
[metadata.files]
|
||||
@@ -518,6 +540,21 @@ jinja2 = [
|
||||
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
||||
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
||||
]
|
||||
libsass = [
|
||||
{file = "libsass-0.20.0-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:107c409524c6a4ed14410fa9dafa9ee59c6bd3ecae75d73af749ab2b75685726"},
|
||||
{file = "libsass-0.20.0-cp27-cp27m-win32.whl", hash = "sha256:98f6dee9850b29e62977a963e3beb3cfeb98b128a267d59d2c3d675e298c8d57"},
|
||||
{file = "libsass-0.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:b077261a04ba1c213e932943208471972c5230222acb7fa97373e55a40872cbb"},
|
||||
{file = "libsass-0.20.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e6a547c0aa731dcb4ed71f198e814bee0400ce04d553f3f12a53bc3a17f2a481"},
|
||||
{file = "libsass-0.20.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:74f6fb8da58179b5d86586bc045c16d93d55074bc7bb48b6354a4da7ac9f9dfd"},
|
||||
{file = "libsass-0.20.0-cp36-cp36m-win32.whl", hash = "sha256:a43f3830d83ad9a7f5013c05ce239ca71744d0780dad906587302ac5257bce60"},
|
||||
{file = "libsass-0.20.0-cp36-cp36m-win_amd64.whl", hash = "sha256:fd19c8f73f70ffc6cbcca8139da08ea9a71fc48e7dfc4bb236ad88ab2d6558f1"},
|
||||
{file = "libsass-0.20.0-cp37-abi3-macosx_10_14_x86_64.whl", hash = "sha256:8cf72552b39e78a1852132e16b706406bc76029fe3001583284ece8d8752a60a"},
|
||||
{file = "libsass-0.20.0-cp37-cp37m-win32.whl", hash = "sha256:7555d9b24e79943cfafac44dbb4ca7e62105c038de7c6b999838c9ff7b88645d"},
|
||||
{file = "libsass-0.20.0-cp37-cp37m-win_amd64.whl", hash = "sha256:794f4f4661667263e7feafe5cc866e3746c7c8a9192b2aa9afffdadcbc91c687"},
|
||||
{file = "libsass-0.20.0-cp38-cp38-win32.whl", hash = "sha256:3bc0d68778b30b5fa83199e18795314f64b26ca5871e026343e63934f616f7f7"},
|
||||
{file = "libsass-0.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:5c8ff562b233734fbc72b23bb862cc6a6f70b1e9bf85a58422aa75108b94783b"},
|
||||
{file = "libsass-0.20.0.tar.gz", hash = "sha256:b7452f1df274b166dc22ee2e9154c4adca619bcbbdf8041a7aa05f372a1dacbc"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
|
||||
@@ -645,6 +682,10 @@ six = [
|
||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||
]
|
||||
taskipy = [
|
||||
{file = "taskipy-1.2.1-py3-none-any.whl", hash = "sha256:99bdaf5b19791c2345806847147e0fc2d28e1ac9446058def5a8b6b3fc9f23e2"},
|
||||
{file = "taskipy-1.2.1.tar.gz", hash = "sha256:5eb2c3b1606c896c7fa799848e71e8883b880759224958d07ba760e5db263175"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
|
||||
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "jellyfin-accounts"
|
||||
version = "0.3.0"
|
||||
version = "0.3.7"
|
||||
readme = "README.md"
|
||||
description = "A simple account management system for Jellyfin"
|
||||
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
|
||||
@@ -8,15 +8,14 @@ license = "MIT"
|
||||
homepage = "https://github.com/hrfee/jellyfin-accounts"
|
||||
repository = "https://github.com/hrfee/jellyfin-accounts"
|
||||
keywords = ["jellyfin", "jf-accounts"]
|
||||
include = ["jellyfin_accounts/data/*"]
|
||||
exclude = ["images/*", "scss/*"]
|
||||
include = ["jellyfin_accounts/data/*", "jellyfin_accounts/data/static/*.css", "jellyfin_accounts/data/*.html"]
|
||||
exclude = ["images/*", "scss/*", "mail/*"]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.6"
|
||||
pyopenssl = "^19.1.0"
|
||||
@@ -34,11 +33,19 @@ packaging = "^20.4"
|
||||
[tool.poetry.dev-dependencies]
|
||||
neovim = "^0.3.1"
|
||||
black = "^19.10b0"
|
||||
|
||||
taskipy = "^1.2.1"
|
||||
libsass = "^0.20.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
jf-accounts = 'jellyfin_accounts:main'
|
||||
|
||||
[tool.taskipy.tasks]
|
||||
pre_compile-css = "task get-npm-deps"
|
||||
compile-css = "python scss/compile.py"
|
||||
get-npm-deps = "python scss/get_node_deps.py"
|
||||
pre_generate-emails = "task get-npm-deps"
|
||||
generate-emails = "python mail/generate.py"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
||||
@@ -2,15 +2,9 @@
|
||||
|
||||
* `bs<4/5>-jf.scss` contains the source for the customizations to bootstrap. To customize the UI, you can make modifications to this file and then compile it.
|
||||
|
||||
**Note**: For BS5, it is assumed that bootstrap is installed in `../../node_modules/bootstrap` relative to itself.
|
||||
For BS4, it assumes that bootstrap is installed in `../../node_modules/bootstrap4` relative to itself (`npm install bootstrap4@npm:bootstrap`).
|
||||
* Compilation requires a sass compiler of your choice, and `postcss-cli`, `autoprefixer` + `clean-css-cli` from npm.
|
||||
* If you're using `sassc`, run `./compile.sh bs<4/5>-jf.scss` in this directory. This will create a .css file, and minified .css file.
|
||||
* For `node-sass`, replace the `sassc` line in `compile.sh` with
|
||||
```
|
||||
node-sass --output-style expanded --precision 6 $1 $css_file
|
||||
```
|
||||
and run as above.
|
||||
* If you're building from source, copy the minified css to `<jf-accounts git directory>/jellyfin_accounts/data/static/bs<4/5>-jf.css`.
|
||||
* If you're just customizing your install, set `custom_css` in your config as the path to your minified css and change the `theme` option to `Custom CSS`.
|
||||
**Note**: It is assumed that Bootstrap 5 is installed in `../../node_modules/bootstrap` relative to itself, and Bootstrap 4 in `../../node_modules/bootstrap4`.
|
||||
|
||||
* Compilation requires dev dependencies (`poetry update`), bootstrap and some extra npm packages.
|
||||
* If you're buildings from source, you can simply run `poetry run task compile-css` before building to automatically get deps and compile CSS.
|
||||
* If you are creating custom css, run `poetry run task get-npm-deps` to only install the necessary dependencies. Follow along with the commands `scss/compile.py` runs to build your css and then set `custom_css` in your config as the path to your minified css and change the `theme` option to `Custom CSS`.
|
||||
|
||||
|
||||
9548
scss/bs4/bs4-jf.css
9548
scss/bs4/bs4-jf.css
File diff suppressed because one or more lines are too long
7
scss/bs4/bs4-jf.min.css
vendored
7
scss/bs4/bs4-jf.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -120,6 +120,10 @@ $list-group-action-active-bg: $jf-blue-focus;
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.text-bright {
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
css_file=$(echo $1 | sed 's/scss/css/g')
|
||||
min_file=$(echo $1 | sed 's/scss/min.css/g')
|
||||
sassc -t expanded -p 6 $1 $css_file
|
||||
echo "Compiled."
|
||||
postcss $css_file --replace --use autoprefixer
|
||||
echo "Prefixed."
|
||||
echo "Written to $css_file."
|
||||
cleancss --level 1 --format breakWith=lf --source-map --source-map-inline-sources --output $min_file $css_file
|
||||
echo "Minified version written to $min_file."
|
||||
9438
scss/bs5/bs5-jf.css
9438
scss/bs5/bs5-jf.css
File diff suppressed because one or more lines are too long
7
scss/bs5/bs5-jf.min.css
vendored
7
scss/bs5/bs5-jf.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -120,6 +120,10 @@ $list-group-action-active-bg: $jf-blue-focus;
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.text-bright {
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
css_file=$(echo $1 | sed 's/scss/css/g')
|
||||
min_file=$(echo $1 | sed 's/scss/min.css/g')
|
||||
sassc -t expanded -p 6 $1 $css_file
|
||||
echo "Compiled."
|
||||
postcss $css_file --replace --use autoprefixer
|
||||
echo "Prefixed."
|
||||
echo "Written to $css_file."
|
||||
cleancss --level 1 --format breakWith=lf --source-map --source-map-inline-sources --output $min_file $css_file
|
||||
echo "Minified version written to $min_file."
|
||||
35
scss/compile.py
Executable file
35
scss/compile.py
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
import sass
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
def runcmd(cmd):
|
||||
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
|
||||
return proc.communicate()
|
||||
|
||||
local_path = Path(__file__).resolve().parent
|
||||
node_bin = local_path.parent / 'node_modules' / '.bin'
|
||||
|
||||
for bsv in [d for d in local_path.iterdir() if 'bs' in d.name]:
|
||||
scss = bsv / f'{bsv.name}-jf.scss'
|
||||
css = bsv / f'{bsv.name}-jf.css'
|
||||
min_css = bsv.parents[1] / 'jellyfin_accounts' / 'data' / 'static' / f'{bsv.name}-jf.css'
|
||||
with open(css, 'w') as f:
|
||||
f.write(sass.compile(filename=str(scss.resolve()),
|
||||
output_style='expanded',
|
||||
precision=6))
|
||||
if css.exists():
|
||||
print(f'{bsv.name}: Compiled.')
|
||||
runcmd(f'{str((node_bin / "postcss").resolve())} {str(css.resolve())} --replace --use autoprefixer')
|
||||
print(f'{bsv.name}: Prefixed.')
|
||||
runcmd(f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css.resolve())} {str(css.resolve())}')
|
||||
if min_css.exists():
|
||||
print(f'{bsv.name}: Minified and copied to {str(min_css.resolve())}.')
|
||||
|
||||
for v in [('bootstrap', 'bs5'), ('bootstrap4', 'bs4')]:
|
||||
new_path = str((local_path.parent / 'jellyfin_accounts' / 'data' / 'static' / (v[1] + '.css')).resolve())
|
||||
shutil.copy(str((local_path.parent / 'node_modules' / v[0] / 'dist' / 'css' / 'bootstrap.min.css').resolve()),
|
||||
new_path)
|
||||
print(f'Copied {v[1]} to {new_path}')
|
||||
|
||||
17
scss/get_node_deps.py
Normal file
17
scss/get_node_deps.py
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
def runcmd(cmd):
|
||||
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
|
||||
return proc.communicate()
|
||||
|
||||
print('Installing npm packages')
|
||||
|
||||
root_path = Path(__file__).parents[1]
|
||||
runcmd(f'npm install --prefix {root_path}')
|
||||
|
||||
if (root_path / 'node_modules' / 'cleancss').exists():
|
||||
print(f'Installed successfully in {str((root_path / "node_modules").resolve())}.')
|
||||
|
||||
Reference in New Issue
Block a user