6 Commits

Author SHA1 Message Date
27169e4e0d 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.
2020-07-10 16:15:17 +01:00
db3b992857 refactor admin.js, add initial ui elements for multi-use invites
multi-use invites will have a set limit of how many times they can be
used. They can also be set to have no limit. An additional menu is
planned for multi use invites to see when they have been used, and by
who.
2020-07-09 23:05:01 +01:00
89c132e92e bump to 0.3.5 2020-07-09 20:34:31 +01:00
7bda2f4141 Fix UI error when 'send to address' option disabled 2020-07-09 20:33:14 +01:00
71f05f2348 Replace jquery ajax in setup.js 2020-07-07 20:10:27 +01:00
94e69ad090 Small CSS tweaks, add days input 2020-07-07 15:30:16 +01:00
12 changed files with 689 additions and 452 deletions

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
__version__ = "0.3.3"
__version__ = "0.3.6"
import secrets
import configparser

View File

@@ -16,6 +16,7 @@ function serializeForm(id) {
case 'password':
case 'select-one':
case 'email':
case 'number':
formData[name] = el.value;
break;
};

View File

@@ -13,6 +13,7 @@ for (var i = 0; i < authRadios.length; i++) {
checkAuthRadio();
});
};
function checkEmailRadio() {
document.getElementById('emailNextButton').href = '#page-5';
document.getElementById('valBackButton').href = '#page-7';
@@ -35,6 +36,7 @@ for (var i = 0; i < emailRadios.length; i++) {
checkEmailRadio();
});
};
function checkSSL() {
var label = document.getElementById('emailSSL_TLSLabel');
if (document.getElementById('emailSSL_TLS').checked) {
@@ -101,16 +103,15 @@ document.getElementById('jfTestButton').onclick = function() {
jfData['jfHost'] = document.getElementById('jfHost').value;
jfData['jfUser'] = document.getElementById('jfUser').value;
jfData['jfPassword'] = document.getElementById('jfPassword').value;
$.ajax('/testJF', {
type : 'POST',
dataType : 'json',
contentType : 'application/json',
data : JSON.stringify(jfData),
complete: function(response) {
var req = new XMLHttpRequest();
req.open("POST", "/testJF", true);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.responseType = 'json';
req.onreadystatechange = function() {
if (this.readyState == 4) {
testButton.disabled = false;
testButton.className = '';
var success = response['responseJSON']['success'];
if (success == true) {
if (this.response['success'] == true) {
testButton.classList.add('btn', 'btn-success');
testButton.textContent = 'Success';
nextButton.classList.remove('disabled');
@@ -118,9 +119,10 @@ document.getElementById('jfTestButton').onclick = function() {
} else {
testButton.classList.add('btn', 'btn-danger');
testButton.textContent = 'Failed';
}
}
});
};
};
};
req.send(JSON.stringify(jfData));
};
document.getElementById('submitButton').onclick = function() {
@@ -158,7 +160,7 @@ document.getElementById('submitButton').onclick = function() {
if (document.getElementById('emailDisabledRadio').checked) {
config['password_resets']['enabled'] = 'false';
config['invite_emails']['enabled'] = 'false';
} else {
} else {
if (document.getElementById('emailSMTPRadio').checked) {
if (document.getElementById('emailSSL_TLS').checked) {
config['smtp']['encryption'] = 'ssl_tls';
@@ -217,18 +219,18 @@ document.getElementById('submitButton').onclick = function() {
config['ui']['contact_message'] = document.getElementById('msgContact').value;
config['ui']['help_message'] = document.getElementById('msgHelp').value;
config['ui']['success_message'] = document.getElementById('msgSuccess').value;
console.log(config);
$.ajax('/modifyConfig', {
type : 'POST',
dataType : 'json',
contentType : 'application/json',
data : JSON.stringify(config),
complete: function(response) {
// Send it
var req = new XMLHttpRequest();
req.open("POST", "/modifyConfig", true);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.responseType = 'json';
req.onreadystatechange = function() {
if (this.readyState == 4) {
submitButton.disabled = false;
submitButton.className = '';
submitButton.classList.add('btn', 'btn-success');
submitButton.textContent = 'Success';
}
});
};
};
req.send(JSON.stringify(config));
};

View File

@@ -31,21 +31,24 @@
return "";
};
{% if bs5 %}
var bsVersion = 5;
const bsVersion = 5;
{% else %}
var bsVersion = 4;
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);
// document.querySelectorAll('link[rel="stylesheet"][type="text/css"]')[0].href = cssCookie;
</script>
{% if not bs5 %}
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
@@ -91,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>
@@ -234,29 +252,66 @@
<div class="card mb-3">
<div class="card-header">Generate Invite</div>
<div class="card-body">
<form action="#" method="POST" id="inviteForm">
<div class="form-group">
<label for="hours">Hours</label>
<select class="form-control" id="hours" name="hours">
</select>
</div>
<div class="form-group">
<label for="minutes">Minutes</label>
<select class="form-control" id="minutes" name="minutes">
</select>
</div>
{% if email_enabled %}
<div class="form-group">
<label for="send_to_address">Send invite to address</label>
<div class="input-group">
<div class="input-group-text">
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
</div>
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
<form action="#" method="POST" id="inviteForm" class="container">
<div class="row align-items-start">
<div class="col">
<div class="form-group">
<label for="days">Days</label>
<select class="form-control form-select" id="days" name="days">
</select>
</div>
<div class="form-group">
<label for="hours">Hours</label>
<select class="form-control form-select" id="hours" name="hours">
</select>
</div>
<div class="form-group">
<label for="minutes">Minutes</label>
<select class="form-control form-select" id="minutes" name="minutes">
</select>
</div>
</div>
{% endif %}
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">Generate</button>
<div class="col">
<div class="form-group">
<label for="multiUseCount">
Multiple uses
</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" 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; if (this.checked) { document.getElementById('noLimitWarning').style = 'display: block;' } else { document.getElementById('noLimitWarning').style = 'display: none;'; }">
<label class="form-check-label" for="noUseLimit">
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">
<label for="send_to_address">Send invite to address</label>
<div class="input-group">
<div class="input-group-text">
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
</div>
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
</div>
</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group d-flex float-right">
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">
Generate
</button>
</div>
</div>
</div>
</form>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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(

View File

@@ -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"},
)

View File

@@ -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
@@ -200,9 +228,19 @@ def newUser():
def generateInvite():
current_time = datetime.datetime.now()
data = request.get_json()
delta = datetime.timedelta(hours=int(data["hours"]), minutes=int(data["minutes"]))
delta = datetime.timedelta(
days=int(data["days"]), hours=int(data["hours"]), minutes=int(data["minutes"])
)
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")
@@ -245,9 +283,20 @@ def getInvites():
valid_for = expiry - current_time
invite = {
"code": code,
"days": valid_for.days,
"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)

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "jellyfin-accounts"
version = "0.3.3"
version = "0.3.6"
readme = "README.md"
description = "A simple account management system for Jellyfin"
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]

View File

@@ -120,6 +120,10 @@ $list-group-action-active-bg: $jf-blue-focus;
color: $jf-text-bold;
}
.icon-button {
color: $text-muted;
}
.text-bright {
color: $jf-text-bold;
}

View File

@@ -120,6 +120,10 @@ $list-group-action-active-bg: $jf-blue-focus;
color: $jf-text-bold;
}
.icon-button:active {
color: $text-muted;
}
.text-bright {
color: $jf-text-bold;
}