mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2025-12-20 01:01:13 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5af2e7f9d | |||
| dea613fa85 | |||
| b8fdb64f68 | |||
| e80b233af2 | |||
| 3e53bcab27 | |||
| 2551307877 | |||
| 290e6b3dca | |||
| a49b4d9027 | |||
| d615b21c7d | |||
| 9afbd31faa | |||
| 27169e4e0d | |||
| db3b992857 | |||
| 89c132e92e | |||
| 7bda2f4141 | |||
| 71f05f2348 | |||
| 94e69ad090 |
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.
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,3 +19,5 @@ requirements.txt
|
|||||||
video/
|
video/
|
||||||
scss/bs5/*.css*
|
scss/bs5/*.css*
|
||||||
scss/bs4/*.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)
|
* Password resets are handled using smtplib, requests, and [jinja](https://github.com/pallets/jinja)
|
||||||
## Interface
|
## Interface
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<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/main/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/create.png" width="48%" style="margin-left: 1.5%;" alt="Account creation page"></img>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ server = http://jellyfin.local:8096
|
|||||||
public_server = https://jellyf.in:443
|
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.
|
; this and below settings will show on the jellyfin dashboard when the program connects. you may as well leave them alone.
|
||||||
client = jf-accounts
|
client = jf-accounts
|
||||||
version = 0.3.2
|
version = 0.3.7
|
||||||
device = jf-accounts
|
device = jf-accounts
|
||||||
device_id = jf-accounts-0.3.2
|
device_id = jf-accounts-0.3.7
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
; settings related to the ui and program functionality.
|
; settings related to the ui and program functionality.
|
||||||
@@ -28,10 +28,12 @@ admin_only = true
|
|||||||
username = your username
|
username = your username
|
||||||
; password for admin page (leave blank if using jellyfin_login)
|
; password for admin page (leave blank if using jellyfin_login)
|
||||||
password = your password
|
password = your password
|
||||||
|
; address to send notifications to (leave blank if using jellyfin_login)
|
||||||
|
email = example@example.com
|
||||||
debug = false
|
debug = false
|
||||||
; displayed at bottom of all pages except admin
|
; displayed at bottom of all pages except admin
|
||||||
contact_message = Need help? contact me.
|
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.
|
help_message = Enter your details to create an account.
|
||||||
; displayed when a user creates an account
|
; displayed when a user creates an account
|
||||||
success_message = Your account has been created. Click below to continue to Jellyfin.
|
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.
|
; 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
|
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]
|
||||||
; mailgun api connection settings
|
; mailgun api connection settings
|
||||||
api_url = https://api.mailgun.net...
|
api_url = https://api.mailgun.net...
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
# Runs it!
|
||||||
__version__ = "0.3.3"
|
__version__ = "0.3.7"
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
import configparser
|
import configparser
|
||||||
@@ -13,6 +13,7 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Flask, jsonify, g
|
from flask import Flask, jsonify, g
|
||||||
from jellyfin_accounts.data_store import JSONStorage
|
from jellyfin_accounts.data_store import JSONStorage
|
||||||
|
from jellyfin_accounts.config import Config
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="jellyfin-accounts")
|
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:
|
else:
|
||||||
config_path = data_dir / "config.ini"
|
config_path = data_dir / "config.ini"
|
||||||
|
|
||||||
|
# Temp config so logger knows whether to use debug mode or not
|
||||||
temp_config = configparser.RawConfigParser()
|
temp_config = configparser.RawConfigParser()
|
||||||
temp_config.read(config_path)
|
temp_config.read(config_path)
|
||||||
|
|
||||||
@@ -93,61 +94,7 @@ def create_log(name):
|
|||||||
|
|
||||||
log = create_log("main")
|
log = create_log("main")
|
||||||
|
|
||||||
|
config = Config(config_path, secrets.token_urlsafe(16), data_dir, local_dir, log)
|
||||||
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)
|
|
||||||
|
|
||||||
web_log = create_log("waitress")
|
web_log = create_log("waitress")
|
||||||
if not first_run:
|
if not first_run:
|
||||||
@@ -343,13 +290,6 @@ def main():
|
|||||||
success = True
|
success = True
|
||||||
|
|
||||||
else:
|
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
|
global app
|
||||||
app = Flask(__name__, root_path=str(local_dir))
|
app = Flask(__name__, root_path=str(local_dir))
|
||||||
app.config["DEBUG"] = config.getboolean("ui", "debug")
|
app.config["DEBUG"] = config.getboolean("ui", "debug")
|
||||||
@@ -359,6 +299,13 @@ def main():
|
|||||||
from waitress import serve
|
from waitress import serve
|
||||||
|
|
||||||
if first_run:
|
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
|
import jellyfin_accounts.setup
|
||||||
|
|
||||||
host = config["ui"]["host"]
|
host = config["ui"]["host"]
|
||||||
@@ -368,6 +315,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
import jellyfin_accounts.web_api
|
import jellyfin_accounts.web_api
|
||||||
import jellyfin_accounts.web
|
import jellyfin_accounts.web
|
||||||
|
import jellyfin_accounts.invite_daemon
|
||||||
|
|
||||||
host = config["ui"]["host"]
|
host = config["ui"]["host"]
|
||||||
port = config["ui"]["port"]
|
port = config["ui"]["port"]
|
||||||
@@ -383,4 +331,12 @@ def main():
|
|||||||
log.info("Starting email thread")
|
log.info("Starting email thread")
|
||||||
pwr.start()
|
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))
|
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"
|
||||||
@@ -133,6 +133,15 @@
|
|||||||
"value": "your password",
|
"value": "your password",
|
||||||
"description": "Password for admin page (Leave blank if using jellyfin_login)"
|
"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": {
|
"debug": {
|
||||||
"name": "Debug logging",
|
"name": "Debug logging",
|
||||||
"required": false,
|
"required": false,
|
||||||
@@ -143,7 +152,7 @@
|
|||||||
"contact_message": {
|
"contact_message": {
|
||||||
"name": "Contact message",
|
"name": "Contact message",
|
||||||
"required": false,
|
"required": false,
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "Need help? contact me.",
|
"value": "Need help? contact me.",
|
||||||
"description": "Displayed at bottom of all pages except admin"
|
"description": "Displayed at bottom of all pages except admin"
|
||||||
@@ -151,15 +160,15 @@
|
|||||||
"help_message": {
|
"help_message": {
|
||||||
"name": "Help message",
|
"name": "Help message",
|
||||||
"required": false,
|
"required": false,
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "Enter your details to create an account.",
|
"value": "Enter your details to create an account.",
|
||||||
"description": "Display at top of invite form."
|
"description": "Displayed at top of invite form."
|
||||||
},
|
},
|
||||||
"success_message": {
|
"success_message": {
|
||||||
"name": "Success message",
|
"name": "Success message",
|
||||||
"required": false,
|
"required": false,
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
||||||
"description": "Displayed when a user creates an account"
|
"description": "Displayed when a user creates an account"
|
||||||
@@ -167,7 +176,7 @@
|
|||||||
"bs5": {
|
"bs5": {
|
||||||
"name": "Use Bootstrap 5",
|
"name": "Use Bootstrap 5",
|
||||||
"required": false,
|
"required": false,
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"value": false,
|
"value": false,
|
||||||
"description": "Use Bootstrap 5 (currently in alpha). This also removes the need for jQuery, so the page should load faster."
|
"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": {
|
"enabled": {
|
||||||
"name": "Enabled",
|
"name": "Enabled",
|
||||||
"required": false,
|
"required": false,
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"value": true
|
"value": true
|
||||||
},
|
},
|
||||||
"min_length": {
|
"min_length": {
|
||||||
"name": "Minimum Length",
|
"name": "Minimum Length",
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"depends_true": "enabled",
|
"depends_true": "enabled",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "8"
|
"value": "8"
|
||||||
},
|
},
|
||||||
"upper": {
|
"upper": {
|
||||||
"name": "Minimum uppercase characters",
|
"name": "Minimum uppercase characters",
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"depends_true": "enabled",
|
"depends_true": "enabled",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "1"
|
"value": "1"
|
||||||
},
|
},
|
||||||
"lower": {
|
"lower": {
|
||||||
"name": "Minimum lowercase characters",
|
"name": "Minimum lowercase characters",
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"depends_true": "enabled",
|
"depends_true": "enabled",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "0"
|
"value": "0"
|
||||||
},
|
},
|
||||||
"number": {
|
"number": {
|
||||||
"name": "Minimum number count",
|
"name": "Minimum number count",
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"depends_true": "enabled",
|
"depends_true": "enabled",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "1"
|
"value": "1"
|
||||||
},
|
},
|
||||||
"special": {
|
"special": {
|
||||||
"name": "Minimum number of special characters",
|
"name": "Minimum number of special characters",
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"depends_true": "enabled",
|
"depends_true": "enabled",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "0"
|
"value": "0"
|
||||||
@@ -229,7 +238,7 @@
|
|||||||
"no_username": {
|
"no_username": {
|
||||||
"name": "Use email addresses as username",
|
"name": "Use email addresses as username",
|
||||||
"required": false,
|
"required": false,
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"depends_true": "method",
|
"depends_true": "method",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"value": false,
|
"value": false,
|
||||||
@@ -350,7 +359,7 @@
|
|||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "Enabled",
|
"name": "Enabled",
|
||||||
"required": false,
|
"required": false,
|
||||||
"requires_restart": true,
|
"requires_restart": false,
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"value": true
|
"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."
|
"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": {
|
"mailgun": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Mailgun (Email)",
|
"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>
|
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ function serializeForm(id) {
|
|||||||
case 'password':
|
case 'password':
|
||||||
case 'select-one':
|
case 'select-one':
|
||||||
case 'email':
|
case 'email':
|
||||||
|
case 'number':
|
||||||
formData[name] = el.value;
|
formData[name] = el.value;
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,16 +13,20 @@ for (var i = 0; i < authRadios.length; i++) {
|
|||||||
checkAuthRadio();
|
checkAuthRadio();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkEmailRadio() {
|
function checkEmailRadio() {
|
||||||
document.getElementById('emailNextButton').href = '#page-5';
|
document.getElementById('emailNextButton').href = '#page-5';
|
||||||
document.getElementById('valBackButton').href = '#page-7';
|
document.getElementById('valBackButton').href = '#page-7';
|
||||||
if (document.getElementById('emailSMTPRadio').checked) {
|
if (document.getElementById('emailSMTPRadio').checked) {
|
||||||
|
document.getElementById('emailCommonArea').style.display = '';
|
||||||
document.getElementById('emailSMTPArea').style.display = '';
|
document.getElementById('emailSMTPArea').style.display = '';
|
||||||
document.getElementById('emailMailgunArea').style.display = 'none';
|
document.getElementById('emailMailgunArea').style.display = 'none';
|
||||||
} else if (document.getElementById('emailMailgunRadio').checked) {
|
} else if (document.getElementById('emailMailgunRadio').checked) {
|
||||||
|
document.getElementById('emailCommonArea').style.display = '';
|
||||||
document.getElementById('emailSMTPArea').style.display = 'none';
|
document.getElementById('emailSMTPArea').style.display = 'none';
|
||||||
document.getElementById('emailMailgunArea').style.display = '';
|
document.getElementById('emailMailgunArea').style.display = '';
|
||||||
} else if (document.getElementById('emailDisabledRadio').checked) {
|
} else if (document.getElementById('emailDisabledRadio').checked) {
|
||||||
|
document.getElementById('emailCommonArea').style.display = 'none';
|
||||||
document.getElementById('emailSMTPArea').style.display = 'none';
|
document.getElementById('emailSMTPArea').style.display = 'none';
|
||||||
document.getElementById('emailMailgunArea').style.display = 'none';
|
document.getElementById('emailMailgunArea').style.display = 'none';
|
||||||
document.getElementById('emailNextButton').href = '#page-8';
|
document.getElementById('emailNextButton').href = '#page-8';
|
||||||
@@ -35,6 +39,7 @@ for (var i = 0; i < emailRadios.length; i++) {
|
|||||||
checkEmailRadio();
|
checkEmailRadio();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkSSL() {
|
function checkSSL() {
|
||||||
var label = document.getElementById('emailSSL_TLSLabel');
|
var label = document.getElementById('emailSSL_TLSLabel');
|
||||||
if (document.getElementById('emailSSL_TLS').checked) {
|
if (document.getElementById('emailSSL_TLS').checked) {
|
||||||
@@ -101,16 +106,15 @@ document.getElementById('jfTestButton').onclick = function() {
|
|||||||
jfData['jfHost'] = document.getElementById('jfHost').value;
|
jfData['jfHost'] = document.getElementById('jfHost').value;
|
||||||
jfData['jfUser'] = document.getElementById('jfUser').value;
|
jfData['jfUser'] = document.getElementById('jfUser').value;
|
||||||
jfData['jfPassword'] = document.getElementById('jfPassword').value;
|
jfData['jfPassword'] = document.getElementById('jfPassword').value;
|
||||||
$.ajax('/testJF', {
|
var req = new XMLHttpRequest();
|
||||||
type : 'POST',
|
req.open("POST", "/testJF", true);
|
||||||
dataType : 'json',
|
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||||
contentType : 'application/json',
|
req.responseType = 'json';
|
||||||
data : JSON.stringify(jfData),
|
req.onreadystatechange = function() {
|
||||||
complete: function(response) {
|
if (this.readyState == 4) {
|
||||||
testButton.disabled = false;
|
testButton.disabled = false;
|
||||||
testButton.className = '';
|
testButton.className = '';
|
||||||
var success = response['responseJSON']['success'];
|
if (this.response['success'] == true) {
|
||||||
if (success == true) {
|
|
||||||
testButton.classList.add('btn', 'btn-success');
|
testButton.classList.add('btn', 'btn-success');
|
||||||
testButton.textContent = 'Success';
|
testButton.textContent = 'Success';
|
||||||
nextButton.classList.remove('disabled');
|
nextButton.classList.remove('disabled');
|
||||||
@@ -118,9 +122,10 @@ document.getElementById('jfTestButton').onclick = function() {
|
|||||||
} else {
|
} else {
|
||||||
testButton.classList.add('btn', 'btn-danger');
|
testButton.classList.add('btn', 'btn-danger');
|
||||||
testButton.textContent = 'Failed';
|
testButton.textContent = 'Failed';
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
});
|
};
|
||||||
|
req.send(JSON.stringify(jfData));
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('submitButton').onclick = function() {
|
document.getElementById('submitButton').onclick = function() {
|
||||||
@@ -138,6 +143,7 @@ document.getElementById('submitButton').onclick = function() {
|
|||||||
config['invite_emails'] = {};
|
config['invite_emails'] = {};
|
||||||
config['mailgun'] = {};
|
config['mailgun'] = {};
|
||||||
config['smtp'] = {};
|
config['smtp'] = {};
|
||||||
|
config['notifications'] = {};
|
||||||
// Page 2: Auth
|
// Page 2: Auth
|
||||||
if (document.getElementById('jfAuthRadio').checked) {
|
if (document.getElementById('jfAuthRadio').checked) {
|
||||||
config['ui']['jellyfin_login'] = 'true';
|
config['ui']['jellyfin_login'] = 'true';
|
||||||
@@ -149,6 +155,7 @@ document.getElementById('submitButton').onclick = function() {
|
|||||||
} else {
|
} else {
|
||||||
config['ui']['username'] = document.getElementById('manualAuthUsername').value;
|
config['ui']['username'] = document.getElementById('manualAuthUsername').value;
|
||||||
config['ui']['password'] = document.getElementById('manualAuthPassword').value;
|
config['ui']['password'] = document.getElementById('manualAuthPassword').value;
|
||||||
|
config['ui']['email'] = document.getElementById('manualAuthEmail').value;
|
||||||
};
|
};
|
||||||
// Page 3: Connect to jellyfin
|
// Page 3: Connect to jellyfin
|
||||||
config['jellyfin']['server'] = document.getElementById('jfHost').value;
|
config['jellyfin']['server'] = document.getElementById('jfHost').value;
|
||||||
@@ -158,7 +165,7 @@ document.getElementById('submitButton').onclick = function() {
|
|||||||
if (document.getElementById('emailDisabledRadio').checked) {
|
if (document.getElementById('emailDisabledRadio').checked) {
|
||||||
config['password_resets']['enabled'] = 'false';
|
config['password_resets']['enabled'] = 'false';
|
||||||
config['invite_emails']['enabled'] = 'false';
|
config['invite_emails']['enabled'] = 'false';
|
||||||
} else {
|
} else {
|
||||||
if (document.getElementById('emailSMTPRadio').checked) {
|
if (document.getElementById('emailSMTPRadio').checked) {
|
||||||
if (document.getElementById('emailSSL_TLS').checked) {
|
if (document.getElementById('emailSSL_TLS').checked) {
|
||||||
config['smtp']['encryption'] = 'ssl_tls';
|
config['smtp']['encryption'] = 'ssl_tls';
|
||||||
@@ -176,6 +183,7 @@ document.getElementById('submitButton').onclick = function() {
|
|||||||
config['mailgun']['api_key'] = document.getElementById('emailMailgunKey').value;
|
config['mailgun']['api_key'] = document.getElementById('emailMailgunKey').value;
|
||||||
config['email']['address'] = document.getElementById('emailMailgunAddress').value;
|
config['email']['address'] = document.getElementById('emailMailgunAddress').value;
|
||||||
};
|
};
|
||||||
|
config['notifications']['enabled'] = document.getElementById('notificationsEnabled').checked.toString();
|
||||||
// Page 5: Email formatting
|
// Page 5: Email formatting
|
||||||
config['email']['from'] = document.getElementById('emailSender').value;
|
config['email']['from'] = document.getElementById('emailSender').value;
|
||||||
config['email']['date_format'] = document.getElementById('emailDateFormat').value;
|
config['email']['date_format'] = document.getElementById('emailDateFormat').value;
|
||||||
@@ -217,18 +225,18 @@ document.getElementById('submitButton').onclick = function() {
|
|||||||
config['ui']['contact_message'] = document.getElementById('msgContact').value;
|
config['ui']['contact_message'] = document.getElementById('msgContact').value;
|
||||||
config['ui']['help_message'] = document.getElementById('msgHelp').value;
|
config['ui']['help_message'] = document.getElementById('msgHelp').value;
|
||||||
config['ui']['success_message'] = document.getElementById('msgSuccess').value;
|
config['ui']['success_message'] = document.getElementById('msgSuccess').value;
|
||||||
console.log(config);
|
// Send it
|
||||||
$.ajax('/modifyConfig', {
|
var req = new XMLHttpRequest();
|
||||||
type : 'POST',
|
req.open("POST", "/modifyConfig", true);
|
||||||
dataType : 'json',
|
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||||
contentType : 'application/json',
|
req.responseType = 'json';
|
||||||
data : JSON.stringify(config),
|
req.onreadystatechange = function() {
|
||||||
complete: function(response) {
|
if (this.readyState == 4) {
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
submitButton.className = '';
|
submitButton.className = '';
|
||||||
submitButton.classList.add('btn', 'btn-success');
|
submitButton.classList.add('btn', 'btn-success');
|
||||||
submitButton.textContent = 'Success';
|
submitButton.textContent = 'Success';
|
||||||
}
|
};
|
||||||
});
|
};
|
||||||
|
req.send(JSON.stringify(config));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,9 @@
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
{% if bs5 %}
|
{% if bs5 %}
|
||||||
var bsVersion = 5;
|
const bsVersion = 5;
|
||||||
{% else %}
|
{% else %}
|
||||||
var bsVersion = 4;
|
const bsVersion = 4;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
var css = document.createElement('link');
|
var css = document.createElement('link');
|
||||||
css.setAttribute('rel', 'stylesheet');
|
css.setAttribute('rel', 'stylesheet');
|
||||||
@@ -45,7 +45,6 @@
|
|||||||
css.setAttribute('href', '{{ css_file }}');
|
css.setAttribute('href', '{{ css_file }}');
|
||||||
};
|
};
|
||||||
document.head.appendChild(css);
|
document.head.appendChild(css);
|
||||||
// document.querySelectorAll('link[rel="stylesheet"][type="text/css"]')[0].href = cssCookie;
|
|
||||||
</script>
|
</script>
|
||||||
{% if not bs5 %}
|
{% 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>
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||||
@@ -91,16 +90,31 @@
|
|||||||
height: 1rem;
|
height: 1rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
z-index: 5000;*/
|
z-index: 5000;*/
|
||||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
-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);
|
-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);
|
-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 */
|
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeInCubic */
|
||||||
}
|
}
|
||||||
.smooth-transition {
|
.smooth-transition {
|
||||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
-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);
|
-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);
|
-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 */
|
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>
|
</style>
|
||||||
<title>Admin</title>
|
<title>Admin</title>
|
||||||
@@ -234,29 +248,66 @@
|
|||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">Generate Invite</div>
|
<div class="card-header">Generate Invite</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form action="#" method="POST" id="inviteForm">
|
<form action="#" method="POST" id="inviteForm" class="container">
|
||||||
<div class="form-group">
|
<div class="row align-items-start">
|
||||||
<label for="hours">Hours</label>
|
<div class="col">
|
||||||
<select class="form-control" id="hours" name="hours">
|
<div class="form-group">
|
||||||
</select>
|
<label for="days">Days</label>
|
||||||
</div>
|
<select class="form-control form-select" id="days" name="days">
|
||||||
<div class="form-group">
|
</select>
|
||||||
<label for="minutes">Minutes</label>
|
</div>
|
||||||
<select class="form-control" id="minutes" name="minutes">
|
<div class="form-group">
|
||||||
</select>
|
<label for="hours">Hours</label>
|
||||||
</div>
|
<select class="form-control form-select" id="hours" name="hours">
|
||||||
{% if email_enabled %}
|
</select>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="send_to_address">Send invite to address</label>
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<label for="minutes">Minutes</label>
|
||||||
<div class="input-group-text">
|
<select class="form-control form-select" id="minutes" name="minutes">
|
||||||
<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">
|
</select>
|
||||||
</div>
|
|
||||||
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="col">
|
||||||
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">Generate</button>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -156,6 +156,9 @@
|
|||||||
submitButton.replaceChild(newSpan, oldSpan);
|
submitButton.replaceChild(newSpan, oldSpan);
|
||||||
};
|
};
|
||||||
document.getElementById('accountForm').onsubmit = function() {
|
document.getElementById('accountForm').onsubmit = function() {
|
||||||
|
if (document.getElementById('errorMessage')) {
|
||||||
|
document.getElementById('errorMessage').remove();
|
||||||
|
}
|
||||||
toggleSpinner();
|
toggleSpinner();
|
||||||
var send = serializeForm('accountForm');
|
var send = serializeForm('accountForm');
|
||||||
send['code'] = code;
|
send['code'] = code;
|
||||||
@@ -171,12 +174,18 @@
|
|||||||
if (this.readyState == 4) {
|
if (this.readyState == 4) {
|
||||||
toggleSpinner();
|
toggleSpinner();
|
||||||
var data = this.response;
|
var data = this.response;
|
||||||
if ('error' in data) {
|
if ('error' in data || data['success'] == false) {
|
||||||
var text = document.createTextNode(data['error']);
|
if (typeof(data['error']) != 'undefined') {
|
||||||
|
var errorMessage = data['error'];
|
||||||
|
} else {
|
||||||
|
var errorMessage = 'Unknown Error';
|
||||||
|
}
|
||||||
|
var text = document.createTextNode(errorMessage);
|
||||||
var error = document.createElement('button');
|
var error = document.createElement('button');
|
||||||
error.classList.add('btn', 'btn-outline-danger');
|
error.classList.add('btn', 'btn-outline-danger');
|
||||||
error.setAttribute('disabled', '');
|
error.setAttribute('disabled', '');
|
||||||
error.appendChild(text);
|
error.appendChild(text);
|
||||||
|
error.id = 'errorMessage';
|
||||||
document.getElementById('errorBox').appendChild(error);
|
document.getElementById('errorBox').appendChild(error);
|
||||||
} else {
|
} else {
|
||||||
var valid = true
|
var valid = true
|
||||||
|
|||||||
@@ -89,6 +89,11 @@
|
|||||||
<label for="manualAuthPassword">Password</label>
|
<label for="manualAuthPassword">Password</label>
|
||||||
<input type="password" class="form-control" id="manualAuthPassword" placeholder="Password">
|
<input type="password" class="form-control" id="manualAuthPassword" placeholder="Password">
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</p>
|
</p>
|
||||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
<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">
|
<input type="email" class="form-control" id="emailMailgunAddress" placeholder="jellyfin@jellyf.in">
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</p>
|
||||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||||
<a class="btn btn-secondary backButton" href="#page-3">Back</a>
|
<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 json
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
@@ -42,7 +43,10 @@ class JSONFile(dict):
|
|||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
data = self.readJSON(self.path)
|
data = self.readJSON(self.path)
|
||||||
super(JSONFile, self).__init__(data)
|
super(JSONFile, self).__init__(data)
|
||||||
del data[key]
|
try:
|
||||||
|
del data[key]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
self.writeJSON(self.path, data)
|
self.writeJSON(self.path, data)
|
||||||
super(JSONFile, self).__delitem__(key)
|
super(JSONFile, self).__delitem__(key)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Handles everything related to emails
|
||||||
import datetime
|
import datetime
|
||||||
import pytz
|
import pytz
|
||||||
import requests
|
import requests
|
||||||
@@ -12,6 +13,15 @@ from jellyfin_accounts import config
|
|||||||
from jellyfin_accounts import email_log as log
|
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:
|
class Email:
|
||||||
def __init__(self, address):
|
def __init__(self, address):
|
||||||
self.address = address
|
self.address = address
|
||||||
@@ -43,9 +53,7 @@ class Email:
|
|||||||
if expires_in["hours"] == 0:
|
if expires_in["hours"] == 0:
|
||||||
expires_in = f'{str(expires_in["minutes"])}m'
|
expires_in = f'{str(expires_in["minutes"])}m'
|
||||||
else:
|
else:
|
||||||
expires_in = (
|
expires_in = f'{str(expires_in["hours"])}h {str(expires_in["minutes"])}m'
|
||||||
f'{str(expires_in["hours"])}h ' + f'{str(expires_in["minutes"])}m'
|
|
||||||
)
|
|
||||||
log.debug(f"{self.address}: Expires in {expires_in}")
|
log.debug(f"{self.address}: Expires in {expires_in}")
|
||||||
return {"date": date, "time": time, "expires_in": expires_in}
|
return {"date": date, "time": time, "expires_in": expires_in}
|
||||||
|
|
||||||
@@ -76,6 +84,47 @@ class Email:
|
|||||||
self.content[key] = c
|
self.content[key] = c
|
||||||
log.info(f"{self.address}: {key} constructed")
|
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):
|
def construct_reset(self, reset):
|
||||||
self.subject = config["password_resets"]["subject"]
|
self.subject = config["password_resets"]["subject"]
|
||||||
log.debug(f"{self.address}: Using subject {self.subject}")
|
log.debug(f"{self.address}: Using subject {self.subject}")
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Generates config file
|
||||||
import configparser
|
import configparser
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
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 requests
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ class Jellyfin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def newUser(self, username: str, password: str):
|
def newUser(self, username: str, password: str):
|
||||||
for user in self.getUsers():
|
for user in self.getUsers(public=False):
|
||||||
if user["Name"] == username:
|
if user["Name"] == username:
|
||||||
raise self.UserExistsError
|
raise self.UserExistsError
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
# Handles authentication
|
||||||
|
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
from itsdangerous import (
|
from itsdangerous import (
|
||||||
@@ -106,6 +106,8 @@ def verify_password(username, password):
|
|||||||
user = Account().verify_token(username, accounts)
|
user = Account().verify_token(username, accounts)
|
||||||
if user:
|
if user:
|
||||||
verified = True
|
verified = True
|
||||||
|
if user in accounts:
|
||||||
|
user = accounts[user]
|
||||||
if not user:
|
if not user:
|
||||||
log.debug(f"User {username} not found on Jellyfin")
|
log.debug(f"User {username} not found on Jellyfin")
|
||||||
return False
|
return False
|
||||||
@@ -116,10 +118,10 @@ def verify_password(username, password):
|
|||||||
if username == user.username and user.verify_password(password):
|
if username == user.username and user.verify_password(password):
|
||||||
g.user = user
|
g.user = user
|
||||||
log.debug("HTTPAuth Allowed")
|
log.debug("HTTPAuth Allowed")
|
||||||
return True
|
return user
|
||||||
else:
|
else:
|
||||||
log.debug("HTTPAuth Denied")
|
log.debug("HTTPAuth Denied")
|
||||||
return False
|
return False
|
||||||
g.user = user
|
g.user = user
|
||||||
log.debug("HTTPAuth Allowed")
|
log.debug("HTTPAuth Allowed")
|
||||||
return True
|
return user
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Watches Jellyfin for password resets and sends emails.
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Views and endpoints for the initial setup
|
||||||
from flask import request, jsonify, render_template
|
from flask import request, jsonify, render_template
|
||||||
from configparser import RawConfigParser
|
from configparser import RawConfigParser
|
||||||
from jellyfin_accounts.jf_api import Jellyfin
|
from jellyfin_accounts.jf_api import Jellyfin
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Password validation
|
||||||
specials = ['[', '@', '_', '!', '#', '$', '%', '^', '&', '*', '(', ')',
|
specials = ['[', '@', '_', '!', '#', '$', '%', '^', '&', '*', '(', ')',
|
||||||
'<', '>', '?', '/', '\\', '|', '}', '{', '~', ':', ']']
|
'<', '>', '?', '/', '\\', '|', '}', '{', '~', ':', ']']
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
|
# Web views
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Flask, send_from_directory, render_template
|
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 import web_log as log
|
||||||
from jellyfin_accounts.web_api import config, checkInvite, validator
|
from jellyfin_accounts.web_api import checkInvite, validator
|
||||||
|
|
||||||
|
|
||||||
if config.getboolean("ui", "bs5"):
|
def bsVersion():
|
||||||
bsVersion = 5
|
if config.getboolean("ui", "bs5"):
|
||||||
else:
|
return 5
|
||||||
bsVersion = 4
|
return 4
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
@@ -42,9 +43,12 @@ def static_proxy(path):
|
|||||||
if "html" not in path:
|
if "html" not in path:
|
||||||
if "admin.js" in path:
|
if "admin.js" in path:
|
||||||
return (
|
return (
|
||||||
render_template("admin.js",
|
render_template(
|
||||||
bsVersion=bsVersion,
|
"admin.js",
|
||||||
css_file=css_file),
|
bsVersion=bsVersion(),
|
||||||
|
css_file=css_file,
|
||||||
|
notifications=config.getboolean("notifications", "enabled"),
|
||||||
|
),
|
||||||
200,
|
200,
|
||||||
{"Content-Type": "text/javascript"},
|
{"Content-Type": "text/javascript"},
|
||||||
)
|
)
|
||||||
@@ -77,7 +81,7 @@ def inviteProxy(path):
|
|||||||
successMessage=config["ui"]["success_message"],
|
successMessage=config["ui"]["success_message"],
|
||||||
jfLink=config["jellyfin"]["public_server"],
|
jfLink=config["jellyfin"]["public_server"],
|
||||||
validate=config.getboolean("password_validation", "enabled"),
|
validate=config.getboolean("password_validation", "enabled"),
|
||||||
requirements=validator.getCriteria(),
|
requirements=validator().getCriteria(),
|
||||||
email=email,
|
email=email,
|
||||||
username=(not config.getboolean("email", "no_username")),
|
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 flask import request, jsonify
|
||||||
from jellyfin_accounts.jf_api import Jellyfin
|
from jellyfin_accounts.jf_api import Jellyfin
|
||||||
import json
|
import json
|
||||||
@@ -7,8 +8,6 @@ import time
|
|||||||
from jellyfin_accounts import (
|
from jellyfin_accounts import (
|
||||||
config,
|
config,
|
||||||
config_path,
|
config_path,
|
||||||
load_config,
|
|
||||||
data_dir,
|
|
||||||
app,
|
app,
|
||||||
g,
|
g,
|
||||||
data_store,
|
data_store,
|
||||||
@@ -16,25 +15,70 @@ from jellyfin_accounts import (
|
|||||||
configparser,
|
configparser,
|
||||||
config_base_path,
|
config_base_path,
|
||||||
)
|
)
|
||||||
|
from jellyfin_accounts.email import Mailgun, Smtp
|
||||||
from jellyfin_accounts import web_log as log
|
from jellyfin_accounts import web_log as log
|
||||||
from jellyfin_accounts.validate_password import PasswordValidator
|
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()
|
current_time = datetime.datetime.now()
|
||||||
invites = dict(data_store.invites)
|
invites = dict(data_store.invites)
|
||||||
match = False
|
match = False
|
||||||
for invite in invites:
|
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(
|
expiry = datetime.datetime.strptime(
|
||||||
invites[invite]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
invites[invite]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
||||||
)
|
)
|
||||||
if current_time >= expiry:
|
if current_time >= expiry or (
|
||||||
log.debug(f"Housekeeping: Deleting old invite {invite}")
|
"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]
|
del data_store.invites[invite]
|
||||||
elif invite == code:
|
elif invite == code:
|
||||||
match = True
|
match = True
|
||||||
if delete:
|
if used:
|
||||||
del data_store.invites[code]
|
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
|
return match
|
||||||
|
|
||||||
|
|
||||||
@@ -108,11 +152,11 @@ if (
|
|||||||
version.parse(jf.info["Version"]) >= version.parse("10.6.0")
|
version.parse(jf.info["Version"]) >= version.parse("10.6.0")
|
||||||
and bool(data_store.user_template) is not False
|
and bool(data_store.user_template) is not False
|
||||||
):
|
):
|
||||||
log.info("Updating user_template for Jellyfin >= 10.6.0")
|
|
||||||
if (
|
if (
|
||||||
data_store.user_template["AuthenticationProviderId"]
|
data_store.user_template["AuthenticationProviderId"]
|
||||||
== "Emby.Server.Implementations.Library.DefaultAuthenticationProvider"
|
== "Emby.Server.Implementations.Library.DefaultAuthenticationProvider"
|
||||||
):
|
):
|
||||||
|
log.info("Updating user_template for Jellyfin >= 10.6.0")
|
||||||
data_store.user_template[
|
data_store.user_template[
|
||||||
"AuthenticationProviderId"
|
"AuthenticationProviderId"
|
||||||
] = "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider"
|
] = "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider"
|
||||||
@@ -125,16 +169,16 @@ if (
|
|||||||
] = "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider"
|
] = "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider"
|
||||||
|
|
||||||
|
|
||||||
if config.getboolean("password_validation", "enabled"):
|
def validator():
|
||||||
validator = PasswordValidator(
|
if config.getboolean("password_validation", "enabled"):
|
||||||
config["password_validation"]["min_length"],
|
return PasswordValidator(
|
||||||
config["password_validation"]["upper"],
|
config["password_validation"]["min_length"],
|
||||||
config["password_validation"]["lower"],
|
config["password_validation"]["upper"],
|
||||||
config["password_validation"]["number"],
|
config["password_validation"]["lower"],
|
||||||
config["password_validation"]["special"],
|
config["password_validation"]["number"],
|
||||||
)
|
config["password_validation"]["special"],
|
||||||
else:
|
)
|
||||||
validator = PasswordValidator(0, 0, 0, 0, 0)
|
return PasswordValidator(0, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/newUser", methods=["POST"])
|
@app.route("/newUser", methods=["POST"])
|
||||||
@@ -142,7 +186,7 @@ def newUser():
|
|||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
log.debug("Attempted newUser")
|
log.debug("Attempted newUser")
|
||||||
if checkInvite(data["code"]):
|
if checkInvite(data["code"]):
|
||||||
validation = validator.validate(data["password"])
|
validation = validator().validate(data["password"])
|
||||||
valid = True
|
valid = True
|
||||||
for criterion in validation:
|
for criterion in validation:
|
||||||
if validation[criterion] is False:
|
if validation[criterion] is False:
|
||||||
@@ -157,7 +201,28 @@ def newUser():
|
|||||||
return jsonify({"error": error})
|
return jsonify({"error": error})
|
||||||
except:
|
except:
|
||||||
return jsonify({"error": "Unknown error"})
|
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:
|
if user.status_code == 200:
|
||||||
try:
|
try:
|
||||||
policy = data_store.user_template
|
policy = data_store.user_template
|
||||||
@@ -175,9 +240,7 @@ def newUser():
|
|||||||
jf.setDisplayPreferences(user.json()["Id"], displayprefs)
|
jf.setDisplayPreferences(user.json()["Id"], displayprefs)
|
||||||
log.debug("Set homescreen layout.")
|
log.debug("Set homescreen layout.")
|
||||||
else:
|
else:
|
||||||
log.debug(
|
log.debug("user configuration and/or displayprefs were blank")
|
||||||
"user configuration and/or " + "displayprefs were blank"
|
|
||||||
)
|
|
||||||
except:
|
except:
|
||||||
log.error("Failed to set new user homescreen layout")
|
log.error("Failed to set new user homescreen layout")
|
||||||
if config.getboolean("password_resets", "enabled"):
|
if config.getboolean("password_resets", "enabled"):
|
||||||
@@ -200,9 +263,19 @@ def newUser():
|
|||||||
def generateInvite():
|
def generateInvite():
|
||||||
current_time = datetime.datetime.now()
|
current_time = datetime.datetime.now()
|
||||||
data = request.get_json()
|
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_code = secrets.token_urlsafe(16)
|
||||||
invite = {}
|
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}")
|
log.debug(f"Creating new invite: {invite_code}")
|
||||||
valid_till = current_time + delta
|
valid_till = current_time + delta
|
||||||
invite["valid_till"] = valid_till.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
invite["valid_till"] = valid_till.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
||||||
@@ -223,6 +296,11 @@ def generateInvite():
|
|||||||
response = email.send()
|
response = email.send()
|
||||||
if response is False or type(response) != bool:
|
if response is False or type(response) != bool:
|
||||||
invite["email"] = f"Failed to send to {address}"
|
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
|
data_store.invites[invite_code] = invite
|
||||||
log.info(f"New invite created: {invite_code}")
|
log.info(f"New invite created: {invite_code}")
|
||||||
return resp()
|
return resp()
|
||||||
@@ -245,11 +323,36 @@ def getInvites():
|
|||||||
valid_for = expiry - current_time
|
valid_for = expiry - current_time
|
||||||
invite = {
|
invite = {
|
||||||
"code": code,
|
"code": code,
|
||||||
|
"days": valid_for.days,
|
||||||
"hours": valid_for.seconds // 3600,
|
"hours": valid_for.seconds // 3600,
|
||||||
"minutes": (valid_for.seconds // 60) % 60,
|
"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]:
|
if "email" in invites[code]:
|
||||||
invite["email"] = invites[code]["email"]
|
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)
|
response["invites"].append(invite)
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
@@ -343,18 +446,11 @@ def modifyConfig():
|
|||||||
log.debug(f"{section}/{item} modified")
|
log.debug(f"{section}/{item} modified")
|
||||||
with open(config_path, "w") as config_file:
|
with open(config_path, "w") as config_file:
|
||||||
temp_config.write(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.")
|
log.info("Config written. Restart may be needed to load settings.")
|
||||||
return resp()
|
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"])
|
@app.route("/getConfig", methods=["GET"])
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
def getConfig():
|
def getConfig():
|
||||||
@@ -368,3 +464,29 @@ def getConfig():
|
|||||||
if entry in config[section]:
|
if entry in config[section]:
|
||||||
response_config[section][entry]["value"] = config[section][entry]
|
response_config[section][entry]["value"] = config[section][entry]
|
||||||
return jsonify(response_config), 200
|
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 }}
|
||||||
1497
package-lock.json
generated
1497
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,12 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/hrfee/jellyfin-accounts#readme",
|
"homepage": "https://github.com/hrfee/jellyfin-accounts#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"autoprefixer": "^9.8.4",
|
"autoprefixer": "^9.8.5",
|
||||||
"bootstrap": "^5.0.0-alpha1",
|
"bootstrap": "^5.0.0-alpha1",
|
||||||
"bootstrap4": "npm:bootstrap@^4.5.0",
|
"bootstrap4": "npm:bootstrap@^4.5.0",
|
||||||
"clean-css-cli": "^4.3.0",
|
"clean-css-cli": "^4.3.0",
|
||||||
|
"lodash": "^4.17.19",
|
||||||
|
"mjml": "^4.6.3",
|
||||||
"postcss-cli": "^7.1.1"
|
"postcss-cli": "^7.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "jellyfin-accounts"
|
name = "jellyfin-accounts"
|
||||||
version = "0.3.3"
|
version = "0.3.7"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
description = "A simple account management system for Jellyfin"
|
description = "A simple account management system for Jellyfin"
|
||||||
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
|
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
|
||||||
@@ -8,8 +8,8 @@ license = "MIT"
|
|||||||
homepage = "https://github.com/hrfee/jellyfin-accounts"
|
homepage = "https://github.com/hrfee/jellyfin-accounts"
|
||||||
repository = "https://github.com/hrfee/jellyfin-accounts"
|
repository = "https://github.com/hrfee/jellyfin-accounts"
|
||||||
keywords = ["jellyfin", "jf-accounts"]
|
keywords = ["jellyfin", "jf-accounts"]
|
||||||
include = ["jellyfin_accounts/data/*", "jellyfin_accounts/data/static/*.css"]
|
include = ["jellyfin_accounts/data/*", "jellyfin_accounts/data/static/*.css", "jellyfin_accounts/data/*.html"]
|
||||||
exclude = ["images/*", "scss/*"]
|
exclude = ["images/*", "scss/*", "mail/*"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
@@ -43,6 +43,8 @@ jf-accounts = 'jellyfin_accounts:main'
|
|||||||
pre_compile-css = "task get-npm-deps"
|
pre_compile-css = "task get-npm-deps"
|
||||||
compile-css = "python scss/compile.py"
|
compile-css = "python scss/compile.py"
|
||||||
get-npm-deps = "python scss/get_node_deps.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]
|
[build-system]
|
||||||
requires = ["poetry>=0.12"]
|
requires = ["poetry>=0.12"]
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ $list-group-action-active-bg: $jf-blue-focus;
|
|||||||
color: $jf-text-bold;
|
color: $jf-text-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
.text-bright {
|
.text-bright {
|
||||||
color: $jf-text-bold;
|
color: $jf-text-bold;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ $list-group-action-active-bg: $jf-blue-focus;
|
|||||||
color: $jf-text-bold;
|
color: $jf-text-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-button:active {
|
||||||
|
color: $text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
.text-bright {
|
.text-bright {
|
||||||
color: $jf-text-bold;
|
color: $jf-text-bold;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user