From 12ce669566e4443d352e4a7e29c5f2d06e8da883 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 22 Jun 2023 18:31:16 +0100 Subject: [PATCH] userpage: add password change card, validation, rearrange page functionality not done yet, just comitting here because there were lots of adjustments to layout stuff, accomodating for most combinations of card presence/size. --- html/user.html | 40 +++++++++++++-- lang/form/en-us.json | 5 +- ts/form.ts | 29 +++++++---- ts/modules/validator.ts | 110 +++++++++++++++++++++++----------------- ts/pwr.ts | 16 ++++-- ts/user.ts | 70 +++++++++++++++++++------ views.go | 3 +- 7 files changed, 191 insertions(+), 82 deletions(-) diff --git a/html/user.html b/html/user.html index 2c5cf5d..d75b1b9 100644 --- a/html/user.html +++ b/html/user.html @@ -23,6 +23,7 @@ window.matrixEnabled = {{ .matrixEnabled }}; window.matrixRequired = {{ .matrixRequired }}; window.matrixUserID = "{{ .matrixUser }}"; + window.validationStrings = JSON.parse({{ .validationStrings }}); {{ template "header.html" . }} {{ .lang.Strings.myAccount }} @@ -113,10 +114,41 @@ {{ .strings.contactMethods }}
-
- {{ .strings.expiry }} - -
+
+
+ {{ .strings.changePassword }} +
+
+ {{ .strings.passwordRequirementsHeader }} +
    + {{ range $key, $value := .requirements }} +
  • + +
  • + {{ end }} +
