From 301f502052a1763d9995c624bbab96cd0dae9de2 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 22 Oct 2020 17:50:40 +0100 Subject: [PATCH] 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. --- .gitignore | 1 + Makefile | 6 +- api.go | 4 +- data/templates/admin.html | 30 ++- data/templates/form-base.html | 7 + data/templates/form-loader.html | 1 + data/templates/form.html | 21 +-- package-lock.json | 6 +- package.json | 2 +- pwval.go | 2 +- ts/accounts.ts | 130 ++----------- ts/admin.ts | 131 +++++--------- ts/bs4.ts | 36 ---- ts/bs5.ts | 34 ---- ts/form.ts | 53 +++--- ts/invites.ts | 311 +------------------------------- ts/modules/accounts.ts | 106 +++++++++++ ts/modules/admin.ts | 68 +++++++ ts/{ => modules}/animation.ts | 18 +- ts/modules/bs4.ts | 45 +++++ ts/modules/bs5.ts | 37 ++++ ts/{ => modules}/common.ts | 16 +- ts/modules/invites.ts | 297 ++++++++++++++++++++++++++++++ ts/modules/settings.ts | 164 +++++++++++++++++ ts/ombi.ts | 5 +- ts/settings.ts | 228 +++++------------------ ts/tsconfig.json | 2 +- ts/typings/d.ts | 61 +++++++ views.go | 12 +- 29 files changed, 988 insertions(+), 846 deletions(-) create mode 100644 data/templates/form-base.html create mode 100644 data/templates/form-loader.html delete mode 100644 ts/bs4.ts delete mode 100644 ts/bs5.ts create mode 100644 ts/modules/accounts.ts create mode 100644 ts/modules/admin.ts rename ts/{ => modules}/animation.ts (85%) create mode 100644 ts/modules/bs4.ts create mode 100644 ts/modules/bs5.ts rename ts/{ => modules}/common.ts (80%) create mode 100644 ts/modules/invites.ts create mode 100644 ts/modules/settings.ts create mode 100644 ts/typings/d.ts diff --git a/.gitignore b/.gitignore index c07ba1a..131f700 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile index 42f69ea..d126cfb 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/api.go b/api.go index d9e361b..0c40db8 100644 --- a/api.go +++ b/api.go @@ -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, diff --git a/data/templates/admin.html b/data/templates/admin.html index 6860a79..6633b1a 100644 --- a/data/templates/admin.html +++ b/data/templates/admin.html @@ -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 @@

{{ .contactMessage }}

- - - {{ if .bs5 }} - - {{ else }} - - {{ end }} - - - - - + + {{ if .ombiEnabled }} - + {{ end }} diff --git a/data/templates/form-base.html b/data/templates/form-base.html new file mode 100644 index 0000000..087ad69 --- /dev/null +++ b/data/templates/form-base.html @@ -0,0 +1,7 @@ +{{ define "form-base" }} + + +{{ end }} diff --git a/data/templates/form-loader.html b/data/templates/form-loader.html new file mode 100644 index 0000000..6585322 --- /dev/null +++ b/data/templates/form-loader.html @@ -0,0 +1 @@ +{{ template "form.html" . }} diff --git a/data/templates/form.html b/data/templates/form.html index d9da755..c1e87fe 100644 --- a/data/templates/form.html +++ b/data/templates/form.html @@ -14,11 +14,11 @@ - {{ if not .bs5 }} + {{ if not .settings.bs5 }} {{ end }} - {{ if .bs5 }} + {{ if .settings.bs5 }} {{ else }} @@ -74,9 +74,9 @@
- +
- {{ if .username }} + {{ if .settings.username }}
@@ -114,10 +114,8 @@
- - - + {{ template "form-base" .settings }} - + + diff --git a/package-lock.json b/package-lock.json index 817fae0..f39312e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": "*" } diff --git a/package.json b/package.json index 09c2a49..1f8fa12 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pwval.go b/pwval.go index abe3415..8699e7e 100644 --- a/pwval.go +++ b/pwval.go @@ -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 { diff --git a/ts/accounts.ts b/ts/accounts.ts index f81fa8c..f44f922 100644 --- a/ts/accounts.ts +++ b/ts/accounts.ts @@ -1,25 +1,17 @@ -const checkCheckboxes = (): void => { - const defaultsButton = document.getElementById('accountsTabSetDefaults'); - const deleteButton = document.getElementById('accountsTabDelete'); - const checkboxes: NodeListOf = 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; - -function populateUsers(): void { - const acList = document.getElementById('accountsList'); - acList.innerHTML = ` -
- Getting Users... - -
- `; - 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 = ` - - - `; - 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 ` - - ${username} - ${generateEmail(id, name, email)} - ${lastActive} - ${isAdmin} - `; - }; - - _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 = ` - - `; - radioList.appendChild(radio); - } -} +console.log("bruh"); (document.getElementById('selectAll')).onclick = function (): void { const checkboxes: NodeListOf = 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(); }; (document.getElementById('selectAll')).checked = false; @@ -236,7 +152,7 @@ function populateRadios(): void { (document.getElementById('accountsTabSetDefaults')).onclick = function (): void { const checkboxes: NodeListOf = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked'); let userIDs: Array = 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 += ` - + `; } 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(); }; (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) { (document.getElementById('newUserName')).value = ''; } - newUserModal.show(); + window.Modals.newUser.show(); }; - - - - - - diff --git a/ts/admin.ts b/ts/admin.ts index 0134512..ecd200d 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -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; @@ -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; 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): void { - // not sure if this does anything, but w/e - this.disabled = true; - this.innerHTML = - '' + - '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(); } }); diff --git a/ts/bs4.ts b/ts/bs4.ts deleted file mode 100644 index 61131e0..0000000 --- a/ts/bs4.ts +++ /dev/null @@ -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(); - }); -} - diff --git a/ts/bs5.ts b/ts/bs5.ts deleted file mode 100644 index 5dc69cd..0000000 --- a/ts/bs5.ts +++ /dev/null @@ -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); - }); -} - diff --git a/ts/form.ts b/ts/form.ts index 804b113..247c46f 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -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 { diff --git a/ts/invites.ts b/ts/invites.ts index 8eb5002..0b2efd7 100644 --- a/ts/invites.ts +++ b/ts/invites.ts @@ -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>; - 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>): string { - let uB = ""; - if (usedBy && usedBy.length != 0) { - uB = ` -
    -
  • Users created:
  • - `; - for (const i in usedBy) { - uB += ` -
  • -
    ${usedBy[i][0]}
    -
    ${usedBy[i][1]}
    -
  • - `; - } - uB += `
