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{