From 27169e4e0d0f4a3707ca0c6a8538b44840adf2d4 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 10 Jul 2020 16:15:17 +0100 Subject: [PATCH] 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. --- jellyfin_accounts/__init__.py | 2 +- jellyfin_accounts/data/templates/admin.html | 28 +++- jellyfin_accounts/data/templates/admin.js | 134 +++++++++++++++++++- jellyfin_accounts/data/templates/form.html | 13 +- jellyfin_accounts/jf_api.py | 2 +- jellyfin_accounts/web.py | 4 +- jellyfin_accounts/web_api.py | 58 ++++++++- pyproject.toml | 2 +- 8 files changed, 217 insertions(+), 26 deletions(-) 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 */ } Admin @@ -263,16 +278,17 @@
- +
- + +
{% if email_enabled %}
diff --git a/jellyfin_accounts/data/templates/admin.js b/jellyfin_accounts/data/templates/admin.js index ebb1035..d6d626e 100644 --- a/jellyfin_accounts/data/templates/admin.js +++ b/jellyfin_accounts/data/templates/admin.js @@ -158,7 +158,7 @@ var userDefaultsModal = createModal('userDefaults'); var usersModal = createModal('users'); var restartModal = createModal('restartModal'); -// Parsed invite: [, , <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 "]