` - } - 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 = `None`; - if (invite.empty) { - item.innerHTML = ` -
- ${innerHTML} -
- `; - container.appendChild(item); - links.appendChild(container); - return; - } - link = window.location.href.split('#')[0] + "invite/" + invite.code; - innerHTML = ` -
- ${invite.code.replace(/-/g, '-')} - - `; - if (invite.email) { - let email = invite.email; - if (!invite.email.includes("Failed to send to")) { - email = `Sent to ${email}`; - } - innerHTML += ` - ${email} - `; - } - innerHTML += ` -
-
- ${invite.expiresIn} -
- - -
-
- `; - - item.innerHTML = innerHTML; - container.appendChild(item); - - let profiles = ` - - `; - - let dateCreated: string; - if (invite.created) { - dateCreated = `
  • Created: ${invite.created}
  • `; - } - - let middle: string; - if (notifications_enabled) { - middle = ` -
    -
      - Notify on: -
    • - - -
    • -
    • - - -
    • -
    -
    - `; - } - - 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 = ` -
    -
    -
      -
    • - ${profiles} -
    • - ${dateCreated} -
    • Remaining uses: ${invite.remainingUses}
    • -
    -
    - ${middle} -
    - ${right} -
    -
    - `; - - 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 += ` - - `; - } - innerHTML += ` - - `; - 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 = [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 = [+(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 = ["days", "hours", "minutes"]; for (const i in nE) { document.getElementById(nE[i]).addEventListener("change", checkDuration); diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts new file mode 100644 index 0000000..386f9ff --- /dev/null +++ b/ts/modules/accounts.ts @@ -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 = 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 = ` +
    + Getting Users... + +
    + `; + 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 = ` + + + `; + 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 ` + + ${username} + ${generateEmail(id, name, email)} + ${lastActive} + ${isAdmin} + `; + }; + + _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 = ` + + `; + radioList.appendChild(radio); + } +} + diff --git a/ts/modules/admin.ts b/ts/modules/admin.ts new file mode 100644 index 0000000..7fb983d --- /dev/null +++ b/ts/modules/admin.ts @@ -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): void { + const button = document.getElementById('storeDefaults') as HTMLButtonElement; + button.disabled = true; + button.innerHTML = + '' + + '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); + } + } + }); +} diff --git a/ts/animation.ts b/ts/modules/animation.ts similarity index 85% rename from ts/animation.ts rename to ts/modules/animation.ts index 8b5b8c5..248d753 100644 --- a/ts/animation.ts +++ b/ts/modules/animation.ts @@ -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"); diff --git a/ts/modules/bs4.ts b/ts/modules/bs4.ts new file mode 100644 index 0000000..58c7023 --- /dev/null +++ b/ts/modules/bs4.ts @@ -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); + }; +} diff --git a/ts/modules/bs5.ts b/ts/modules/bs5.ts new file mode 100644 index 0000000..2c2e166 --- /dev/null +++ b/ts/modules/bs5.ts @@ -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); + }; +}; diff --git a/ts/common.ts b/ts/modules/common.ts similarity index 80% rename from ts/common.ts rename to ts/modules/common.ts index f4aa24e..f918161 100644 --- a/ts/common.ts +++ b/ts/modules/common.ts @@ -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 + ":")); diff --git a/ts/modules/invites.ts b/ts/modules/invites.ts new file mode 100644 index 0000000..8e1e2b1 --- /dev/null +++ b/ts/modules/invites.ts @@ -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>): string { + let uB = ""; + if (usedBy && usedBy.length != 0) { + uB = ` +
      +
    • Users created:
    • + `; + for (const i in usedBy) { + uB += ` +
    • +
      ${usedBy[i][0]}
      +
      ${usedBy[i][1]}
      +
    • + `; + } + uB += `
    ` + } + 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 = `None`; + if (invite.empty) { + item.innerHTML = ` +
    + ${innerHTML} +
    + `; + container.appendChild(item); + links.appendChild(container); + return; + } + link = window.location.href.split('#')[0] + "invite/" + invite.code; + innerHTML = ` +
    + ${invite.code.replace(/-/g, '-')} + + `; + if (invite.email) { + let email = invite.email; + if (!invite.email.includes("Failed to send to")) { + email = `Sent to ${email}`; + } + innerHTML += ` + ${email} + `; + } + innerHTML += ` +
    +
    + ${invite.expiresIn} +
    + + +
    +
    + `; + + item.innerHTML = innerHTML; + container.appendChild(item); + + let profiles = ` + + `; + + let dateCreated: string; + if (invite.created) { + dateCreated = `
  • Created: ${invite.created}
  • `; + } + + let middle: string; + if (window.notifications_enabled) { + middle = ` +
    +
      + Notify on: +
    • + + +
    • +
    • + + +
    • +
    +
    + `; + } + + 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 = ` +
    +
    +
      +
    • + ${profiles} +
    • + ${dateCreated} +
    • Remaining uses: ${invite.remainingUses}
    • +
    +
    + ${middle} +
    + ${right} +
    +
    + `; + + 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 += ` + + `; + } + innerHTML += ` + + `; + 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 = [+(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; + } +} diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts new file mode 100644 index 0000000..642b148 --- /dev/null +++ b/ts/modules/settings.ts @@ -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 += ` + ${name} + + ${profile.FromUser} + ${profile.Admin ? "Yes" : "No"} + ${profile.LibraryAccess} + + `; + } + } + } +}); + +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 = ` +
    + ${description} +
    +
    +
    + `; + + 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 += ` *`; + required = true; + } + if (window.config[section][entry]["requires_restart"]) { + entryName += ` R`; + } + if ("description" in window.config[section][entry]) { + entryName +=` + + `; + } + 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 = ` + + + `; + (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 = ` + + + `; + } else if (entryType == 'select') { + entryGroup.classList.add("form-group"); + const entryOptions: Array = window.config[section][entry]["options"]; + let innerGroup = ` + + `; + entryGroup.innerHTML = innerGroup; + } + sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup); + } + + settingsList.innerHTML += ` + + `; + 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; + 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({ block: "center", behavior: "smooth" }), 200); + } +} + diff --git a/ts/ombi.ts b/ts/ombi.ts index 3a14d02..febf691 100644 --- a/ts/ombi.ts +++ b/ts/ombi.ts @@ -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; diff --git a/ts/settings.ts b/ts/settings.ts index e89b2fb..e5da911 100644 --- a/ts/settings.ts +++ b/ts/settings.ts @@ -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 = ` -
    - ${description} -
    -
    -
    - `; - - 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 += ` *`; - required = true; - } - if (config[section][entry]["requires_restart"]) { - entryName += ` R`; - } - if ("description" in config[section][entry]) { - entryName +=` - - `; - } - 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 = ` - - - `; - (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 = ` - - - `; - } else if (entryType == 'select') { - entryGroup.classList.add("form-group"); - const entryOptions: Array = config[section][entry]["options"]; - let innerGroup = ` - - `; - entryGroup.innerHTML = innerGroup; - } - sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup); - } - - settingsList.innerHTML += ` - - `; - 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 += ` - ${name} - - ${profile.FromUser} - ${profile.Admin ? "Yes" : "No"} - ${profile.LibraryAccess} - - `; - } - } - } -}); - -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; - 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({ 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(); diff --git a/ts/tsconfig.json b/ts/tsconfig.json index b380a8b..0905779 100644 --- a/ts/tsconfig.json +++ b/ts/tsconfig.json @@ -3,6 +3,6 @@ "outDir": "../data/static", "target": "es6", "lib": ["dom", "es2017"], - "types": ["jquery"] + "typeRoots": ["./node_modules/@types", "./typings"] } } diff --git a/ts/typings/d.ts b/ts/typings/d.ts new file mode 100644 index 0000000..7bc6c51 --- /dev/null +++ b/ts/typings/d.ts @@ -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; + jfUsers: Array; + 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>; + created?: string; + notifyExpiry?: boolean; + notifyCreation?: boolean; + profile?: string; +} + +declare var config: Object; +declare var modifiedConfig: Object; diff --git a/views.go b/views.go index 305ca34..e60156f 100644 --- a/views.go +++ b/views.go @@ -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{