1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-06-18 07:27:49 +02:00

Rework typescript to use modules

web UI now uses modules, and relies less on bodge to make things work.
Also fixes an issue where invites where "failed to send to xx" appeared
in invite form.
This commit is contained in:
Harvey Tindall 2020-10-22 17:50:40 +01:00
parent 2d6b1717db
commit 301f502052
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
29 changed files with 988 additions and 846 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ data/static/*.css
data/static/*.js
data/static/*.js.map
data/static/ts/
data/static/modules/
!data/static/setup.js
data/config-base.json
data/config-default.ini

View File

@ -18,12 +18,15 @@ email:
typescript:
$(info Compiling typescript)
npx esbuild ts/* --outdir=data/static --minify
npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify
-rm -r data/static/ts
-rm -r data/static/typings
-rm data/static/*.map
ts-debug:
-npx tsc -p ts/ --sourceMap
-rm -r data/static/ts
-rm -r data/static/typings
cp -r ts data/static/
swagger:
@ -51,3 +54,4 @@ install:
cp -r build $(DESTDIR)/jfa-go
all: configuration sass email version typescript swagger compile copy
debug: configuration sass email version ts-debug swagger compile copy

4
api.go
View File

@ -626,12 +626,12 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
// @tags Invites
func (app *appContext) GetInvites(gc *gin.Context) {
app.debug.Println("Invites requested")
current_time := time.Now()
currentTime := time.Now()
app.storage.loadInvites()
app.checkInvites()
var invites []inviteDTO
for code, inv := range app.storage.invites {
_, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time)
_, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
invite := inviteDTO{
Code: code,
Days: days,

View File

@ -31,11 +31,11 @@
return "";
}
{{ if .bs5 }}
var bsVersion = 5;
window.bsVersion = 5;
{{ else }}
var bsVersion = 4;
window.bsVersion = 4;
{{ end }}
var cssFile = "{{ .cssFile }}";
window.cssFile = "{{ .cssFile }}";
var css = document.createElement('link');
css.setAttribute('rel', 'stylesheet');
css.setAttribute('type', 'text/css');
@ -465,27 +465,19 @@
<p>{{ .contactMessage }}</p>
</div>
</div>
<script src="common.js"></script>
<script>
var availableProfiles = [];
<script>
window.bs5 = {{ .bs5 }};
window.availableProfiles = [];
{{ if .notifications }}
var notifications_enabled = true;
window.notifications_enabled = true;
{{ else }}
var notifications_enabled = false;
window.notifications_enabled = false;
{{ end }}
</script>
{{ if .bs5 }}
<script src="bs5.js"></script>
{{ else }}
<script src="bs4.js"></script>
{{ end }}
<script src="animation.js"></script>
<script src="accounts.js"></script>
<script src="invites.js"></script>
<script src="admin.js"></script>
<script src="settings.js"></script>
<script src="admin.js" type="module"></script>
<script src="invites.js" type="module"></script>
{{ if .ombiEnabled }}
<script src="ombi.js"></script>
<script src="ombi.js" type="module"></script>
{{ end }}
</body>
</html>

View File

@ -0,0 +1,7 @@
{{ define "form-base" }}
<script>
window.bs5 = {{ .bs5 }};
window.usernameEnabled = {{ .username }};
</script>
<script src="form.js" type="module"></script>
{{ end }}

View File

@ -0,0 +1 @@
{{ template "form.html" . }}

View File

@ -14,11 +14,11 @@
<!-- Bootstrap CSS -->
<link rel="stylesheet" type="text/css" href="{{ .cssFile }}">
{{ if not .bs5 }}
{{ if not .settings.bs5 }}
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
{{ end }}
<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 }}
{{ if .settings.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>
@ -74,9 +74,9 @@
<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{{ end }}" name="{{ if .username }}email{{ else }}username{{ end }}" placeholder="Email" value="{{ .email }}" required>
<input type="email" class="form-control" id="{{ if .settings.username }}inputEmail{{ else }}inputUsername{{ end }}" name="{{ if .settings.username }}email{{ else }}username{{ end }}" placeholder="Email" value="{{ .email }}" required>
</div>
{{ if .username }}
{{ if .settings.username }}
<div class="form-group">
<label for="inputUsername">Username</label>
<input type="username" class="form-control" id="inputUsername" name="username" placeholder="Username" required>
@ -114,10 +114,8 @@
</div>
</div>
</div>
<script src="common.js"></script>
<script>
var usernameEnabled = {{ .username }}
var validationStrings = {
<script>
window.validationStrings = {
"length": {
"singular": "Must have at least {n} character",
"plural": "Must have a least {n} characters"
@ -138,8 +136,9 @@
"singular": "Must have at least {n} special character",
"plural": "Must have at least {n} special characters"
}
}
};
</script>
<script src="form.js"></script>
{{ template "form-base" .settings }}
</body>
</html>
</html>

6
package-lock.json generated
View File

@ -50,9 +50,9 @@
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"@types/jquery": {
"version": "3.5.1",
"resolved": "https://registry.npm.taobao.org/@types/jquery/download/@types/jquery-3.5.1.tgz",
"integrity": "sha1-zrsFes9QccQOQ58w6EDFejDUBsM=",
"version": "3.5.3",
"resolved": "https://registry.npm.taobao.org/@types/jquery/download/@types/jquery-3.5.3.tgz?cache=0&sync_timestamp=1602524936372&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fjquery%2Fdownload%2F%40types%2Fjquery-3.5.3.tgz",
"integrity": "sha1-rcxkfkxnW9nrrn+5gOnKddWO6Mc=",
"requires": {
"@types/sizzle": "*"
}

View File

@ -17,7 +17,7 @@
},
"homepage": "https://github.com/hrfee/jellyfin-accounts#readme",
"dependencies": {
"@types/jquery": "^3.5.1",
"@types/jquery": "^3.5.3",
"autoprefixer": "^9.8.5",
"bootstrap": "^5.0.0-alpha1",
"bootstrap4": "npm:bootstrap@^4.5.0",

View File

@ -38,7 +38,7 @@ func (vd *Validator) validate(password string) map[string]bool {
} else if unicode.IsLower(c) {
count["lowercase"] += 1
} else if unicode.IsNumber(c) {
count["numbers"] += 1
count["number"] += 1
} else {
for _, s := range vd.specialChars {
if c == s {

View File

@ -1,25 +1,17 @@
const checkCheckboxes = (): void => {
const defaultsButton = document.getElementById('accountsTabSetDefaults');
const deleteButton = document.getElementById('accountsTabDelete');
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
let checked = checkboxes.length;
if (checked == 0) {
Unfocus(defaultsButton);
Unfocus(deleteButton);
} else {
Focus(defaultsButton);
Focus(deleteButton);
if (checked == 1) {
deleteButton.textContent = 'Delete User';
} else {
deleteButton.textContent = 'Delete Users';
}
}
import { checkCheckboxes, populateUsers, populateRadios } from "./modules/accounts.js";
import { _post, _get, _delete, rmAttr, addAttr } from "./modules/common.js";
import { populateProfiles } from "./modules/settings.js";
import { Focus, Unfocus, createEl, storeDefaults } from "./modules/admin.js";
interface aWindow extends Window {
changeEmail(icon: HTMLElement, id: string): void;
}
declare var window: aWindow;
const validateEmail = (email: string): boolean => /\S+@\S+\.\S+/.test(email);
function changeEmail(icon: HTMLElement, id: string): void {
window.changeEmail = (icon: HTMLElement, id: string): void => {
const iconContent = icon.outerHTML;
icon.setAttribute('class', '');
const entry = icon.nextElementSibling as HTMLInputElement;
@ -79,83 +71,7 @@ function changeEmail(icon: HTMLElement, id: string): void {
icon.parentNode.appendChild(cross);
};
var jfUsers: Array<Object>;
function populateUsers(): void {
const acList = document.getElementById('accountsList');
acList.innerHTML = `
<div class="d-flex align-items-center">
<strong>Getting Users...</strong>
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
</div>
`;
Unfocus(acList.parentNode.querySelector('thead'));
const accountsList = document.createElement('tbody');
accountsList.id = 'accountsList';
const generateEmail = (id: string, name: string, email: string): string => {
let entry: HTMLDivElement = document.createElement('div');
entry.id = 'email_' + id;
let emailValue: string = email;
if (emailValue == undefined) {
emailValue = "";
}
entry.innerHTML = `
<i class="fa fa-edit d-inline-block icon-button" style="margin-right: 2%;" onclick="changeEmail(this, '${id}')"></i>
<input type="email" class="form-control-plaintext form-control-sm text-muted d-inline-block addressText" id="address_${id}" style="width: auto;" value="${emailValue}" readonly>
`;
return entry.outerHTML;
};
const template = (id: string, username: string, email: string, lastActive: string, admin: boolean): string => {
let isAdmin = "No";
if (admin) {
isAdmin = "Yes";
}
let fci = "form-check-input";
if (bsVersion != 5) {
fci = "";
}
return `
<td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
<td nowrap="nowrap" class="align-middle">${username}</td>
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
<td nowrap="nowrap" class="align-middle">${isAdmin}</td>
`;
};
_get("/users", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
jfUsers = this.response['users'];
for (const user of jfUsers) {
let tr = document.createElement('tr');
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
accountsList.appendChild(tr);
}
Focus(acList.parentNode.querySelector('thead'));
acList.replaceWith(accountsList);
}
});
}
function populateRadios(): void {
const radioList = document.getElementById('defaultUserRadios');
radioList.textContent = '';
let first = true;
for (const i in jfUsers) {
const user = jfUsers[i];
const radio = document.createElement('div');
radio.classList.add('form-check');
let checked = '';
if (first) {
checked = 'checked';
first = false;
}
radio.innerHTML = `
<input class="form-check-input" type="radio" name="defaultRadios" id="default_${user['id']}" ${checked}>
<label class="form-check-label" for="default_${user['id']}">${user['name']}</label>`;
radioList.appendChild(radio);
}
}
console.log("bruh");
(<HTMLInputElement>document.getElementById('selectAll')).onclick = function (): void {
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
@ -217,18 +133,18 @@ function populateRadios(): void {
}
setTimeout((): void => {
Unfocus(deleteButton);
deleteModal.hide();
window.Modals.delete.hide();
}, 4000);
} else {
Unfocus(deleteButton);
deleteModal.hide()
window.Modals.delete.hide()
}
populateUsers();
checkCheckboxes();
}
});
};
deleteModal.show();
window.Modals.delete.show();
};
(<HTMLInputElement>document.getElementById('selectAll')).checked = false;
@ -236,7 +152,7 @@ function populateRadios(): void {
(<HTMLButtonElement>document.getElementById('accountsTabSetDefaults')).onclick = function (): void {
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
let userIDs: Array<string> = new Array(checkboxes.length);
for (let i = 0; i < checkboxes.length; i++){
for (let i = 0; i < checkboxes.length; i++){
userIDs[i] = checkboxes[i].id.replace("select_", "");
}
if (userIDs.length == 0) {
@ -250,9 +166,9 @@ function populateRadios(): void {
populateProfiles(true);
const profileSelect = document.getElementById('profileSelect') as HTMLSelectElement;
profileSelect.textContent = '';
for (let i = 0; i < availableProfiles.length; i++) {
for (let i = 0; i < window.availableProfiles.length; i++) {
profileSelect.innerHTML += `
<option value="${availableProfiles[i]}" ${(i == 0) ? "selected" : ""}>${availableProfiles[i]}</option>
<option value="${window.availableProfiles[i]}" ${(i == 0) ? "selected" : ""}>${window.availableProfiles[i]}</option>
`;
}
document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userString}`;
@ -266,7 +182,7 @@ function populateRadios(): void {
Unfocus(document.getElementById('defaultUserRadiosBox'));
Unfocus(document.getElementById('newProfileBox'));
document.getElementById('storeDefaults').onclick = (): void => storeDefaults(userIDs);
userDefaultsModal.show();
window.Modals.userDefaults.show();
};
(<HTMLSelectElement>document.getElementById('defaultsSource')).addEventListener('change', function (): void {
@ -311,7 +227,7 @@ function populateRadios(): void {
rmAttr(button, 'btn-success');
addAttr(button, 'btn-primary');
button.textContent = ogText;
newUserModal.hide();
window.Modals.newUser.hide();
}, 1000);
populateUsers();
} else {
@ -338,11 +254,5 @@ function populateRadios(): void {
if (document.getElementById('newUserName') != null) {
(<HTMLInputElement>document.getElementById('newUserName')).value = '';
}
newUserModal.show();
window.Modals.newUser.show();
};

View File

@ -1,8 +1,19 @@
// Set in admin.html
var cssFile: string;
import { serializeForm, rmAttr, addAttr, _get, _post, _delete } from "./modules/common.js";
import { Focus, Unfocus } from "./modules/admin.js";
import { toggleCSS } from "./modules/animation.js";
import { populateUsers, checkCheckboxes } from "./modules/accounts.js";
import { generateInvites, addOptions, checkDuration } from "./modules/invites.js";
import { showSetting, openSettings } from "./modules/settings.js";
import { BS4 } from "./modules/bs4.js";
import { BS5 } from "./modules/bs5.js";
import "./accounts.js";
import "./settings.js";
const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused');
const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused');
interface aWindow extends Window {
toClipboard(str: string): void;
}
declare var window: aWindow;
interface TabSwitcher {
els: Array<HTMLDivElement>;
@ -35,27 +46,43 @@ const tabs: TabSwitcher = {
tabs.focus(1);
},
settings: (): void => openSettings(document.getElementById('settingsSections'), document.getElementById('settingsContent'), (): void => {
triggerTooltips();
window.BS.triggerTooltips();
showSetting("ui");
tabs.focus(2);
})
};
// for (let i = 0; i < tabs.els.length; i++) {
// tabs.tabButtons[i].onclick = (): void => tabs.focus(i);
// }
window.bsVersion = window.bs5 ? 5 : 4
if (window.bs5) {
window.BS = new BS5;
} else {
window.BS = new BS4;
window.BS.Compat();
}
window.Modals = {} as BSModals;
window.Modals.login = window.BS.newModal('login');
window.Modals.userDefaults = window.BS.newModal('userDefaults');
window.Modals.users = window.BS.newModal('users');
window.Modals.restart = window.BS.newModal('restartModal');
window.Modals.refresh = window.BS.newModal('refreshModal');
window.Modals.about = window.BS.newModal('aboutModal');
window.Modals.delete = window.BS.newModal('deleteModal');
window.Modals.newUser = window.BS.newModal('newUserModal');
tabs.tabButtons[0].onclick = tabs.invites;
tabs.tabButtons[1].onclick = tabs.accounts;
tabs.tabButtons[2].onclick = tabs.settings;
tabs.invites();
// Predefined colors for the theme button.
var buttonColor: string = "custom";
if (cssFile.includes("jf")) {
if (window.cssFile.includes("jf")) {
buttonColor = "rgb(255,255,255)";
} else if (cssFile == ("bs" + bsVersion + ".css")) {
} else if (window.cssFile == ("bs" + window.bsVersion + ".css")) {
buttonColor = "rgb(16,16,16)";
}
@ -70,20 +97,11 @@ if (buttonColor != "custom") {
document.getElementById('headerButtons').appendChild(switchButton);
}
var loginModal = createModal('login');
var userDefaultsModal = createModal('userDefaults');
var usersModal = createModal('users');
var restartModal = createModal('restartModal');
var refreshModal = createModal('refreshModal');
var aboutModal = createModal('aboutModal');
var deleteModal = createModal('deleteModal');
var newUserModal = createModal('newUserModal');
var availableProfiles: Array<string>;
window["token"] = "";
function toClipboard(str: string): void {
window.toClipboard = (str: string): void => {
const el = document.createElement('textarea') as HTMLTextAreaElement;
el.value = str;
el.readOnly = true;
@ -123,7 +141,7 @@ function login(username: string, password: string, modal: boolean, button?: HTML
button.textContent = "Login";
}, 4000);
} else {
loginModal.show();
window.Modals.login.show();
}
} else {
const data = this.response;
@ -137,7 +155,7 @@ function login(username: string, password: string, modal: boolean, button?: HTML
minutes.value = "30";
checkDuration();
if (modal) {
loginModal.hide();
window.Modals.login.hide();
}
Focus(document.getElementById('logoutButton'));
}
@ -149,12 +167,6 @@ function login(username: string, password: string, modal: boolean, button?: HTML
req.send();
}
function createEl(html: string): HTMLElement {
let div = document.createElement('div') as HTMLDivElement;
div.innerHTML = html;
return div.firstElementChild as HTMLElement;
}
(document.getElementById('loginForm') as HTMLFormElement).onsubmit = function (): boolean {
window.token = "";
const details = serializeForm('loginForm');
@ -169,70 +181,11 @@ function createEl(html: string): HTMLElement {
return false;
};
function storeDefaults(users: string | Array<string>): void {
// not sure if this does anything, but w/e
this.disabled = true;
this.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
const button = document.getElementById('storeDefaults') as HTMLButtonElement;
let data = { "homescreen": false };
if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'profile') {
data["from"] = "profile";
data["profile"] = (document.getElementById('profileSelect') as HTMLSelectElement).value;
} else {
const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
let id = radio.id.replace("default_", "");
data["from"] = "user";
data["id"] = id;
}
if (users != "all") {
data["apply_to"] = users;
}
if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) {
data["homescreen"] = true;
}
_post("/users/settings", data, function (): void {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
button.textContent = "Success";
addAttr(button, "btn-success");
rmAttr(button, "btn-danger");
rmAttr(button, "btn-primary");
button.disabled = false;
setTimeout((): void => {
button.textContent = "Submit";
addAttr(button, "btn-primary");
rmAttr(button, "btn-success");
button.disabled = false;
userDefaultsModal.hide();
}, 1000);
} else {
if ("error" in this.response) {
button.textContent = this.response["error"];
} else if (("policy" in this.response) || ("homescreen" in this.response)) {
button.textContent = "Failed (check console)";
} else {
button.textContent = "Failed";
}
addAttr(button, "btn-danger");
rmAttr(button, "btn-primary");
setTimeout((): void => {
button.textContent = "Submit";
addAttr(button, "btn-primary");
rmAttr(button, "btn-danger");
button.disabled = false;
}, 1000);
}
}
});
}
generateInvites(true);
login("", "", false, null, (status: number): void => {
if (!(status == 200 || status == 204)) {
loginModal.show();
window.Modals.login.show();
}
});

View File

@ -1,36 +0,0 @@
var bsVersion = 4;
const send_to_addess_enabled = document.getElementById('send_to_addess_enabled');
if (send_to_addess_enabled) {
send_to_addess_enabled.classList.remove("form-check-input");
}
const multiUseEnabled = document.getElementById('multiUseEnabled');
if (multiUseEnabled) {
multiUseEnabled.classList.remove("form-check-input");
}
function createModal(id: string, find?: boolean): any {
$(`#${id}`).on("shown.bs.modal", (): void => document.body.classList.add("modal-open"));
return {
show: function (): any {
const temp = ($(`#${id}`) as any).modal("show");
return temp;
},
hide: function (): any {
return ($(`#${id}`) as any).modal("hide");
}
};
}
function triggerTooltips(): void {
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
for (const i in checkboxes) {
checkboxes[i].click();
checkboxes[i].click();
}
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map((el: HTMLAnchorElement): any => {
return ($(el) as any).tooltip();
});
}

View File

@ -1,34 +0,0 @@
declare var bootstrap: any;
var bsVersion = 5;
function createModal(id: string, find?: boolean): any {
let modal: any;
if (find) {
modal = bootstrap.Modal.getInstance(document.getElementById(id));
} else {
modal = new bootstrap.Modal(document.getElementById(id));
}
document.getElementById(id).addEventListener('shown.bs.modal', (): void => document.body.classList.add("modal-open"));
return {
modal: modal,
show: function (): any {
const temp = this.modal.show();
return temp;
},
hide: function (): any { return this.modal.hide(); }
};
}
function triggerTooltips(): void {
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
for (const i in checkboxes) {
checkboxes[i].click();
checkboxes[i].click();
}
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map((el: HTMLAnchorElement): any => {
return new bootstrap.Tooltip(el);
});
}

View File

@ -1,3 +1,14 @@
import { serializeForm, _post, _get, _delete, addAttr, rmAttr } from "./modules/common.js";
import { BS5 } from "./modules/bs5.js";
import { BS4 } from "./modules/bs4.js";
interface formWindow extends Window {
usernameEnabled: boolean;
validationStrings: pwValStrings;
}
declare var window: formWindow;
interface pwValString {
singular: string;
plural: string;
@ -7,9 +18,6 @@ interface pwValStrings {
length, uppercase, lowercase, number, special: pwValString;
}
var validationStrings: pwValStrings;
var bsVersion: number;
var defaultPwValStrings: pwValStrings = {
length: {
singular: "Must have at least {n} character",
@ -45,49 +53,30 @@ const toggleSpinner = (): void => {
}
};
for (let key in validationStrings) {
if (validationStrings[key].singular == "" || !(validationStrings[key].plural.includes("{n}"))) {
validationStrings[key].singular = defaultPwValStrings[key].singular;
for (let key in window.validationStrings) {
if (window.validationStrings[key].singular == "" || !(window.validationStrings[key].plural.includes("{n}"))) {
window.validationStrings[key].singular = defaultPwValStrings[key].singular;
}
if (validationStrings[key].plural == "" || !(validationStrings[key].plural.includes("{n}"))) {
validationStrings[key].plural = defaultPwValStrings[key].plural;
if (window.validationStrings[key].plural == "" || !(window.validationStrings[key].plural.includes("{n}"))) {
window.validationStrings[key].plural = defaultPwValStrings[key].plural;
}
let el = document.getElementById(key) as HTMLUListElement;
if (el) {
const min: number = +el.getAttribute("min");
let text = "";
if (min == 1) {
text = validationStrings[key].singular.replace("{n}", "1");
text = window.validationStrings[key].singular.replace("{n}", "1");
} else {
text = validationStrings[key].plural.replace("{n}", min.toString());
text = window.validationStrings[key].plural.replace("{n}", min.toString());
}
(document.getElementById(key).children[0] as HTMLDivElement).textContent = text;
}
}
interface Modal {
show: () => void;
hide: () => void;
}
var successBox: Modal;
if (bsVersion == 5) {
var bootstrap: any;
successBox = new bootstrap.Modal(document.getElementById('successBox'));
} else if (bsVersion == 4) {
successBox = {
show: (): void => {
($('#successBox') as any).modal('show');
},
hide: (): void => {
($('#successBox') as any).modal('hide');
}
};
}
window.BS = window.bs5 ? new BS5 : new BS4;
var successBox: BSModal = window.BS.newModal('successBox');;
var code = window.location.href.split('/').pop();
var usernameEnabled: boolean;
(document.getElementById('accountForm') as HTMLFormElement).addEventListener('submit', (event: any): boolean => {
event.preventDefault();
@ -98,7 +87,7 @@ var usernameEnabled: boolean;
toggleSpinner();
let send: Object = serializeForm('accountForm');
send["code"] = code;
if (!usernameEnabled) {
if (!window.usernameEnabled) {
send["email"] = send["username"];
}
_post("/newUser", send, function (): void {

View File

@ -1,297 +1,11 @@
// Actually defined by templating in admin.html, this is just to avoid errors from tsc.
var notifications_enabled: any;
import { serializeForm, rmAttr, addAttr, _get, _post, _delete } from "./modules/common.js";
import { generateInvites, checkDuration } from "./modules/invites.js";
interface Invite {
code?: string;
expiresIn?: string;
empty: boolean;
remainingUses?: string;
email?: string;
usedBy?: Array<Array<string>>;
created?: string;
notifyExpiry?: boolean;
notifyCreation?: boolean;
profile?: string;
interface aWindow extends Window {
setProfile(el: HTMLElement): void;
}
const emptyInvite = (): Invite => { return { code: "None", empty: true } as Invite; }
function parseInvite(invite: Object): Invite {
let inv: Invite = { code: invite["code"], empty: false, };
if (invite["email"]) {
inv.email = invite["email"];
}
let time = ""
const f = ["days", "hours", "minutes"];
for (const i in f) {
if (invite[f[i]] != 0) {
time += `${invite[f[i]]}${f[i][0]} `;
}
}
inv.expiresIn = `Expires in ${time.slice(0, -1)}`;
if (invite["no-limit"]) {
inv.remainingUses = "∞";
} else if ("remaining-uses" in invite) {
inv.remainingUses = invite["remaining-uses"];
}
if ("used-by" in invite) {
inv.usedBy = invite["used-by"];
}
if ("created" in invite) {
inv.created = invite["created"];
}
if ("notify-expiry" in invite) {
inv.notifyExpiry = invite["notify-expiry"];
}
if ("notify-creation" in invite) {
inv.notifyCreation = invite["notify-creation"];
}
if ("profile" in invite) {
inv.profile = invite["profile"];
}
return inv;
}
function setNotify(el: HTMLElement): void {
let send = {};
let code: string;
let notifyType: string;
if (el.id.includes("Expiry")) {
code = el.id.replace("_notifyExpiry", "");
notifyType = "notify-expiry";
} else if (el.id.includes("Creation")) {
code = el.id.replace("_notifyCreation", "");
notifyType = "notify-creation";
}
send[code] = {};
send[code][notifyType] = (el as HTMLInputElement).checked;
_post("/invites/notify", send, function (): void {
if (this.readyState == 4 && this.status != 200) {
(el as HTMLInputElement).checked = !(el as HTMLInputElement).checked;
}
});
}
function genUsedBy(usedBy: Array<Array<string>>): string {
let uB = "";
if (usedBy && usedBy.length != 0) {
uB = `
<ul class="list-group list-group-flush">
<li class="list-group-item py-1">Users created:</li>
`;
for (const i in usedBy) {
uB += `
<li class="list-group-item py-1 disabled">
<div class="d-flex float-left">${usedBy[i][0]}</div>
<div class="d-flex float-right">${usedBy[i][1]}</div>
</li>
`;
}
uB += `</ul>`
}
return uB;
}
function addItem(invite: Invite): void {
const links = document.getElementById('invites');
const container = document.createElement('div') as HTMLDivElement;
container.id = invite.code;
const item = document.createElement('div') as HTMLDivElement;
item.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
let link = "";
let innerHTML = `<a>None</a>`;
if (invite.empty) {
item.innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
${innerHTML}
</div>
`;
container.appendChild(item);
links.appendChild(container);
return;
}
link = window.location.href.split('#')[0] + "invite/" + invite.code;
innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
<a class="invite-link" href="${link}">${invite.code.replace(/-/g, '-')}</a>
<i class="fa fa-clipboard icon-button" onclick="toClipboard('${link}')" style="margin-right: 0.5rem; margin-left: 0.5rem;"></i>
`;
if (invite.email) {
let email = invite.email;
if (!invite.email.includes("Failed to send to")) {
email = `Sent to ${email}`;
}
innerHTML += `
<span class="text-muted" style="margin-left: 0.4rem; font-style: italic; font-size: 0.8rem;">${email}</span>
`;
}
innerHTML += `
</div>
<div style="text-align: right;">
<span id="${invite.code}_expiry" style="margin-right: 1rem;">${invite.expiresIn}</span>
<div style="display: inline-block;">
<button class="btn btn-outline-danger" onclick="deleteInvite('${invite.code}')">Delete</button>
<i class="fa fa-angle-down collapsed icon-button not-rotated" style="padding: 1rem; margin: -1rem -1rem -1rem 0;" data-toggle="collapse" aria-expanded="false" data-target="#${CSS.escape(invite.code)}_collapse" onclick="rotateButton(this)"></i>
</div>
</div>
`;
item.innerHTML = innerHTML;
container.appendChild(item);
let profiles = `
<label class="input-group-text" for="profile_${CSS.escape(invite.code)}">Profile: </label>
<select class="form-select" id="profile_${CSS.escape(invite.code)}" onchange="setProfile(this)">
<option value="NoProfile" selected>No Profile</option>
`;
for (const i in availableProfiles) {
let selected = "";
if (availableProfiles[i] == invite.profile) {
selected = "selected";
}
profiles += `<option value="${availableProfiles[i]}" ${selected}>${availableProfiles[i]}</option>`;
}
profiles += `</select>`;
let dateCreated: string;
if (invite.created) {
dateCreated = `<li class="list-group-item py-1">Created: ${invite.created}</li>`;
}
let middle: string;
if (notifications_enabled) {
middle = `
<div class="col" id="${CSS.escape(invite.code)}_notifyButtons">
<ul class="list-group list-group-flush">
Notify on:
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyExpiry" onclick="setNotify(this)" ${invite.notifyExpiry ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyExpiry">Expiry</label>
</li>
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyCreation" onclick="setNotify(this)" ${invite.notifyCreation ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyCreation">User creation</label>
</li>
</ul>
</div>
`;
}
let right: string = genUsedBy(invite.usedBy)
const dropdown = document.createElement('div') as HTMLDivElement;
dropdown.id = `${CSS.escape(invite.code)}_collapse`;
dropdown.classList.add("collapse");
dropdown.innerHTML = `
<div class="container row align-items-start card-body">
<div class="col">
<ul class="list-group list-group-flush">
<li class="input-group py-1">
${profiles}
</li>
${dateCreated}
<li class="list-group-item py-1" id="${CSS.escape(invite.code)}_remainingUses">Remaining uses: ${invite.remainingUses}</li>
</ul>
</div>
${middle}
<div class="col" id="${CSS.escape(invite.code)}_usersCreated">
${right}
</div>
</div>
`;
container.appendChild(dropdown);
links.appendChild(container);
}
function updateInvite(invite: Invite): void {
document.getElementById(invite.code + "_expiry").textContent = invite.expiresIn;
const remainingUses: any = document.getElementById(CSS.escape(invite.code) + "_remainingUses");
if (remainingUses) {
remainingUses.textContent = `Remaining uses: ${invite.remainingUses}`;
}
document.getElementById(CSS.escape(invite.code) + "_usersCreated").innerHTML = genUsedBy(invite.usedBy);
}
// delete invite from DOM
const hideInvite = (code: string): void => document.getElementById(CSS.escape(code)).remove();
// delete invite from jfa-go
const deleteInvite = (code: string): void => _delete("/invites", { "code": code }, function (): void {
if (this.readyState == 4) {
generateInvites();
}
});
function generateInvites(empty?: boolean): void {
if (empty) {
document.getElementById('invites').textContent = '';
addItem(emptyInvite());
return;
}
_get("/invites", null, function (): void {
if (this.readyState == 4) {
let data = this.response;
availableProfiles = data['profiles'];
const Profiles = document.getElementById('inviteProfile') as HTMLSelectElement;
let innerHTML = "";
for (let i = 0; i < availableProfiles.length; i++) {
const profile = availableProfiles[i];
innerHTML += `
<option value="${profile}" ${(i == 0) ? "selected" : ""}>${profile}</option>
`;
}
innerHTML += `
<option value="NoProfile" ${(availableProfiles.length == 0) ? "selected" : ""}>No Profile</option>
`;
Profiles.innerHTML = innerHTML;
if (data['invites'] == null || data['invites'].length == 0) {
document.getElementById('invites').textContent = '';
addItem(emptyInvite());
return;
}
let items = document.getElementById('invites').children;
for (const i in data['invites']) {
let match = false;
const inv = parseInvite(data['invites'][i]);
for (const x in items) {
if (items[x].id == inv.code) {
match = true;
updateInvite(inv);
break;
}
}
if (!match) {
addItem(inv);
}
}
// second pass to check for expired invites
items = document.getElementById('invites').children;
for (let i = 0; i < items.length; i++) {
let exists = false;
for (const x in data['invites']) {
if (items[i].id == data['invites'][x]['code']) {
exists = true;
break;
}
}
if (!exists) {
hideInvite(items[i].id);
}
}
}
});
}
const addOptions = (length: number, el: HTMLSelectElement): void => {
for (let v = 0; v <= length; v++) {
const opt = document.createElement('option');
opt.textContent = ""+v;
opt.value = ""+v;
el.appendChild(opt);
}
el.value = "0";
};
declare var window: aWindow;
function fixCheckboxes(): void {
const send_to_address: Array<HTMLInputElement> = [document.getElementById('send_to_address') as HTMLInputElement, document.getElementById('send_to_address_enabled') as HTMLInputElement];
@ -329,7 +43,6 @@ fixCheckboxes();
delete send['send_to_address'];
delete send['send_to_address_enabled'];
}
console.log(send);
_post("/invites", send, function (): void {
if (this.readyState == 4) {
button.textContent = 'Generate';
@ -340,9 +53,9 @@ fixCheckboxes();
return false;
};
triggerTooltips();
window.BS.triggerTooltips();
function setProfile(select: HTMLSelectElement): void {
window.setProfile= (select: HTMLSelectElement): void => {
if (!select.value) {
return;
}
@ -362,16 +75,6 @@ function setProfile(select: HTMLSelectElement): void {
});
}
function checkDuration(): void {
const boxVals: Array<number> = [+(document.getElementById("days") as HTMLSelectElement).value, +(document.getElementById("hours") as HTMLSelectElement).value, +(document.getElementById("minutes") as HTMLSelectElement).value];
const submit = document.getElementById('generateSubmit') as HTMLButtonElement;
if (boxVals.reduce((a: number, b: number): number => a + b) == 0) {
submit.disabled = true;
} else {
submit.disabled = false;
}
}
const nE: Array<string> = ["days", "hours", "minutes"];
for (const i in nE) {
document.getElementById(nE[i]).addEventListener("change", checkDuration);

106
ts/modules/accounts.ts Normal file
View File

@ -0,0 +1,106 @@
import { _get, _post, _delete } from "../modules/common.js";
import { Focus, Unfocus } from "../modules/admin.js";
interface aWindow extends Window {
checkCheckboxes: () => void;
}
declare var window: aWindow;
export const checkCheckboxes = (): void => {
const defaultsButton = document.getElementById('accountsTabSetDefaults');
const deleteButton = document.getElementById('accountsTabDelete');
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
let checked = checkboxes.length;
if (checked == 0) {
Unfocus(defaultsButton);
Unfocus(deleteButton);
} else {
Focus(defaultsButton);
Focus(deleteButton);
if (checked == 1) {
deleteButton.textContent = 'Delete User';
} else {
deleteButton.textContent = 'Delete Users';
}
}
}
window.checkCheckboxes = checkCheckboxes;
export function populateUsers(): void {
const acList = document.getElementById('accountsList');
acList.innerHTML = `
<div class="d-flex align-items-center">
<strong>Getting Users...</strong>
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
</div>
`;
Unfocus(acList.parentNode.querySelector('thead'));
const accountsList = document.createElement('tbody');
accountsList.id = 'accountsList';
const generateEmail = (id: string, name: string, email: string): string => {
let entry: HTMLDivElement = document.createElement('div');
entry.id = 'email_' + id;
let emailValue: string = email;
if (emailValue == undefined) {
emailValue = "";
}
entry.innerHTML = `
<i class="fa fa-edit d-inline-block icon-button" style="margin-right: 2%;" onclick="changeEmail(this, '${id}')"></i>
<input type="email" class="form-control-plaintext form-control-sm text-muted d-inline-block addressText" id="address_${id}" style="width: auto;" value="${emailValue}" readonly>
`;
return entry.outerHTML;
};
const template = (id: string, username: string, email: string, lastActive: string, admin: boolean): string => {
let isAdmin = "No";
if (admin) {
isAdmin = "Yes";
}
let fci = "form-check-input";
if (window.bsVersion != 5) {
fci = "";
}
return `
<td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
<td nowrap="nowrap" class="align-middle">${username}</td>
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
<td nowrap="nowrap" class="align-middle">${isAdmin}</td>
`;
};
_get("/users", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
window.jfUsers = this.response['users'];
for (const user of window.jfUsers) {
let tr = document.createElement('tr');
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
accountsList.appendChild(tr);
}
Focus(acList.parentNode.querySelector('thead'));
acList.replaceWith(accountsList);
}
});
}
export function populateRadios(): void {
const radioList = document.getElementById('defaultUserRadios');
radioList.textContent = '';
let first = true;
for (const i in window.jfUsers) {
const user = window.jfUsers[i];
const radio = document.createElement('div');
radio.classList.add('form-check');
let checked = '';
if (first) {
checked = 'checked';
first = false;
}
radio.innerHTML = `
<input class="form-check-input" type="radio" name="defaultRadios" id="default_${user['id']}" ${checked}>
<label class="form-check-label" for="default_${user['id']}">${user['name']}</label>`;
radioList.appendChild(radio);
}
}

68
ts/modules/admin.ts Normal file
View File

@ -0,0 +1,68 @@
import { rmAttr, addAttr, _post, _get, _delete } from "../modules/common.js";
export const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused');
export const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused');
export function createEl(html: string): HTMLElement {
let div = document.createElement('div') as HTMLDivElement;
div.innerHTML = html;
return div.firstElementChild as HTMLElement;
}
export function storeDefaults(users: string | Array<string>): void {
const button = document.getElementById('storeDefaults') as HTMLButtonElement;
button.disabled = true;
button.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
let data = { "homescreen": false };
if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'profile') {
data["from"] = "profile";
data["profile"] = (document.getElementById('profileSelect') as HTMLSelectElement).value;
} else {
const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
let id = radio.id.replace("default_", "");
data["from"] = "user";
data["id"] = id;
}
if (users != "all") {
data["apply_to"] = users;
}
if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) {
data["homescreen"] = true;
}
_post("/users/settings", data, function (): void {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
button.textContent = "Success";
addAttr(button, "btn-success");
rmAttr(button, "btn-danger");
rmAttr(button, "btn-primary");
button.disabled = false;
setTimeout((): void => {
button.textContent = "Submit";
addAttr(button, "btn-primary");
rmAttr(button, "btn-success");
button.disabled = false;
window.Modals.userDefaults.hide();
}, 1000);
} else {
if ("error" in this.response) {
button.textContent = this.response["error"];
} else if (("policy" in this.response) || ("homescreen" in this.response)) {
button.textContent = "Failed (check console)";
} else {
button.textContent = "Failed";
}
addAttr(button, "btn-danger");
rmAttr(button, "btn-primary");
setTimeout((): void => {
button.textContent = "Submit";
addAttr(button, "btn-primary");
rmAttr(button, "btn-danger");
button.disabled = false;
}, 1000);
}
}
});
}

View File

@ -1,3 +1,11 @@
import { rmAttr, addAttr } from "../modules/common.js";
interface aWindow extends Window {
rotateButton(el: HTMLElement): void;
}
declare var window: aWindow;
// Used for animation on theme change
const whichTransitionEvent = (): string => {
const el = document.createElement('fakeElement');
@ -26,7 +34,7 @@ const _toggleCSS = (): void => {
cssEl = 1;
remove = true
}
let href: string = "bs" + bsVersion;
let href: string = "bs" + window.bsVersion;
if (!els[cssEl].href.includes(href + "-jf")) {
href += "-jf";
}
@ -41,8 +49,8 @@ const _toggleCSS = (): void => {
}
// Toggles between light and dark themes, but runs animation if window small enough.
var buttonWidth = 0;
const toggleCSS = (el: HTMLElement): void => {
window.buttonWidth = 0;
export const toggleCSS = (el: HTMLElement): void => {
const switchToColor = window.getComputedStyle(document.body, null).backgroundColor;
// Max page width for animation to take place
let maxWidth = 1500;
@ -51,7 +59,7 @@ const toggleCSS = (el: HTMLElement): void => {
const radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2));
const currentRadius = el.getBoundingClientRect().width / 2;
const scale = radius / currentRadius;
buttonWidth = +window.getComputedStyle(el, null).width;
window.buttonWidth = +window.getComputedStyle(el, null).width;
document.body.classList.remove('smooth-transition');
el.style.transform = `scale(${scale})`;
el.style.color = switchToColor;
@ -68,7 +76,7 @@ const toggleCSS = (el: HTMLElement): void => {
}
};
const rotateButton = (el: HTMLElement): void => {
window.rotateButton = (el: HTMLElement): void => {
if (el.classList.contains("rotated")) {
rmAttr(el, "rotated")
addAttr(el, "not-rotated");

45
ts/modules/bs4.ts Normal file
View File

@ -0,0 +1,45 @@
declare var $: any;
class Modal implements BSModal {
el: HTMLDivElement;
modal: any;
constructor(id: string, find?: boolean) {
this.el = document.getElementById(id) as HTMLDivElement;
this.modal = $(this.el) as any;
this.modal.on("shown.b.modal", (): void => document.body.classList.add('modal-open'));
};
show(): void { this.modal.modal("show"); };
hide(): void { this.modal.modal("hide"); };
}
export class BS4 implements Bootstrap {
triggerTooltips: tooltipTrigger = function (): void {
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
for (const i in checkboxes) {
checkboxes[i].click();
checkboxes[i].click();
}
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map((el: HTMLAnchorElement): any => {
return ($(el) as any).tooltip();
});
};
Compat(): void {
console.log('Fixing BS4 Compatability');
const send_to_address_enabled = document.getElementById('send_to_address_enabled');
if (send_to_address_enabled) {
send_to_address_enabled.classList.remove("form-check-input");
}
const multiUseEnabled = document.getElementById('multiUseEnabled');
if (multiUseEnabled) {
multiUseEnabled.classList.remove("form-check-input");
}
}
newModal: ModalConstructor = function (id: string, find?: boolean): BSModal {
return new Modal(id, find);
};
}

37
ts/modules/bs5.ts Normal file
View File

@ -0,0 +1,37 @@
declare var bootstrap: any;
class Modal implements BSModal {
el: HTMLDivElement;
modal: any;
constructor(id: string, find?: boolean) {
this.el = document.getElementById(id) as HTMLDivElement;
if (find) {
this.modal = bootstrap.Modal.getInstance(this.el);
} else {
this.modal = new bootstrap.Modal(this.el);
}
this.el.addEventListener('shown.bs.modal', (): void => document.body.classList.add("modal-open"));
};
show(): void { this.modal.show(); };
hide(): void { this.modal.hide(); };
}
export class BS5 implements Bootstrap {
triggerTooltips: tooltipTrigger = function (): void {
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
for (const i in checkboxes) {
checkboxes[i].click();
checkboxes[i].click();
}
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map((el: HTMLAnchorElement): any => {
return new bootstrap.Tooltip(el);
});
};
newModal: ModalConstructor = function (id: string, find?: boolean): BSModal {
return new Modal(id, find);
};
};

View File

@ -1,8 +1,6 @@
interface Window {
token: string;
}
declare var window: Window;
function serializeForm(id: string): Object {
export function serializeForm(id: string): Object {
const form = document.getElementById(id) as HTMLFormElement;
let formData = {};
for (let i = 0; i < form.elements.length; i++) {
@ -38,15 +36,15 @@ function serializeForm(id: string): Object {
return formData;
}
const rmAttr = (el: HTMLElement, attr: string): void => {
export const rmAttr = (el: HTMLElement, attr: string): void => {
if (el.classList.contains(attr)) {
el.classList.remove(attr);
}
};
const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
export const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
let req = new XMLHttpRequest();
req.open("GET", url, true);
req.responseType = 'json';
@ -56,7 +54,7 @@ const _get = (url: string, data: Object, onreadystatechange: () => void): void =
req.send(JSON.stringify(data));
};
const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => {
export const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => {
let req = new XMLHttpRequest();
req.open("POST", url, true);
if (response) {
@ -68,7 +66,7 @@ const _post = (url: string, data: Object, onreadystatechange: () => void, respon
req.send(JSON.stringify(data));
};
function _delete(url: string, data: Object, onreadystatechange: () => void): void {
export function _delete(url: string, data: Object, onreadystatechange: () => void): void {
let req = new XMLHttpRequest();
req.open("DELETE", url, true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));

297
ts/modules/invites.ts Normal file
View File

@ -0,0 +1,297 @@
import { _get, _post, _delete } from "../modules/common.js";
interface aWindow extends Window {
setNotify(el: HTMLElement): void;
deleteInvite(code: string): void;
}
declare var window: aWindow;
const emptyInvite = (): Invite => { return { code: "None", empty: true } as Invite; }
function genUsedBy(usedBy: Array<Array<string>>): string {
let uB = "";
if (usedBy && usedBy.length != 0) {
uB = `
<ul class="list-group list-group-flush">
<li class="list-group-item py-1">Users created:</li>
`;
for (const i in usedBy) {
uB += `
<li class="list-group-item py-1 disabled">
<div class="d-flex float-left">${usedBy[i][0]}</div>
<div class="d-flex float-right">${usedBy[i][1]}</div>
</li>
`;
}
uB += `</ul>`
}
return uB;
}
function addItem(invite: Invite): void {
const links = document.getElementById('invites');
const container = document.createElement('div') as HTMLDivElement;
container.id = invite.code;
const item = document.createElement('div') as HTMLDivElement;
item.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
let link = "";
let innerHTML = `<a>None</a>`;
if (invite.empty) {
item.innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
${innerHTML}
</div>
`;
container.appendChild(item);
links.appendChild(container);
return;
}
link = window.location.href.split('#')[0] + "invite/" + invite.code;
innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
<a class="invite-link" href="${link}">${invite.code.replace(/-/g, '-')}</a>
<i class="fa fa-clipboard icon-button" onclick="window.toClipboard('${link}')" style="margin-right: 0.5rem; margin-left: 0.5rem;"></i>
`;
if (invite.email) {
let email = invite.email;
if (!invite.email.includes("Failed to send to")) {
email = `Sent to ${email}`;
}
innerHTML += `
<span class="text-muted" style="margin-left: 0.4rem; font-style: italic; font-size: 0.8rem;">${email}</span>
`;
}
innerHTML += `
</div>
<div style="text-align: right;">
<span id="${invite.code}_expiry" style="margin-right: 1rem;">${invite.expiresIn}</span>
<div style="display: inline-block;">
<button class="btn btn-outline-danger" onclick="deleteInvite('${invite.code}')">Delete</button>
<i class="fa fa-angle-down collapsed icon-button not-rotated" style="padding: 1rem; margin: -1rem -1rem -1rem 0;" data-toggle="collapse" aria-expanded="false" data-target="#${CSS.escape(invite.code)}_collapse" onclick="window.rotateButton(this)"></i>
</div>
</div>
`;
item.innerHTML = innerHTML;
container.appendChild(item);
let profiles = `
<label class="input-group-text" for="profile_${CSS.escape(invite.code)}">Profile: </label>
<select class="form-select" id="profile_${CSS.escape(invite.code)}" onchange="window.setProfile(this)">
<option value="NoProfile" selected>No Profile</option>
`;
for (const i in window.availableProfiles) {
let selected = "";
if (window.availableProfiles[i] == invite.profile) {
selected = "selected";
}
profiles += `<option value="${window.availableProfiles[i]}" ${selected}>${window.availableProfiles[i]}</option>`;
}
profiles += `</select>`;
let dateCreated: string;
if (invite.created) {
dateCreated = `<li class="list-group-item py-1">Created: ${invite.created}</li>`;
}
let middle: string;
if (window.notifications_enabled) {
middle = `
<div class="col" id="${CSS.escape(invite.code)}_notifyButtons">
<ul class="list-group list-group-flush">
Notify on:
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyExpiry" onclick="setNotify(this)" ${invite.notifyExpiry ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyExpiry">Expiry</label>
</li>
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyCreation" onclick="setNotify(this)" ${invite.notifyCreation ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyCreation">User creation</label>
</li>
</ul>
</div>
`;
}
let right: string = genUsedBy(invite.usedBy)
const dropdown = document.createElement('div') as HTMLDivElement;
dropdown.id = `${CSS.escape(invite.code)}_collapse`;
dropdown.classList.add("collapse");
dropdown.innerHTML = `
<div class="container row align-items-start card-body">
<div class="col">
<ul class="list-group list-group-flush">
<li class="input-group py-1">
${profiles}
</li>
${dateCreated}
<li class="list-group-item py-1" id="${CSS.escape(invite.code)}_remainingUses">Remaining uses: ${invite.remainingUses}</li>
</ul>
</div>
${middle}
<div class="col" id="${CSS.escape(invite.code)}_usersCreated">
${right}
</div>
</div>
`;
container.appendChild(dropdown);
links.appendChild(container);
}
function parseInvite(invite: Object): Invite {
let inv: Invite = { code: invite["code"], empty: false, };
if (invite["email"]) {
inv.email = invite["email"];
}
let time = ""
const f = ["days", "hours", "minutes"];
for (const i in f) {
if (invite[f[i]] != 0) {
time += `${invite[f[i]]}${f[i][0]} `;
}
}
inv.expiresIn = `Expires in ${time.slice(0, -1)}`;
if (invite["no-limit"]) {
inv.remainingUses = "∞";
} else if ("remaining-uses" in invite) {
inv.remainingUses = invite["remaining-uses"];
}
if ("used-by" in invite) {
inv.usedBy = invite["used-by"];
}
if ("created" in invite) {
inv.created = invite["created"];
}
if ("notify-expiry" in invite) {
inv.notifyExpiry = invite["notify-expiry"];
}
if ("notify-creation" in invite) {
inv.notifyCreation = invite["notify-creation"];
}
if ("profile" in invite) {
inv.profile = invite["profile"];
}
return inv;
}
window.setNotify = (el: HTMLElement): void => {
let send = {};
let code: string;
let notifyType: string;
if (el.id.includes("Expiry")) {
code = el.id.replace("_notifyExpiry", "");
notifyType = "notify-expiry";
} else if (el.id.includes("Creation")) {
code = el.id.replace("_notifyCreation", "");
notifyType = "notify-creation";
}
send[code] = {};
send[code][notifyType] = (el as HTMLInputElement).checked;
_post("/invites/notify", send, function (): void {
if (this.readyState == 4 && this.status != 200) {
(el as HTMLInputElement).checked = !(el as HTMLInputElement).checked;
}
});
}
function updateInvite(invite: Invite): void {
document.getElementById(invite.code + "_expiry").textContent = invite.expiresIn;
const remainingUses: any = document.getElementById(CSS.escape(invite.code) + "_remainingUses");
if (remainingUses) {
remainingUses.textContent = `Remaining uses: ${invite.remainingUses}`;
}
document.getElementById(CSS.escape(invite.code) + "_usersCreated").innerHTML = genUsedBy(invite.usedBy);
}
// delete invite from DOM
const hideInvite = (code: string): void => document.getElementById(CSS.escape(code)).remove();
// delete invite from jfa-go
window.deleteInvite = (code: string): void => _delete("/invites", { "code": code }, function (): void {
if (this.readyState == 4) {
generateInvites();
}
});
export function generateInvites(empty?: boolean): void {
if (empty) {
document.getElementById('invites').textContent = '';
addItem(emptyInvite());
return;
}
_get("/invites", null, function (): void {
if (this.readyState == 4) {
let data = this.response;
window.availableProfiles = data['profiles'];
const Profiles = document.getElementById('inviteProfile') as HTMLSelectElement;
let innerHTML = "";
for (let i = 0; i < window.availableProfiles.length; i++) {
const profile = window.availableProfiles[i];
innerHTML += `
<option value="${profile}" ${(i == 0) ? "selected" : ""}>${profile}</option>
`;
}
innerHTML += `
<option value="NoProfile" ${(window.availableProfiles.length == 0) ? "selected" : ""}>No Profile</option>
`;
Profiles.innerHTML = innerHTML;
if (data['invites'] == null || data['invites'].length == 0) {
document.getElementById('invites').textContent = '';
addItem(emptyInvite());
return;
}
let items = document.getElementById('invites').children;
for (const i in data['invites']) {
let match = false;
const inv = parseInvite(data['invites'][i]);
for (const x in items) {
if (items[x].id == inv.code) {
match = true;
updateInvite(inv);
break;
}
}
if (!match) {
addItem(inv);
}
}
// second pass to check for expired invites
items = document.getElementById('invites').children;
for (let i = 0; i < items.length; i++) {
let exists = false;
for (const x in data['invites']) {
if (items[i].id == data['invites'][x]['code']) {
exists = true;
break;
}
}
if (!exists) {
hideInvite(items[i].id);
}
}
}
});
}
export const addOptions = (length: number, el: HTMLSelectElement): void => {
for (let v = 0; v <= length; v++) {
const opt = document.createElement('option');
opt.textContent = ""+v;
opt.value = ""+v;
el.appendChild(opt);
}
el.value = "0";
};
export function checkDuration(): void {
const boxVals: Array<number> = [+(document.getElementById("days") as HTMLSelectElement).value, +(document.getElementById("hours") as HTMLSelectElement).value, +(document.getElementById("minutes") as HTMLSelectElement).value];
const submit = document.getElementById('generateSubmit') as HTMLButtonElement;
if (boxVals.reduce((a: number, b: number): number => a + b) == 0) {
submit.disabled = true;
} else {
submit.disabled = false;
}
}

164
ts/modules/settings.ts Normal file
View File

@ -0,0 +1,164 @@
import { _get, _post, _delete, rmAttr, addAttr } from "../modules/common.js";
import { Focus, Unfocus } from "../modules/admin.js";
interface Profile {
Admin: boolean;
LibraryAccess: string;
FromUser: string;
}
export const populateProfiles = (noTable?: boolean): void => _get("/profiles", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
const profileList = document.getElementById('profileList');
profileList.textContent = '';
window.availableProfiles = [this.response["default_profile"]];
for (let name in this.response["profiles"]) {
if (name != window.availableProfiles[0]) {
window.availableProfiles.push(name);
}
const reqProfile = this.response["profiles"][name];
if (!noTable && name != "default_profile") {
const profile: Profile = {
Admin: reqProfile["admin"],
LibraryAccess: reqProfile["libraries"],
FromUser: reqProfile["fromUser"]
};
profileList.innerHTML += `
<td nowrap="nowrap" class="align-middle"><strong>${name}</strong></td>
<td nowrap="nowrap" class="align-middle"><input class="${window.bs5 ? "form-check-input" : ""}" type="radio" name="defaultProfile" onclick="setDefaultProfile('${name}')" ${(name == window.availableProfiles[0]) ? "checked" : ""}></td>
<td nowrap="nowrap" class="align-middle">${profile.FromUser}</td>
<td nowrap="nowrap" class="align-middle">${profile.Admin ? "Yes" : "No"}</td>
<td nowrap="nowrap" class="align-middle">${profile.LibraryAccess}</td>
<td nowrap="nowrap" class="align-middle"><button class="btn btn-outline-danger" id="defaultProfile_${name}" onclick="deleteProfile('${name}')">Delete</button></td>
`;
}
}
}
});
export const openSettings = (settingsList: HTMLElement, settingsContent: HTMLElement, callback?: () => void): void => _get("/config", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
settingsList.textContent = '';
window.config = this.response;
for (const i in window.config["order"]) {
const section: string = window.config["order"][i]
const sectionCollapse = document.createElement('div') as HTMLDivElement;
Unfocus(sectionCollapse);
sectionCollapse.id = section;
const title: string = window.config[section]["meta"]["name"];
const description: string = window.config[section]["meta"]["description"];
const entryListID: string = `${section}_entryList`;
// const footerID: string = `${section}_footer`;
sectionCollapse.innerHTML = `
<div class="card card-body">
<small class="text-muted">${description}</small>
<div class="${entryListID}">
</div>
</div>
`;
for (const x in config[section]["order"]) {
const entry: string = config[section]["order"][x];
if (entry == "meta") {
continue;
}
let entryName: string = window.config[section][entry]["name"];
let required = false;
if (window.config[section][entry]["required"]) {
entryName += ` <sup class="text-danger">*</sup>`;
required = true;
}
if (window.config[section][entry]["requires_restart"]) {
entryName += ` <sup class="text-danger">R</sup>`;
}
if ("description" in window.config[section][entry]) {
entryName +=`
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${window.config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
`;
}
const entryValue: boolean | string = window.config[section][entry]["value"];
const entryType: string = window.config[section][entry]["type"];
const entryGroup = document.createElement('div');
if (entryType == "bool") {
entryGroup.classList.add("form-check");
entryGroup.innerHTML = `
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}" ${(entryValue as boolean) ? 'checked': ''} ${required ? 'required' : ''}>
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
`;
(entryGroup.querySelector('input[type=checkbox]') as HTMLInputElement).onclick = function (): void {
const me = this as HTMLInputElement;
for (const y in window.config["order"]) {
const sect: string = window.config["order"][y];
for (const z in window.config[sect]["order"]) {
const ent: string = window.config[sect]["order"][z];
if (`${sect}_${window.config[sect][ent]['depends_true']}` == me.id) {
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = !(me.checked);
} else if (`${sect}_${window.config[sect][ent]['depends_false']}` == me.id) {
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = me.checked;
}
}
}
};
} 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}" ${required ? 'required' : ''}>
`;
} else if (entryType == 'select') {
entryGroup.classList.add("form-group");
const entryOptions: Array<string> = window.config[section][entry]["options"];
let innerGroup = `
<label for="${section}_${entry}">${entryName}</label>
<select class="form-control" id="${section}_${entry}" ${required ? 'required' : ''}>
`;
for (const z in entryOptions) {
const entryOption = entryOptions[z];
let selected: boolean = (entryOption == entryValue);
innerGroup += `
<option value="${entryOption}" ${selected ? 'selected' : ''}>${entryOption}</option>
`;
}
innerGroup += `</select>`;
entryGroup.innerHTML = innerGroup;
}
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
}
settingsList.innerHTML += `
<button type="button" class="list-group-item list-group-item-action" id="${section}_button" onclick="showSetting('${section}')">${title}</button>
`;
settingsContent.appendChild(sectionCollapse);
}
if (callback) {
callback();
}
}
});
export function showSetting(id: string, runBefore?: () => void): void {
const els = document.getElementById('settingsLeft').querySelectorAll("button[type=button]:not(.static)") as NodeListOf<HTMLButtonElement>;
for (let i = 0; i < els.length; i++) {
const el = els[i];
if (el.id != `${id}_button`) {
rmAttr(el, "active");
}
const sectEl = document.getElementById(el.id.replace("_button", ""));
if (sectEl.id != id) {
Unfocus(sectEl);
}
}
addAttr(document.getElementById(`${id}_button`), "active");
const section = document.getElementById(id);
if (runBefore) {
runBefore();
}
Focus(section);
if (screen.width <= 1100) {
// ugly
setTimeout((): void => section.scrollIntoView(<ScrollIntoViewOptions>{ block: "center", behavior: "smooth" }), 200);
}
}

View File

@ -1,4 +1,7 @@
const ombiDefaultsModal = createModal('ombiDefaults');
import { _get, _post, _delete, rmAttr, addAttr } from "modules/common.js";
const ombiDefaultsModal = window.BS.newModal('ombiDefaults');
(document.getElementById('openOmbiDefaults') as HTMLButtonElement).onclick = function (): void {
let button = this as HTMLButtonElement;
button.disabled = true;

View File

@ -1,9 +1,28 @@
var config: Object = {};
var modifiedConfig: Object = {};
import { _post, _get, _delete, rmAttr, addAttr } from "./modules/common.js";
import { generateInvites } from "./modules/invites.js";
import { populateRadios } from "./modules/accounts.js";
import { Focus, Unfocus } from "./modules/admin.js";
import { showSetting, populateProfiles } from "./modules/settings.js";
interface aWindow extends Window {
setDefaultProfile(name: string): void;
deleteProfile(name: string): void;
createProfile(): void;
showSetting(id: string, runBefore?: () => void): void;
config: Object;
modifiedConfig: Object;
}
declare var window: aWindow;
window.config = {};
window.modifiedConfig = {};
window.showSetting = showSetting;
function sendConfig(restart?: boolean): void {
modifiedConfig["restart-program"] = restart;
_post("/config", modifiedConfig, function (): void {
window.modifiedConfig["restart-program"] = restart;
_post("/config", window.modifiedConfig, function (): void {
if (this.readyState == 4) {
const save = document.getElementById("settingsSave") as HTMLButtonElement
if (this.status == 200 || this.status == 204) {
@ -19,159 +38,22 @@ function sendConfig(restart?: boolean): void {
save.textContent = "Save";
}
if (restart) {
refreshModal.show();
window.Modals.refresh.show();
}
}
});
}
(document.getElementById('openAbout') as HTMLButtonElement).onclick = (): void => {
aboutModal.show();
window.Modals.about.show();
};
const openSettings = (settingsList: HTMLElement, settingsContent: HTMLElement, callback?: () => void): void => _get("/config", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
settingsList.textContent = '';
config = this.response;
for (const i in config["order"]) {
const section: string = config["order"][i]
const sectionCollapse = document.createElement('div') as HTMLDivElement;
Unfocus(sectionCollapse);
sectionCollapse.id = section;
const title: string = config[section]["meta"]["name"];
const description: string = config[section]["meta"]["description"];
const entryListID: string = `${section}_entryList`;
// const footerID: string = `${section}_footer`;
sectionCollapse.innerHTML = `
<div class="card card-body">
<small class="text-muted">${description}</small>
<div class="${entryListID}">
</div>
</div>
`;
for (const x in config[section]["order"]) {
const entry: string = config[section]["order"][x];
if (entry == "meta") {
continue;
}
let entryName: string = config[section][entry]["name"];
let 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 ("description" in config[section][entry]) {
entryName +=`
<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>
`;
}
const entryValue: boolean | string = config[section][entry]["value"];
const entryType: string = config[section][entry]["type"];
const entryGroup = document.createElement('div');
if (entryType == "bool") {
entryGroup.classList.add("form-check");
entryGroup.innerHTML = `
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}" ${(entryValue as boolean) ? 'checked': ''} ${required ? 'required' : ''}>
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
`;
(entryGroup.querySelector('input[type=checkbox]') as HTMLInputElement).onclick = function (): void {
const me = this as HTMLInputElement;
for (const y in config["order"]) {
const sect: string = config["order"][y];
for (const z in config[sect]["order"]) {
const ent: string = config[sect]["order"][z];
if (`${sect}_${config[sect][ent]['depends_true']}` == me.id) {
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = !(me.checked);
} else if (`${sect}_${config[sect][ent]['depends_false']}` == me.id) {
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = me.checked;
}
}
}
};
} 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}" ${required ? 'required' : ''}>
`;
} else if (entryType == 'select') {
entryGroup.classList.add("form-group");
const entryOptions: Array<string> = config[section][entry]["options"];
let innerGroup = `
<label for="${section}_${entry}">${entryName}</label>
<select class="form-control" id="${section}_${entry}" ${required ? 'required' : ''}>
`;
for (const z in entryOptions) {
const entryOption = entryOptions[z];
let selected: boolean = (entryOption == entryValue);
innerGroup += `
<option value="${entryOption}" ${selected ? 'selected' : ''}>${entryOption}</option>
`;
}
innerGroup += `</select>`;
entryGroup.innerHTML = innerGroup;
}
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
}
settingsList.innerHTML += `
<button type="button" class="list-group-item list-group-item-action" id="${section}_button" onclick="showSetting('${section}')">${title}</button>
`;
settingsContent.appendChild(sectionCollapse);
}
if (callback) {
callback();
}
}
});
interface Profile {
Admin: boolean;
LibraryAccess: string;
FromUser: string;
}
(document.getElementById('profiles_button') as HTMLButtonElement).onclick = (): void => showSetting("profiles", populateProfiles);
const populateProfiles = (noTable?: boolean): void => _get("/profiles", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
const profileList = document.getElementById('profileList');
profileList.textContent = '';
availableProfiles = [this.response["default_profile"]];
for (let name in this.response["profiles"]) {
if (name != availableProfiles[0]) {
availableProfiles.push(name);
}
const reqProfile = this.response["profiles"][name];
if (!noTable && name != "default_profile") {
const profile: Profile = {
Admin: reqProfile["admin"],
LibraryAccess: reqProfile["libraries"],
FromUser: reqProfile["fromUser"]
};
profileList.innerHTML += `
<td nowrap="nowrap" class="align-middle"><strong>${name}</strong></td>
<td nowrap="nowrap" class="align-middle"><input class="${(bsVersion == 5) ? "form-check-input" : ""}" type="radio" name="defaultProfile" onclick="setDefaultProfile('${name}')" ${(name == availableProfiles[0]) ? "checked" : ""}></td>
<td nowrap="nowrap" class="align-middle">${profile.FromUser}</td>
<td nowrap="nowrap" class="align-middle">${profile.Admin ? "Yes" : "No"}</td>
<td nowrap="nowrap" class="align-middle">${profile.LibraryAccess}</td>
<td nowrap="nowrap" class="align-middle"><button class="btn btn-outline-danger" id="defaultProfile_${name}" onclick="deleteProfile('${name}')">Delete</button></td>
`;
}
}
}
});
const setDefaultProfile = (name: string): void => _post("/profiles/default", { "name": name }, function (): void {
window.setDefaultProfile = (name: string): void => _post("/profiles/default", { "name": name }, function (): void {
if (this.readyState == 4) {
if (this.status != 200) {
(document.getElementById(`defaultProfile_${availableProfiles[0]}`) as HTMLInputElement).checked = true;
(document.getElementById(`defaultProfile_${window.availableProfiles[0]}`) as HTMLInputElement).checked = true;
(document.getElementById(`defaultProfile_${name}`) as HTMLInputElement).checked = false;
} else {
generateInvites();
@ -179,7 +61,7 @@ const setDefaultProfile = (name: string): void => _post("/profiles/default", { "
}
});
const deleteProfile = (name: string): void => _delete("/profiles", { "name": name }, function (): void {
window.deleteProfile = (name: string): void => _delete("/profiles", { "name": name }, function (): void {
if (this.readyState == 4 && this.status == 200) {
populateProfiles();
}
@ -187,7 +69,7 @@ const deleteProfile = (name: string): void => _delete("/profiles", { "name": nam
const createProfile = (): void => _get("/users", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
jfUsers = this.response["users"];
window.jfUsers = this.response["users"];
populateRadios();
const submitButton = document.getElementById('storeDefaults') as HTMLButtonElement;
submitButton.disabled = false;
@ -205,10 +87,12 @@ const createProfile = (): void => _get("/users", null, function (): void {
Focus(document.getElementById('newProfileBox'));
(document.getElementById('newProfileName') as HTMLInputElement).value = '';
Focus(document.getElementById('defaultUserRadiosBox'));
userDefaultsModal.show();
window.Modals.userDefaults.show();
}
});
window.createProfile = createProfile;
function storeProfile(): void {
this.disabled = true;
this.innerHTML =
@ -239,7 +123,7 @@ function storeProfile(): void {
addAttr(button, "btn-primary");
rmAttr(button, "btn-success");
button.disabled = false;
userDefaultsModal.hide();
window.Modals.userDefaults.hide();
}, 1000);
populateProfiles();
@ -265,41 +149,17 @@ function storeProfile(): void {
});
}
function showSetting(id: string, runBefore?: () => void): void {
const els = document.getElementById('settingsLeft').querySelectorAll("button[type=button]:not(.static)") as NodeListOf<HTMLButtonElement>;
for (let i = 0; i < els.length; i++) {
const el = els[i];
if (el.id != `${id}_button`) {
rmAttr(el, "active");
}
const sectEl = document.getElementById(el.id.replace("_button", ""));
if (sectEl.id != id) {
Unfocus(sectEl);
}
}
addAttr(document.getElementById(`${id}_button`), "active");
const section = document.getElementById(id);
if (runBefore) {
runBefore();
}
Focus(section);
if (screen.width <= 1100) {
// ugly
setTimeout((): void => section.scrollIntoView(<ScrollIntoViewOptions>{ block: "center", behavior: "smooth" }), 200);
}
}
// (document.getElementById('openSettings') as HTMLButtonElement).onclick = (): void => openSettings(document.getElementById('settingsList'), document.getElementById('settingsList'), (): void => settingsModal.show());
(document.getElementById('settingsSave') as HTMLButtonElement).onclick = function (): void {
modifiedConfig = {};
window.modifiedConfig = {};
const save = this as HTMLButtonElement;
let restartSettingsChanged = false;
let settingsChanged = false;
for (const i in config["order"]) {
const section = config["order"][i];
for (const x in config[section]["order"]) {
const entry = config[section]["order"][x];
for (const i in window.config["order"]) {
const section = window.config["order"][i];
for (const x in window.config[section]["order"]) {
const entry = window.config[section]["order"][x];
if (entry == "meta") {
continue;
}
@ -311,13 +171,13 @@ function showSetting(id: string, runBefore?: () => void): void {
} else {
val = el.value.toString();
}
if (val != config[section][entry]["value"].toString()) {
if (!(section in modifiedConfig)) {
modifiedConfig[section] = {};
if (val != window.config[section][entry]["value"].toString()) {
if (!(section in window.modifiedConfig)) {
window.modifiedConfig[section] = {};
}
modifiedConfig[section][entry] = val;
window.modifiedConfig[section][entry] = val;
settingsChanged = true;
if (config[section][entry]["requires_restart"]) {
if (window.config[section][entry]["requires_restart"]) {
restartSettingsChanged = true;
}
}
@ -333,7 +193,7 @@ function showSetting(id: string, runBefore?: () => void): void {
if (restartButton) {
restartButton.onclick = (): void => sendConfig(true);
}
restartModal.show();
window.Modals.restart.show();
} else if (settingsChanged) {
save.innerHTML = spinnerHTML;
sendConfig();

View File

@ -3,6 +3,6 @@
"outDir": "../data/static",
"target": "es6",
"lib": ["dom", "es2017"],
"types": ["jquery"]
"typeRoots": ["./node_modules/@types", "./typings"]
}
}

61
ts/typings/d.ts Normal file
View File

@ -0,0 +1,61 @@
declare interface ModalConstructor {
(id: string, find?: boolean): BSModal;
}
declare interface BSModal {
el: HTMLDivElement;
modal: any;
show: () => void;
hide: () => void;
}
declare interface Window {
getComputedStyle(element: HTMLElement, pseudoElt: HTMLElement): any;
bsVersion: number;
bs5: boolean;
BS: Bootstrap;
Modals: BSModals;
cssFile: string;
availableProfiles: Array<any>;
jfUsers: Array<Object>;
notifications_enabled: boolean;
token: string;
buttonWidth: number;
}
declare interface tooltipTrigger {
(): void;
}
declare interface Bootstrap {
newModal: ModalConstructor;
triggerTooltips: tooltipTrigger;
Compat?(): void;
}
declare interface BSModals {
login: BSModal;
userDefaults: BSModal;
users: BSModal;
restart: BSModal;
refresh: BSModal;
about: BSModal;
delete: BSModal;
newUser: BSModal;
}
interface Invite {
code?: string;
expiresIn?: string;
empty: boolean;
remainingUses?: string;
email?: string;
usedBy?: Array<Array<string>>;
created?: string;
notifyExpiry?: boolean;
notifyCreation?: boolean;
profile?: string;
}
declare var config: Object;
declare var modifiedConfig: Object;

View File

@ -2,6 +2,7 @@ package main
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
@ -30,8 +31,10 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
// if app.checkInvite(code, false, "") {
if _, ok := app.storage.invites[code]; ok {
email := app.storage.invites[code].Email
gc.HTML(http.StatusOK, "form.html", gin.H{
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
if strings.Contains(email, "Failed") {
email = ""
}
gc.HTML(http.StatusOK, "form-loader.html", gin.H{
"cssFile": app.cssFile,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"helpMessage": app.config.Section("ui").Key("help_message").String(),
@ -40,7 +43,10 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"validate": app.config.Section("password_validation").Key("enabled").MustBool(false),
"requirements": app.validator.getCriteria(),
"email": email,
"username": !app.config.Section("email").Key("no_username").MustBool(false),
"settings": map[string]bool{
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
"username": !app.config.Section("email").Key("no_username").MustBool(false),
},
})
} else {
gc.HTML(404, "invalidCode.html", gin.H{