mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2024-12-22 00:50:12 +00:00
Merge pull request #28 from hrfee/bs5
Add themes, Bootstrap 5 support, bump to 0.3.0
This commit is contained in:
commit
9da3832e3a
7
.gitignore
vendored
7
.gitignore
vendored
@ -4,13 +4,16 @@ MANIFEST.in
|
||||
dist/
|
||||
build/
|
||||
test.txt
|
||||
jellyfin_accounts/data/node_modules/
|
||||
node_modules/
|
||||
jellyfin_accounts/data/config-default.ini
|
||||
*.egg-info/
|
||||
pw-reset/
|
||||
jfa/
|
||||
colors.txt
|
||||
theme.css
|
||||
jellyfin_accounts/data/static/bootstrap-jf.css
|
||||
jellyfin_accounts/__pycache__/
|
||||
old/
|
||||
.jf-accounts/
|
||||
requirements.txt
|
||||
package-lock.json
|
||||
video/
|
||||
|
@ -1,11 +1,11 @@
|
||||
# ![jellyfin-accounts](https://raw.githubusercontent.com/hrfee/jellyfin-accounts/master/images/jellyfin-accounts-banner-wide.svg)
|
||||
# ![jellyfin-accounts](https://raw.githubusercontent.com/hrfee/jellyfin-accounts/bs5/images/jellyfin-accounts-banner-wide.svg)
|
||||
|
||||
A basic account management system for [Jellyfin](https://github.com/jellyfin/jellyfin).
|
||||
* Provides a web interface for creating invite codes, and a simple account creation form
|
||||
* Provides a web interface for creating/sending invites
|
||||
* Sends out emails when a user requests a password reset
|
||||
* Uses a basic python jellyfin API client for communication with the server.
|
||||
* Uses [Flask](https://github.com/pallets/flask), [HTTPAuth](https://github.com/miguelgrinberg/Flask-HTTPAuth), [itsdangerous](https://github.com/pallets/itsdangerous), and [Waitress](https://github.com/Pylons/waitress)
|
||||
* Frontend uses [Bootstrap](https://getbootstrap.com), [jQuery](https://jquery.com) and [jQuery-serialize-object](https://github.com/macek/jquery-serialize-object)
|
||||
* Frontend uses [Bootstrap](https://v5.getbootstrap.com)
|
||||
* Password resets are handled using smtplib, requests, and [jinja](https://github.com/pallets/jinja)
|
||||
## Interface
|
||||
<p align="center">
|
||||
|
@ -9,12 +9,14 @@ server = http://jellyfin.local:8096
|
||||
public_server = https://jellyf.in:443
|
||||
; this and below settings will show on the jellyfin dashboard when the program connects. you may as well leave them alone.
|
||||
client = jf-accounts
|
||||
version = 0.2.5
|
||||
version = 0.3.0
|
||||
device = jf-accounts
|
||||
device_id = jf-accounts-0.2.5
|
||||
device_id = jf-accounts-0.3.0
|
||||
|
||||
[ui]
|
||||
; settings related to the ui and program functionality.
|
||||
; choose the look of jellyfin-accounts.
|
||||
theme = Jellyfin (Dark)
|
||||
; set 0.0.0.0 to run on localhost
|
||||
host = 0.0.0.0
|
||||
port = 8056
|
||||
@ -33,6 +35,8 @@ contact_message = Need help? contact me.
|
||||
help_message = Enter your details to create an account.
|
||||
; displayed when a user creates an account
|
||||
success_message = Your account has been created. Click below to continue to Jellyfin.
|
||||
; use bootstrap 5 (currently in alpha). this also removes the need for jquery, so the page should load faster.
|
||||
bs5 = false
|
||||
|
||||
[password_validation]
|
||||
; password validation (minimum length, etc.)
|
||||
|
BIN
images/admin.png
BIN
images/admin.png
Binary file not shown.
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 74 KiB |
Binary file not shown.
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 106 KiB |
BIN
images/jfa.gif
BIN
images/jfa.gif
Binary file not shown.
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 6.7 MiB |
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
__version__ = "0.2.6"
|
||||
__version__ = "0.3.0"
|
||||
|
||||
import secrets
|
||||
import configparser
|
||||
@ -142,6 +142,8 @@ def load_config(config_path, data_dir):
|
||||
or config["jellyfin"]["public_server"] == ""
|
||||
):
|
||||
config["jellyfin"]["public_server"] = config["jellyfin"]["server"]
|
||||
if "bs5" not in config["ui"] or config["ui"]["bs5"] == "":
|
||||
config["ui"]["bs5"] = "false"
|
||||
return config
|
||||
|
||||
|
||||
@ -185,31 +187,39 @@ data_store = JSONStorage(
|
||||
config["files"]["user_configuration"],
|
||||
)
|
||||
|
||||
|
||||
def default_css():
|
||||
css = {}
|
||||
css[
|
||||
"href"
|
||||
] = "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
|
||||
css[
|
||||
"integrity"
|
||||
] = "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
|
||||
css["crossorigin"] = "anonymous"
|
||||
return css
|
||||
if config.getboolean("ui", "bs5"):
|
||||
css_file = "bs5-jf.css"
|
||||
log.debug("Using Bootstrap 5")
|
||||
else:
|
||||
css_file = "bs4-jf.css"
|
||||
|
||||
|
||||
css = {}
|
||||
css = default_css()
|
||||
if "custom_css" in config["files"]:
|
||||
with open(config_base_path, "r") as f:
|
||||
themes = json.load(f)["ui"]["theme"]
|
||||
|
||||
theme_options = themes["options"]
|
||||
|
||||
if "theme" not in config["ui"] or config["ui"]["theme"] not in theme_options:
|
||||
config["ui"]["theme"] = themes["value"]
|
||||
|
||||
if config.getboolean("ui", "bs5"):
|
||||
num = 5
|
||||
else:
|
||||
num = 4
|
||||
|
||||
current_theme = config["ui"]["theme"]
|
||||
|
||||
if "Bootstrap" in current_theme:
|
||||
css_file = f"bs{num}.css"
|
||||
elif "Jellyfin" in current_theme:
|
||||
css_file = f"bs{num}-jf.css"
|
||||
elif "Custom" in current_theme and "custom_css" in config["files"]:
|
||||
if config["files"]["custom_css"] != "":
|
||||
try:
|
||||
shutil.copy(
|
||||
config["files"]["custom_css"], (local_dir / "static" / "bootstrap.css")
|
||||
)
|
||||
log.debug("Loaded custom CSS")
|
||||
css["href"] = "/bootstrap.css"
|
||||
css["integrity"] = ""
|
||||
css["crossorigin"] = ""
|
||||
css_path = Path(config["files"]["custom_css"])
|
||||
shutil.copy(css_path, (local_dir / "static" / css_path.name))
|
||||
log.debug('Loaded custom CSS "{css_path.name}"')
|
||||
css_file = css_path.name
|
||||
except FileNotFoundError:
|
||||
log.error(
|
||||
f'Custom CSS {config["files"]["custom_css"]} not found, using default.'
|
||||
|
@ -70,6 +70,19 @@
|
||||
"name": "General",
|
||||
"description": "Settings related to the UI and program functionality."
|
||||
},
|
||||
"theme": {
|
||||
"name": "Look",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Bootstrap (Light)",
|
||||
"Jellyfin (Dark)",
|
||||
"Custom CSS"
|
||||
],
|
||||
"value": "Jellyfin (Dark)",
|
||||
"description": "Choose the look of jellyfin-accounts."
|
||||
},
|
||||
"host": {
|
||||
"name": "Address",
|
||||
"required": true,
|
||||
@ -150,6 +163,14 @@
|
||||
"type": "text",
|
||||
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
||||
"description": "Displayed when a user creates an account"
|
||||
},
|
||||
"bs5": {
|
||||
"name": "Use Bootstrap 5",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use Bootstrap 5 (currently in alpha). This also removes the need for jQuery, so the page should load faster."
|
||||
}
|
||||
},
|
||||
"password_validation": {
|
||||
@ -208,7 +229,7 @@
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
|
@ -1,665 +0,0 @@
|
||||
function parseInvite(invite, empty = false) {
|
||||
if (empty === true) {
|
||||
return ["None", "", "1"]
|
||||
} else {
|
||||
var i = ["", "", "0", invite['email']];
|
||||
i[0] = invite['code'];
|
||||
if (invite['hours'] == 0) {
|
||||
i[1] = invite['minutes'] + 'm';
|
||||
} else if (invite['minutes'] == 0) {
|
||||
i[1] = invite['hours'] + 'h';
|
||||
} else {
|
||||
i[1] = invite['hours'] + 'h ' + invite['minutes'] + 'm';
|
||||
}
|
||||
i[1] = "Expires in " + i[1] + " ";
|
||||
return i
|
||||
}
|
||||
}
|
||||
function addItem(invite) {
|
||||
var links = document.getElementById('invites');
|
||||
var listItem = document.createElement('li');
|
||||
listItem.id = invite[0]
|
||||
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
|
||||
var listCode = document.createElement('div');
|
||||
listCode.classList.add('d-flex', 'align-items-center', 'text-monospace');
|
||||
var codeLink = document.createElement('a');
|
||||
codeLink.setAttribute('style', 'margin-right: 2%;');
|
||||
codeLink.appendChild(document.createTextNode(invite[0].replace(/-/g, '‑')));
|
||||
listCode.appendChild(codeLink);
|
||||
listItem.appendChild(listCode);
|
||||
var listRight = document.createElement('div');
|
||||
listText = document.createElement('span');
|
||||
listText.id = invite[0] + '_expiry'
|
||||
listText.appendChild(document.createTextNode(invite[1]));
|
||||
listRight.appendChild(listText);
|
||||
if (invite[2] == 0) {
|
||||
var inviteCode = window.location.href + 'invite/' + invite[0];
|
||||
codeLink.href = inviteCode;
|
||||
// listCode.appendChild(document.createTextNode(" "));
|
||||
var codeCopy = document.createElement('i');
|
||||
codeCopy.onclick = function(){toClipboard(inviteCode)};
|
||||
codeCopy.classList.add('fa', 'fa-clipboard');
|
||||
listCode.appendChild(codeCopy);
|
||||
if (typeof(invite[3]) != 'undefined') {
|
||||
var sentTo = document.createElement('span');
|
||||
sentTo.setAttribute('style', 'color: grey; margin-left: 2%; font-style: italic; font-size: 75%;');
|
||||
if (invite[3].includes('Failed to send to')) {
|
||||
sentTo.appendChild(document.createTextNode(invite[3]));
|
||||
} else {
|
||||
sentTo.appendChild(document.createTextNode('Sent to ' + invite[3]));
|
||||
}
|
||||
listCode.appendChild(sentTo);
|
||||
};
|
||||
var listDelete = document.createElement('button');
|
||||
listDelete.onclick = function(){deleteInvite(invite[0])};
|
||||
listDelete.classList.add('btn', 'btn-outline-danger');
|
||||
listDelete.appendChild(document.createTextNode('Delete'));
|
||||
listRight.appendChild(listDelete);
|
||||
};
|
||||
listItem.appendChild(listRight);
|
||||
links.appendChild(listItem);
|
||||
};
|
||||
function updateInvite(invite) {
|
||||
var expiry = document.getElementById(invite[0] + '_expiry');
|
||||
expiry.textContent = invite[1];
|
||||
}
|
||||
function removeInvite(code) {
|
||||
var item = document.getElementById(code);
|
||||
item.parentNode.removeChild(item);
|
||||
}
|
||||
function generateInvites(empty = false) {
|
||||
// document.getElementById('invites').textContent = '';
|
||||
if (empty === false) {
|
||||
$.ajax('/getInvites', {
|
||||
type : 'GET',
|
||||
dataType : 'json',
|
||||
contentType: 'json',
|
||||
xhrFields : {
|
||||
withCredentials: true
|
||||
},
|
||||
beforeSend : function (xhr) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
},
|
||||
data: { get_param: 'value' },
|
||||
complete: function(response) {
|
||||
var data = JSON.parse(response['responseText']);
|
||||
if (data['invites'].length == 0) {
|
||||
document.getElementById('invites').textContent = '';
|
||||
addItem(parseInvite([], true));
|
||||
} else {
|
||||
data['invites'].forEach(function(invite) {
|
||||
var match = false;
|
||||
var items = document.getElementById('invites').children;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].id == invite['code']) {
|
||||
match = true;
|
||||
updateInvite(parseInvite(invite));
|
||||
};
|
||||
};
|
||||
if (match == false) {
|
||||
addItem(parseInvite(invite));
|
||||
};
|
||||
});
|
||||
var items = document.getElementById('invites').children;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var exists = false;
|
||||
data['invites'].forEach(function(invite) {
|
||||
if (items[i].id == invite['code']) {
|
||||
exists = true;
|
||||
}
|
||||
});
|
||||
if (exists == false) {
|
||||
removeInvite(items[i].id);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
});
|
||||
} else if (empty === true) {
|
||||
document.getElementById('invites').textContent = '';
|
||||
addItem(parseInvite([], true));
|
||||
};
|
||||
};
|
||||
function deleteInvite(code) {
|
||||
var send = JSON.stringify({ "code": code });
|
||||
$.ajax('/deleteInvite', {
|
||||
data : send,
|
||||
contentType : 'application/json',
|
||||
type : 'POST',
|
||||
xhrFields : {
|
||||
withCredentials: true
|
||||
},
|
||||
beforeSend : function (xhr) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
},
|
||||
success: function() { generateInvites(); },
|
||||
});
|
||||
};
|
||||
function addOptions(le, sel) {
|
||||
for (v = 0; v <= le; v++) {
|
||||
var opt = document.createElement('option');
|
||||
opt.appendChild(document.createTextNode(v))
|
||||
opt.value = v
|
||||
sel.appendChild(opt)
|
||||
}
|
||||
};
|
||||
function toClipboard(str) {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = str;
|
||||
el.setAttribute('readonly', '');
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
document.body.appendChild(el);
|
||||
const selected =
|
||||
document.getSelection().rangeCount > 0
|
||||
? document.getSelection().getRangeAt(0)
|
||||
: false;
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
if (selected) {
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(selected);
|
||||
}
|
||||
};
|
||||
|
||||
$("form#inviteForm").submit(function() {
|
||||
var button = document.getElementById('generateSubmit');
|
||||
button.disabled = true;
|
||||
button.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
var send_object = $("form#inviteForm").serializeObject();
|
||||
if (document.getElementById('send_to_address') != null) {
|
||||
if (document.getElementById('send_to_address_enabled').checked) {
|
||||
send_object['email'] = document.getElementById('send_to_address').value;
|
||||
}
|
||||
}
|
||||
var send = JSON.stringify(send_object);
|
||||
$.ajax('/generateInvite', {
|
||||
data : send,
|
||||
contentType : 'application/json',
|
||||
type : 'POST',
|
||||
xhrFields : {
|
||||
withCredentials: true
|
||||
},
|
||||
beforeSend : function (xhr) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
},
|
||||
success: function() {
|
||||
button.textContent = 'Generate';
|
||||
button.disabled = false;
|
||||
generateInvites();
|
||||
},
|
||||
|
||||
});
|
||||
return false;
|
||||
});
|
||||
$("form#loginForm").submit(function() {
|
||||
window.token = "";
|
||||
var details = $("form#loginForm").serializeObject();
|
||||
var errorArea = document.getElementById('loginErrorArea');
|
||||
errorArea.textContent = '';
|
||||
var button = document.getElementById('loginSubmit');
|
||||
button.disabled = true;
|
||||
button.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
$.ajax('/getToken', {
|
||||
type : 'GET',
|
||||
dataType : 'json',
|
||||
contentType: 'json',
|
||||
xhrFields : {
|
||||
withCredentials: true
|
||||
},
|
||||
beforeSend : function (xhr) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(details['username'] + ":" + details['password']));
|
||||
},
|
||||
data: { get_param: 'value' },
|
||||
complete: function(data) {
|
||||
if (data['status'] == 401) {
|
||||
button.disabled = false;
|
||||
button.textContent = 'Login';
|
||||
var wrongPassword = document.createElement('div');
|
||||
wrongPassword.classList.add('alert', 'alert-danger');
|
||||
wrongPassword.setAttribute('role', 'alert');
|
||||
wrongPassword.appendChild(document.createTextNode('Incorrect username or password.'));
|
||||
errorArea.appendChild(wrongPassword);
|
||||
} else {
|
||||
window.token = JSON.parse(data['responseText'])['token'];
|
||||
generateInvites();
|
||||
var interval = setInterval(function() { generateInvites(); }, 60 * 1000);
|
||||
var hour = document.getElementById('hours');
|
||||
addOptions(24, hour);
|
||||
hour.selected = "0";
|
||||
var minutes = document.getElementById('minutes');
|
||||
addOptions(59, minutes);
|
||||
minutes.selected = "30";
|
||||
$('#login').modal('hide');
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
document.getElementById('openDefaultsWizard').onclick = function () {
|
||||
this.disabled = true;
|
||||
this.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
$.ajax('getUsers', {
|
||||
type : 'GET',
|
||||
dataType : 'json',
|
||||
contentType : 'json',
|
||||
xhrFields : {
|
||||
withCredentials: true
|
||||
},
|
||||
beforeSend : function (xhr) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
},
|
||||
complete : function(data) {
|
||||
if (data['status'] == 200) {
|
||||
var radioList = document.getElementById('defaultUserRadios');
|
||||
radioList.textContent = '';
|
||||
if (document.getElementById('setDefaultUser')) {
|
||||
document.getElementById('setDefaultUser').remove();
|
||||
};
|
||||
var users = data['responseJSON']['users'];
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
var user = users[i]
|
||||
var radio = document.createElement('div');
|
||||
radio.classList.add('radio');
|
||||
if (i == 0) {
|
||||
var checked = 'checked';
|
||||
} else {
|
||||
var checked = '';
|
||||
};
|
||||
radio.innerHTML =
|
||||
'<label><input type="radio" name="defaultRadios" id="default_' +
|
||||
user['name'] + '" style="margin-right: 1rem;"' + checked + '>' +
|
||||
user['name'] + '</label>';
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
var button = document.getElementById('openDefaultsWizard');
|
||||
button.disabled = false;
|
||||
button.innerHTML = 'Set new account defaults';
|
||||
var submitButton = document.getElementById('storeDefaults');
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Submit';
|
||||
if (submitButton.classList.contains('btn-success')) {
|
||||
submitButton.classList.remove('btn-success');
|
||||
submitButton.classList.add('btn-primary');
|
||||
} else if (submitButton.classList.contains('btn-danger')) {
|
||||
submitButton.classList.remove('btn-danger');
|
||||
submitButton.classList.add('btn-primary');
|
||||
};
|
||||
$('#settingsMenu').modal('hide');
|
||||
$('#userDefaults').modal('show');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
document.getElementById('storeDefaults').onclick = function () {
|
||||
this.disabled = true;
|
||||
this.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
var button = document.getElementById('storeDefaults');
|
||||
var radios = document.getElementsByName('defaultRadios');
|
||||
for (var i = 0; i < radios.length; i++) {
|
||||
if (radios[i].checked) {
|
||||
var data = {'username':radios[i].id.slice(8), 'homescreen':false};
|
||||
if (document.getElementById('storeDefaultHomescreen').checked) {
|
||||
data['homescreen'] = true;
|
||||
}
|
||||
$.ajax('/setDefaults', {
|
||||
data : JSON.stringify(data),
|
||||
contentType : 'application/json',
|
||||
type : 'POST',
|
||||
xhrFields : {
|
||||
withCredentials: true
|
||||
},
|
||||
beforeSend : function (xhr) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
},
|
||||
success: function() {
|
||||
button.textContent = 'Success';
|
||||
if (button.classList.contains('btn-danger')) {
|
||||
button.classList.remove('btn-danger');
|
||||
} else if (button.classList.contains('btn-primary')) {
|
||||
button.classList.remove('btn-primary');
|
||||
};
|
||||
button.classList.add('btn-success');
|
||||
button.disabled = false;
|
||||
setTimeout(function(){$('#userDefaults').modal('hide');}, 1000);
|
||||
},
|
||||
error: function() {
|
||||
button.textContent = 'Failed';
|
||||
config_base_path = local_dir / "config-base.json"
|
||||
button.classList.remove('btn-primary');
|
||||
button.classList.add('btn-danger');
|
||||
setTimeout(function(){
|
||||
var button = document.getElementById('storeDefaults');
|
||||
button.textContent = 'Submit';
|
||||
button.classList.remove('btn-danger');
|
||||
button.classList.add('btn-primary');
|
||||
button.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
document.getElementById('openUsers').onclick = function () {
|
||||
this.disabled = true;
|
||||
this.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
$.ajax('/getUsers', {
|
||||
type : 'GET',
|
||||
dataType : 'json',
|
||||
contentType: 'json',
|
||||
xhrFields : {
|
||||
withCredentials: true
|
||||
},
|
||||
beforeSend : function (xhr) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
},
|
||||
data: { get_param: 'value' },
|
||||
complete : function(data) {
|
||||
if (data['status'] == 200) {
|
||||
var list = document.getElementById('userList');
|
||||
list.textContent = '';
|
||||
if (document.getElementById('saveUsers')) {
|
||||
document.getElementById('saveUsers').remove();
|
||||
};
|
||||
var users = data['responseJSON']['users'];
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
var user = users[i]
|
||||
var entry = document.createElement('p');
|
||||
entry.id = 'user_' + user['name'];
|
||||
entry.appendChild(document.createTextNode(user['name']));
|
||||
var address = document.createElement('span');
|
||||
address.setAttribute('style', 'margin-left: 2%; margin-right: 2%; color: grey;');
|
||||
address.classList.add('addressText');
|
||||
address.id = 'address_' + user['email'];
|
||||
if (typeof(user['email']) != 'undefined') {
|
||||
address.appendChild(document.createTextNode(user['email']));
|
||||
};
|
||||
var editButton = document.createElement('i');
|
||||
editButton.classList.add('fa', 'fa-edit');
|
||||
editButton.onclick = function() {
|
||||
this.classList.remove('fa', 'fa-edit');
|
||||
var input = document.createElement('input');
|
||||
input.setAttribute('type', 'email');
|
||||
input.setAttribute('style', 'margin-left: 2%; color: grey;');
|
||||
var addressElement = this.parentNode.getElementsByClassName('addressText')[0];
|
||||
if (addressElement.textContent != '') {
|
||||
input.value = addressElement.textContent;
|
||||
} else {
|
||||
input.placeholder = 'Email Address';
|
||||
};
|
||||
this.parentNode.replaceChild(input, addressElement);
|
||||
if (document.getElementById('saveUsers') == null) {
|
||||
var footer = document.getElementById('userFooter')
|
||||
var saveUsers = document.createElement('input');
|
||||
saveUsers.classList.add('btn', 'btn-primary');
|
||||
saveUsers.setAttribute('type', 'button');
|
||||
saveUsers.value = 'Save Changes';
|
||||
saveUsers.id = 'saveUsers';
|
||||
saveUsers.onclick = function() {
|
||||
var send = {}
|
||||
var entries = document.getElementById('userList').children;
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entry = entries[i];
|
||||
if (typeof(entry.getElementsByTagName('input')[0]) != 'undefined') {
|
||||
var name = entry.id.replace(/user_/g, '')
|
||||
var address = entry.getElementsByTagName('input')[0].value;
|
||||
send[name] = address
|
||||
};
|
||||
};
|
||||
send = JSON.stringify(send);
|
||||
$.ajax('/modifyUsers', {
|
||||
data : send,
|
||||
contentType : 'application/json',
|
||||
type : 'POST',
|
||||
xhrFields : {
|
||||
withCredentials: true
|
||||
},
|
||||
beforeSend : function (xhr) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
},
|
||||
success: function() { $('#users').modal('hide'); },
|
||||
});
|
||||
};
|
||||
footer.appendChild(saveUsers);
|
||||
};
|
||||
};
|
||||
entry.appendChild(address);
|
||||
entry.appendChild(editButton);
|
||||
list.appendChild(entry);
|
||||
};
|
||||
var button = document.getElementById('openUsers');
|
||||
button.disabled = false;
|
||||
button.innerHTML = 'Users <i class="fa fa-user"></i>';
|
||||
$('#settingsMenu').modal('hide');
|
||||
$('#users').modal('show');
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
generateInvites(empty = true);
|
||||
$("#login").modal('show');
|
||||
|
||||
var config = {};
|
||||
var modifiedConfig = {};
|
||||
|
||||
document.getElementById('openSettings').onclick = function () {
|
||||
restart_setting_changed = false;
|
||||
$.ajax('getConfig', {
|
||||
type : 'GET',
|
||||
dataType : 'json',
|
||||
contentType : 'json',
|
||||
xhrFields : {
|
||||
withCredentials: true
|
||||
},
|
||||
beforeSend : function (xhr) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
},
|
||||
complete : function(data) {
|
||||
if (data['status'] == 200) {
|
||||
var settingsList = document.getElementById('settingsList');
|
||||
settingsList.textContent = '';
|
||||
config = data['responseJSON'];
|
||||
for (var section of Object.keys(config)) {
|
||||
var sectionCollapse = document.createElement('div');
|
||||
sectionCollapse.classList.add('collapse');
|
||||
sectionCollapse.id = section;
|
||||
|
||||
var sectionTitle = config[section]['meta']['name'];
|
||||
var sectionDescription = config[section]['meta']['description'];
|
||||
var entryListID = section + '_entryList';
|
||||
var sectionFooter = section + '_footer';
|
||||
|
||||
var innerCollapse = `
|
||||
<div class="card card-body">
|
||||
<small class="text-muted">${sectionDescription}</small>
|
||||
<div class="${entryListID}">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
sectionCollapse.innerHTML = innerCollapse;
|
||||
|
||||
for (var entry of Object.keys(config[section])) {
|
||||
if (entry != 'meta') {
|
||||
var entryName = config[section][entry]['name'];
|
||||
var required = false;
|
||||
if (config[section][entry]['required']) {
|
||||
entryName += ' <sup class="text-danger">*</sup>';
|
||||
required = true;
|
||||
};
|
||||
if (config[section][entry]['requires_restart']) {
|
||||
entryName += ' <sup class="text-danger">R</sup>';
|
||||
};
|
||||
if (config[section][entry].hasOwnProperty('description')) {
|
||||
var tooltip = `
|
||||
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
|
||||
`;
|
||||
entryName += ' ';
|
||||
entryName += tooltip;
|
||||
};
|
||||
var entryValue = config[section][entry]['value'];
|
||||
var entryType = config[section][entry]['type'];
|
||||
var entryGroup = document.createElement('div');
|
||||
if (entryType == 'bool') {
|
||||
entryGroup.classList.add('form-check');
|
||||
if (entryValue.toString() == 'true') {
|
||||
var checked = true;
|
||||
} else {
|
||||
var checked = false;
|
||||
};
|
||||
entryGroup.innerHTML = `
|
||||
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}">
|
||||
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
|
||||
`;
|
||||
entryGroup.getElementsByClassName('form-check-input')[0].required = required;
|
||||
entryGroup.getElementsByClassName('form-check-input')[0].checked = checked;
|
||||
entryGroup.getElementsByClassName('form-check-input')[0].onclick = function() {
|
||||
var state = this.checked;
|
||||
for (var sect of Object.keys(config)) {
|
||||
for (var ent of Object.keys(config[sect])) {
|
||||
if ((sect + '_' + config[sect][ent]['depends_true']) == this.id) {
|
||||
document.getElementById(sect + '_' + ent).disabled = !state;
|
||||
} else if ((sect + '_' + config[sect][ent]['depends_false']) == this.id) {
|
||||
document.getElementById(sect + '_' + ent).disabled = state;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
} else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) {
|
||||
entryGroup.classList.add('form-group');
|
||||
entryGroup.innerHTML = `
|
||||
<label for="${section}_${entry}">${entryName}</label>
|
||||
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}">
|
||||
`;
|
||||
entryGroup.getElementsByClassName('form-control')[0].required = required;
|
||||
} else if (entryType == 'select') {
|
||||
entryGroup.classList.add('form-group');
|
||||
var entryOptions = config[section][entry]['options'];
|
||||
var innerGroup = `
|
||||
<label for="${section}_${entry}">${entryName}</label>
|
||||
<select class="form-control" id="${section}_${entry}">
|
||||
`;
|
||||
for (var i = 0; i < entryOptions.length; i++) {
|
||||
if (entryOptions[i] == entryValue) {
|
||||
var selected = 'selected';
|
||||
} else {
|
||||
var selected = '';
|
||||
}
|
||||
innerGroup += `
|
||||
<option value="${entryOptions[i]}" ${selected}>${entryOptions[i]}</option>
|
||||
`;
|
||||
};
|
||||
innerGroup += '</select>';
|
||||
entryGroup.innerHTML = innerGroup;
|
||||
entryGroup.getElementsByClassName('form-control')[0].required = required;
|
||||
|
||||
};
|
||||
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
|
||||
};
|
||||
};
|
||||
var sectionButton = document.createElement('button');
|
||||
sectionButton.setAttribute('type', 'button');
|
||||
sectionButton.classList.add('list-group-item', 'list-group-item-action');
|
||||
sectionButton.appendChild(document.createTextNode(sectionTitle));
|
||||
sectionButton.id = section + '_button';
|
||||
sectionButton.setAttribute('data-toggle', 'collapse');
|
||||
sectionButton.setAttribute('data-target', '#' + section);
|
||||
settingsList.appendChild(sectionButton);
|
||||
settingsList.appendChild(sectionCollapse);
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
$('#settingsMenu').modal('show');
|
||||
};
|
||||
|
||||
$('#settingsMenu').on('shown.bs.modal', function() {
|
||||
$("a[data-toggle='tooltip']").each(function (i, obj) {
|
||||
$(obj).tooltip();
|
||||
});
|
||||
});
|
||||
|
||||
function sendConfig(modalId) {
|
||||
var modal = document.getElementById(modalId);
|
||||
var send = JSON.stringify(modifiedConfig);
|
||||
$.ajax('/modifyConfig', {
|
||||
data : send,
|
||||
contentType : 'application/json',
|
||||
type : 'POST',
|
||||
xhrFields : {
|
||||
withCredentials: true
|
||||
},
|
||||
beforeSend : function (xhr) {
|
||||
xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
},
|
||||
success: function() {
|
||||
$('#' + modalId).modal('hide');
|
||||
if (modalId != 'settingsMenu') {
|
||||
$('#settingsMenu').modal('hide');
|
||||
};
|
||||
},
|
||||
fail: function(xhr, textStatus, errorThrown) {
|
||||
var footer = modal.getElementsByClassName('modal-dialog')[0].getElementsByClassName('modal-content')[0].getElementsByClassName('modal-footer')[0];
|
||||
var alert = document.createElement('div');
|
||||
alert.classList.add('alert', 'alert-danger');
|
||||
alert.setAttribute('role', 'alert');
|
||||
alert.appendChild(document.createTextNode('Error: ' + errorThrown));
|
||||
footer.appendChild(alert);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('settingsSave').onclick = function() {
|
||||
modifiedConfig = {};
|
||||
var restart_setting_changed = false;
|
||||
var settings_changed = false;
|
||||
|
||||
for (var section of Object.keys(config)) {
|
||||
for (var entry of Object.keys(config[section])) {
|
||||
if (entry != 'meta') {
|
||||
var entryID = section + '_' + entry;
|
||||
var el = document.getElementById(entryID);
|
||||
if (el.type == 'checkbox') {
|
||||
var value = el.checked.toString();
|
||||
} else {
|
||||
var value = el.value.toString();
|
||||
};
|
||||
if (value != config[section][entry]['value'].toString()) {
|
||||
if (!modifiedConfig.hasOwnProperty(section)) {
|
||||
modifiedConfig[section] = {};
|
||||
};
|
||||
modifiedConfig[section][entry] = value;
|
||||
settings_changed = true;
|
||||
if (config[section][entry]['requires_restart']) {
|
||||
restart_setting_changed = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
// if (restart_setting_changed) {
|
||||
if (restart_setting_changed) {
|
||||
document.getElementById('applyRestarts').onclick = function(){sendConfig('restartModal');};
|
||||
$('#settingsMenu').modal('hide');
|
||||
$('#restartModal').modal({
|
||||
backdrop: 'static',
|
||||
show: true
|
||||
});
|
||||
} else if (settings_changed) {
|
||||
sendConfig('settingsMenu');
|
||||
} else {
|
||||
$('#settingsMenu').modal('hide');
|
||||
};
|
||||
};
|
||||
|
File diff suppressed because one or more lines are too long
7
jellyfin_accounts/data/static/bs4-jf.css
Normal file
7
jellyfin_accounts/data/static/bs4-jf.css
Normal file
File diff suppressed because one or more lines are too long
7
jellyfin_accounts/data/static/bs4.css
Normal file
7
jellyfin_accounts/data/static/bs4.css
Normal file
File diff suppressed because one or more lines are too long
9438
jellyfin_accounts/data/static/bs5-jf.css
Normal file
9438
jellyfin_accounts/data/static/bs5-jf.css
Normal file
File diff suppressed because one or more lines are too long
7
jellyfin_accounts/data/static/bs5.css
Normal file
7
jellyfin_accounts/data/static/bs5.css
Normal file
File diff suppressed because one or more lines are too long
25
jellyfin_accounts/data/static/serialize.js
Normal file
25
jellyfin_accounts/data/static/serialize.js
Normal file
@ -0,0 +1,25 @@
|
||||
function serializeForm(id) {
|
||||
var form = document.getElementById(id);
|
||||
var formData = {};
|
||||
for (var i = 0; i < form.elements.length; i++) {
|
||||
var el = form.elements[i];
|
||||
if (el.type != 'submit') {
|
||||
var name = el.name;
|
||||
if (name == '') {
|
||||
name = el.id;
|
||||
};
|
||||
switch (el.type) {
|
||||
case 'checkbox':
|
||||
formData[name] = el.checked;
|
||||
break;
|
||||
case 'text':
|
||||
case 'password':
|
||||
case 'select-one':
|
||||
case 'email':
|
||||
formData[name] = el.value;
|
||||
break;
|
||||
};
|
||||
};
|
||||
};
|
||||
return formData;
|
||||
};
|
@ -1,35 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#603cba">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#603cba">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<title>404</title>
|
||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
||||
<style>
|
||||
.messageBox {
|
||||
margin: 20%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="messageBox">
|
||||
<h1>Page not found.</h1>
|
||||
<p>
|
||||
{{ contactMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
<title>404</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
{% if bs5 %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||
{% else %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<style>
|
||||
.messageBox {
|
||||
margin: 20%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="messageBox">
|
||||
<h1>Page not found.</h1>
|
||||
<p>
|
||||
{{ contactMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -12,14 +12,18 @@
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#603cba">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
||||
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
|
||||
{% if bs5 %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||
{% else %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<style>
|
||||
.pageContainer {
|
||||
@ -49,17 +53,11 @@
|
||||
margin-top: 5%;
|
||||
color: grey;
|
||||
}
|
||||
.fa-clipboard {
|
||||
color: grey;
|
||||
}
|
||||
.fa-clipboard:hover {
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
<title>Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="modal fade" id="login" tabindex="-1" role="dialog" aria-labelledby="login" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal fade" id="login" role="dialog" aria-labelledby="login" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@ -82,7 +80,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="settingsMenu" tabindex="-1" role="dialog" aria-labelledby="settings menu" aria-hidden="true">
|
||||
<div class="modal fade" id="settingsMenu" role="dialog" aria-labelledby="settings menu" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@ -111,7 +109,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="users" tabindex="-1" role="dialog" aria-labelledby="users" aria-hidden="true">
|
||||
<div class="modal fade" id="users" role="dialog" aria-labelledby="users" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@ -130,7 +128,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="userDefaults" tabindex="-1" role="dialog" aria-labelledby="users" aria-hidden="true">
|
||||
<div class="modal fade" id="userDefaults" role="dialog" aria-labelledby="users" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@ -153,7 +151,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="restartModal" tabindex="-1" role="dialog" aria-labelledby"Restart Warning" aria-hidden="true">
|
||||
<div class="modal fade" id="restartModal" role="dialog" aria-labelledby"Restart Warning" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@ -176,13 +174,13 @@
|
||||
<button type="button" class="btn btn-secondary" id="openSettings">
|
||||
Settings <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
<div class="card bg-light mb-3 linkGroup">
|
||||
<div class="card mb-3 linkGroup">
|
||||
<div class="card-header">Current Invites</div>
|
||||
<ul class="list-group list-group-flush" id="invites">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="linkForm">
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Generate Invite</div>
|
||||
<div class="card-body">
|
||||
<form action="#" method="POST" id="inviteForm">
|
||||
@ -200,16 +198,14 @@
|
||||
<div class="form-group">
|
||||
<label for="send_to_address">Send invite to address</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<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>
|
||||
<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 %}
|
||||
<button type="submit" id="generateSubmit" class="btn btn-primary">Generate</button>
|
||||
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">Generate</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -218,6 +214,7 @@
|
||||
<p>{{ contactMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="serialize.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
745
jellyfin_accounts/data/templates/admin.js
Normal file
745
jellyfin_accounts/data/templates/admin.js
Normal file
@ -0,0 +1,745 @@
|
||||
var bsVersion = {{ bsVersion }};
|
||||
|
||||
if (bsVersion == 5) {
|
||||
function createModal(id, find = false) {
|
||||
if (find) {
|
||||
return bootstrap.Modal.getInstance(document.getElementById(id));
|
||||
};
|
||||
return new bootstrap.Modal(document.getElementById(id));
|
||||
};
|
||||
function triggerTooltips() {
|
||||
document.getElementById('settingsMenu').addEventListener('shown.bs.modal', function() {
|
||||
// Hack to ensure anything dependent on checkboxes are disabled if necessary
|
||||
var checkboxes = document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]');
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].click();
|
||||
checkboxes[i].click();
|
||||
};
|
||||
// Initialize tooltips
|
||||
var to_trigger = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
|
||||
var tooltips = to_trigger.map(function(el) {
|
||||
return new bootstrap.Tooltip(el);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// var loginModal = new bootstrap.Modal(document.getElementById('login'));
|
||||
// var settingsModal = new bootstrap.Modal(document.getElementById('settingsMenu'));
|
||||
// var userDefaultsModal = new bootstrap.Modal(document.getElementById('userDefaults'));
|
||||
// var usersModal = new bootstrap.Modal(document.getElementById('users'));
|
||||
// var restartModal = new bootstrap.Modal(document.getElementById('restartModal'));
|
||||
} else if (bsVersion == 4) {
|
||||
document.getElementById('send_to_address_enabled').classList.remove('form-check-input');
|
||||
function createModal(id, find = false) {
|
||||
return {
|
||||
show : function() {
|
||||
return $('#' + id).modal('show');
|
||||
},
|
||||
hide : function() {
|
||||
return $('#' + id).modal('hide');
|
||||
}
|
||||
};
|
||||
};
|
||||
function triggerTooltips() {
|
||||
$('#settingsMenu').on('shown.bs.modal', function() {
|
||||
var checkboxes = document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]');
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].click();
|
||||
checkboxes[i].click();
|
||||
};
|
||||
$("a[data-toggle='tooltip']").each(function (i, obj) {
|
||||
$(obj).tooltip();
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
var loginModal = createModal('login');
|
||||
var settingsModal = createModal('settingsMenu');
|
||||
var userDefaultsModal = createModal('userDefaults');
|
||||
var usersModal = createModal('users');
|
||||
var restartModal = createModal('restartModal');
|
||||
|
||||
function parseInvite(invite, empty = false) {
|
||||
if (empty === true) {
|
||||
return ["None", "", "1"]
|
||||
} else {
|
||||
var i = ["", "", "0", invite['email']];
|
||||
i[0] = invite['code'];
|
||||
if (invite['hours'] == 0) {
|
||||
i[1] = invite['minutes'] + 'm';
|
||||
} else if (invite['minutes'] == 0) {
|
||||
i[1] = invite['hours'] + 'h';
|
||||
} else {
|
||||
i[1] = invite['hours'] + 'h ' + invite['minutes'] + 'm';
|
||||
}
|
||||
i[1] = "Expires in " + i[1] + " ";
|
||||
return i
|
||||
}
|
||||
}
|
||||
function addItem(invite) {
|
||||
var links = document.getElementById('invites');
|
||||
var listItem = document.createElement('li');
|
||||
listItem.id = invite[0]
|
||||
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
|
||||
var listCode = document.createElement('div');
|
||||
listCode.classList.add('d-flex', 'align-items-center', 'font-monospace');
|
||||
var codeLink = document.createElement('a');
|
||||
codeLink.setAttribute('style', 'margin-right: 0.5rem;');
|
||||
codeLink.appendChild(document.createTextNode(invite[0].replace(/-/g, '‑')));
|
||||
listCode.appendChild(codeLink);
|
||||
listItem.appendChild(listCode);
|
||||
var listRight = document.createElement('div');
|
||||
listText = document.createElement('span');
|
||||
listText.id = invite[0] + '_expiry'
|
||||
listText.appendChild(document.createTextNode(invite[1]));
|
||||
listRight.appendChild(listText);
|
||||
if (invite[2] == 0) {
|
||||
var inviteCode = window.location.href.replace('#', '') + 'invite/' + invite[0];
|
||||
codeLink.href = inviteCode;
|
||||
// listCode.appendChild(document.createTextNode(" "));
|
||||
var codeCopy = document.createElement('i');
|
||||
codeCopy.onclick = function(){toClipboard(inviteCode)};
|
||||
codeCopy.classList.add('fa', 'fa-clipboard', 'icon-button');
|
||||
listCode.appendChild(codeCopy);
|
||||
if (typeof(invite[3]) != 'undefined') {
|
||||
var sentTo = document.createElement('span');
|
||||
sentTo.setAttribute('style', 'color: grey; margin-left: 2%; font-style: italic; font-size: 75%;');
|
||||
if (invite[3].includes('Failed to send to')) {
|
||||
sentTo.appendChild(document.createTextNode(invite[3]));
|
||||
} else {
|
||||
sentTo.appendChild(document.createTextNode('Sent to ' + invite[3]));
|
||||
}
|
||||
listCode.appendChild(sentTo);
|
||||
};
|
||||
var listDelete = document.createElement('button');
|
||||
listDelete.onclick = function(){deleteInvite(invite[0])};
|
||||
listDelete.classList.add('btn', 'btn-outline-danger');
|
||||
listDelete.appendChild(document.createTextNode('Delete'));
|
||||
listRight.appendChild(listDelete);
|
||||
};
|
||||
listItem.appendChild(listRight);
|
||||
links.appendChild(listItem);
|
||||
};
|
||||
function updateInvite(invite) {
|
||||
var expiry = document.getElementById(invite[0] + '_expiry');
|
||||
expiry.textContent = invite[1];
|
||||
}
|
||||
function removeInvite(code) {
|
||||
var item = document.getElementById(code);
|
||||
item.parentNode.removeChild(item);
|
||||
}
|
||||
function generateInvites(empty = false) {
|
||||
// document.getElementById('invites').textContent = '';
|
||||
if (empty === false) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("GET", "/getInvites", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
var data = this.response;
|
||||
if (data['invites'].length == 0) {
|
||||
document.getElementById('invites').textContent = '';
|
||||
addItem(parseInvite([], true));
|
||||
} else {
|
||||
data['invites'].forEach(function(invite) {
|
||||
var match = false;
|
||||
var items = document.getElementById('invites').children;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].id == invite['code']) {
|
||||
match = true;
|
||||
updateInvite(parseInvite(invite));
|
||||
};
|
||||
};
|
||||
if (match == false) {
|
||||
addItem(parseInvite(invite));
|
||||
};
|
||||
});
|
||||
var items = document.getElementById('invites').children;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var exists = false;
|
||||
data['invites'].forEach(function(invite) {
|
||||
if (items[i].id == invite['code']) {
|
||||
exists = true;
|
||||
}
|
||||
});
|
||||
if (exists == false) {
|
||||
removeInvite(items[i].id);
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send();
|
||||
} else if (empty === true) {
|
||||
document.getElementById('invites').textContent = '';
|
||||
addItem(parseInvite([], true));
|
||||
};
|
||||
};
|
||||
function deleteInvite(code) {
|
||||
var send = JSON.stringify({ "code": code });
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/deleteInvite", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
generateInvites();
|
||||
};
|
||||
};
|
||||
req.send(send);
|
||||
};
|
||||
function addOptions(le, sel) {
|
||||
for (v = 0; v <= le; v++) {
|
||||
var opt = document.createElement('option');
|
||||
opt.appendChild(document.createTextNode(v));
|
||||
opt.value = v;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
};
|
||||
function toClipboard(str) {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = str;
|
||||
el.setAttribute('readOnly', '');
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
document.body.appendChild(el);
|
||||
const selected =
|
||||
document.getSelection().rangeCount > 0
|
||||
? document.getSelection().getRangeAt(0)
|
||||
: false;
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
if (selected) {
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(selected);
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('inviteForm').onsubmit = function() {
|
||||
var button = document.getElementById('generateSubmit');
|
||||
button.disabled = true;
|
||||
button.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
send_object = serializeForm('inviteForm');
|
||||
console.log(send_object);
|
||||
if (document.getElementById('send_to_address') != null) {
|
||||
if (send_object['send_to_address_enabled']) {
|
||||
send_object['email'] = send_object['send_to_address'];
|
||||
delete send_object['send_to_address'];
|
||||
delete send_object['send_to_address_enabled'];
|
||||
}
|
||||
}
|
||||
var send = JSON.stringify(send_object);
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/generateInvite", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
button.textContent = 'Generate';
|
||||
button.disabled = false;
|
||||
generateInvites();
|
||||
};
|
||||
};
|
||||
req.send(send);
|
||||
return false;
|
||||
};
|
||||
document.getElementById('loginForm').onsubmit = function() {
|
||||
window.token = "";
|
||||
var details = serializeForm('loginForm');
|
||||
var errorArea = document.getElementById('loginErrorArea');
|
||||
errorArea.textContent = '';
|
||||
var button = document.getElementById('loginSubmit');
|
||||
button.disabled = true;
|
||||
button.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
var req = new XMLHttpRequest();
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 401) {
|
||||
button.disabled = false;
|
||||
button.textContent = 'Login';
|
||||
var wrongPassword = document.createElement('div');
|
||||
wrongPassword.classList.add('alert', 'alert-danger');
|
||||
wrongPassword.setAttribute('role', 'alert');
|
||||
wrongPassword.appendChild(document.createTextNode('Incorrect username or password.'));
|
||||
errorArea.appendChild(wrongPassword);
|
||||
} else {
|
||||
var data = this.response;
|
||||
window.token = data['token'];
|
||||
generateInvites();
|
||||
var interval = setInterval(function() { generateInvites(); }, 60 * 1000);
|
||||
var hour = document.getElementById('hours');
|
||||
addOptions(24, hour);
|
||||
hour.selected = "0";
|
||||
var minutes = document.getElementById('minutes');
|
||||
addOptions(59, minutes);
|
||||
minutes.selected = "30";
|
||||
loginModal.hide();
|
||||
};
|
||||
};
|
||||
};
|
||||
req.open("GET", "/getToken", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(details['username'] + ":" + details['password']));
|
||||
req.send();
|
||||
return false;
|
||||
};
|
||||
document.getElementById('openDefaultsWizard').onclick = function() {
|
||||
this.disabled = true
|
||||
this.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
var req = new XMLHttpRequest();
|
||||
req.responseType = 'json';
|
||||
req.open("GET", "/getUsers", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200) {
|
||||
var users = req.response['users'];
|
||||
var radioList = document.getElementById('defaultUserRadios');
|
||||
radioList.textContent = '';
|
||||
if (document.getElementById('setDefaultUser')) {
|
||||
document.getElementById('setDefaultUser').remove();
|
||||
};
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
var user = users[i]
|
||||
var radio = document.createElement('div');
|
||||
radio.classList.add('radio');
|
||||
if (i == 0) {
|
||||
var checked = 'checked';
|
||||
} else {
|
||||
var checked = '';
|
||||
};
|
||||
radio.innerHTML =
|
||||
'<label><input type="radio" name="defaultRadios" id="default_' +
|
||||
user['name'] + '" style="margin-right: 1rem;"' + checked + '>' +
|
||||
user['name'] + '</label>';
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
var button = document.getElementById('openDefaultsWizard');
|
||||
button.disabled = false;
|
||||
button.innerHTML = 'Set new account defaults';
|
||||
var submitButton = document.getElementById('storeDefaults');
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Submit';
|
||||
if (submitButton.classList.contains('btn-success')) {
|
||||
submitButton.classList.remove('btn-success');
|
||||
submitButton.classList.add('btn-primary');
|
||||
} else if (submitButton.classList.contains('btn-danger')) {
|
||||
submitButton.classList.remove('btn-danger');
|
||||
submitButton.classList.add('btn-primary');
|
||||
};
|
||||
settingsModal.hide();
|
||||
userDefaultsModal.show();
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send();
|
||||
};
|
||||
document.getElementById('storeDefaults').onclick = function () {
|
||||
this.disabled = true;
|
||||
this.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
var button = document.getElementById('storeDefaults');
|
||||
var radios = document.getElementsByName('defaultRadios');
|
||||
for (var i = 0; i < radios.length; i++) {
|
||||
if (radios[i].checked) {
|
||||
var data = {'username':radios[i].id.slice(8), 'homescreen':false};
|
||||
if (document.getElementById('storeDefaultHomescreen').checked) {
|
||||
data['homescreen'] = true;
|
||||
}
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/setDefaults", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
button.textContent = 'Success';
|
||||
if (button.classList.contains('btn-danger')) {
|
||||
button.classList.remove('btn-danger');
|
||||
} else if (button.classList.contains('btn-primary')) {
|
||||
button.classList.remove('btn-primary');
|
||||
};
|
||||
button.classList.add('btn-success');
|
||||
button.disabled = false;
|
||||
setTimeout(function(){$('#userDefaults').modal('hide');}, 1000);
|
||||
} else {
|
||||
button.textContent = 'Failed';
|
||||
button.classList.remove('btn-primary');
|
||||
button.classList.add('btn-danger');
|
||||
setTimeout(function(){
|
||||
var button = document.getElementById('storeDefaults');
|
||||
button.textContent = 'Submit';
|
||||
button.classList.remove('btn-danger');
|
||||
button.classList.add('btn-primary');
|
||||
button.disabled = false;
|
||||
}, 1000);
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send(JSON.stringify(data));
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// $.ajax('/setDefaults', {
|
||||
// data : JSON.stringify(data),
|
||||
// contentType : 'application/json',
|
||||
// type : 'POST',
|
||||
// xhrFields : {
|
||||
// withCredentials: true
|
||||
// },
|
||||
// beforeSend : function (xhr) {
|
||||
// xhr.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
// },
|
||||
// success: function() {
|
||||
// button.textContent = 'Success';
|
||||
// if (button.classList.contains('btn-danger')) {
|
||||
// button.classList.remove('btn-danger');
|
||||
// } else if (button.classList.contains('btn-primary')) {
|
||||
// button.classList.remove('btn-primary');
|
||||
// };
|
||||
// button.classList.add('btn-success');
|
||||
// button.disabled = false;
|
||||
// setTimeout(function(){$('#userDefaults').modal('hide');}, 1000);
|
||||
// },
|
||||
// error: function() {
|
||||
// button.textContent = 'Failed';
|
||||
// button.classList.remove('btn-primary');
|
||||
// button.classList.add('btn-danger');
|
||||
// setTimeout(function(){
|
||||
// var button = document.getElementById('storeDefaults');
|
||||
// button.textContent = 'Submit';
|
||||
// button.classList.remove('btn-danger');
|
||||
// button.classList.add('btn-primary');
|
||||
// button.disabled = false;
|
||||
// }, 1000);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
document.getElementById('openUsers').onclick = function () {
|
||||
this.disabled = true;
|
||||
this.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("GET", "/getUsers", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200) {
|
||||
var list = document.getElementById('userList');
|
||||
list.textContent = '';
|
||||
if (document.getElementById('saveUsers')) {
|
||||
document.getElementById('saveUsers').remove();
|
||||
};
|
||||
var users = req.response['users'];
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
var user = users[i]
|
||||
var entry = document.createElement('div');
|
||||
entry.classList.add('form-group', 'list-group-item', 'py-1');
|
||||
entry.id = 'user_' + user['name'];
|
||||
var label = document.createElement('label');
|
||||
label.classList.add('d-inline-block');
|
||||
label.setAttribute('for', 'address_' + user['email']);
|
||||
label.appendChild(document.createTextNode(user['name']));
|
||||
entry.appendChild(label);
|
||||
var address = document.createElement('input');
|
||||
address.setAttribute('type', 'email');
|
||||
address.readOnly = true;
|
||||
address.classList.add('form-control-plaintext', 'text-muted', 'd-inline-block');
|
||||
//address.setAttribute('style', 'margin-left: 2%; margin-right: 2%; color: grey;');
|
||||
address.classList.add('addressText');
|
||||
address.id = 'address_' + user['email'];
|
||||
if (typeof(user['email']) != 'undefined') {
|
||||
address.value = user['email'];
|
||||
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
||||
};
|
||||
var editButton = document.createElement('i');
|
||||
editButton.classList.add('fa', 'fa-edit', 'd-inline-block', 'icon-button');
|
||||
editButton.setAttribute('style', 'margin-left: 2%;');
|
||||
editButton.onclick = function() {
|
||||
this.classList.remove('fa', 'fa-edit');
|
||||
// var input = document.createElement('input');
|
||||
// input.setAttribute('type', 'email');
|
||||
// input.classList.add('email-input');
|
||||
//var addressElement = this.parentNode.getElementsByClassName('addressText')[0];
|
||||
var addressElement = this.parentNode.getElementsByClassName('form-control-plaintext')[0];
|
||||
addressElement.classList.remove('form-control-plaintext', 'text-muted');
|
||||
addressElement.classList.add('form-control');
|
||||
addressElement.readOnly = false;
|
||||
if (addressElement.value == '') {
|
||||
// input.value = addressElement.textContent;
|
||||
// } else {
|
||||
addressElement.placeholder = 'Email Address';
|
||||
address.setAttribute('style', 'width: auto; margin-left: 2%;');
|
||||
};
|
||||
// this.parentNode.replaceChild(input, addressElement);
|
||||
if (document.getElementById('saveUsers') == null) {
|
||||
var footer = document.getElementById('userFooter')
|
||||
var saveUsers = document.createElement('input');
|
||||
saveUsers.classList.add('btn', 'btn-primary');
|
||||
saveUsers.setAttribute('type', 'button');
|
||||
saveUsers.value = 'Save Changes';
|
||||
saveUsers.id = 'saveUsers';
|
||||
saveUsers.onclick = function() {
|
||||
var send = {}
|
||||
var entries = document.getElementById('userList').children;
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entry = entries[i];
|
||||
if (typeof(entry.getElementsByTagName('input')[0]) != 'undefined') {
|
||||
var name = entry.id.replace(/user_/g, '')
|
||||
var address = entry.getElementsByTagName('input')[0].value;
|
||||
send[name] = address
|
||||
};
|
||||
};
|
||||
send = JSON.stringify(send);
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyUsers", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
usersModal.hide();
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send(send);
|
||||
};
|
||||
footer.appendChild(saveUsers);
|
||||
};
|
||||
};
|
||||
entry.appendChild(editButton);
|
||||
entry.appendChild(address);
|
||||
list.appendChild(entry);
|
||||
};
|
||||
var button = document.getElementById('openUsers');
|
||||
button.disabled = false;
|
||||
button.innerHTML = 'Users <i class="fa fa-user"></i>';
|
||||
settingsModal.hide();
|
||||
usersModal.show();
|
||||
};
|
||||
}
|
||||
};
|
||||
req.send();
|
||||
};
|
||||
|
||||
generateInvites(empty = true);
|
||||
loginModal.show()
|
||||
|
||||
var config = {};
|
||||
var modifiedConfig = {};
|
||||
|
||||
document.getElementById('openSettings').onclick = function () {
|
||||
restart_setting_changed = false;
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("GET", "/getConfig", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var settingsList = document.getElementById('settingsList');
|
||||
settingsList.textContent = '';
|
||||
config = this.response;
|
||||
for (var section of Object.keys(config)) {
|
||||
var sectionCollapse = document.createElement('div');
|
||||
sectionCollapse.classList.add('collapse');
|
||||
sectionCollapse.id = section;
|
||||
|
||||
var sectionTitle = config[section]['meta']['name'];
|
||||
var sectionDescription = config[section]['meta']['description'];
|
||||
var entryListID = section + '_entryList';
|
||||
var sectionFooter = section + '_footer';
|
||||
|
||||
var innerCollapse = `
|
||||
<div class="card card-body">
|
||||
<small class="text-muted">${sectionDescription}</small>
|
||||
<div class="${entryListID}">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
sectionCollapse.innerHTML = innerCollapse;
|
||||
|
||||
for (var entry of Object.keys(config[section])) {
|
||||
if (entry != 'meta') {
|
||||
var entryName = config[section][entry]['name'];
|
||||
var required = false;
|
||||
if (config[section][entry]['required']) {
|
||||
entryName += ' <sup class="text-danger">*</sup>';
|
||||
required = true;
|
||||
};
|
||||
if (config[section][entry]['requires_restart']) {
|
||||
entryName += ' <sup class="text-danger">R</sup>';
|
||||
};
|
||||
if (config[section][entry].hasOwnProperty('description')) {
|
||||
var tooltip = `
|
||||
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
|
||||
`;
|
||||
entryName += ' ';
|
||||
entryName += tooltip;
|
||||
};
|
||||
var entryValue = config[section][entry]['value'];
|
||||
var entryType = config[section][entry]['type'];
|
||||
var entryGroup = document.createElement('div');
|
||||
if (entryType == 'bool') {
|
||||
entryGroup.classList.add('form-check');
|
||||
if (entryValue.toString() == 'true') {
|
||||
var checked = true;
|
||||
} else {
|
||||
var checked = false;
|
||||
};
|
||||
entryGroup.innerHTML = `
|
||||
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}">
|
||||
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
|
||||
`;
|
||||
entryGroup.getElementsByClassName('form-check-input')[0].required = required;
|
||||
entryGroup.getElementsByClassName('form-check-input')[0].checked = checked;
|
||||
entryGroup.getElementsByClassName('form-check-input')[0].onclick = function() {
|
||||
var state = this.checked;
|
||||
for (var sect of Object.keys(config)) {
|
||||
for (var ent of Object.keys(config[sect])) {
|
||||
if ((sect + '_' + config[sect][ent]['depends_true']) == this.id) {
|
||||
document.getElementById(sect + '_' + ent).disabled = !state;
|
||||
} else if ((sect + '_' + config[sect][ent]['depends_false']) == this.id) {
|
||||
document.getElementById(sect + '_' + ent).disabled = state;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
} else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) {
|
||||
entryGroup.classList.add('form-group');
|
||||
entryGroup.innerHTML = `
|
||||
<label for="${section}_${entry}">${entryName}</label>
|
||||
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}">
|
||||
`;
|
||||
entryGroup.getElementsByClassName('form-control')[0].required = required;
|
||||
} else if (entryType == 'select') {
|
||||
entryGroup.classList.add('form-group');
|
||||
var entryOptions = config[section][entry]['options'];
|
||||
var innerGroup = `
|
||||
<label for="${section}_${entry}">${entryName}</label>
|
||||
<select class="form-control" id="${section}_${entry}">
|
||||
`;
|
||||
for (var i = 0; i < entryOptions.length; i++) {
|
||||
if (entryOptions[i] == entryValue) {
|
||||
var selected = 'selected';
|
||||
} else {
|
||||
var selected = '';
|
||||
}
|
||||
innerGroup += `
|
||||
<option value="${entryOptions[i]}" ${selected}>${entryOptions[i]}</option>
|
||||
`;
|
||||
};
|
||||
innerGroup += '</select>';
|
||||
entryGroup.innerHTML = innerGroup;
|
||||
entryGroup.getElementsByClassName('form-control')[0].required = required;
|
||||
|
||||
};
|
||||
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
|
||||
};
|
||||
};
|
||||
var sectionButton = document.createElement('button');
|
||||
sectionButton.setAttribute('type', 'button');
|
||||
sectionButton.classList.add('list-group-item', 'list-group-item-action');
|
||||
sectionButton.appendChild(document.createTextNode(sectionTitle));
|
||||
sectionButton.id = section + '_button';
|
||||
sectionButton.setAttribute('data-toggle', 'collapse');
|
||||
sectionButton.setAttribute('data-target', '#' + section);
|
||||
settingsList.appendChild(sectionButton);
|
||||
settingsList.appendChild(sectionCollapse);
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send();
|
||||
settingsModal.show();
|
||||
};
|
||||
|
||||
triggerTooltips();
|
||||
//
|
||||
// $('#settingsMenu').on('shown.bs.modal', function() {
|
||||
// $("a[data-toggle='tooltip']").each(function (i, obj) {
|
||||
// $(obj).tooltip();
|
||||
// });
|
||||
// });
|
||||
//
|
||||
function sendConfig(modalId) {
|
||||
var modal = document.getElementById(modalId);
|
||||
var send = JSON.stringify(modifiedConfig);
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyConfig", true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
createModal(modalId, true).hide();
|
||||
if (modalId != 'settingsMenu') {
|
||||
settingsModal.hide();
|
||||
};
|
||||
};
|
||||
};
|
||||
// fail: function(xhr, textStatus, errorThrown) {
|
||||
// var footer = modal.getElementsByClassName('modal-dialog')[0].getElementsByClassName('modal-content')[0].getElementsByClassName('modal-footer')[0];
|
||||
// var alert = document.createElement('div');
|
||||
// alert.classList.add('alert', 'alert-danger');
|
||||
// alert.setAttribute('role', 'alert');
|
||||
// alert.appendChild(document.createTextNode('Error: ' + errorThrown));
|
||||
// footer.appendChild(alert);
|
||||
// },
|
||||
};
|
||||
req.send(send);
|
||||
};
|
||||
|
||||
document.getElementById('settingsSave').onclick = function() {
|
||||
modifiedConfig = {};
|
||||
var restart_setting_changed = false;
|
||||
var settings_changed = false;
|
||||
|
||||
for (var section of Object.keys(config)) {
|
||||
for (var entry of Object.keys(config[section])) {
|
||||
if (entry != 'meta') {
|
||||
var entryID = section + '_' + entry;
|
||||
var el = document.getElementById(entryID);
|
||||
if (el.type == 'checkbox') {
|
||||
var value = el.checked.toString();
|
||||
} else {
|
||||
var value = el.value.toString();
|
||||
};
|
||||
if (value != config[section][entry]['value'].toString()) {
|
||||
if (!modifiedConfig.hasOwnProperty(section)) {
|
||||
modifiedConfig[section] = {};
|
||||
};
|
||||
modifiedConfig[section][entry] = value;
|
||||
settings_changed = true;
|
||||
if (config[section][entry]['requires_restart']) {
|
||||
restart_setting_changed = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
// if (restart_setting_changed) {
|
||||
if (restart_setting_changed) {
|
||||
document.getElementById('applyRestarts').onclick = function(){sendConfig('restartModal');};
|
||||
settingsModal.hide();
|
||||
restartModal.show();
|
||||
} else if (settings_changed) {
|
||||
sendConfig('settingsMenu');
|
||||
} else {
|
||||
settingsModal.hide();
|
||||
};
|
||||
};
|
@ -12,12 +12,18 @@
|
||||
<meta name="msapplication-TileColor" content="#603cba">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
{% if bs5 %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||
{% else %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<style>
|
||||
.pageContainer {
|
||||
margin: 5% 20% 5% 20%;
|
||||
@ -61,11 +67,11 @@
|
||||
<p class="contactBox">{{ contactMessage }}</p>
|
||||
<div class="container" id="container">
|
||||
<div class="row" id="cardContainer">
|
||||
<div class="col-sm" id="accountForm">
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="col-sm">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Details</div>
|
||||
<div class="card-body">
|
||||
<form action="#" method="POST">
|
||||
<form action="#" method="POST" id="accountForm">
|
||||
<div class="form-group">
|
||||
<label for="inputEmail">Email</label>
|
||||
<input type="email" class="form-control" id="{% if username %}inputEmail{% else %}inputUsername{% endif %}" name="{% if username %}email{% else %}username{% endif %}" placeholder="Email" value="{{ email }}" required>
|
||||
@ -80,7 +86,7 @@
|
||||
<label for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="Password" required>
|
||||
</div>
|
||||
<div class="btn-group" role="group" aria-label="Button & Error" id="errorBox">
|
||||
<div class="btn-group" role="group" aria-label="Button & Error" id="errorBox" style="margin-top: 1rem;">
|
||||
<button type="submit" class="btn btn-outline-primary" id="submitButton">
|
||||
<span id="createAccount">Create Account</span>
|
||||
</button>
|
||||
@ -91,7 +97,7 @@
|
||||
</div>
|
||||
{% if validate %}
|
||||
<div class="col-sm" id="requirementBox">
|
||||
<div class="card bg-light mb-3 requirementBox">
|
||||
<div class="card mb-3 requirementBox">
|
||||
<div class="card-header">Password Requirements</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group">
|
||||
@ -108,7 +114,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="serialize.js"></script>
|
||||
<script>
|
||||
{% if bs5 %}
|
||||
var bsVersion = 5;
|
||||
{% else %}
|
||||
var bsVersion = 4;
|
||||
{% endif %}
|
||||
if (bsVersion == 5) {
|
||||
var successBox = new bootstrap.Modal(document.getElementById('successBox'));
|
||||
} else if (bsVersion == 4) {
|
||||
var successBox = {
|
||||
show : function() {
|
||||
return $('#successBox').modal('show');
|
||||
},
|
||||
hide : function() {
|
||||
return $('#successBox').modal('hide');
|
||||
}
|
||||
};
|
||||
};
|
||||
var code = window.location.href.split('/').pop();
|
||||
function toggleSpinner () {
|
||||
var submitButton = document.getElementById('submitButton');
|
||||
@ -131,25 +155,24 @@
|
||||
}
|
||||
submitButton.replaceChild(newSpan, oldSpan);
|
||||
};
|
||||
$("form").submit(function() {
|
||||
document.getElementById('accountForm').onsubmit = function() {
|
||||
toggleSpinner();
|
||||
var send = $("form").serializeObject();
|
||||
var send = serializeForm('accountForm');
|
||||
send['code'] = code;
|
||||
{% if not username %}
|
||||
send['email'] = send['username'];
|
||||
{% endif %}
|
||||
send = JSON.stringify(send);
|
||||
$.ajax('/newUser', {
|
||||
data : send,
|
||||
contentType : 'application/json',
|
||||
type : 'POST',
|
||||
crossDomain : true,
|
||||
complete : function(response){
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/newUser", true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
toggleSpinner();
|
||||
var data = response['responseJSON'];
|
||||
var data = this.response;
|
||||
if ('error' in data) {
|
||||
var text = document.createTextNode(data['error']);
|
||||
// <div class="alert alert-danger" id="errorBox"></div>
|
||||
var error = document.createElement('button');
|
||||
error.classList.add('btn', 'btn-outline-danger');
|
||||
error.setAttribute('disabled', '');
|
||||
@ -177,13 +200,14 @@
|
||||
};
|
||||
};
|
||||
if (valid == true) {
|
||||
$('#successBox').modal('show');
|
||||
successBox.show();
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send(send);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,25 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Invalid Code</title>
|
||||
<link rel="stylesheet" href="{{ css_href }}" integrity="{{ css_integrity }}" crossorigin="{{ css_crossorigin }}">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
|
||||
<style>
|
||||
.messageBox {
|
||||
margin: 20%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="messageBox">
|
||||
<h1>Invalid Code.</h1>
|
||||
<p>The above code is either incorrect, or has expired.</p>
|
||||
<p>{{ contactMessage }}</p>
|
||||
</div>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Invalid Code</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
{% if bs5 %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js" integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/" crossorigin="anonymous"></script>
|
||||
{% else %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<style>
|
||||
.messageBox {
|
||||
margin: 20%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="messageBox">
|
||||
<h1>Invalid Code.</h1>
|
||||
<p>The above code is either incorrect, or has expired.</p>
|
||||
<p>{{ contactMessage }}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
from flask import Flask, send_from_directory, render_template
|
||||
|
||||
from jellyfin_accounts import app, g, css, data_store
|
||||
from jellyfin_accounts import app, g, css_file, data_store
|
||||
from jellyfin_accounts import web_log as log
|
||||
from jellyfin_accounts.web_api import config, checkInvite, validator
|
||||
|
||||
@ -11,9 +11,8 @@ def page_not_found(e):
|
||||
return (
|
||||
render_template(
|
||||
"404.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
css_file=css_file,
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
),
|
||||
404,
|
||||
@ -25,9 +24,8 @@ def admin():
|
||||
# return app.send_static_file('admin.html')
|
||||
return render_template(
|
||||
"admin.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
css_file=css_file,
|
||||
contactMessage="",
|
||||
email_enabled=config.getboolean("invite_emails", "enabled"),
|
||||
)
|
||||
@ -36,13 +34,18 @@ def admin():
|
||||
@app.route("/<path:path>")
|
||||
def static_proxy(path):
|
||||
if "html" not in path:
|
||||
if "admin.js" in path:
|
||||
if config.getboolean("ui", "bs5"):
|
||||
bsVersion = 5
|
||||
else:
|
||||
bsVersion = 4
|
||||
return render_template("admin.js", bsVersion=bsVersion)
|
||||
return app.send_static_file(path)
|
||||
return (
|
||||
render_template(
|
||||
"404.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
css_file=css_file,
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
),
|
||||
404,
|
||||
@ -59,9 +62,8 @@ def inviteProxy(path):
|
||||
email = ""
|
||||
return render_template(
|
||||
"form.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
css_file=css_file,
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
helpMessage=config["ui"]["help_message"],
|
||||
successMessage=config["ui"]["success_message"],
|
||||
@ -77,8 +79,7 @@ def inviteProxy(path):
|
||||
log.debug("Attempted use of invalid invite")
|
||||
return render_template(
|
||||
"invalidCode.html",
|
||||
css_href=css["href"],
|
||||
css_integrity=css["integrity"],
|
||||
css_crossorigin=css["crossorigin"],
|
||||
bs5=config.getboolean("ui", "bs5"),
|
||||
css_file=css_file,
|
||||
contactMessage=config["ui"]["contact_message"],
|
||||
)
|
||||
|
@ -338,13 +338,9 @@ def modifyConfig():
|
||||
for section in data:
|
||||
if section in temp_config:
|
||||
for item in data[section]:
|
||||
if item in temp_config[section]:
|
||||
temp_config[section][item] = data[section][item]
|
||||
data[section][item] = True
|
||||
log.debug(f"{section}/{item} modified")
|
||||
else:
|
||||
data[section][item] = False
|
||||
log.debug(f"{section}/{item} does not exist in config")
|
||||
temp_config[section][item] = data[section][item]
|
||||
data[section][item] = True
|
||||
log.debug(f"{section}/{item} modified")
|
||||
with open(config_path, "w") as config_file:
|
||||
temp_config.write(config_file)
|
||||
config = load_config(config_path, data_dir)
|
||||
|
8
jf-accounts.service
Normal file
8
jf-accounts.service
Normal file
@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=A basic account management system for Jellyfin.
|
||||
|
||||
[Service]
|
||||
ExecStart=/home/hrfee/.cache/pypoetry/virtualenvs/jellyfin-accounts-r2jcKHws-py3.8/bin/jf-accounts
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
2569
package-lock.json
generated
Normal file
2569
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "jellyfin-accounts"
|
||||
version = "0.2.6"
|
||||
version = "0.3.0"
|
||||
readme = "README.md"
|
||||
description = "A simple account management system for Jellyfin"
|
||||
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
|
||||
@ -9,7 +9,7 @@ homepage = "https://github.com/hrfee/jellyfin-accounts"
|
||||
repository = "https://github.com/hrfee/jellyfin-accounts"
|
||||
keywords = ["jellyfin", "jf-accounts"]
|
||||
include = ["jellyfin_accounts/data/*"]
|
||||
exclude = ["images/*"]
|
||||
exclude = ["images/*", "scss/*"]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
|
16
scss/README.md
Normal file
16
scss/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
## SCSS
|
||||
|
||||
* `bs<4/5>-jf.scss` contains the source for the customizations to bootstrap. To customize the UI, you can make modifications to this file and then compile it.
|
||||
|
||||
**Note**: For BS5, it is assumed that bootstrap is installed in `../../node_modules/bootstrap` relative to itself.
|
||||
For BS4, it assumes that bootstrap is installed in `../../node_modules/bootstrap4` relative to itself (`npm install bootstrap4@npm:bootstrap`).
|
||||
* Compilation requires a sass compiler of your choice, and `postcss-cli`, `autoprefixer` + `clean-css-cli` from npm.
|
||||
* If you're using `sassc`, run `./compile.sh bs<4/5>-jf.scss` in this directory. This will create a .css file, and minified .css file.
|
||||
* For `node-sass`, replace the `sassc` line in `compile.sh` with
|
||||
```
|
||||
node-sass --output-style expanded --precision 6 $1 $css_file
|
||||
```
|
||||
and run as above.
|
||||
* If you're building from source, copy the minified css to `<jf-accounts git directory>/jellyfin_accounts/data/static/bs<4/5>-jf.css`.
|
||||
* If you're just customizing your install, set `custom_css` in your config as the path to your minified css and change the `theme` option to `Custom CSS`.
|
||||
|
9548
scss/bs4/bs4-jf.css
Normal file
9548
scss/bs4/bs4-jf.css
Normal file
File diff suppressed because one or more lines are too long
7
scss/bs4/bs4-jf.min.css
vendored
Normal file
7
scss/bs4/bs4-jf.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
scss/bs4/bs4-jf.min.css.map
Normal file
1
scss/bs4/bs4-jf.min.css.map
Normal file
File diff suppressed because one or more lines are too long
136
scss/bs4/bs4-jf.scss
Normal file
136
scss/bs4/bs4-jf.scss
Normal file
@ -0,0 +1,136 @@
|
||||
$jf-blue: rgb(0, 164, 220);
|
||||
$jf-blue-hover: rgba(0, 164, 220, 0.2);
|
||||
$jf-blue-focus: rgb(12, 176, 232);
|
||||
$jf-blue-light: #4bb3dd;
|
||||
|
||||
$jf-red: rgb(204, 0, 0);
|
||||
$jf-red-light: #e12026;
|
||||
$jf-yellower: #ffc107;
|
||||
$jf-yellow: #e1b222;
|
||||
$jf-orange: #ff870f;
|
||||
$jf-green: #6fbd45;
|
||||
$jf-green-dark: #008040;
|
||||
|
||||
|
||||
$jf-black: #101010; // 16 16 16
|
||||
$jf-gray-90: #202020; // 32 32 32
|
||||
$jf-gray-80: #242424; // jf-card 36 36 36
|
||||
$jf-gray-70: #292929; // jf-input 41 41 41
|
||||
$jf-gray-60: #303030; // jf-button 48 48 48
|
||||
$jf-gray-50: #383838; // jf-button-focus 56 56 56
|
||||
$jf-text-bold: rgba(255, 255, 255, 0.87);
|
||||
$jf-text-primary: rgba(255, 255, 255, 0.8);
|
||||
$jf-text-secondary: rgb(153, 153, 153);
|
||||
|
||||
$primary: $jf-blue;
|
||||
$secondary: $jf-gray-50;
|
||||
$success: $jf-green-dark;
|
||||
$danger: $jf-red-light;
|
||||
$light: $jf-text-primary;
|
||||
$dark: $jf-gray-90;
|
||||
$info: $jf-yellow;
|
||||
$warning: $jf-yellower;
|
||||
|
||||
|
||||
|
||||
$enable-gradients: false;
|
||||
$enable-shadows: false;
|
||||
|
||||
$enable-rounded: false;
|
||||
$body-bg: $jf-black;
|
||||
$body-color: $jf-text-primary;
|
||||
$border-color: $jf-gray-60;
|
||||
$component-active-color: $jf-text-bold;
|
||||
$component-active-bg: $jf-blue-focus;
|
||||
$text-muted: $jf-text-secondary;
|
||||
$link-color: $jf-blue-focus;
|
||||
$btn-link-disabled-color: $jf-text-secondary;
|
||||
$input-bg: $jf-gray-90;
|
||||
$input-color: $jf-text-primary;
|
||||
$input-focus-bg: $jf-gray-60;
|
||||
$input-focus-border-color: $jf-blue-focus;
|
||||
$input-disabled-bg: $jf-gray-70;
|
||||
input:disabled {
|
||||
color: $text-muted;
|
||||
}
|
||||
$input-border-color: $jf-gray-60;
|
||||
$input-placeholder-color: $text-muted;
|
||||
|
||||
$form-check-input-bg: $jf-gray-60;
|
||||
$form-check-input-border: $jf-gray-50;
|
||||
$form-check-input-checked-color: $jf-blue-focus;
|
||||
$form-check-input-checked-bg-color: $jf-blue-hover;
|
||||
|
||||
$input-group-addon-bg: $input-bg;
|
||||
|
||||
$form-select-disabled-color: $jf-text-secondary;
|
||||
$form-select-disabled-bg: $input-disabled-bg;
|
||||
$form-select-indicator-color: $jf-gray-50;
|
||||
|
||||
$card-bg: $jf-gray-80;
|
||||
$card-border-color: null;
|
||||
|
||||
$tooltip-color: $jf-text-bold;
|
||||
$tooltip-bg: $jf-gray-50;
|
||||
|
||||
$modal-content-bg: $jf-gray-80;
|
||||
$modal-content-border-color: $jf-gray-50;
|
||||
$modal-header-border-color: null;
|
||||
$modal-footer-border-color: null;
|
||||
|
||||
$list-group-bg: $card-bg;
|
||||
$list-group-border-color: $jf-gray-50;
|
||||
$list-group-hover-bg: $jf-blue-hover;
|
||||
$list-group-active-bg: $jf-blue-focus;
|
||||
$list-group-action-color: $jf-text-primary;
|
||||
$list-group-action-hover-color: $jf-text-bold;
|
||||
$list-group-action-active-color: $jf-text-bold;
|
||||
$list-group-action-active-bg: $jf-blue-focus;
|
||||
|
||||
// idk why but i had to put these above and below the import
|
||||
.list-group-item-danger {
|
||||
color: $jf-text-bold;
|
||||
background-color: $danger;
|
||||
}
|
||||
|
||||
.list-group-item-success {
|
||||
color: $jf-text-bold;
|
||||
background-color: $success;
|
||||
}
|
||||
|
||||
@import "../../node_modules/bootstrap4/scss/bootstrap";
|
||||
|
||||
.btn-primary, .btn-outline-primary:hover, .btn-outline-primary:active {
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: $jf-text-secondary;
|
||||
}
|
||||
|
||||
.close:hover, .close:active {
|
||||
color: $jf-text-primary;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
.text-bright {
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
.list-group-item-danger {
|
||||
color: $jf-text-bold;
|
||||
background-color: $danger;
|
||||
}
|
||||
|
||||
.list-group-item-success {
|
||||
color: $jf-text-bold;
|
||||
background-color: $success;
|
||||
}
|
||||
|
10
scss/bs4/compile.sh
Executable file
10
scss/bs4/compile.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
css_file=$(echo $1 | sed 's/scss/css/g')
|
||||
min_file=$(echo $1 | sed 's/scss/min.css/g')
|
||||
sassc -t expanded -p 6 $1 $css_file
|
||||
echo "Compiled."
|
||||
postcss $css_file --replace --use autoprefixer
|
||||
echo "Prefixed."
|
||||
echo "Written to $css_file."
|
||||
cleancss --level 1 --format breakWith=lf --source-map --source-map-inline-sources --output $min_file $css_file
|
||||
echo "Minified version written to $min_file."
|
9438
scss/bs5/bs5-jf.css
Normal file
9438
scss/bs5/bs5-jf.css
Normal file
File diff suppressed because one or more lines are too long
7
scss/bs5/bs5-jf.min.css
vendored
Normal file
7
scss/bs5/bs5-jf.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
scss/bs5/bs5-jf.min.css.map
Normal file
1
scss/bs5/bs5-jf.min.css.map
Normal file
File diff suppressed because one or more lines are too long
136
scss/bs5/bs5-jf.scss
Normal file
136
scss/bs5/bs5-jf.scss
Normal file
@ -0,0 +1,136 @@
|
||||
$jf-blue: rgb(0, 164, 220);
|
||||
$jf-blue-hover: rgba(0, 164, 220, 0.2);
|
||||
$jf-blue-focus: rgb(12, 176, 232);
|
||||
$jf-blue-light: #4bb3dd;
|
||||
|
||||
$jf-red: rgb(204, 0, 0);
|
||||
$jf-red-light: #e12026;
|
||||
$jf-yellower: #ffc107;
|
||||
$jf-yellow: #e1b222;
|
||||
$jf-orange: #ff870f;
|
||||
$jf-green: #6fbd45;
|
||||
$jf-green-dark: #008040;
|
||||
|
||||
|
||||
$jf-black: #101010; // 16 16 16
|
||||
$jf-gray-90: #202020; // 32 32 32
|
||||
$jf-gray-80: #242424; // jf-card 36 36 36
|
||||
$jf-gray-70: #292929; // jf-input 41 41 41
|
||||
$jf-gray-60: #303030; // jf-button 48 48 48
|
||||
$jf-gray-50: #383838; // jf-button-focus 56 56 56
|
||||
$jf-text-bold: rgba(255, 255, 255, 0.87);
|
||||
$jf-text-primary: rgba(255, 255, 255, 0.8);
|
||||
$jf-text-secondary: rgb(153, 153, 153);
|
||||
|
||||
$primary: $jf-blue;
|
||||
$secondary: $jf-gray-50;
|
||||
$success: $jf-green-dark;
|
||||
$danger: $jf-red-light;
|
||||
$light: $jf-text-primary;
|
||||
$dark: $jf-gray-90;
|
||||
$info: $jf-yellow;
|
||||
$warning: $jf-yellower;
|
||||
|
||||
|
||||
|
||||
$enable-gradients: false;
|
||||
$enable-shadows: false;
|
||||
|
||||
$enable-rounded: false;
|
||||
$body-bg: $jf-black;
|
||||
$body-color: $jf-text-primary;
|
||||
$border-color: $jf-gray-60;
|
||||
$component-active-color: $jf-text-bold;
|
||||
$component-active-bg: $jf-blue-focus;
|
||||
$text-muted: $jf-text-secondary;
|
||||
$link-color: $jf-blue-focus;
|
||||
$btn-link-disabled-color: $jf-text-secondary;
|
||||
$input-bg: $jf-gray-90;
|
||||
$input-color: $jf-text-primary;
|
||||
$input-focus-bg: $jf-gray-60;
|
||||
$input-focus-border-color: $jf-blue-focus;
|
||||
$input-disabled-bg: $jf-gray-70;
|
||||
input:disabled {
|
||||
color: $text-muted;
|
||||
}
|
||||
$input-border-color: $jf-gray-60;
|
||||
$input-placeholder-color: $text-muted;
|
||||
|
||||
$form-check-input-bg: $jf-gray-60;
|
||||
$form-check-input-border: $jf-gray-50;
|
||||
$form-check-input-checked-color: $jf-blue-focus;
|
||||
$form-check-input-checked-bg-color: $jf-blue-hover;
|
||||
|
||||
$input-group-addon-bg: $input-bg;
|
||||
|
||||
$form-select-disabled-color: $jf-text-secondary;
|
||||
$form-select-disabled-bg: $input-disabled-bg;
|
||||
$form-select-indicator-color: $jf-gray-50;
|
||||
|
||||
$card-bg: $jf-gray-80;
|
||||
$card-border-color: null;
|
||||
|
||||
$tooltip-color: $jf-text-bold;
|
||||
$tooltip-bg: $jf-gray-50;
|
||||
|
||||
$modal-content-bg: $jf-gray-80;
|
||||
$modal-content-border-color: $jf-gray-50;
|
||||
$modal-header-border-color: null;
|
||||
$modal-footer-border-color: null;
|
||||
|
||||
$list-group-bg: $card-bg;
|
||||
$list-group-border-color: $jf-gray-50;
|
||||
$list-group-hover-bg: $jf-blue-hover;
|
||||
$list-group-active-bg: $jf-blue-focus;
|
||||
$list-group-action-color: $jf-text-primary;
|
||||
$list-group-action-hover-color: $jf-text-bold;
|
||||
$list-group-action-active-color: $jf-text-bold;
|
||||
$list-group-action-active-bg: $jf-blue-focus;
|
||||
|
||||
// idk why but i had to put these above and below the import
|
||||
.list-group-item-danger {
|
||||
color: $jf-text-bold;
|
||||
background-color: $danger;
|
||||
}
|
||||
|
||||
.list-group-item-success {
|
||||
color: $jf-text-bold;
|
||||
background-color: $success;
|
||||
}
|
||||
|
||||
@import "../../node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
.btn-primary, .btn-outline-primary:hover, .btn-outline-primary:active {
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: $jf-text-secondary;
|
||||
}
|
||||
|
||||
.close:hover, .close:active {
|
||||
color: $jf-text-primary;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
.text-bright {
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
.list-group-item-danger {
|
||||
color: $jf-text-bold;
|
||||
background-color: $danger;
|
||||
}
|
||||
|
||||
.list-group-item-success {
|
||||
color: $jf-text-bold;
|
||||
background-color: $success;
|
||||
}
|
||||
|
10
scss/bs5/compile.sh
Executable file
10
scss/bs5/compile.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
css_file=$(echo $1 | sed 's/scss/css/g')
|
||||
min_file=$(echo $1 | sed 's/scss/min.css/g')
|
||||
sassc -t expanded -p 6 $1 $css_file
|
||||
echo "Compiled."
|
||||
postcss $css_file --replace --use autoprefixer
|
||||
echo "Prefixed."
|
||||
echo "Written to $css_file."
|
||||
cleancss --level 1 --format breakWith=lf --source-map --source-map-inline-sources --output $min_file $css_file
|
||||
echo "Minified version written to $min_file."
|
Loading…
Reference in New Issue
Block a user