mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2024-12-22 09:00:14 +00:00
Added per-invite notifications for expiry and user creation
Notifications must be enabled in settings; they can then be toggled in the dropdown menu of each invite.
This commit is contained in:
parent
e80b233af2
commit
b8fdb64f68
@ -290,13 +290,6 @@ def main():
|
|||||||
success = True
|
success = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def signal_handler(sig, frame):
|
|
||||||
print("Quitting...")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
|
||||||
global app
|
global app
|
||||||
app = Flask(__name__, root_path=str(local_dir))
|
app = Flask(__name__, root_path=str(local_dir))
|
||||||
app.config["DEBUG"] = config.getboolean("ui", "debug")
|
app.config["DEBUG"] = config.getboolean("ui", "debug")
|
||||||
@ -306,6 +299,13 @@ def main():
|
|||||||
from waitress import serve
|
from waitress import serve
|
||||||
|
|
||||||
if first_run:
|
if first_run:
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
print("Quitting...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
import jellyfin_accounts.setup
|
import jellyfin_accounts.setup
|
||||||
|
|
||||||
host = config["ui"]["host"]
|
host = config["ui"]["host"]
|
||||||
@ -315,6 +315,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
import jellyfin_accounts.web_api
|
import jellyfin_accounts.web_api
|
||||||
import jellyfin_accounts.web
|
import jellyfin_accounts.web
|
||||||
|
import jellyfin_accounts.invite_daemon
|
||||||
|
|
||||||
host = config["ui"]["host"]
|
host = config["ui"]["host"]
|
||||||
port = config["ui"]["port"]
|
port = config["ui"]["port"]
|
||||||
@ -330,4 +331,12 @@ def main():
|
|||||||
log.info("Starting email thread")
|
log.info("Starting email thread")
|
||||||
pwr.start()
|
pwr.start()
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
print("Quitting...")
|
||||||
|
if config.getboolean("notifications", "enabled"):
|
||||||
|
jellyfin_accounts.invite_daemon.inviteDaemon.stop()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
serve(app, host=host, port=int(port))
|
serve(app, host=host, port=int(port))
|
||||||
|
@ -16,6 +16,7 @@ class Config:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_config(config_path, data_dir, local_dir, log):
|
def load_config(config_path, data_dir, local_dir, log):
|
||||||
|
# Lord forgive me for this mess
|
||||||
config = configparser.RawConfigParser()
|
config = configparser.RawConfigParser()
|
||||||
config.read(config_path)
|
config.read(config_path)
|
||||||
for key in config["files"]:
|
for key in config["files"]:
|
||||||
@ -64,6 +65,31 @@ class Config:
|
|||||||
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
||||||
if "bs5" not in config["ui"] or config["ui"]["bs5"] == "":
|
if "bs5" not in config["ui"] or config["ui"]["bs5"] == "":
|
||||||
config["ui"]["bs5"] = "false"
|
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
|
return config
|
||||||
|
|
||||||
def __init__(self, file, instance, data_dir, local_dir, log):
|
def __init__(self, file, instance, data_dir, local_dir, log):
|
||||||
|
@ -133,6 +133,15 @@
|
|||||||
"value": "your password",
|
"value": "your password",
|
||||||
"description": "Password for admin page (Leave blank if using jellyfin_login)"
|
"description": "Password for admin page (Leave blank if using jellyfin_login)"
|
||||||
},
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "Admin email address",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": false,
|
||||||
|
"depends_false": "jellyfin_login",
|
||||||
|
"type": "text",
|
||||||
|
"value": "example@example.com",
|
||||||
|
"description": "Address to send notifications to (Leave blank if using jellyfin_login)"
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"name": "Debug logging",
|
"name": "Debug logging",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -391,6 +400,56 @@
|
|||||||
"description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
|
"description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"meta": {
|
||||||
|
"name": "Notifications",
|
||||||
|
"description": "Notification related settings."
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "Enabled",
|
||||||
|
"required": "false",
|
||||||
|
"requires_restart": true,
|
||||||
|
"type": "bool",
|
||||||
|
"value": true,
|
||||||
|
"description": "Enabling adds optional toggles to invites to notify on expiry and user creation."
|
||||||
|
},
|
||||||
|
"expiry_html": {
|
||||||
|
"name": "Expiry email (HTML)",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": false,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"type": "text",
|
||||||
|
"value": "",
|
||||||
|
"description": "Path to expiry notification email HTML."
|
||||||
|
},
|
||||||
|
"expiry_text": {
|
||||||
|
"name": "Expiry email (Plaintext)",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": "false",
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"type": "text",
|
||||||
|
"value": "",
|
||||||
|
"description": "Path to expiry notification email in plaintext."
|
||||||
|
},
|
||||||
|
"created_html": {
|
||||||
|
"name": "User created email (HTML)",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": false,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"type": "text",
|
||||||
|
"value": "",
|
||||||
|
"description": "Path to user creation notification email HTML."
|
||||||
|
},
|
||||||
|
"created_text": {
|
||||||
|
"name": "User created email (Plaintext)",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": false,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"type": "text",
|
||||||
|
"value": "",
|
||||||
|
"description": "Path to user creation notification email in plaintext."
|
||||||
|
}
|
||||||
|
},
|
||||||
"mailgun": {
|
"mailgun": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Mailgun (Email)",
|
"name": "Mailgun (Email)",
|
||||||
|
7
jellyfin_accounts/data/created.txt
Normal file
7
jellyfin_accounts/data/created.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
A user was created using code {{ code }}.
|
||||||
|
|
||||||
|
Name: {{ username }}
|
||||||
|
Address: {{ address }}
|
||||||
|
Time: {{ time }}
|
||||||
|
|
||||||
|
Note: Notification emails can be toggled on the admin dashboard.
|
10
jellyfin_accounts/data/email.txt
Normal file
10
jellyfin_accounts/data/email.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Hi {{ username }},
|
||||||
|
|
||||||
|
Someone has recently requests a password reset on Jellyfin.
|
||||||
|
If this was you, enter the below pin into the prompt.
|
||||||
|
This code will expire on {{ expiry_date }}, at {{ expiry_time }} , which is in {{ expires_in }}.
|
||||||
|
If this wasn't you, please ignore this email.
|
||||||
|
|
||||||
|
PIN: {{ pin }}
|
||||||
|
|
||||||
|
{{ message }}
|
5
jellyfin_accounts/data/expired.txt
Normal file
5
jellyfin_accounts/data/expired.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Invite expired.
|
||||||
|
|
||||||
|
Code {{ code }} expired at {{ expiry }}.
|
||||||
|
|
||||||
|
Note: Notification emails can be toggled on the admin dashboard.
|
8
jellyfin_accounts/data/invite-email.txt
Normal file
8
jellyfin_accounts/data/invite-email.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Hi,
|
||||||
|
You've been invited to Jellyfin.
|
||||||
|
To join, follow the below link.
|
||||||
|
This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.
|
||||||
|
|
||||||
|
{{ invite_link }}
|
||||||
|
|
||||||
|
{{ message }}
|
@ -158,7 +158,7 @@ var userDefaultsModal = createModal('userDefaults');
|
|||||||
var usersModal = createModal('users');
|
var usersModal = createModal('users');
|
||||||
var restartModal = createModal('restartModal');
|
var restartModal = createModal('restartModal');
|
||||||
|
|
||||||
// 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) {
|
function parseInvite(invite, empty = false) {
|
||||||
if (empty) {
|
if (empty) {
|
||||||
return ["None", "", 1];
|
return ["None", "", 1];
|
||||||
@ -185,6 +185,12 @@ function parseInvite(invite, empty = false) {
|
|||||||
if ('created' in invite) {
|
if ('created' in invite) {
|
||||||
i[6] = invite['created'];
|
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;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,6 +298,78 @@ function addItem(parsedInvite) {
|
|||||||
dropdownLeft.appendChild(leftList);
|
dropdownLeft.appendChild(leftList);
|
||||||
dropdownContent.appendChild(dropdownLeft);
|
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');
|
let dropdownRight = document.createElement('div');
|
||||||
dropdownRight.id = parsedInvite[0] + '_usersCreated';
|
dropdownRight.id = parsedInvite[0] + '_usersCreated';
|
||||||
dropdownRight.classList.add('col');
|
dropdownRight.classList.add('col');
|
||||||
|
@ -43,7 +43,10 @@ class JSONFile(dict):
|
|||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
data = self.readJSON(self.path)
|
data = self.readJSON(self.path)
|
||||||
super(JSONFile, self).__init__(data)
|
super(JSONFile, self).__init__(data)
|
||||||
|
try:
|
||||||
del data[key]
|
del data[key]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
self.writeJSON(self.path, data)
|
self.writeJSON(self.path, data)
|
||||||
super(JSONFile, self).__delitem__(key)
|
super(JSONFile, self).__delitem__(key)
|
||||||
|
|
||||||
|
@ -13,6 +13,15 @@ from jellyfin_accounts import config
|
|||||||
from jellyfin_accounts import email_log as log
|
from jellyfin_accounts import email_log as log
|
||||||
|
|
||||||
|
|
||||||
|
def format_datetime(dt):
|
||||||
|
result = dt.strftime(config["email"]["date_format"])
|
||||||
|
if config.getboolean("email", "use_24h"):
|
||||||
|
result += f' {dt.strftime("%H:%M")}'
|
||||||
|
else:
|
||||||
|
result += f' {dt.strftime("%I:%M %p")}'
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class Email:
|
class Email:
|
||||||
def __init__(self, address):
|
def __init__(self, address):
|
||||||
self.address = address
|
self.address = address
|
||||||
@ -75,6 +84,47 @@ class Email:
|
|||||||
self.content[key] = c
|
self.content[key] = c
|
||||||
log.info(f"{self.address}: {key} constructed")
|
log.info(f"{self.address}: {key} constructed")
|
||||||
|
|
||||||
|
def construct_expiry(self, invite):
|
||||||
|
self.subject = "Notice: Invite expired"
|
||||||
|
log.debug(f'Constructing expiry notification for {invite["code"]}')
|
||||||
|
expiry = format_datetime(invite["expiry"])
|
||||||
|
for key in ["text", "html"]:
|
||||||
|
sp = Path(config["notifications"]["expiry_" + key]) / ".."
|
||||||
|
sp = str(sp.resolve()) + "/"
|
||||||
|
template_loader = FileSystemLoader(searchpath=sp)
|
||||||
|
template_env = Environment(loader=template_loader)
|
||||||
|
fname = Path(config["notifications"]["expiry_" + key]).name
|
||||||
|
template = template_env.get_template(fname)
|
||||||
|
c = template.render(code=invite["code"], expiry=expiry)
|
||||||
|
self.content[key] = c
|
||||||
|
log.info(f"{self.address}: {key} constructed")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def construct_created(self, invite):
|
||||||
|
self.subject = "Notice: User created"
|
||||||
|
log.debug(f'Constructing expiry notification for {invite["code"]}')
|
||||||
|
created = format_datetime(invite["created"])
|
||||||
|
if config.getboolean("email", "no_username"):
|
||||||
|
email = "n/a"
|
||||||
|
else:
|
||||||
|
email = invite["address"]
|
||||||
|
for key in ["text", "html"]:
|
||||||
|
sp = Path(config["notifications"]["created_" + key]) / ".."
|
||||||
|
sp = str(sp.resolve()) + "/"
|
||||||
|
template_loader = FileSystemLoader(searchpath=sp)
|
||||||
|
template_env = Environment(loader=template_loader)
|
||||||
|
fname = Path(config["notifications"]["created_" + key]).name
|
||||||
|
template = template_env.get_template(fname)
|
||||||
|
c = template.render(
|
||||||
|
code=invite["code"],
|
||||||
|
username=invite["username"],
|
||||||
|
address=email,
|
||||||
|
time=created,
|
||||||
|
)
|
||||||
|
self.content[key] = c
|
||||||
|
log.info(f"{self.address}: {key} constructed")
|
||||||
|
return True
|
||||||
|
|
||||||
def construct_reset(self, reset):
|
def construct_reset(self, reset):
|
||||||
self.subject = config["password_resets"]["subject"]
|
self.subject = config["password_resets"]["subject"]
|
||||||
log.debug(f"{self.address}: Using subject {self.subject}")
|
log.debug(f"{self.address}: Using subject {self.subject}")
|
||||||
|
42
jellyfin_accounts/invite_daemon.py
Normal file
42
jellyfin_accounts/invite_daemon.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from threading import Timer
|
||||||
|
import time
|
||||||
|
from jellyfin_accounts import config, data_store
|
||||||
|
from jellyfin_accounts.web_api import checkInvite
|
||||||
|
|
||||||
|
|
||||||
|
class Repeat:
|
||||||
|
def __init__(self, interval, function, *args, **kwargs):
|
||||||
|
self._timer = None
|
||||||
|
self.interval = interval
|
||||||
|
self.function = function
|
||||||
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self.is_running = False
|
||||||
|
self.next_call = time.time()
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
self.is_running = False
|
||||||
|
self.start()
|
||||||
|
self.function(*self.args, **self.kwargs)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if not self.is_running:
|
||||||
|
self.next_call += self.interval
|
||||||
|
self._timer = Timer(self.next_call - time.time(), self._run)
|
||||||
|
self._timer.start()
|
||||||
|
self.is_running = True
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._timer.cancel()
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
|
||||||
|
def checkInvites():
|
||||||
|
invites = dict(data_store.invites)
|
||||||
|
# checkInvite already loops over everything, no point running it multiple times.
|
||||||
|
checkInvite(list(invites.keys())[0])
|
||||||
|
|
||||||
|
|
||||||
|
if config.getboolean("notifications", "enabled"):
|
||||||
|
inviteDaemon = Repeat(60, checkInvites)
|
@ -106,6 +106,8 @@ def verify_password(username, password):
|
|||||||
user = Account().verify_token(username, accounts)
|
user = Account().verify_token(username, accounts)
|
||||||
if user:
|
if user:
|
||||||
verified = True
|
verified = True
|
||||||
|
if user in accounts:
|
||||||
|
user = accounts[user]
|
||||||
if not user:
|
if not user:
|
||||||
log.debug(f"User {username} not found on Jellyfin")
|
log.debug(f"User {username} not found on Jellyfin")
|
||||||
return False
|
return False
|
||||||
@ -116,10 +118,10 @@ def verify_password(username, password):
|
|||||||
if username == user.username and user.verify_password(password):
|
if username == user.username and user.verify_password(password):
|
||||||
g.user = user
|
g.user = user
|
||||||
log.debug("HTTPAuth Allowed")
|
log.debug("HTTPAuth Allowed")
|
||||||
return True
|
return user
|
||||||
else:
|
else:
|
||||||
log.debug("HTTPAuth Denied")
|
log.debug("HTTPAuth Denied")
|
||||||
return False
|
return False
|
||||||
g.user = user
|
g.user = user
|
||||||
log.debug("HTTPAuth Allowed")
|
log.debug("HTTPAuth Allowed")
|
||||||
return True
|
return user
|
||||||
|
@ -43,7 +43,12 @@ def static_proxy(path):
|
|||||||
if "html" not in path:
|
if "html" not in path:
|
||||||
if "admin.js" in path:
|
if "admin.js" in path:
|
||||||
return (
|
return (
|
||||||
render_template("admin.js", bsVersion=bsVersion(), css_file=css_file),
|
render_template(
|
||||||
|
"admin.js",
|
||||||
|
bsVersion=bsVersion(),
|
||||||
|
css_file=css_file,
|
||||||
|
notifications=config.getboolean("notifications", "enabled"),
|
||||||
|
),
|
||||||
200,
|
200,
|
||||||
{"Content-Type": "text/javascript"},
|
{"Content-Type": "text/javascript"},
|
||||||
)
|
)
|
||||||
|
@ -15,6 +15,7 @@ from jellyfin_accounts import (
|
|||||||
configparser,
|
configparser,
|
||||||
config_base_path,
|
config_base_path,
|
||||||
)
|
)
|
||||||
|
from jellyfin_accounts.email import Mailgun, Smtp
|
||||||
from jellyfin_accounts import web_log as log
|
from jellyfin_accounts import web_log as log
|
||||||
from jellyfin_accounts.validate_password import PasswordValidator
|
from jellyfin_accounts.validate_password import PasswordValidator
|
||||||
|
|
||||||
@ -45,6 +46,22 @@ def checkInvite(code, used=False, username=None):
|
|||||||
"no-limit" not in invites[invite] and invites[invite]["remaining-uses"] < 1
|
"no-limit" not in invites[invite] and invites[invite]["remaining-uses"] < 1
|
||||||
):
|
):
|
||||||
log.debug(f"Housekeeping: Deleting expired invite {invite}")
|
log.debug(f"Housekeeping: Deleting expired invite {invite}")
|
||||||
|
if (
|
||||||
|
config.getboolean("notifications", "enabled")
|
||||||
|
and "notify" in invites[invite]
|
||||||
|
):
|
||||||
|
for address in invites[invite]["notify"]:
|
||||||
|
if "notify-expiry" in invites[invite]["notify"][address]:
|
||||||
|
if invites[invite]["notify"][address]["notify-expiry"]:
|
||||||
|
method = config["email"]["method"]
|
||||||
|
if method == "mailgun":
|
||||||
|
email = Mailgun(address)
|
||||||
|
elif method == "smtp":
|
||||||
|
email = Smtp(address)
|
||||||
|
if email.construct_expiry(
|
||||||
|
{"code": invite, "expiry": expiry}
|
||||||
|
):
|
||||||
|
email.send()
|
||||||
del data_store.invites[invite]
|
del data_store.invites[invite]
|
||||||
elif invite == code:
|
elif invite == code:
|
||||||
match = True
|
match = True
|
||||||
@ -184,7 +201,28 @@ def newUser():
|
|||||||
return jsonify({"error": error})
|
return jsonify({"error": error})
|
||||||
except:
|
except:
|
||||||
return jsonify({"error": "Unknown error"})
|
return jsonify({"error": "Unknown error"})
|
||||||
|
invites = dict(data_store.invites)
|
||||||
checkInvite(data["code"], used=True, username=data["username"])
|
checkInvite(data["code"], used=True, username=data["username"])
|
||||||
|
if (
|
||||||
|
config.getboolean("notifications", "enabled")
|
||||||
|
and "notify" in invites[data["code"]]
|
||||||
|
):
|
||||||
|
for address in invites[data["code"]]["notify"]:
|
||||||
|
if "notify-creation" in invites[data["code"]]["notify"][address]:
|
||||||
|
if invites[data["code"]]["notify"][address]["notify-creation"]:
|
||||||
|
method = config["email"]["method"]
|
||||||
|
if method == "mailgun":
|
||||||
|
email = Mailgun(address)
|
||||||
|
elif method == "smtp":
|
||||||
|
email = Smtp(address)
|
||||||
|
if email.construct_created(
|
||||||
|
{
|
||||||
|
"code": data["code"],
|
||||||
|
"username": data["username"],
|
||||||
|
"created": datetime.datetime.now(),
|
||||||
|
}
|
||||||
|
):
|
||||||
|
email.send()
|
||||||
if user.status_code == 200:
|
if user.status_code == 200:
|
||||||
try:
|
try:
|
||||||
policy = data_store.user_template
|
policy = data_store.user_template
|
||||||
@ -258,6 +296,11 @@ def generateInvite():
|
|||||||
response = email.send()
|
response = email.send()
|
||||||
if response is False or type(response) != bool:
|
if response is False or type(response) != bool:
|
||||||
invite["email"] = f"Failed to send to {address}"
|
invite["email"] = f"Failed to send to {address}"
|
||||||
|
if config.getboolean("notifications", "enabled"):
|
||||||
|
if "notify-creation" in data:
|
||||||
|
invite["notify-creation"] = data["notify-creation"]
|
||||||
|
if "notify-expiry" in data:
|
||||||
|
invite["notify-expiry"] = data["notify-expiry"]
|
||||||
data_store.invites[invite_code] = invite
|
data_store.invites[invite_code] = invite
|
||||||
log.info(f"New invite created: {invite_code}")
|
log.info(f"New invite created: {invite_code}")
|
||||||
return resp()
|
return resp()
|
||||||
@ -296,6 +339,20 @@ def getInvites():
|
|||||||
invite["remaining-uses"] = 1
|
invite["remaining-uses"] = 1
|
||||||
if "email" in invites[code]:
|
if "email" in invites[code]:
|
||||||
invite["email"] = invites[code]["email"]
|
invite["email"] = invites[code]["email"]
|
||||||
|
if "notify" in invites[code]:
|
||||||
|
if config.getboolean("ui", "jellyfin_login"):
|
||||||
|
address = data_store.emails[g.user.id]
|
||||||
|
else:
|
||||||
|
address = config["ui"]["email"]
|
||||||
|
if address in invites[code]["notify"]:
|
||||||
|
if "notify-expiry" in invites[code]["notify"][address]:
|
||||||
|
invite["notify-expiry"] = invites[code]["notify"][address][
|
||||||
|
"notify-expiry"
|
||||||
|
]
|
||||||
|
if "notify-creation" in invites[code]["notify"][address]:
|
||||||
|
invite["notify-creation"] = invites[code]["notify"][address][
|
||||||
|
"notify-creation"
|
||||||
|
]
|
||||||
response["invites"].append(invite)
|
response["invites"].append(invite)
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
@ -407,3 +464,29 @@ def getConfig():
|
|||||||
if entry in config[section]:
|
if entry in config[section]:
|
||||||
response_config[section][entry]["value"] = config[section][entry]
|
response_config[section][entry]["value"] = config[section][entry]
|
||||||
return jsonify(response_config), 200
|
return jsonify(response_config), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/setNotify", methods=["POST"])
|
||||||
|
@auth.login_required
|
||||||
|
def setNotify():
|
||||||
|
data = request.get_json()
|
||||||
|
change = False
|
||||||
|
for code in data:
|
||||||
|
for key in data[code]:
|
||||||
|
if key in ["notify-expiry", "notify-creation"]:
|
||||||
|
inv = data_store.invites[code]
|
||||||
|
if config.getboolean("ui", "jellyfin_login"):
|
||||||
|
address = data_store.emails[g.user.id]
|
||||||
|
else:
|
||||||
|
address = config["ui"]["email"]
|
||||||
|
if "notify" not in inv:
|
||||||
|
inv["notify"] = {}
|
||||||
|
if address not in inv["notify"]:
|
||||||
|
inv["notify"][address] = {}
|
||||||
|
inv["notify"][address][key] = data[code][key]
|
||||||
|
log.debug(f"{code}: Notification settings changed")
|
||||||
|
change = True
|
||||||
|
if change:
|
||||||
|
data_store.invites[code] = inv
|
||||||
|
return resp()
|
||||||
|
return resp(success=False)
|
||||||
|
47
mail/created.mjml
Normal file
47
mail/created.mjml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<mjml>
|
||||||
|
<mj-head>
|
||||||
|
<mj-attributes>
|
||||||
|
<mj-class name="bg" background-color="#101010" />
|
||||||
|
<mj-class name="bg2" background-color="#242424" />
|
||||||
|
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||||
|
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||||
|
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||||
|
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||||
|
</mj-attributes>
|
||||||
|
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||||
|
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||||
|
</mj-head>
|
||||||
|
<mj-body>
|
||||||
|
<mj-section mj-class="bg2">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> jellyfin-accounts </mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
<mj-section mj-class="bg">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||||
|
<h3>User Created</h3>
|
||||||
|
<p>A user was created using code {{ code }}.</p>
|
||||||
|
</mj-text>
|
||||||
|
<mj-table mj-class="text" container-background-color="#242424">
|
||||||
|
<tr style="text-align: left;">
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>Time</th>
|
||||||
|
</tr>
|
||||||
|
<tr style="font-style: italic; text-align: left; color: rgb(153,153,153);">
|
||||||
|
<th>{{ username }}</th>
|
||||||
|
<th>{{ address }}</th>
|
||||||
|
<th>{{ time }}</th>
|
||||||
|
</mj-table>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
<mj-section mj-class="bg2">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||||
|
Notification emails can be toggled on the admin dashboard.
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
</body>
|
||||||
|
</mjml>
|
7
mail/created.txt
Normal file
7
mail/created.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
A user was created using code {{ code }}.
|
||||||
|
|
||||||
|
Name: {{ username }}
|
||||||
|
Address: {{ address }}
|
||||||
|
Time: {{ time }}
|
||||||
|
|
||||||
|
Note: Notification emails can be toggled on the admin dashboard.
|
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.
|
Loading…
Reference in New Issue
Block a user