diff --git a/jellyfin_accounts/__init__.py b/jellyfin_accounts/__init__.py index 2d0e3f8..3e0b451 100755 --- a/jellyfin_accounts/__init__.py +++ b/jellyfin_accounts/__init__.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -__version__ = "0.3.5" +__version__ = "0.3.6" import secrets import configparser diff --git a/jellyfin_accounts/data/templates/admin.html b/jellyfin_accounts/data/templates/admin.html index 175f98a..13ca279 100644 --- a/jellyfin_accounts/data/templates/admin.html +++ b/jellyfin_accounts/data/templates/admin.html @@ -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 */ }
, , <1: Empty invite (no delete/link), 0: Actual invite>, ]
+// Parsed invite: [, , <1: Empty invite (no delete/link), 0: Actual invite>, , , [], ]
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 = 'Users created: ';
+ 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 = 'Users created: ';
+ 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
diff --git a/jellyfin_accounts/data/templates/form.html b/jellyfin_accounts/data/templates/form.html
index 01141c7..4e0edb3 100644
--- a/jellyfin_accounts/data/templates/form.html
+++ b/jellyfin_accounts/data/templates/form.html
@@ -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
diff --git a/jellyfin_accounts/jf_api.py b/jellyfin_accounts/jf_api.py
index 33cdf13..be458e1 100644
--- a/jellyfin_accounts/jf_api.py
+++ b/jellyfin_accounts/jf_api.py
@@ -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(
diff --git a/jellyfin_accounts/web.py b/jellyfin_accounts/web.py
index 63a14b3..a972a0d 100644
--- a/jellyfin_accounts/web.py
+++ b/jellyfin_accounts/web.py
@@ -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"},
)
diff --git a/jellyfin_accounts/web_api.py b/jellyfin_accounts/web_api.py
index 97bd3e0..769451f 100644
--- a/jellyfin_accounts/web_api.py
+++ b/jellyfin_accounts/web_api.py
@@ -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)
diff --git a/pyproject.toml b/pyproject.toml
index 1fe8131..c63fea2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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 "]