mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2024-12-22 00:50:12 +00:00
Added dropdown menu for invites, multi-use invites, bump to 0.3.6
Dropdown menu includes time created, and for multi-use invites, remaining uses, as well as a list of usernames created using the code.
This commit is contained in:
parent
db3b992857
commit
27169e4e0d
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
__version__ = "0.3.5"
|
||||
__version__ = "0.3.6"
|
||||
|
||||
import secrets
|
||||
import configparser
|
||||
|
@ -94,16 +94,31 @@
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
z-index: 5000;*/
|
||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeInCubic */
|
||||
}
|
||||
.smooth-transition {
|
||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeInCubic */
|
||||
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeincubic */
|
||||
}
|
||||
.rotated {
|
||||
transform: rotate(180deg);
|
||||
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||
}
|
||||
|
||||
.not-rotated {
|
||||
transform: rotate(0deg);
|
||||
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||
}
|
||||
</style>
|
||||
<title>Admin</title>
|
||||
@ -263,16 +278,17 @@
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('multiUseCount').disabled = !this.checked; document.getElementById('noUseLimit').disabled = !this.checked; document.getElementById('noUseLimit').checked = false;" aria-label="Checkbox to allow choice of invite usage limit" name="multiple-uses" id="multiUseEnabled">
|
||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('multiUseCount').disabled = !this.checked; document.getElementById('noUseLimit').disabled = !this.checked" aria-label="Checkbox to allow choice of invite usage limit" name="multiple-uses" id="multiUseEnabled">
|
||||
</div>
|
||||
<input type="number" class="form-control" name="remaining-uses" id="multiUseCount">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check" style="margin-top: 1rem; margin-bottom: 1rem;">
|
||||
<input class="form-check-input" type="checkbox" value="" name="no-limit" id="noUseLimit" onchange="document.getElementById('multiUseCount').disabled = this.checked;">
|
||||
<input class="form-check-input" type="checkbox" value="" name="no-limit" id="noUseLimit" onchange="document.getElementById('multiUseCount').disabled = this.checked; if (this.checked) { document.getElementById('noLimitWarning').style = 'display: block;' } else { document.getElementById('noLimitWarning').style = 'display: none;'; }">
|
||||
<label class="form-check-label" for="noUseLimit">
|
||||
No limit
|
||||
No use limit
|
||||
</label>
|
||||
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
|
||||
</div>
|
||||
{% if email_enabled %}
|
||||
<div class="form-group">
|
||||
|
@ -158,7 +158,7 @@ var userDefaultsModal = createModal('userDefaults');
|
||||
var usersModal = createModal('users');
|
||||
var restartModal = createModal('restartModal');
|
||||
|
||||
// Parsed invite: [<code>, <expires in _>, <1: Empty invite (no delete/link), 0: Actual invite>, <email address>]
|
||||
// Parsed invite: [<code>, <expires in _>, <1: Empty invite (no delete/link), 0: Actual invite>, <email address>, <remaining uses>, [<used-by>], <date created>]
|
||||
function parseInvite(invite, empty = false) {
|
||||
if (empty) {
|
||||
return ["None", "", 1];
|
||||
@ -170,14 +170,30 @@ function parseInvite(invite, empty = false) {
|
||||
time += `${invite[m]}${m[0]} `;
|
||||
}
|
||||
}
|
||||
i[1] = `Expires in ${time.slice(0, -1)}`
|
||||
i[1] = `Expires in ${time.slice(0, -1)}`;
|
||||
if ('remaining-uses' in invite) {
|
||||
i[4] = invite['remaining-uses'];
|
||||
}
|
||||
if (invite['no-limit']) {
|
||||
i[4] = '∞';
|
||||
}
|
||||
if ('used-by' in invite) {
|
||||
i[5] = invite['used-by'];
|
||||
} else {
|
||||
i[5] = [];
|
||||
}
|
||||
if ('created' in invite) {
|
||||
i[6] = invite['created'];
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
function addItem(parsedInvite) {
|
||||
let links = document.getElementById('invites');
|
||||
let listItem = document.createElement('li');
|
||||
listItem.id = parsedInvite[0];
|
||||
let itemContainer = document.createElement('div');
|
||||
itemContainer.id = parsedInvite[0];
|
||||
let listItem = document.createElement('div');
|
||||
// listItem.id = parsedInvite[0];
|
||||
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
|
||||
|
||||
let code = document.createElement('div');
|
||||
@ -199,7 +215,6 @@ function addItem(parsedInvite) {
|
||||
listRight.appendChild(listText);
|
||||
|
||||
if (parsedInvite[2] == 0) {
|
||||
// Check this works!
|
||||
let inviteCode = window.location.href.split('#')[0] + 'invite/' + parsedInvite[0];
|
||||
//
|
||||
codeLink.href = inviteCode;
|
||||
@ -227,15 +242,122 @@ function addItem(parsedInvite) {
|
||||
deleteButton.textContent = "Delete";
|
||||
|
||||
listRight.appendChild(deleteButton);
|
||||
let dropButton = document.createElement('i');
|
||||
dropButton.classList.add('fa', 'fa-angle-down', 'collapsed', 'icon-button', 'not-rotated');
|
||||
dropButton.setAttribute('data-toggle', 'collapse');
|
||||
dropButton.setAttribute('aria-expanded', 'false');
|
||||
dropButton.setAttribute('data-target', '#' + CSS.escape(parsedInvite[0]) + '_collapse');
|
||||
dropButton.onclick = function() {
|
||||
if (this.classList.contains('rotated')) {
|
||||
this.classList.remove('rotated');
|
||||
this.classList.add('not-rotated');
|
||||
} else {
|
||||
this.classList.remove('not-rotated');
|
||||
this.classList.add('rotated');
|
||||
}
|
||||
};
|
||||
dropButton.setAttribute('style', 'margin-left: 1rem;');
|
||||
listRight.appendChild(dropButton);
|
||||
}
|
||||
|
||||
listItem.appendChild(listRight);
|
||||
links.appendChild(listItem);
|
||||
itemContainer.appendChild(listItem);
|
||||
if (parsedInvite[2] == 0) {
|
||||
let itemDropdown = document.createElement('div');
|
||||
itemDropdown.id = parsedInvite[0] + '_collapse';
|
||||
itemDropdown.classList.add('collapse');
|
||||
|
||||
let dropdownContent = document.createElement('div');
|
||||
dropdownContent.classList.add('container', 'row', 'align-items-start', 'card-body');
|
||||
|
||||
let dropdownLeft = document.createElement('div');
|
||||
dropdownLeft.classList.add('col');
|
||||
|
||||
let leftList = document.createElement('ul');
|
||||
leftList.classList.add('list-group', 'list-group-flush');
|
||||
|
||||
if (typeof(parsedInvite[6]) != 'undefined') {
|
||||
let createdDate = document.createElement('li');
|
||||
createdDate.classList.add('list-group-item', 'py-1');
|
||||
createdDate.textContent = `Created: ${parsedInvite[6]}`;
|
||||
leftList.appendChild(createdDate);
|
||||
}
|
||||
|
||||
let remainingUses = document.createElement('li');
|
||||
remainingUses.classList.add('list-group-item', 'py-1');
|
||||
remainingUses.id = parsedInvite[0] + '_remainingUses';
|
||||
remainingUses.textContent = `Remaining uses: ${parsedInvite[4]}`;
|
||||
leftList.appendChild(remainingUses);
|
||||
|
||||
dropdownLeft.appendChild(leftList);
|
||||
dropdownContent.appendChild(dropdownLeft);
|
||||
|
||||
let dropdownRight = document.createElement('div');
|
||||
dropdownRight.id = parsedInvite[0] + '_usersCreated';
|
||||
dropdownRight.classList.add('col');
|
||||
if (parsedInvite[5].length != 0) {
|
||||
let userList = document.createElement('ul');
|
||||
userList.classList.add('list-group', 'list-group-flush');
|
||||
userList.innerHTML = '<li class="list-group-item py-1">Users created:</li>';
|
||||
for (let user of parsedInvite[5]) {
|
||||
let li = document.createElement('li');
|
||||
li.classList.add('list-group-item', 'py-1', 'disabled');
|
||||
let username = document.createElement('div');
|
||||
username.classList.add('d-flex', 'float-left');
|
||||
username.textContent = user[0];
|
||||
li.appendChild(username);
|
||||
let date = document.createElement('div');
|
||||
date.classList.add('d-flex', 'float-right');
|
||||
date.textContent = user[1];
|
||||
li.appendChild(date);
|
||||
userList.appendChild(li);
|
||||
}
|
||||
dropdownRight.appendChild(userList);
|
||||
}
|
||||
dropdownContent.appendChild(dropdownRight);
|
||||
|
||||
itemDropdown.appendChild(dropdownContent);
|
||||
|
||||
itemContainer.appendChild(itemDropdown);
|
||||
}
|
||||
links.appendChild(itemContainer);
|
||||
}
|
||||
|
||||
function updateInvite(parsedInvite) {
|
||||
let expiry = document.getElementById(parsedInvite[0] + '_expiry');
|
||||
expiry.textContent = parsedInvite[1];
|
||||
|
||||
let remainingUses = document.getElementById(parsedInvite[0] + '_remainingUses');
|
||||
if (remainingUses) {
|
||||
remainingUses.textContent = `Remaining uses: ${parsedInvite[4]}`;
|
||||
}
|
||||
|
||||
if (parsedInvite[5].length != 0) {
|
||||
let usersCreated = document.getElementById(parsedInvite[0] + '_usersCreated');
|
||||
let dropdownRight = document.createElement('div');
|
||||
dropdownRight.id = parsedInvite[0] + '_usersCreated';
|
||||
dropdownRight.classList.add('col');
|
||||
let userList = document.createElement('ul');
|
||||
userList.classList.add('list-group', 'list-group-flush');
|
||||
userList.innerHTML = '<li class="list-group-item py-1">Users created:</li>';
|
||||
for (let user of parsedInvite[5]) {
|
||||
let li = document.createElement('li');
|
||||
li.classList.add('list-group-item', 'py-1', 'disabled');
|
||||
let username = document.createElement('div');
|
||||
username.classList.add('d-flex', 'float-left');
|
||||
username.textContent = user[0];
|
||||
li.appendChild(username);
|
||||
let date = document.createElement('div');
|
||||
date.classList.add('d-flex', 'float-right');
|
||||
date.textContent = user[1];
|
||||
li.appendChild(date);
|
||||
userList.appendChild(li);
|
||||
}
|
||||
dropdownRight.appendChild(userList);
|
||||
usersCreated.replaceWith(dropdownRight);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// delete from list on page
|
||||
|
@ -156,6 +156,9 @@
|
||||
submitButton.replaceChild(newSpan, oldSpan);
|
||||
};
|
||||
document.getElementById('accountForm').onsubmit = function() {
|
||||
if (document.getElementById('errorMessage')) {
|
||||
document.getElementById('errorMessage').remove();
|
||||
}
|
||||
toggleSpinner();
|
||||
var send = serializeForm('accountForm');
|
||||
send['code'] = code;
|
||||
@ -171,12 +174,18 @@
|
||||
if (this.readyState == 4) {
|
||||
toggleSpinner();
|
||||
var data = this.response;
|
||||
if ('error' in data) {
|
||||
var text = document.createTextNode(data['error']);
|
||||
if ('error' in data || data['success'] == false) {
|
||||
if (typeof(data['error']) != 'undefined') {
|
||||
var errorMessage = data['error'];
|
||||
} else {
|
||||
var errorMessage = 'Unknown Error';
|
||||
}
|
||||
var text = document.createTextNode(errorMessage);
|
||||
var error = document.createElement('button');
|
||||
error.classList.add('btn', 'btn-outline-danger');
|
||||
error.setAttribute('disabled', '');
|
||||
error.appendChild(text);
|
||||
error.id = 'errorMessage';
|
||||
document.getElementById('errorBox').appendChild(error);
|
||||
} else {
|
||||
var valid = true
|
||||
|
@ -190,7 +190,7 @@ class Jellyfin:
|
||||
)
|
||||
|
||||
def newUser(self, username: str, password: str):
|
||||
for user in self.getUsers():
|
||||
for user in self.getUsers(public=False):
|
||||
if user["Name"] == username:
|
||||
raise self.UserExistsError
|
||||
response = requests.post(
|
||||
|
@ -42,9 +42,7 @@ 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),
|
||||
200,
|
||||
{"Content-Type": "text/javascript"},
|
||||
)
|
||||
|
@ -20,21 +20,49 @@ from jellyfin_accounts import web_log as log
|
||||
from jellyfin_accounts.validate_password import PasswordValidator
|
||||
|
||||
|
||||
def checkInvite(code, delete=False):
|
||||
def format_datetime(dt):
|
||||
result = dt.strftime(config["email"]["date_format"])
|
||||
if config.getboolean("email", "use_24h"):
|
||||
result += f' {dt.strftime("%H:%M")}'
|
||||
else:
|
||||
result += f' {dt.strftime("%I:%M %p")}'
|
||||
return result
|
||||
|
||||
|
||||
def checkInvite(code, used=False, username=None):
|
||||
current_time = datetime.datetime.now()
|
||||
invites = dict(data_store.invites)
|
||||
match = False
|
||||
for invite in invites:
|
||||
if (
|
||||
"remaining-uses" not in invites[invite]
|
||||
and "no-limit" not in invites[invite]
|
||||
):
|
||||
invites[invite]["remaining-uses"] = 1
|
||||
expiry = datetime.datetime.strptime(
|
||||
invites[invite]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
||||
)
|
||||
if current_time >= expiry:
|
||||
log.debug(f"Housekeeping: Deleting old invite {invite}")
|
||||
if current_time >= expiry or (
|
||||
"no-limit" not in invites[invite] and invites[invite]["remaining-uses"] < 1
|
||||
):
|
||||
log.debug(f"Housekeeping: Deleting expired invite {invite}")
|
||||
del data_store.invites[invite]
|
||||
elif invite == code:
|
||||
match = True
|
||||
if delete:
|
||||
del data_store.invites[code]
|
||||
if used:
|
||||
delete = False
|
||||
inv = dict(data_store.invites[code])
|
||||
if "used-by" not in inv:
|
||||
inv["used-by"] = []
|
||||
if "remaining-uses" in inv:
|
||||
if inv["remaining-uses"] == 1:
|
||||
delete = True
|
||||
del data_store.invites[code]
|
||||
elif "no-limit" not in invites[invite]:
|
||||
inv["remaining-uses"] -= 1
|
||||
inv["used-by"].append([username, format_datetime(current_time)])
|
||||
if not delete:
|
||||
data_store.invites[code] = inv
|
||||
return match
|
||||
|
||||
|
||||
@ -157,7 +185,7 @@ def newUser():
|
||||
return jsonify({"error": error})
|
||||
except:
|
||||
return jsonify({"error": "Unknown error"})
|
||||
checkInvite(data["code"], delete=True)
|
||||
checkInvite(data["code"], used=True, username=data["username"])
|
||||
if user.status_code == 200:
|
||||
try:
|
||||
policy = data_store.user_template
|
||||
@ -205,6 +233,14 @@ def generateInvite():
|
||||
)
|
||||
invite_code = secrets.token_urlsafe(16)
|
||||
invite = {}
|
||||
invite["created"] = format_datetime(current_time)
|
||||
if data["multiple-uses"]:
|
||||
if data["no-limit"]:
|
||||
invite["no-limit"] = True
|
||||
else:
|
||||
invite["remaining-uses"] = int(data["remaining-uses"])
|
||||
else:
|
||||
invite["remaining-uses"] = 1
|
||||
log.debug(f"Creating new invite: {invite_code}")
|
||||
valid_till = current_time + delta
|
||||
invite["valid_till"] = valid_till.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
||||
@ -251,6 +287,16 @@ def getInvites():
|
||||
"hours": valid_for.seconds // 3600,
|
||||
"minutes": (valid_for.seconds // 60) % 60,
|
||||
}
|
||||
if "created" in invites[code]:
|
||||
invite["created"] = invites[code]["created"]
|
||||
if "used-by" in invites[code]:
|
||||
invite["used-by"] = invites[code]["used-by"]
|
||||
if "no-limit" in invites[code]:
|
||||
invite["no-limit"] = invites[code]["no-limit"]
|
||||
if "remaining-uses" in invites[code]:
|
||||
invite["remaining-uses"] = invites[code]["remaining-uses"]
|
||||
else:
|
||||
invite["remaining-uses"] = 1
|
||||
if "email" in invites[code]:
|
||||
invite["email"] = invites[code]["email"]
|
||||
response["invites"].append(invite)
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "jellyfin-accounts"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
readme = "README.md"
|
||||
description = "A simple account management system for Jellyfin"
|
||||
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
|
||||
|
Loading…
Reference in New Issue
Block a user