+
+
+ + + + + + + + + {{ .strings.changePassword }} + +
+
+
+
+
+
+ {{ .strings.expiry }} + +
+
diff --git a/lang/form/en-us.json b/lang/form/en-us.json index 23a120c..13ef671 100644 --- a/lang/form/en-us.json +++ b/lang/form/en-us.json @@ -8,6 +8,8 @@ "accountDetails": "Details", "emailAddress": "Email", "username": "Username", + "oldPassword": "Old Password", + "newPassword": "New Password", "password": "Password", "reEnterPassword": "Re-enter Password", "reEnterPasswordInvalid": "Passwords are not the same.", @@ -31,7 +33,8 @@ "resetPasswordThroughJellyfin": "To reset your password, visit {jfLink} and press the \"Forgot Password\" button.", "resetPasswordThroughLink": "To reset your password, enter your username, email address or a linked contact method username, and submit. A link will be sent to reset your password.", "resetSent": "Reset Sent.", - "resetSentDescription": "If an account with the given username/contact method exists, a password reset link has been sent via all contact methods available. The code will expire in 30 minutes." + "resetSentDescription": "If an account with the given username/contact method exists, a password reset link has been sent via all contact methods available. The code will expire in 30 minutes.", + "changePassword": "Change Password" }, "notifications": { "errorUserExists": "User already exists.", diff --git a/ts/form.ts b/ts/form.ts index 3cd4ba2..8707aa9 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -2,7 +2,7 @@ import { Modal } from "./modules/modal.js"; import { notificationBox, whichAnimationEvent } from "./modules/common.js"; import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from "./modules/common.js"; import { loadLangSelector } from "./modules/lang.js"; -import { initValidator } from "./modules/validator.js"; +import { Validator, ValidatorConf } from "./modules/validator.js"; import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js"; interface formWindow extends Window { @@ -69,7 +69,7 @@ if (window.telegramEnabled) { const radio = document.getElementById("contact-via-telegram") as HTMLInputElement; radio.parentElement.classList.remove("unfocused"); radio.checked = true; - validatorFunc(); + validator.validate(); } }; @@ -101,7 +101,7 @@ if (window.discordEnabled) { const radio = document.getElementById("contact-via-discord") as HTMLInputElement; radio.parentElement.classList.remove("unfocused") radio.checked = true; - validatorFunc(); + validator.validate(); } }; @@ -133,7 +133,7 @@ if (window.matrixEnabled) { const radio = document.getElementById("contact-via-matrix") as HTMLInputElement; radio.parentElement.classList.remove("unfocused"); radio.checked = true; - validatorFunc(); + validator.validate(); } }; @@ -162,7 +162,7 @@ if (window.userExpiryEnabled) { } const form = document.getElementById("form-create") as HTMLFormElement; -const submitButton = form.querySelector("input[type=submit]") as HTMLInputElement; +const submitInput = form.querySelector("input[type=submit]") as HTMLInputElement; const submitSpan = form.querySelector("span.submit") as HTMLSpanElement; const submitText = submitSpan.textContent; let usernameField = document.getElementById("create-username") as HTMLInputElement; @@ -242,12 +242,19 @@ interface GreCAPTCHA { declare var grecaptcha: GreCAPTCHA -let r = initValidator(passwordField, rePasswordField, submitButton, submitSpan, baseValidator); -var requirements = r[0]; -var validatorFunc = r[1] as () => void; +let validatorConf: ValidatorConf = { + passwordField: passwordField, + rePasswordField: rePasswordField, + submitInput: submitInput, + submitButton: submitSpan, + validatorFunc: baseValidator +}; + +let validator = new Validator(validatorConf); +var requirements = validator.requirements; if (window.emailRequired) { - emailField.addEventListener("keyup", validatorFunc) + emailField.addEventListener("keyup", validator.validate) } interface respDTO { @@ -287,7 +294,7 @@ const genCaptcha = () => { if (window.captcha && !window.reCAPTCHA) { genCaptcha(); (document.getElementById("captcha-regen") as HTMLSpanElement).onclick = genCaptcha; - captchaInput.onkeyup = validatorFunc; + captchaInput.onkeyup = validator.validate; } const create = (event: SubmitEvent) => { @@ -386,6 +393,6 @@ const create = (event: SubmitEvent) => { }); }; -validatorFunc(); +validator.validate(); form.onsubmit = create; diff --git a/ts/modules/validator.ts b/ts/modules/validator.ts index 728a800..e535070 100644 --- a/ts/modules/validator.ts +++ b/ts/modules/validator.ts @@ -60,8 +60,21 @@ class Requirement { validate = (count: number) => { this.valid = (count >= this._minCount); } } -export function initValidator(passwordField: HTMLInputElement, rePasswordField: HTMLInputElement, submitButton: HTMLInputElement, submitSpan: HTMLSpanElement, validatorFunc?: (oncomplete: (valid: boolean) => void) => void): ({ [category: string]: Requirement }|(() => void))[] { - var defaultPwValStrings: pwValStrings = { +export interface ValidatorConf { + passwordField: HTMLInputElement; + rePasswordField: HTMLInputElement; + submitInput?: HTMLInputElement; + submitButton: HTMLSpanElement; + validatorFunc?: (oncomplete: (valid: boolean) => void) => void; +} + +export interface Validation { [name: string]: number } +export interface Requirements { [category: string]: Requirement }; + +export class Validator { + private _conf: ValidatorConf; + private _requirements: Requirements = {}; + private _defaultPwValStrings: pwValStrings = { length: { singular: "Must have at least {n} character", plural: "Must have at least {n} characters" @@ -82,39 +95,34 @@ export function initValidator(passwordField: HTMLInputElement, rePasswordField: singular: "Must have at least {n} special character", plural: "Must have at least {n} special characters" } + }; + + private _checkPasswords = () => { + return this._conf.passwordField.value == this._conf.rePasswordField.value; } - const checkPasswords = () => { - return passwordField.value == rePasswordField.value; - } - - const checkValidity = () => { - const pw = checkPasswords(); - validatorFunc((valid: boolean) => { + validate = () => { + const pw = this._checkPasswords(); + this._conf.validatorFunc((valid: boolean) => { if (pw && valid) { - rePasswordField.setCustomValidity(""); - submitButton.disabled = false; - submitSpan.removeAttribute("disabled"); + this._conf.rePasswordField.setCustomValidity(""); + if (this._conf.submitInput) this._conf.submitInput.disabled = false; + this._conf.submitButton.removeAttribute("disabled"); } else if (!pw) { - rePasswordField.setCustomValidity(window.invalidPassword); - submitButton.disabled = true; - submitSpan.setAttribute("disabled", ""); + this._conf.rePasswordField.setCustomValidity(window.invalidPassword); + if (this._conf.submitInput) this._conf.submitInput.disabled = true; + this._conf.submitButton.setAttribute("disabled", ""); } else { - rePasswordField.setCustomValidity(""); - submitButton.disabled = true; - submitSpan.setAttribute("disabled", ""); + this._conf.rePasswordField.setCustomValidity(""); + if (this._conf.submitInput) this._conf.submitInput.disabled = true; + this._conf.submitButton.setAttribute("disabled", ""); } }); }; - rePasswordField.addEventListener("keyup", checkValidity); - passwordField.addEventListener("keyup", checkValidity); - - - // Incredible code right here - const isInt = (s: string): boolean => { return (s in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); } - - const testStrings = (f: pwValString): boolean => { + private _isInt = (s: string): boolean => { return (s >= '0' && s <= '9'); } + + private _testStrings = (f: pwValString): boolean => { const testString = (s: string): boolean => { if (s == "" || !s.includes("{n}")) { return false; } return true; @@ -122,14 +130,12 @@ export function initValidator(passwordField: HTMLInputElement, rePasswordField: return testString(f.singular) && testString(f.plural); } - interface Validation { [name: string]: number } - - const validate = (s: string): Validation => { + private _validate = (s: string): Validation => { let v: Validation = {}; for (let criteria of ["length", "lowercase", "uppercase", "number", "special"]) { v[criteria] = 0; } v["length"] = s.length; for (let c of s) { - if (isInt(c)) { v["number"]++; } + if (this._isInt(c)) { v["number"]++; } else { const upper = c.toUpperCase(); if (upper == c.toLowerCase()) { v["special"]++; } @@ -141,27 +147,37 @@ export function initValidator(passwordField: HTMLInputElement, rePasswordField: } return v } - passwordField.addEventListener("keyup", () => { - const v = validate(passwordField.value); - for (let criteria in requirements) { - requirements[criteria].validate(v[criteria]); - } - }); - - var requirements: { [category: string]: Requirement } = {}; - - if (!window.validationStrings) { - window.validationStrings = defaultPwValStrings; - } else { + + private _bindRequirements = () => { for (let category in window.validationStrings) { - if (!testStrings(window.validationStrings[category])) { - window.validationStrings[category] = defaultPwValStrings[category]; + if (!this._testStrings(window.validationStrings[category])) { + window.validationStrings[category] = this._defaultPwValStrings[category]; } const el = document.getElementById("requirement-" + category); - if (el) { - requirements[category] = new Requirement(category, el as HTMLLIElement); + if (typeof(el) === 'undefined' || el == null) continue; + this._requirements[category] = new Requirement(category, el as HTMLLIElement); + } + }; + + get requirements(): Requirements { return this._requirements }; + + constructor(conf: ValidatorConf) { + this._conf = conf; + if (!(this._conf.validatorFunc)) { + this._conf.validatorFunc = (oncomplete: (valid: boolean) => void) => { oncomplete(true); }; + } + this._conf.rePasswordField.addEventListener("keyup", this.validate); + this._conf.passwordField.addEventListener("keyup", this.validate); + this._conf.passwordField.addEventListener("keyup", () => { + const v = this._validate(this._conf.passwordField.value); + for (let criteria in this._requirements) { + this._requirements[criteria].validate(v[criteria]); } + }); + if (!window.validationStrings) { + window.validationStrings = this._defaultPwValStrings; + } else { + this._bindRequirements(); } } - return [requirements, checkValidity] } diff --git a/ts/pwr.ts b/ts/pwr.ts index 83ee992..d5a40a0 100644 --- a/ts/pwr.ts +++ b/ts/pwr.ts @@ -1,5 +1,5 @@ import { Modal } from "./modules/modal.js"; -import { initValidator } from "./modules/validator.js"; +import { Validator, ValidatorConf } from "./modules/validator.js"; import { _post, addLoader, removeLoader } from "./modules/common.js"; import { loadLangSelector } from "./modules/lang.js"; @@ -35,14 +35,22 @@ loadLangSelector("pwr"); declare var window: formWindow; const form = document.getElementById("form-create") as HTMLFormElement; -const submitButton = form.querySelector("input[type=submit]") as HTMLInputElement; +const submitInput = form.querySelector("input[type=submit]") as HTMLInputElement; const submitSpan = form.querySelector("span.submit") as HTMLSpanElement; const passwordField = document.getElementById("create-password") as HTMLInputElement; const rePasswordField = document.getElementById("create-reenter-password") as HTMLInputElement; window.successModal = new Modal(document.getElementById("modal-success"), true); -var requirements = initValidator(passwordField, rePasswordField, submitButton, submitSpan) +let validatorConf: ValidatorConf = { + passwordField: passwordField, + rePasswordField: rePasswordField, + submitInput: submitInput, + submitButton: submitSpan +}; + +var validator = new Validator(validatorConf); +var requirements = validator.requirements; interface sendDTO { pin: string; @@ -81,3 +89,5 @@ form.onsubmit = (event: Event) => { } }, true); }; + +validator.validate(); diff --git a/ts/user.ts b/ts/user.ts index 8fef411..a48c0f2 100644 --- a/ts/user.ts +++ b/ts/user.ts @@ -4,6 +4,7 @@ import { Modal } from "./modules/modal.js"; import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader } from "./modules/common.js"; import { Login } from "./modules/login.js"; import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js"; +import { Validator, ValidatorConf } from "./modules/validator.js"; interface userWindow extends Window { jellyfinID: string; @@ -88,6 +89,7 @@ const grid = document.querySelector(".grid"); var rootCard = document.getElementById("card-user"); var contactCard = document.getElementById("card-contact"); var statusCard = document.getElementById("card-status"); +var passwordCard = document.getElementById("card-password"); interface MyDetailsContactMethod { value: string; @@ -385,6 +387,29 @@ const matrixConf: MatrixConfiguration = { let matrix = new Matrix(matrixConf); +const oldPasswordField = document.getElementById("user-old-password") as HTMLInputElement; +const newPasswordField = document.getElementById("user-new-password") as HTMLInputElement; +const rePasswordField = document.getElementById("user-reenter-new-password") as HTMLInputElement; +const changePasswordButton = document.getElementById("user-password-submit") as HTMLSpanElement; + +let baseValidator = (oncomplete: (valid: boolean) => void): void => { + if (oldPasswordField.value.length == 0) return oncomplete(false); + oncomplete(true); +}; + +let validatorConf: ValidatorConf = { + passwordField: newPasswordField, + rePasswordField: rePasswordField, + submitButton: changePasswordButton, + validatorFunc: baseValidator +}; + +let validator = new Validator(validatorConf); +let requirements = validator.requirements; + +oldPasswordField.addEventListener("keyup", validator.validate); +// FIXME: Submit & Validate + document.addEventListener("details-reload", () => { _get("/my/details", null, (req: XMLHttpRequest) => { if (req.readyState == 4) { @@ -448,20 +473,10 @@ document.addEventListener("details-reload", () => { } if (typeof(messageCard) != "undefined" && messageCard != null) { - let largestNonMessageCardHeight = 0; - const cards = grid.querySelectorAll(".card") as NodeListOf; - for (let i = 0; i < cards.length; i++) { - if (cards[i].id == "card-message") continue; - if (computeRealHeight(cards[i]) > largestNonMessageCardHeight) { - largestNonMessageCardHeight = computeRealHeight(cards[i]); - } - } - - - let rowSpan = Math.ceil(computeRealHeight(messageCard) / largestNonMessageCardHeight); - - if (rowSpan > 0) - messageCard.style.gridRow = `span ${rowSpan}`; + setBestRowSpan(messageCard, false); + // contactCard.querySelector(".content").classList.add("h-100"); + } else if (!statusCard.classList.contains("unfocused")) { + setBestRowSpan(passwordCard, true); } } }); @@ -474,11 +489,36 @@ login.onLogin = () => { document.dispatchEvent(new CustomEvent("details-reload")); }; +const setBestRowSpan = (el: HTMLElement, setOnParent: boolean) => { + let largestNonMessageCardHeight = 0; + const cards = grid.querySelectorAll(".card") as NodeListOf; + for (let i = 0; i < cards.length; i++) { + if (cards[i].id == el.id) continue; + if (computeRealHeight(cards[i]) > largestNonMessageCardHeight) { + largestNonMessageCardHeight = computeRealHeight(cards[i]); + } + } + + let rowSpan = Math.ceil(computeRealHeight(el) / largestNonMessageCardHeight); + + if (rowSpan > 0) + (setOnParent ? el.parentElement : el).style.gridRow = `span ${rowSpan}`; +}; + const computeRealHeight = (el: HTMLElement): number => { let children = el.children as HTMLCollectionOf; let total = 0; for (let i = 0; i < children.length; i++) { - total += children[i].offsetHeight; + // Cope with the contact method card expanding to fill, by counting each contact method individually + if (el.id == "card-contact" && children[i].classList.contains("content")) { + // console.log("FOUND CARD_CONTACT, OG:", total + children[i].offsetHeight); + for (let j = 0; j < children[i].children.length; j++) { + total += (children[i].children[j] as HTMLElement).offsetHeight; + } + // console.log("NEW:", total); + } else { + total += children[i].offsetHeight; + } } return total; } diff --git a/views.go b/views.go index 43f6fb1..2570469 100644 --- a/views.go +++ b/views.go @@ -183,10 +183,11 @@ func (app *appContext) MyUserPage(gc *gin.Context) { "notifications": notificationsEnabled, "username": !app.config.Section("email").Key("no_username").MustBool(false), "strings": app.storage.lang.User[lang].Strings, - "validationStrings": app.storage.lang.User[lang].ValidationStrings, + "validationStrings": app.storage.lang.User[lang].validationStringsJSON, "language": app.storage.lang.User[lang].JSON, "langName": lang, "jfLink": app.config.Section("ui").Key("redirect_url").String(), + "requirements": app.validator.getCriteria(), } if telegramEnabled { data["telegramUsername"] = app.telegram.username