From ab05c074697a38e14a80a67405fc4826c4806df6 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sat, 23 Dec 2023 18:20:09 +0000 Subject: [PATCH] form: modularize captcha somewhat --- ts/form.ts | 59 +++++++-------------------------------- ts/modules/captcha.ts | 64 +++++++++++++++++++++++++++++++++++++++++++ ts/pwr.ts | 3 ++ views.go | 3 ++ 4 files changed, 80 insertions(+), 49 deletions(-) create mode 100644 ts/modules/captcha.ts diff --git a/ts/form.ts b/ts/form.ts index f278d864..b263481b 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -4,6 +4,7 @@ import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from import { loadLangSelector } from "./modules/lang.js"; import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js"; import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js"; +import { Captcha } from "./modules/captcha.js"; interface formWindow extends Window { invalidPassword: string; @@ -172,35 +173,7 @@ if (!window.usernameEnabled) { usernameField.parentElement.remove(); usernameFie const passwordField = document.getElementById("create-password") as HTMLInputElement; const rePasswordField = document.getElementById("create-reenter-password") as HTMLInputElement; -let captchaVerified = false; -let captchaID = ""; -let captchaInput = document.getElementById("captcha-input") as HTMLInputElement; -const captchaCheckbox = document.getElementById("captcha-success") as HTMLSpanElement; -let prevCaptcha = ""; - -let baseValidator = (oncomplete: (valid: boolean) => void): void => { - if (window.captcha && !window.reCAPTCHA && (captchaInput.value != prevCaptcha)) { - prevCaptcha = captchaInput.value; - _post("/captcha/verify/" + window.code + "/" + captchaID + "/" + captchaInput.value, null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status == 204) { - captchaCheckbox.innerHTML = ``; - captchaCheckbox.classList.add("~positive"); - captchaCheckbox.classList.remove("~critical"); - captchaVerified = true; - } else { - captchaCheckbox.innerHTML = ``; - captchaCheckbox.classList.add("~critical"); - captchaCheckbox.classList.remove("~positive"); - captchaVerified = false; - } - _baseValidator(oncomplete, captchaVerified); - } - }); - } else { - _baseValidator(oncomplete, captchaVerified); - } -} +let captcha = new Captcha(window.code, window.captcha, window.reCAPTCHA); function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void { if (window.emailRequired) { @@ -228,6 +201,8 @@ function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: bool oncomplete(true); } +let baseValidator = captcha.baseValidatorWrapper(_baseValidator); + interface GreCAPTCHA { render: (container: HTMLDivElement, parameters: { sitekey?: string, @@ -273,29 +248,15 @@ interface sendDTO { captcha_text?: string; } -const genCaptcha = () => { - _get("/captcha/gen/"+window.code, null, (req: XMLHttpRequest) => { - if (req.readyState == 4) { - if (req.status == 200) { - captchaID = req.response["id"]; - document.getElementById("captcha-img").innerHTML = ` - - `; - captchaInput.value = ""; - } - } - }); -}; - if (window.captcha && !window.reCAPTCHA) { - genCaptcha(); - (document.getElementById("captcha-regen") as HTMLSpanElement).onclick = genCaptcha; - captchaInput.onkeyup = validator.validate; + captcha.generate(); + (document.getElementById("captcha-regen") as HTMLSpanElement).onclick = captcha.generate; + captcha.input.onkeyup = validator.validate; } const create = (event: SubmitEvent) => { event.preventDefault(); - if (window.captcha && !window.reCAPTCHA && !captchaVerified) { + if (window.captcha && !window.reCAPTCHA && !captcha.verified) { } addLoader(submitSpan); @@ -330,8 +291,8 @@ const create = (event: SubmitEvent) => { if (window.reCAPTCHA) { send.captcha_text = grecaptcha.getResponse(); } else { - send.captcha_id = captchaID; - send.captcha_text = captchaInput.value; + send.captcha_id = captcha.captchaID; + send.captcha_text = captcha.input.value; } } _post("/newUser", send, (req: XMLHttpRequest) => { diff --git a/ts/modules/captcha.ts b/ts/modules/captcha.ts new file mode 100644 index 00000000..ae9e5fc6 --- /dev/null +++ b/ts/modules/captcha.ts @@ -0,0 +1,64 @@ +import { _get, _post } from "./common.js"; + +export class Captcha { + enabled = true; + verified = false; + captchaID = ""; + input = document.getElementById("captcha-input") as HTMLInputElement; + checkbox = document.getElementById("captcha-success") as HTMLSpanElement; + previous = ""; + reCAPTCHA = false; + code = ""; + + get value(): string { return this.input.value; } + + hasChanged = (): boolean => { return this.value != this.previous; } + + baseValidatorWrapper = (_baseValidator: (oncomplete: (valid: boolean) => void, captchaValid: boolean) => void) => { + return (oncomplete: (valid: boolean) => void): void => { + if (this.enabled && !this.reCAPTCHA && this.hasChanged()) { + this.previous = this.value; + this.verify(() => { + _baseValidator(oncomplete, this.verified); + }); + } else { + _baseValidator(oncomplete, this.verified); + } + }; + }; + + verify = (callback: () => void) => _post("/captcha/verify/" + this.code + "/" + this.captchaID + "/" + this.input.value, null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 204) { + this.checkbox.innerHTML = ``; + this.checkbox.classList.add("~positive"); + this.checkbox.classList.remove("~critical"); + this.verified = true; + } else { + this.checkbox.innerHTML = ``; + this.checkbox.classList.add("~critical"); + this.checkbox.classList.remove("~positive"); + this.verified = false; + } + callback(); + } + }); + + generate = () => _get("/captcha/gen/"+this.code, null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200) { + this.captchaID = req.response["id"]; + document.getElementById("captcha-img").innerHTML = ` + + `; + this.input.value = ""; + } + } + }); + + constructor(code: string, enabled: boolean, reCAPTCHA: boolean) { + this.code = code; + this.enabled = enabled; + this.reCAPTCHA = reCAPTCHA; + } +} diff --git a/ts/pwr.ts b/ts/pwr.ts index d5a40a0c..7cdaee69 100644 --- a/ts/pwr.ts +++ b/ts/pwr.ts @@ -28,6 +28,9 @@ interface formWindow extends Window { userExpiryHours: number; userExpiryMinutes: number; userExpiryMessage: string; + captcha: boolean; + reCAPTCHA: boolean; + reCAPTCHASiteKey: string; } loadLangSelector("pwr"); diff --git a/views.go b/views.go index 51ccca85..48967070 100644 --- a/views.go +++ b/views.go @@ -296,6 +296,9 @@ func (app *appContext) ResetPassword(gc *gin.Context) { data["telegramEnabled"] = false data["discordEnabled"] = false data["matrixEnabled"] = false + data["captcha"] = app.config.Section("captcha").Key("enabled").MustBool(false) + data["reCAPTCHA"] = app.config.Section("captcha").Key("recaptcha").MustBool(false) + data["reCAPTCHASiteKey"] = app.config.Section("captcha").Key("recaptcha_site_key").MustString("") gcHTML(gc, http.StatusOK, "form-loader.html", data) return }