mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2025-12-19 16:51:13 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e20466925 | |||
| ef8ff531e3 | |||
| b863706d26 | |||
| 7ec8650467 | |||
| d5ce6d31c5 | |||
| 95989840f1 | |||
| 658f660e19 | |||
| b5af2e7f9d | |||
| dea613fa85 | |||
| b8fdb64f68 | |||
| e80b233af2 | |||
| 3e53bcab27 | |||
| 2551307877 | |||
| 290e6b3dca | |||
| a49b4d9027 | |||
| d615b21c7d | |||
| 9afbd31faa |
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.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,3 +19,6 @@ requirements.txt
|
||||
video/
|
||||
scss/bs5/*.css*
|
||||
scss/bs4/*.css*
|
||||
mail/*.html
|
||||
jellyfin_accounts/data/*.html
|
||||
jellyfin_accounts/data/*.txt
|
||||
|
||||
@@ -9,12 +9,12 @@ A basic account management system for [Jellyfin](https://github.com/jellyfin/jel
|
||||
* Password resets are handled using smtplib, requests, and [jinja](https://github.com/pallets/jinja)
|
||||
## Interface
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/jfa.gif" width="100%"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/jfa.gif" width="100%"></img>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/admin.png" width="48%" style="margin-right: 1.5%;" alt="Admin page"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/create.png" width="48%" style="margin-left: 1.5%;" alt="Account creation page"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/admin.png" width="48%" style="margin-right: 1.5%;" alt="Admin page"></img>
|
||||
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/create.png" width="48%" style="margin-left: 1.5%;" alt="Account creation page"></img>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ server = http://jellyfin.local:8096
|
||||
public_server = https://jellyf.in:443
|
||||
; this and below settings will show on the jellyfin dashboard when the program connects. you may as well leave them alone.
|
||||
client = jf-accounts
|
||||
version = 0.3.2
|
||||
version = 0.3.7
|
||||
device = jf-accounts
|
||||
device_id = jf-accounts-0.3.2
|
||||
device_id = jf-accounts-0.3.7
|
||||
|
||||
[ui]
|
||||
; settings related to the ui and program functionality.
|
||||
@@ -28,10 +28,12 @@ admin_only = true
|
||||
username = your username
|
||||
; password for admin page (leave blank if using jellyfin_login)
|
||||
password = your password
|
||||
; address to send notifications to (leave blank if using jellyfin_login)
|
||||
email = example@example.com
|
||||
debug = false
|
||||
; displayed at bottom of all pages except admin
|
||||
contact_message = Need help? contact me.
|
||||
; display at top of invite form.
|
||||
; displayed at top of invite form.
|
||||
help_message = Enter your details to create an account.
|
||||
; displayed when a user creates an account
|
||||
success_message = Your account has been created. Click below to continue to Jellyfin.
|
||||
@@ -88,6 +90,19 @@ subject = Invite - Jellyfin
|
||||
; base url for jf-accounts. this is necessary because using a reverse proxy means the program has no way of knowing the url itself.
|
||||
url_base = http://accounts.jellyf.in:8056/invite
|
||||
|
||||
[notifications]
|
||||
; notification related settings.
|
||||
; enabling adds optional toggles to invites to notify on expiry and user creation.
|
||||
enabled = true
|
||||
; path to expiry notification email html.
|
||||
expiry_html =
|
||||
; path to expiry notification email in plaintext.
|
||||
expiry_text =
|
||||
; path to user creation notification email html.
|
||||
created_html =
|
||||
; path to user creation notification email in plaintext.
|
||||
created_text =
|
||||
|
||||
[mailgun]
|
||||
; mailgun api connection settings
|
||||
api_url = https://api.mailgun.net...
|
||||
|
||||
BIN
images/jfa.gif
BIN
images/jfa.gif
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 MiB After Width: | Height: | Size: 3.2 MiB |
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
__version__ = "0.3.6"
|
||||
# Runs it!
|
||||
__version__ = "0.3.7"
|
||||
|
||||
import secrets
|
||||
import configparser
|
||||
@@ -13,6 +13,7 @@ import json
|
||||
from pathlib import Path
|
||||
from flask import Flask, jsonify, g
|
||||
from jellyfin_accounts.data_store import JSONStorage
|
||||
from jellyfin_accounts.config import Config
|
||||
|
||||
parser = argparse.ArgumentParser(description="jellyfin-accounts")
|
||||
|
||||
@@ -69,7 +70,7 @@ if data_dir.exists() is False or (data_dir / "config.ini").exists() is False:
|
||||
else:
|
||||
config_path = data_dir / "config.ini"
|
||||
|
||||
|
||||
# Temp config so logger knows whether to use debug mode or not
|
||||
temp_config = configparser.RawConfigParser()
|
||||
temp_config.read(config_path)
|
||||
|
||||
@@ -93,61 +94,7 @@ def create_log(name):
|
||||
|
||||
log = create_log("main")
|
||||
|
||||
|
||||
def load_config(config_path, data_dir):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(config_path)
|
||||
global log
|
||||
for key in config["files"]:
|
||||
if config["files"][key] == "":
|
||||
if key != "custom_css":
|
||||
log.debug(f"Using default {key}")
|
||||
config["files"][key] = str(data_dir / (key + ".json"))
|
||||
|
||||
for key in ["user_configuration", "user_displayprefs"]:
|
||||
if key not in config["files"]:
|
||||
log.debug(f"Using default {key}")
|
||||
config["files"][key] = str(data_dir / (key + ".json"))
|
||||
|
||||
if "no_username" not in config["email"]:
|
||||
config["email"]["no_username"] = "false"
|
||||
log.debug("Set no_username to false")
|
||||
if (
|
||||
"email_html" not in config["password_resets"]
|
||||
or config["password_resets"]["email_html"] == ""
|
||||
):
|
||||
log.debug("Using default password reset email HTML template")
|
||||
config["password_resets"]["email_html"] = str(local_dir / "email.html")
|
||||
if (
|
||||
"email_text" not in config["password_resets"]
|
||||
or config["password_resets"]["email_text"] == ""
|
||||
):
|
||||
log.debug("Using default password reset email plaintext template")
|
||||
config["password_resets"]["email_text"] = str(local_dir / "email.txt")
|
||||
|
||||
if (
|
||||
"email_html" not in config["invite_emails"]
|
||||
or config["invite_emails"]["email_html"] == ""
|
||||
):
|
||||
log.debug("Using default invite email HTML template")
|
||||
config["invite_emails"]["email_html"] = str(local_dir / "invite-email.html")
|
||||
if (
|
||||
"email_text" not in config["invite_emails"]
|
||||
or config["invite_emails"]["email_text"] == ""
|
||||
):
|
||||
log.debug("Using default invite email plaintext template")
|
||||
config["invite_emails"]["email_text"] = str(local_dir / "invite-email.txt")
|
||||
if (
|
||||
"public_server" not in config["jellyfin"]
|
||||
or config["jellyfin"]["public_server"] == ""
|
||||
):
|
||||
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
||||
if "bs5" not in config["ui"] or config["ui"]["bs5"] == "":
|
||||
config["ui"]["bs5"] = "false"
|
||||
return config
|
||||
|
||||
|
||||
config = load_config(config_path, data_dir)
|
||||
config = Config(config_path, secrets.token_urlsafe(16), data_dir, local_dir, log)
|
||||
|
||||
web_log = create_log("waitress")
|
||||
if not first_run:
|
||||
@@ -343,13 +290,6 @@ def main():
|
||||
success = True
|
||||
|
||||
else:
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print("Quitting...")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
global app
|
||||
app = Flask(__name__, root_path=str(local_dir))
|
||||
app.config["DEBUG"] = config.getboolean("ui", "debug")
|
||||
@@ -359,6 +299,13 @@ def main():
|
||||
from waitress import serve
|
||||
|
||||
if first_run:
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print("Quitting...")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
import jellyfin_accounts.setup
|
||||
|
||||
host = config["ui"]["host"]
|
||||
@@ -368,6 +315,7 @@ def main():
|
||||
else:
|
||||
import jellyfin_accounts.web_api
|
||||
import jellyfin_accounts.web
|
||||
import jellyfin_accounts.invite_daemon
|
||||
|
||||
host = config["ui"]["host"]
|
||||
port = config["ui"]["port"]
|
||||
@@ -383,4 +331,12 @@ def main():
|
||||
log.info("Starting email thread")
|
||||
pwr.start()
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print("Quitting...")
|
||||
if config.getboolean("notifications", "enabled"):
|
||||
jellyfin_accounts.invite_daemon.inviteDaemon.stop()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
serve(app, host=host, port=int(port))
|
||||
|
||||
121
jellyfin_accounts/config.py
Normal file
121
jellyfin_accounts/config.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import os
|
||||
import configparser
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
Configuration object that can automatically reload modified settings.
|
||||
Behaves mostly like a dictionary.
|
||||
:param file: Path to config.ini, where parameters are set.
|
||||
:param instance: Used to identify specific jf-accounts instances in environment variables.
|
||||
:param data_dir: Path to directory with config, invites, templates, etc.
|
||||
:param local_dir: Path to internally stored config base, emails, etc.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def load_config(config_path, data_dir, local_dir, log):
|
||||
# Lord forgive me for this mess
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(config_path)
|
||||
for key in config["files"]:
|
||||
if config["files"][key] == "":
|
||||
if key != "custom_css":
|
||||
log.debug(f"Using default {key}")
|
||||
config["files"][key] = str(data_dir / (key + ".json"))
|
||||
|
||||
for key in ["user_configuration", "user_displayprefs"]:
|
||||
if key not in config["files"]:
|
||||
log.debug(f"Using default {key}")
|
||||
config["files"][key] = str(data_dir / (key + ".json"))
|
||||
|
||||
if "no_username" not in config["email"]:
|
||||
config["email"]["no_username"] = "false"
|
||||
log.debug("Set no_username to false")
|
||||
if (
|
||||
"email_html" not in config["password_resets"]
|
||||
or config["password_resets"]["email_html"] == ""
|
||||
):
|
||||
log.debug("Using default password reset email HTML template")
|
||||
config["password_resets"]["email_html"] = str(local_dir / "email.html")
|
||||
if (
|
||||
"email_text" not in config["password_resets"]
|
||||
or config["password_resets"]["email_text"] == ""
|
||||
):
|
||||
log.debug("Using default password reset email plaintext template")
|
||||
config["password_resets"]["email_text"] = str(local_dir / "email.txt")
|
||||
|
||||
if (
|
||||
"email_html" not in config["invite_emails"]
|
||||
or config["invite_emails"]["email_html"] == ""
|
||||
):
|
||||
log.debug("Using default invite email HTML template")
|
||||
config["invite_emails"]["email_html"] = str(local_dir / "invite-email.html")
|
||||
if (
|
||||
"email_text" not in config["invite_emails"]
|
||||
or config["invite_emails"]["email_text"] == ""
|
||||
):
|
||||
log.debug("Using default invite email plaintext template")
|
||||
config["invite_emails"]["email_text"] = str(local_dir / "invite-email.txt")
|
||||
if (
|
||||
"public_server" not in config["jellyfin"]
|
||||
or config["jellyfin"]["public_server"] == ""
|
||||
):
|
||||
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
||||
if "bs5" not in config["ui"] or config["ui"]["bs5"] == "":
|
||||
config["ui"]["bs5"] = "false"
|
||||
if (
|
||||
"expiry_html" not in config["notifications"]
|
||||
or config["notifications"]["expiry_html"] == ""
|
||||
):
|
||||
log.debug("Using default expiry notification HTML template")
|
||||
config["notifications"]["expiry_html"] = str(local_dir / "expired.html")
|
||||
if (
|
||||
"expiry_text" not in config["notifications"]
|
||||
or config["notifications"]["expiry_text"] == ""
|
||||
):
|
||||
log.debug("Using default expiry notification plaintext template")
|
||||
config["notifications"]["expiry_text"] = str(local_dir / "expired.txt")
|
||||
if (
|
||||
"created_html" not in config["notifications"]
|
||||
or config["notifications"]["created_html"] == ""
|
||||
):
|
||||
log.debug("Using default user creation notification HTML template")
|
||||
config["notifications"]["created_html"] = str(local_dir / "created.html")
|
||||
if (
|
||||
"created_text" not in config["notifications"]
|
||||
or config["notifications"]["created_text"] == ""
|
||||
):
|
||||
log.debug("Using default user creation notification plaintext template")
|
||||
config["notifications"]["created_text"] = str(local_dir / "created.txt")
|
||||
|
||||
return config
|
||||
|
||||
def __init__(self, file, instance, data_dir, local_dir, log):
|
||||
self.config_path = Path(file)
|
||||
self.data_dir = data_dir
|
||||
self.local_dir = local_dir
|
||||
self.instance = instance
|
||||
self.log = log
|
||||
self.varname = f"JFA_{self.instance}_RELOADCONFIG"
|
||||
os.environ[self.varname] = "true"
|
||||
|
||||
def __getitem__(self, key):
|
||||
if os.environ[self.varname] == "true":
|
||||
self.config = Config.load_config(
|
||||
self.config_path, self.data_dir, self.local_dir, self.log
|
||||
)
|
||||
os.environ[self.varname] = "false"
|
||||
return self.config.__getitem__(key)
|
||||
|
||||
def getboolean(self, sect, key):
|
||||
if os.environ[self.varname] == "true":
|
||||
self.config = Config.load_config(
|
||||
self.config_path, self.data_dir, self.local_dir, self.log
|
||||
)
|
||||
os.environ[self.varname] = "false"
|
||||
return self.config.getboolean(sect, key)
|
||||
|
||||
def trigger_reload(self):
|
||||
os.environ[self.varname] = "true"
|
||||
@@ -133,6 +133,15 @@
|
||||
"value": "your password",
|
||||
"description": "Password for admin page (Leave blank if using jellyfin_login)"
|
||||
},
|
||||
"email": {
|
||||
"name": "Admin email address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_false": "jellyfin_login",
|
||||
"type": "text",
|
||||
"value": "example@example.com",
|
||||
"description": "Address to send notifications to (Leave blank if using jellyfin_login)"
|
||||
},
|
||||
"debug": {
|
||||
"name": "Debug logging",
|
||||
"required": false,
|
||||
@@ -143,7 +152,7 @@
|
||||
"contact_message": {
|
||||
"name": "Contact message",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Need help? contact me.",
|
||||
"description": "Displayed at bottom of all pages except admin"
|
||||
@@ -151,15 +160,15 @@
|
||||
"help_message": {
|
||||
"name": "Help message",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Enter your details to create an account.",
|
||||
"description": "Display at top of invite form."
|
||||
"description": "Displayed at top of invite form."
|
||||
},
|
||||
"success_message": {
|
||||
"name": "Success message",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
||||
"description": "Displayed when a user creates an account"
|
||||
@@ -167,7 +176,7 @@
|
||||
"bs5": {
|
||||
"name": "Use Bootstrap 5",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use Bootstrap 5 (currently in alpha). This also removes the need for jQuery, so the page should load faster."
|
||||
@@ -181,41 +190,41 @@
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
"min_length": {
|
||||
"name": "Minimum Length",
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "8"
|
||||
},
|
||||
"upper": {
|
||||
"name": "Minimum uppercase characters",
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "1"
|
||||
},
|
||||
"lower": {
|
||||
"name": "Minimum lowercase characters",
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "0"
|
||||
},
|
||||
"number": {
|
||||
"name": "Minimum number count",
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "1"
|
||||
},
|
||||
"special": {
|
||||
"name": "Minimum number of special characters",
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "0"
|
||||
@@ -229,7 +238,7 @@
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
@@ -350,7 +359,7 @@
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
@@ -391,6 +400,56 @@
|
||||
"description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"meta": {
|
||||
"name": "Notifications",
|
||||
"description": "Notification related settings."
|
||||
},
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": "false",
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"description": "Enabling adds optional toggles to invites to notify on expiry and user creation."
|
||||
},
|
||||
"expiry_html": {
|
||||
"name": "Expiry email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to expiry notification email HTML."
|
||||
},
|
||||
"expiry_text": {
|
||||
"name": "Expiry email (Plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": "false",
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to expiry notification email in plaintext."
|
||||
},
|
||||
"created_html": {
|
||||
"name": "User created email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to user creation notification email HTML."
|
||||
},
|
||||
"created_text": {
|
||||
"name": "User created email (Plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to user creation notification email in plaintext."
|
||||
}
|
||||
},
|
||||
"mailgun": {
|
||||
"meta": {
|
||||
"name": "Mailgun (Email)",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,12 +18,15 @@ function checkEmailRadio() {
|
||||
document.getElementById('emailNextButton').href = '#page-5';
|
||||
document.getElementById('valBackButton').href = '#page-7';
|
||||
if (document.getElementById('emailSMTPRadio').checked) {
|
||||
document.getElementById('emailCommonArea').style.display = '';
|
||||
document.getElementById('emailSMTPArea').style.display = '';
|
||||
document.getElementById('emailMailgunArea').style.display = 'none';
|
||||
} else if (document.getElementById('emailMailgunRadio').checked) {
|
||||
document.getElementById('emailCommonArea').style.display = '';
|
||||
document.getElementById('emailSMTPArea').style.display = 'none';
|
||||
document.getElementById('emailMailgunArea').style.display = '';
|
||||
} else if (document.getElementById('emailDisabledRadio').checked) {
|
||||
document.getElementById('emailCommonArea').style.display = 'none';
|
||||
document.getElementById('emailSMTPArea').style.display = 'none';
|
||||
document.getElementById('emailMailgunArea').style.display = 'none';
|
||||
document.getElementById('emailNextButton').href = '#page-8';
|
||||
@@ -140,6 +143,7 @@ document.getElementById('submitButton').onclick = function() {
|
||||
config['invite_emails'] = {};
|
||||
config['mailgun'] = {};
|
||||
config['smtp'] = {};
|
||||
config['notifications'] = {};
|
||||
// Page 2: Auth
|
||||
if (document.getElementById('jfAuthRadio').checked) {
|
||||
config['ui']['jellyfin_login'] = 'true';
|
||||
@@ -151,6 +155,7 @@ document.getElementById('submitButton').onclick = function() {
|
||||
} else {
|
||||
config['ui']['username'] = document.getElementById('manualAuthUsername').value;
|
||||
config['ui']['password'] = document.getElementById('manualAuthPassword').value;
|
||||
config['ui']['email'] = document.getElementById('manualAuthEmail').value;
|
||||
};
|
||||
// Page 3: Connect to jellyfin
|
||||
config['jellyfin']['server'] = document.getElementById('jfHost').value;
|
||||
@@ -178,6 +183,7 @@ document.getElementById('submitButton').onclick = function() {
|
||||
config['mailgun']['api_key'] = document.getElementById('emailMailgunKey').value;
|
||||
config['email']['address'] = document.getElementById('emailMailgunAddress').value;
|
||||
};
|
||||
config['notifications']['enabled'] = document.getElementById('notificationsEnabled').checked.toString();
|
||||
// Page 5: Email formatting
|
||||
config['email']['from'] = document.getElementById('emailSender').value;
|
||||
config['email']['date_format'] = document.getElementById('emailDateFormat').value;
|
||||
|
||||
@@ -35,19 +35,15 @@
|
||||
{% else %}
|
||||
const bsVersion = 4;
|
||||
{% endif %}
|
||||
console.log('create');
|
||||
var css = document.createElement('link');
|
||||
css.setAttribute('rel', 'stylesheet');
|
||||
css.setAttribute('type', 'text/css');
|
||||
var cssCookie = getCookie("css");
|
||||
if (cssCookie.includes('bs' + bsVersion)) {
|
||||
console.log('href');
|
||||
css.setAttribute('href', cssCookie);
|
||||
} else {
|
||||
console.log('href');
|
||||
css.setAttribute('href', '{{ css_file }}');
|
||||
};
|
||||
console.log('append');
|
||||
document.head.appendChild(css);
|
||||
</script>
|
||||
{% if not bs5 %}
|
||||
@@ -218,18 +214,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="restartModal" role="dialog" aria-labelledby"Restart Warning" aria-hidden="true">
|
||||
<div class="modal fade" id="restartModal" role="dialog" aria-labelledby="Restart Warning" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Warning</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>A restart is needed to apply some settings. This must be done manually. Apply now?</p>
|
||||
<p>A restart is needed to apply some settings. Restart now, later, or cancel?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="applyRestarts" data-dismiss="alert">Apply</button>
|
||||
<button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-secondary" id="applyRestarts" data-dismiss="modal">Apply, Restart later</button>
|
||||
<button type="button" class="btn btn-primary" id="applyAndRestart" data-dismiss="modal">Apply & Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="refreshModal" role="dialog" aria-labelledby="Refresh page notice" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Settings applied.</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Refresh the page in a few seconds.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -157,8 +157,9 @@ var settingsModal = createModal('settingsMenu');
|
||||
var userDefaultsModal = createModal('userDefaults');
|
||||
var usersModal = createModal('users');
|
||||
var restartModal = createModal('restartModal');
|
||||
var refreshModal = createModal('refreshModal');
|
||||
|
||||
// Parsed invite: [<code>, <expires in _>, <1: Empty invite (no delete/link), 0: Actual invite>, <email address>, <remaining uses>, [<used-by>], <date created>]
|
||||
// Parsed invite: [<code>, <expires in _>, <1: Empty invite (no delete/link), 0: Actual invite>, <email address>, <remaining uses>, [<used-by>], <date created>, <notify on expiry>, <notify on creation>]
|
||||
function parseInvite(invite, empty = false) {
|
||||
if (empty) {
|
||||
return ["None", "", 1];
|
||||
@@ -185,6 +186,12 @@ function parseInvite(invite, empty = false) {
|
||||
if ('created' in invite) {
|
||||
i[6] = invite['created'];
|
||||
}
|
||||
if ('notify-expiry' in invite) {
|
||||
i[7] = invite['notify-expiry'];
|
||||
}
|
||||
if ('notify-creation' in invite) {
|
||||
i[8] = invite['notify-creation'];
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -292,6 +299,78 @@ function addItem(parsedInvite) {
|
||||
dropdownLeft.appendChild(leftList);
|
||||
dropdownContent.appendChild(dropdownLeft);
|
||||
|
||||
{% if notifications %}
|
||||
let dropdownMiddle = document.createElement('div');
|
||||
dropdownMiddle.id = parsedInvite[0] + '_notifyButtons';
|
||||
dropdownMiddle.classList.add('col');
|
||||
|
||||
let middleList = document.createElement('ul');
|
||||
middleList.classList.add('list-group', 'list-group-flush');
|
||||
middleList.textContent = 'Notify on:';
|
||||
|
||||
let notifyExpiry = document.createElement('li');
|
||||
notifyExpiry.classList.add('list-group-item', 'py-1', 'form-check');
|
||||
notifyExpiry.innerHTML = `
|
||||
<input class="form-check-input" type="checkbox" value="" id="${parsedInvite[0]}_notifyExpiry">
|
||||
<label class="form-check-label" for="${parsedInvite[0]}_notifyExpiry">Expiry</label>
|
||||
`;
|
||||
if (typeof(parsedInvite[7]) == 'boolean') {
|
||||
notifyExpiry.getElementsByTagName('input')[0].checked = parsedInvite[7];
|
||||
}
|
||||
|
||||
notifyExpiry.getElementsByTagName('input')[0].onclick = function() {
|
||||
let req = new XMLHttpRequest();
|
||||
var thisEl = this;
|
||||
let send = {};
|
||||
let code = thisEl.id.replace('_notifyExpiry', '');
|
||||
send[code] = {};
|
||||
send[code]['notify-expiry'] = thisEl.checked;
|
||||
req.open("POST", "/setNotify", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status != 200) {
|
||||
thisEl.checked = !thisEl.checked;
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(send));
|
||||
};
|
||||
middleList.appendChild(notifyExpiry);
|
||||
|
||||
let notifyCreation = document.createElement('li');
|
||||
notifyCreation.classList.add('list-group-item', 'py-1', 'form-check');
|
||||
notifyCreation.innerHTML = `
|
||||
<input class="form-check-input" type="checkbox" value="" id="${parsedInvite[0]}_notifyCreation">
|
||||
<label class="form-check-label" for="${parsedInvite[0]}_notifyCreation">User creation</label>
|
||||
`;
|
||||
if (typeof(parsedInvite[8]) == 'boolean') {
|
||||
notifyCreation.getElementsByTagName('input')[0].checked = parsedInvite[8];
|
||||
}
|
||||
notifyCreation.getElementsByTagName('input')[0].onclick = function() {
|
||||
let req = new XMLHttpRequest();
|
||||
var thisEl = this;
|
||||
let send = {};
|
||||
let code = thisEl.id.replace('_notifyCreation', '');
|
||||
send[code] = {};
|
||||
send[code]['notify-creation'] = thisEl.checked;
|
||||
req.open("POST", "/setNotify", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status != 200) {
|
||||
thisEl.checked = !thisEl.checked;
|
||||
}
|
||||
};
|
||||
req.send(JSON.stringify(send));
|
||||
};
|
||||
middleList.appendChild(notifyCreation);
|
||||
|
||||
dropdownMiddle.appendChild(middleList);
|
||||
dropdownContent.appendChild(dropdownMiddle);
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
let dropdownRight = document.createElement('div');
|
||||
dropdownRight.id = parsedInvite[0] + '_usersCreated';
|
||||
dropdownRight.classList.add('col');
|
||||
@@ -883,8 +962,12 @@ document.getElementById('openSettings').onclick = function () {
|
||||
|
||||
triggerTooltips();
|
||||
|
||||
function sendConfig(modalId) {
|
||||
function sendConfig(modalId, restart = false) {
|
||||
let modal = document.getElementById(modalId);
|
||||
modifiedConfig['restart-program'] = false;
|
||||
if (restart) {
|
||||
modifiedConfig['restart-program'] = true;
|
||||
}
|
||||
let send = JSON.stringify(modifiedConfig);
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyConfig", true);
|
||||
@@ -897,6 +980,8 @@ function sendConfig(modalId) {
|
||||
if (modalId != 'settingsMenu') {
|
||||
settingsModal.hide();
|
||||
}
|
||||
} else if (restart) {
|
||||
refreshModal.show();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -932,7 +1017,8 @@ document.getElementById('settingsSave').onclick = function() {
|
||||
}
|
||||
}
|
||||
if (restart_setting_changed) {
|
||||
document.getElementById('applyRestarts').onclick = function(){sendConfig('restartModal');};
|
||||
document.getElementById('applyRestarts').onclick = function(){ sendConfig('restartModal'); };
|
||||
document.getElementById('applyAndRestart').onclick = function(){ sendConfig('restartModal', restart=true); };
|
||||
settingsModal.hide();
|
||||
restartModal.show();
|
||||
} else if (settings_changed) {
|
||||
|
||||
@@ -89,6 +89,11 @@
|
||||
<label for="manualAuthPassword">Password</label>
|
||||
<input type="password" class="form-control" id="manualAuthPassword" placeholder="Password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manualAuthEmail">Email (Optional)</label>
|
||||
<input type="email" class="form-control" id="manualAuthEmail" placeholder="example@example.com">
|
||||
<small class="form-text text-muted">Your email address is only required if you want to recieve activity notifications.</small>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
@@ -180,6 +185,15 @@
|
||||
<input type="email" class="form-control" id="emailMailgunAddress" placeholder="jellyfin@jellyf.in">
|
||||
</div>
|
||||
</div>
|
||||
<div id="emailCommonArea">
|
||||
<h5 class="card-title">Notifications</h5>
|
||||
<p class="card-text">Enabling notifications will allow you to choose (per-invite) to recieve emails when an invite expires, or when a new user is created. If you chose to use Manual auth instead of Jellyfin auth previously, make sure you provided an email address.</p>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="notificationsEnabled">
|
||||
<label for="notificationsEnabled" class="form-check-label">Enabled</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-3">Back</a>
|
||||
@@ -341,7 +355,7 @@
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Finished!</h5>
|
||||
<p class="card-text">
|
||||
Press the button below to submit your settings. The program will quit, so run it again, then refresh this page.
|
||||
Press the button below to submit your settings. The program will restart. Once it's done, refresh this page.
|
||||
</p>
|
||||
<button id="submitButton" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Automatic storage of everything except the config
|
||||
import json
|
||||
import datetime
|
||||
|
||||
@@ -42,7 +43,10 @@ class JSONFile(dict):
|
||||
def __delitem__(self, key):
|
||||
data = self.readJSON(self.path)
|
||||
super(JSONFile, self).__init__(data)
|
||||
del data[key]
|
||||
try:
|
||||
del data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
self.writeJSON(self.path, data)
|
||||
super(JSONFile, self).__delitem__(key)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Handles everything related to emails
|
||||
import datetime
|
||||
import pytz
|
||||
import requests
|
||||
@@ -7,11 +8,20 @@ from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from pathlib import Path
|
||||
from dateutil import parser as date_parser
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from jinja2 import Template
|
||||
from jellyfin_accounts import config
|
||||
from jellyfin_accounts import email_log as log
|
||||
|
||||
|
||||
def format_datetime(dt):
|
||||
result = dt.strftime(config["email"]["date_format"])
|
||||
if config.getboolean("email", "use_24h"):
|
||||
result += f' {dt.strftime("%H:%M")}'
|
||||
else:
|
||||
result += f' {dt.strftime("%I:%M %p")}'
|
||||
return result
|
||||
|
||||
|
||||
class Email:
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
@@ -25,9 +35,16 @@ class Email:
|
||||
+ f"({self.from_name})"
|
||||
)
|
||||
)
|
||||
# sp = Path(config["invite_emails"]["email_
|
||||
# template_loader = FileSystemLoader(searchpath=sp)
|
||||
# template_loader = PackageLoader("jellyfin_accounts", "data")
|
||||
# self.template_env = Environment(loader=template_loader)
|
||||
|
||||
def pretty_time(self, expiry):
|
||||
current_time = datetime.datetime.now()
|
||||
def pretty_time(self, expiry, tzaware=False):
|
||||
if tzaware:
|
||||
current_time = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||
else:
|
||||
current_time = datetime.datetime.now()
|
||||
date = expiry.strftime(config["email"]["date_format"])
|
||||
if config.getboolean("email", "use_24h"):
|
||||
log.debug(f"{self.address}: Using 24h time")
|
||||
@@ -43,9 +60,7 @@ class Email:
|
||||
if expires_in["hours"] == 0:
|
||||
expires_in = f'{str(expires_in["minutes"])}m'
|
||||
else:
|
||||
expires_in = (
|
||||
f'{str(expires_in["hours"])}h ' + f'{str(expires_in["minutes"])}m'
|
||||
)
|
||||
expires_in = f'{str(expires_in["hours"])}h {str(expires_in["minutes"])}m'
|
||||
log.debug(f"{self.address}: Expires in {expires_in}")
|
||||
return {"date": date, "time": time, "expires_in": expires_in}
|
||||
|
||||
@@ -60,12 +75,9 @@ class Email:
|
||||
invite_link = config["invite_emails"]["url_base"]
|
||||
invite_link += "/" + invite["code"]
|
||||
for key in ["text", "html"]:
|
||||
sp = Path(config["invite_emails"]["email_" + key]) / ".."
|
||||
sp = str(sp.resolve()) + "/"
|
||||
template_loader = FileSystemLoader(searchpath=sp)
|
||||
template_env = Environment(loader=template_loader)
|
||||
fname = Path(config["invite_emails"]["email_" + key]).name
|
||||
template = template_env.get_template(fname)
|
||||
fpath = Path(config["invite_emails"]["email_" + key])
|
||||
with open(fpath, 'r') as f:
|
||||
template = Template(f.read())
|
||||
c = template.render(
|
||||
expiry_date=pretty["date"],
|
||||
expiry_time=pretty["time"],
|
||||
@@ -76,28 +88,59 @@ class Email:
|
||||
self.content[key] = c
|
||||
log.info(f"{self.address}: {key} constructed")
|
||||
|
||||
def construct_expiry(self, invite):
|
||||
self.subject = "Notice: Invite expired"
|
||||
log.debug(f'Constructing expiry notification for {invite["code"]}')
|
||||
expiry = format_datetime(invite["expiry"])
|
||||
for key in ["text", "html"]:
|
||||
fpath = Path(config["notifications"]["expiry_" + key])
|
||||
with open(fpath, 'r') as f:
|
||||
template = Template(f.read())
|
||||
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 user creation 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"]:
|
||||
fpath = Path(config["notifications"]["created_" + key])
|
||||
with open(fpath, 'r') as f:
|
||||
template = Template(f.read())
|
||||
c = template.render(
|
||||
code=invite["code"],
|
||||
username=invite["username"],
|
||||
address=email,
|
||||
time=created,
|
||||
)
|
||||
self.content[key] = c
|
||||
log.info(f"{self.address}: {key} constructed")
|
||||
return True
|
||||
|
||||
def construct_reset(self, reset):
|
||||
self.subject = config["password_resets"]["subject"]
|
||||
log.debug(f"{self.address}: Using subject {self.subject}")
|
||||
log.debug(f"{self.address}: Constructing email content")
|
||||
try:
|
||||
expiry = date_parser.parse(reset["ExpirationDate"])
|
||||
expiry = expiry.replace(tzinfo=None)
|
||||
except:
|
||||
log.error(f"{self.address}: Couldn't parse expiry time")
|
||||
return False
|
||||
current_time = datetime.datetime.now()
|
||||
current_time = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||
if expiry >= current_time:
|
||||
log.debug(f"{self.address}: Invite valid")
|
||||
pretty = self.pretty_time(expiry)
|
||||
pretty = self.pretty_time(expiry, tzaware=True)
|
||||
email_message = config["email"]["message"]
|
||||
for key in ["text", "html"]:
|
||||
sp = Path(config["password_resets"]["email_" + key]) / ".."
|
||||
sp = str(sp.resolve()) + "/"
|
||||
template_loader = FileSystemLoader(searchpath=sp)
|
||||
template_env = Environment(loader=template_loader)
|
||||
fname = Path(config["password_resets"]["email_" + key]).name
|
||||
template = template_env.get_template(fname)
|
||||
fpath = Path(config["password_resets"]["email_" + key])
|
||||
with open(fpath, 'r') as f:
|
||||
template = Template(f.read())
|
||||
c = template.render(
|
||||
username=reset["UserName"],
|
||||
expiry_date=pretty["date"],
|
||||
@@ -120,6 +163,11 @@ class Email:
|
||||
|
||||
|
||||
class Mailgun(Email):
|
||||
errors = {
|
||||
400: "Mailgun failed with 400: Bad request",
|
||||
401: "Mailgun failed with 401: Invalid API key",
|
||||
}
|
||||
|
||||
def __init__(self, address):
|
||||
super().__init__(address)
|
||||
self.api_url = config["mailgun"]["api_url"]
|
||||
@@ -141,7 +189,12 @@ class Mailgun(Email):
|
||||
if response.ok:
|
||||
log.info(f"{self.address}: Sent via mailgun.")
|
||||
return True
|
||||
log.debug(f"{self.address}: Mailgun: {response.status_code}")
|
||||
elif response.status_code in Mailgun.errors:
|
||||
log.error(f"{self.address}: {Mailgun.errors[response.status_code]}")
|
||||
else:
|
||||
log.error(
|
||||
f"{self.address}: Mailgun failed with error {response.status_code}"
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@@ -189,9 +242,9 @@ class Smtp(Email):
|
||||
log.info(f"{self.address}: Sent via smtp (starttls)")
|
||||
return True
|
||||
except Exception as e:
|
||||
err = f"{self.address}: Failed to send via smtp: "
|
||||
err += type(e).__name__
|
||||
log.error(err)
|
||||
log.error(
|
||||
f"{self.address}: Failed to send via smtp ({type(e).__name__}: {e.args})"
|
||||
)
|
||||
try:
|
||||
log.error(e.smtp_error)
|
||||
except:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Generates config file
|
||||
import configparser
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
43
jellyfin_accounts/invite_daemon.py
Normal file
43
jellyfin_accounts/invite_daemon.py
Normal file
@@ -0,0 +1,43 @@
|
||||
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.
|
||||
if len(invites) != 0:
|
||||
checkInvite(list(invites.keys())[0])
|
||||
|
||||
|
||||
if config.getboolean("notifications", "enabled"):
|
||||
inviteDaemon = Repeat(60, checkInvites)
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Jellyfin API client
|
||||
import requests
|
||||
import time
|
||||
|
||||
@@ -44,7 +44,7 @@ class Jellyfin:
|
||||
|
||||
pass
|
||||
|
||||
def __init__(self, server, client, version, device, deviceId):
|
||||
def __init__(self, server, client, version, device, deviceId, cacheMinutes=30):
|
||||
"""
|
||||
Initializes the Jellyfin object. All parameters except server
|
||||
have no effect on the client's capability.
|
||||
@@ -61,7 +61,7 @@ class Jellyfin:
|
||||
self.version = version
|
||||
self.device = device
|
||||
self.deviceId = deviceId
|
||||
self.timeout = 30 * 60
|
||||
self.timeout = cacheMinutes * 60
|
||||
self.userCacheAge = time.time() - self.timeout - 1
|
||||
self.userCachePublicAge = self.userCacheAge
|
||||
self.useragent = f"{self.client}/{self.version}"
|
||||
@@ -83,6 +83,16 @@ class Jellyfin:
|
||||
self.info = requests.get(self.server + "/System/Info/Public").json()
|
||||
except:
|
||||
pass
|
||||
|
||||
def reloadCache(self):
|
||||
""" Forces a reload of the user caches """
|
||||
self.userCachePublicAge = time.time() - self.timeout - 1
|
||||
self.getUsers()
|
||||
try:
|
||||
self.userCacheAge = self.userCachePublicAge
|
||||
self.getUsers(public=False)
|
||||
except self.AuthenticationRequiredError:
|
||||
pass
|
||||
|
||||
def getUsers(self, username: str = "all", userId: str = "all", public: bool = True):
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# Handles authentication
|
||||
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from itsdangerous import (
|
||||
@@ -106,6 +106,8 @@ def verify_password(username, password):
|
||||
user = Account().verify_token(username, accounts)
|
||||
if user:
|
||||
verified = True
|
||||
if user in accounts:
|
||||
user = accounts[user]
|
||||
if not user:
|
||||
log.debug(f"User {username} not found on Jellyfin")
|
||||
return False
|
||||
@@ -116,10 +118,10 @@ def verify_password(username, password):
|
||||
if username == user.username and user.verify_password(password):
|
||||
g.user = user
|
||||
log.debug("HTTPAuth Allowed")
|
||||
return True
|
||||
return user
|
||||
else:
|
||||
log.debug("HTTPAuth Denied")
|
||||
return False
|
||||
g.user = user
|
||||
log.debug("HTTPAuth Allowed")
|
||||
return True
|
||||
return user
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Watches Jellyfin for password resets and sends emails.
|
||||
import time
|
||||
import json
|
||||
from watchdog.observers import Observer
|
||||
@@ -18,7 +19,8 @@ class Watcher:
|
||||
self.observer.schedule(event_handler, self.dir, recursive=True)
|
||||
try:
|
||||
self.observer.start()
|
||||
except NotADirectoryError:
|
||||
except (NotADirectoryError,
|
||||
FileNotFoundError):
|
||||
log.error(f"Directory {self.dir} does not exist")
|
||||
try:
|
||||
while True:
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# Views and endpoints for the initial setup
|
||||
from flask import request, jsonify, render_template
|
||||
from configparser import RawConfigParser
|
||||
from jellyfin_accounts.jf_api import Jellyfin
|
||||
from jellyfin_accounts import config, config_path, app, first_run, resp
|
||||
from jellyfin_accounts import web_log as log
|
||||
import os
|
||||
import psutil
|
||||
import sys
|
||||
|
||||
if first_run:
|
||||
|
||||
@@ -50,8 +53,16 @@ if first_run:
|
||||
with open(config_path, "w") as config_file:
|
||||
temp_config.write(config_file)
|
||||
log.debug("Config written")
|
||||
# ugly exit, sorry
|
||||
os._exit(1)
|
||||
log.info('Restarting...')
|
||||
try:
|
||||
p = psutil.Process(os.getpid())
|
||||
for handler in p.open_files() + p.connections():
|
||||
os.close(handler.fd)
|
||||
except:
|
||||
pass
|
||||
|
||||
python = sys.executable
|
||||
os.execl(python, python, *sys.argv)
|
||||
return resp()
|
||||
|
||||
@app.route("/testJF", methods=["GET", "POST"])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Password validation
|
||||
specials = ['[', '@', '_', '!', '#', '$', '%', '^', '&', '*', '(', ')',
|
||||
'<', '>', '?', '/', '\\', '|', '}', '{', '~', ':', ']']
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# Web views
|
||||
from pathlib import Path
|
||||
from flask import Flask, send_from_directory, render_template
|
||||
|
||||
from jellyfin_accounts import app, g, css_file, data_store
|
||||
from jellyfin_accounts import config, app, g, css_file, data_store
|
||||
from jellyfin_accounts import web_log as log
|
||||
from jellyfin_accounts.web_api import config, checkInvite, validator
|
||||
from jellyfin_accounts.web_api import checkInvite, validator
|
||||
|
||||
|
||||
if config.getboolean("ui", "bs5"):
|
||||
bsVersion = 5
|
||||
else:
|
||||
bsVersion = 4
|
||||
def bsVersion():
|
||||
if config.getboolean("ui", "bs5"):
|
||||
return 5
|
||||
return 4
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
@@ -27,7 +28,6 @@ def page_not_found(e):
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def admin():
|
||||
# return app.send_static_file('admin.html')
|
||||
return render_template(
|
||||
"admin.html",
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
@@ -42,7 +42,12 @@ def static_proxy(path):
|
||||
if "html" not in path:
|
||||
if "admin.js" in path:
|
||||
return (
|
||||
render_template("admin.js", bsVersion=bsVersion, css_file=css_file),
|
||||
render_template(
|
||||
"admin.js",
|
||||
bsVersion=bsVersion(),
|
||||
css_file=css_file,
|
||||
notifications=config.getboolean("notifications", "enabled"),
|
||||
),
|
||||
200,
|
||||
{"Content-Type": "text/javascript"},
|
||||
)
|
||||
@@ -75,7 +80,7 @@ def inviteProxy(path):
|
||||
successMessage=config["ui"]["success_message"],
|
||||
jfLink=config["jellyfin"]["public_server"],
|
||||
validate=config.getboolean("password_validation", "enabled"),
|
||||
requirements=validator.getCriteria(),
|
||||
requirements=validator().getCriteria(),
|
||||
email=email,
|
||||
username=(not config.getboolean("email", "no_username")),
|
||||
)
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
# A bit of a mess, but mostly does API endpoints and a couple compatability fixes
|
||||
from flask import request, jsonify
|
||||
from jellyfin_accounts.jf_api import Jellyfin
|
||||
import json
|
||||
import datetime
|
||||
import secrets
|
||||
import time
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
import psutil
|
||||
from jellyfin_accounts import (
|
||||
config,
|
||||
config_path,
|
||||
load_config,
|
||||
data_dir,
|
||||
app,
|
||||
g,
|
||||
data_store,
|
||||
@@ -16,6 +19,7 @@ from jellyfin_accounts import (
|
||||
configparser,
|
||||
config_base_path,
|
||||
)
|
||||
from jellyfin_accounts.email import Mailgun, Smtp
|
||||
from jellyfin_accounts import web_log as log
|
||||
from jellyfin_accounts.validate_password import PasswordValidator
|
||||
|
||||
@@ -46,6 +50,22 @@ def checkInvite(code, used=False, username=None):
|
||||
"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}
|
||||
):
|
||||
threading.Thread(target=email.send).start()
|
||||
del data_store.invites[invite]
|
||||
elif invite == code:
|
||||
match = True
|
||||
@@ -136,11 +156,11 @@ if (
|
||||
version.parse(jf.info["Version"]) >= version.parse("10.6.0")
|
||||
and bool(data_store.user_template) is not False
|
||||
):
|
||||
log.info("Updating user_template for Jellyfin >= 10.6.0")
|
||||
if (
|
||||
data_store.user_template["AuthenticationProviderId"]
|
||||
== "Emby.Server.Implementations.Library.DefaultAuthenticationProvider"
|
||||
):
|
||||
log.info("Updating user_template for Jellyfin >= 10.6.0")
|
||||
data_store.user_template[
|
||||
"AuthenticationProviderId"
|
||||
] = "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider"
|
||||
@@ -153,16 +173,16 @@ if (
|
||||
] = "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider"
|
||||
|
||||
|
||||
if config.getboolean("password_validation", "enabled"):
|
||||
validator = PasswordValidator(
|
||||
config["password_validation"]["min_length"],
|
||||
config["password_validation"]["upper"],
|
||||
config["password_validation"]["lower"],
|
||||
config["password_validation"]["number"],
|
||||
config["password_validation"]["special"],
|
||||
)
|
||||
else:
|
||||
validator = PasswordValidator(0, 0, 0, 0, 0)
|
||||
def validator():
|
||||
if config.getboolean("password_validation", "enabled"):
|
||||
return PasswordValidator(
|
||||
config["password_validation"]["min_length"],
|
||||
config["password_validation"]["upper"],
|
||||
config["password_validation"]["lower"],
|
||||
config["password_validation"]["number"],
|
||||
config["password_validation"]["special"],
|
||||
)
|
||||
return PasswordValidator(0, 0, 0, 0, 0)
|
||||
|
||||
|
||||
@app.route("/newUser", methods=["POST"])
|
||||
@@ -170,7 +190,7 @@ def newUser():
|
||||
data = request.get_json()
|
||||
log.debug("Attempted newUser")
|
||||
if checkInvite(data["code"]):
|
||||
validation = validator.validate(data["password"])
|
||||
validation = validator().validate(data["password"])
|
||||
valid = True
|
||||
for criterion in validation:
|
||||
if validation[criterion] is False:
|
||||
@@ -185,7 +205,28 @@ def newUser():
|
||||
return jsonify({"error": error})
|
||||
except:
|
||||
return jsonify({"error": "Unknown error"})
|
||||
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(),
|
||||
}
|
||||
):
|
||||
threading.Thread(target=email.send).start()
|
||||
if user.status_code == 200:
|
||||
try:
|
||||
policy = data_store.user_template
|
||||
@@ -203,9 +244,7 @@ def newUser():
|
||||
jf.setDisplayPreferences(user.json()["Id"], displayprefs)
|
||||
log.debug("Set homescreen layout.")
|
||||
else:
|
||||
log.debug(
|
||||
"user configuration and/or " + "displayprefs were blank"
|
||||
)
|
||||
log.debug("user configuration and/or displayprefs were blank")
|
||||
except:
|
||||
log.error("Failed to set new user homescreen layout")
|
||||
if config.getboolean("password_resets", "enabled"):
|
||||
@@ -261,6 +300,11 @@ def generateInvite():
|
||||
response = email.send()
|
||||
if response is False or type(response) != bool:
|
||||
invite["email"] = f"Failed to send to {address}"
|
||||
if config.getboolean("notifications", "enabled"):
|
||||
if "notify-creation" in data:
|
||||
invite["notify-creation"] = data["notify-creation"]
|
||||
if "notify-expiry" in data:
|
||||
invite["notify-expiry"] = data["notify-expiry"]
|
||||
data_store.invites[invite_code] = invite
|
||||
log.info(f"New invite created: {invite_code}")
|
||||
return resp()
|
||||
@@ -299,6 +343,20 @@ def getInvites():
|
||||
invite["remaining-uses"] = 1
|
||||
if "email" in invites[code]:
|
||||
invite["email"] = invites[code]["email"]
|
||||
if "notify" in invites[code]:
|
||||
if config.getboolean("ui", "jellyfin_login"):
|
||||
address = data_store.emails[g.user.id]
|
||||
else:
|
||||
address = config["ui"]["email"]
|
||||
if address in invites[code]["notify"]:
|
||||
if "notify-expiry" in invites[code]["notify"][address]:
|
||||
invite["notify-expiry"] = invites[code]["notify"][address][
|
||||
"notify-expiry"
|
||||
]
|
||||
if "notify-creation" in invites[code]["notify"][address]:
|
||||
invite["notify-creation"] = invites[code]["notify"][address][
|
||||
"notify-creation"
|
||||
]
|
||||
response["invites"].append(invite)
|
||||
return jsonify(response)
|
||||
|
||||
@@ -385,25 +443,29 @@ def modifyConfig():
|
||||
)
|
||||
temp_config.read(config_path)
|
||||
for section in data:
|
||||
if section in temp_config:
|
||||
if section in temp_config and 'restart-program' not in section:
|
||||
for item in data[section]:
|
||||
temp_config[section][item] = data[section][item]
|
||||
data[section][item] = True
|
||||
log.debug(f"{section}/{item} modified")
|
||||
with open(config_path, "w") as config_file:
|
||||
temp_config.write(config_file)
|
||||
config = load_config(config_path, data_dir)
|
||||
log.info("Config written. Restart may be needed to load settings.")
|
||||
config.trigger_reload()
|
||||
log.info("Config written.")
|
||||
if 'restart-program' in data:
|
||||
if data['restart-program']:
|
||||
log.info('Restarting...')
|
||||
try:
|
||||
proc = psutil.Process(os.getpid())
|
||||
for handler in proc.open_files() + proc.connections():
|
||||
os.close(handler.fd)
|
||||
except Exception as e:
|
||||
log.error(f'Failed restart: {type(e).__name__}')
|
||||
python = sys.executable
|
||||
os.execl(python, python, *sys.argv)
|
||||
return resp()
|
||||
|
||||
|
||||
# @app.route('/getConfig', methods=["GET"])
|
||||
# @auth.login_required
|
||||
# def getConfig():
|
||||
# log.debug('Config requested')
|
||||
# return jsonify(config._sections), 200
|
||||
|
||||
|
||||
@app.route("/getConfig", methods=["GET"])
|
||||
@auth.login_required
|
||||
def getConfig():
|
||||
@@ -417,3 +479,29 @@ def getConfig():
|
||||
if entry in config[section]:
|
||||
response_config[section][entry]["value"] = config[section][entry]
|
||||
return jsonify(response_config), 200
|
||||
|
||||
|
||||
@app.route("/setNotify", methods=["POST"])
|
||||
@auth.login_required
|
||||
def setNotify():
|
||||
data = request.get_json()
|
||||
change = False
|
||||
for code in data:
|
||||
for key in data[code]:
|
||||
if key in ["notify-expiry", "notify-creation"]:
|
||||
inv = data_store.invites[code]
|
||||
if config.getboolean("ui", "jellyfin_login"):
|
||||
address = data_store.emails[g.user.id]
|
||||
else:
|
||||
address = config["ui"]["email"]
|
||||
if "notify" not in inv:
|
||||
inv["notify"] = {}
|
||||
if address not in inv["notify"]:
|
||||
inv["notify"][address] = {}
|
||||
inv["notify"][address][key] = data[code][key]
|
||||
log.debug(f"{code}: Notification settings changed")
|
||||
change = True
|
||||
if change:
|
||||
data_store.invites[code] = inv
|
||||
return resp()
|
||||
return resp(success=False)
|
||||
|
||||
47
mail/created.mjml
Normal file
47
mail/created.mjml
Normal file
@@ -0,0 +1,47 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
</mj-attributes>
|
||||
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> jellyfin-accounts </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<h3>User Created</h3>
|
||||
<p>A user was created using code {{ code }}.</p>
|
||||
</mj-text>
|
||||
<mj-table mj-class="text" container-background-color="#242424">
|
||||
<tr style="text-align: left;">
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
<tr style="font-style: italic; text-align: left; color: rgb(153,153,153);">
|
||||
<th>{{ username }}</th>
|
||||
<th>{{ address }}</th>
|
||||
<th>{{ time }}</th>
|
||||
</mj-table>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||
Notification emails can be toggled on the admin dashboard.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</body>
|
||||
</mjml>
|
||||
7
mail/created.txt
Normal file
7
mail/created.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
A user was created using code {{ code }}.
|
||||
|
||||
Name: {{ username }}
|
||||
Address: {{ address }}
|
||||
Time: {{ time }}
|
||||
|
||||
Note: Notification emails can be toggled on the admin dashboard.
|
||||
40
mail/email.mjml
Normal file
40
mail/email.mjml
Normal file
@@ -0,0 +1,40 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
</mj-attributes>
|
||||
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> Jellyfin </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<p>Hi {{ username }},</p>
|
||||
<p> Someone has recently requested a password reset on Jellyfin.</p>
|
||||
<p>If this was you, enter the below pin into the prompt.</p>
|
||||
<p>The code will expire on {{ expiry_date }}, at {{ expiry_time }} UTC, 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>
|
||||
@@ -2,7 +2,7 @@ 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 }}.
|
||||
This code will expire on {{ expiry_date }}, at {{ expiry_time }} UTC, which is in {{ expires_in }}.
|
||||
If this wasn't you, please ignore this email.
|
||||
|
||||
PIN: {{ pin }}
|
||||
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>
|
||||
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",
|
||||
"dependencies": {
|
||||
"autoprefixer": "^9.8.4",
|
||||
"autoprefixer": "^9.8.5",
|
||||
"bootstrap": "^5.0.0-alpha1",
|
||||
"bootstrap4": "npm:bootstrap@^4.5.0",
|
||||
"clean-css-cli": "^4.3.0",
|
||||
"lodash": "^4.17.19",
|
||||
"mjml": "^4.6.3",
|
||||
"postcss-cli": "^7.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
98
poetry.lock
generated
98
poetry.lock
generated
@@ -46,7 +46,7 @@ description = "Python package for providing Mozilla's CA Bundle."
|
||||
name = "certifi"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2020.4.5.2"
|
||||
version = "2020.6.20"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
@@ -119,7 +119,7 @@ description = "Basic and Digest HTTP authentication for Flask routes"
|
||||
name = "flask-httpauth"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "3.3.0"
|
||||
version = "4.1.0"
|
||||
|
||||
[package.dependencies]
|
||||
Flask = "*"
|
||||
@@ -138,7 +138,7 @@ description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
name = "idna"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "2.9"
|
||||
version = "2.10"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
@@ -242,6 +242,17 @@ optional = false
|
||||
python-versions = "*"
|
||||
version = "0.1.2"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Cross-platform lib for process and system monitoring in Python."
|
||||
name = "psutil"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "5.7.2"
|
||||
|
||||
[package.extras]
|
||||
test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "C parser in Python"
|
||||
@@ -315,7 +326,7 @@ description = "Alternative regular expression module, to replace re."
|
||||
name = "regex"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2020.6.8"
|
||||
version = "2020.7.14"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
@@ -323,7 +334,7 @@ description = "Python HTTP for Humans."
|
||||
name = "requests"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "2.23.0"
|
||||
version = "2.24.0"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
@@ -401,7 +412,7 @@ description = "Filesystem events monitoring"
|
||||
name = "watchdog"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.10.2"
|
||||
version = "0.10.3"
|
||||
|
||||
[package.dependencies]
|
||||
pathtools = ">=0.1.1"
|
||||
@@ -422,7 +433,7 @@ dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "fa8b5fb1ded41b673b8062a2bfc6467e6a484ff62b578147bec001d7d9d8ca16"
|
||||
content-hash = "1c2741c9be187d9d0be662509fb4a87f5978e5f44420e5049a20504824c29a59"
|
||||
python-versions = "^3.6"
|
||||
|
||||
[metadata.files]
|
||||
@@ -439,8 +450,8 @@ black = [
|
||||
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2020.4.5.2-py2.py3-none-any.whl", hash = "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"},
|
||||
{file = "certifi-2020.4.5.2.tar.gz", hash = "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1"},
|
||||
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
|
||||
{file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"},
|
||||
@@ -506,8 +517,8 @@ flask = [
|
||||
{file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
|
||||
]
|
||||
flask-httpauth = [
|
||||
{file = "Flask-HTTPAuth-3.3.0.tar.gz", hash = "sha256:6ef8b761332e780f9ff74d5f9056c2616f52babc1998b01d9f361a1e439e61b9"},
|
||||
{file = "Flask_HTTPAuth-3.3.0-py2.py3-none-any.whl", hash = "sha256:0149953720489407e51ec24bc2f86273597b7973d71cd51f9443bd0e2a89bd72"},
|
||||
{file = "Flask-HTTPAuth-4.1.0.tar.gz", hash = "sha256:9e028e4375039a49031eb9ecc40be4761f0540476040f6eff329a31dabd4d000"},
|
||||
{file = "Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl", hash = "sha256:29e0288869a213c7387f0323b6bf2c7191584fb1da8aa024d9af118e5cd70de7"},
|
||||
]
|
||||
greenlet = [
|
||||
{file = "greenlet-0.4.16-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:80cb0380838bf4e48da6adedb0c7cd060c187bb4a75f67a5aa9ec33689b84872"},
|
||||
@@ -529,8 +540,8 @@ greenlet = [
|
||||
{file = "greenlet-0.4.16.tar.gz", hash = "sha256:6e06eac722676797e8fce4adb8ad3dc57a1bb3adfb0dd3fdf8306c055a38456c"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
|
||||
{file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
|
||||
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
|
||||
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
|
||||
]
|
||||
itsdangerous = [
|
||||
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
|
||||
@@ -628,6 +639,19 @@ pathspec = [
|
||||
pathtools = [
|
||||
{file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"},
|
||||
]
|
||||
psutil = [
|
||||
{file = "psutil-5.7.2-cp27-none-win32.whl", hash = "sha256:f2018461733b23f308c298653c8903d32aaad7873d25e1d228765e91ae42c3f2"},
|
||||
{file = "psutil-5.7.2-cp27-none-win_amd64.whl", hash = "sha256:66c18ca7680a31bf16ee22b1d21b6397869dda8059dbdb57d9f27efa6615f195"},
|
||||
{file = "psutil-5.7.2-cp35-cp35m-win32.whl", hash = "sha256:5e9d0f26d4194479a13d5f4b3798260c20cecf9ac9a461e718eb59ea520a360c"},
|
||||
{file = "psutil-5.7.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4080869ed93cce662905b029a1770fe89c98787e543fa7347f075ade761b19d6"},
|
||||
{file = "psutil-5.7.2-cp36-cp36m-win32.whl", hash = "sha256:d8a82162f23c53b8525cf5f14a355f5d1eea86fa8edde27287dd3a98399e4fdf"},
|
||||
{file = "psutil-5.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0ee3c36428f160d2d8fce3c583a0353e848abb7de9732c50cf3356dd49ad63f8"},
|
||||
{file = "psutil-5.7.2-cp37-cp37m-win32.whl", hash = "sha256:ff1977ba1a5f71f89166d5145c3da1cea89a0fdb044075a12c720ee9123ec818"},
|
||||
{file = "psutil-5.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a5b120bb3c0c71dfe27551f9da2f3209a8257a178ed6c628a819037a8df487f1"},
|
||||
{file = "psutil-5.7.2-cp38-cp38-win32.whl", hash = "sha256:10512b46c95b02842c225f58fa00385c08fa00c68bac7da2d9a58ebe2c517498"},
|
||||
{file = "psutil-5.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:68d36986ded5dac7c2dcd42f2682af1db80d4bce3faa126a6145c1637e1b559f"},
|
||||
{file = "psutil-5.7.2.tar.gz", hash = "sha256:90990af1c3c67195c44c9a889184f84f5b2320dce3ee3acbd054e3ba0b4a7beb"},
|
||||
]
|
||||
pycparser = [
|
||||
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
|
||||
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
|
||||
@@ -652,31 +676,31 @@ pytz = [
|
||||
{file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"},
|
||||
]
|
||||
regex = [
|
||||
{file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"},
|
||||
{file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"},
|
||||
{file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"},
|
||||
{file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"},
|
||||
{file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"},
|
||||
{file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"},
|
||||
{file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"},
|
||||
{file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"},
|
||||
{file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"},
|
||||
{file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"},
|
||||
{file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"},
|
||||
{file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"},
|
||||
{file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"},
|
||||
{file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"},
|
||||
{file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"},
|
||||
{file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"},
|
||||
{file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"},
|
||||
{file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"},
|
||||
{file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"},
|
||||
{file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"},
|
||||
{file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"},
|
||||
{file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"},
|
||||
{file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"},
|
||||
{file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
|
||||
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
|
||||
{file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
|
||||
{file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
@@ -722,7 +746,7 @@ waitress = [
|
||||
{file = "waitress-1.4.4.tar.gz", hash = "sha256:1bb436508a7487ac6cb097ae7a7fe5413aefca610550baf58f0940e51ecfb261"},
|
||||
]
|
||||
watchdog = [
|
||||
{file = "watchdog-0.10.2.tar.gz", hash = "sha256:c560efb643faed5ef28784b2245cf8874f939569717a4a12826a173ac644456b"},
|
||||
{file = "watchdog-0.10.3.tar.gz", hash = "sha256:4214e1379d128b0588021880ccaf40317ee156d4603ac388b9adcf29165e0c04"},
|
||||
]
|
||||
werkzeug = [
|
||||
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "jellyfin-accounts"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
readme = "README.md"
|
||||
description = "A simple account management system for Jellyfin"
|
||||
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
|
||||
@@ -8,8 +8,8 @@ license = "MIT"
|
||||
homepage = "https://github.com/hrfee/jellyfin-accounts"
|
||||
repository = "https://github.com/hrfee/jellyfin-accounts"
|
||||
keywords = ["jellyfin", "jf-accounts"]
|
||||
include = ["jellyfin_accounts/data/*", "jellyfin_accounts/data/static/*.css"]
|
||||
exclude = ["images/*", "scss/*"]
|
||||
include = ["jellyfin_accounts/data/*", "jellyfin_accounts/data/static/*.css", "jellyfin_accounts/data/*.html"]
|
||||
exclude = ["images/*", "scss/*", "mail/*"]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
@@ -29,6 +29,7 @@ python-dateutil = "^2.8.1"
|
||||
watchdog = "^0.10.2"
|
||||
waitress = "^1.4.3"
|
||||
packaging = "^20.4"
|
||||
psutil = "^5.7.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
neovim = "^0.3.1"
|
||||
@@ -43,6 +44,8 @@ jf-accounts = 'jellyfin_accounts:main'
|
||||
pre_compile-css = "task get-npm-deps"
|
||||
compile-css = "python scss/compile.py"
|
||||
get-npm-deps = "python scss/get_node_deps.py"
|
||||
pre_generate-emails = "task get-npm-deps"
|
||||
generate-emails = "python mail/generate.py"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
|
||||
Reference in New Issue
Block a user