From ab05c074697a38e14a80a67405fc4826c4806df6 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sat, 23 Dec 2023 18:20:09 +0000 Subject: [PATCH 1/3] 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 f278d86..b263481 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 0000000..ae9e5fc --- /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 d5a40a0..7cdaee6 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 51ccca8..4896707 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 } From 278588ca39640dec1c576c4093c87ff77c05e446 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sat, 23 Dec 2023 19:56:21 +0000 Subject: [PATCH 2/3] pwr: functioning captcha/recaptcha --- api-users.go | 2 +- api.go | 10 +++- html/form-base.html | 5 +- main.go | 1 + models.go | 5 +- ts/form.ts | 19 ++----- ts/modules/captcha.ts | 27 ++++++++-- ts/pwr.ts | 49 ++++++++++++++++-- views.go | 115 ++++++++++++++++++++++++++++-------------- 9 files changed, 167 insertions(+), 66 deletions(-) diff --git a/api-users.go b/api-users.go index 138c287..f47674c 100644 --- a/api-users.go +++ b/api-users.go @@ -503,7 +503,7 @@ func (app *appContext) NewUser(gc *gin.Context) { var req newUserDTO gc.BindJSON(&req) app.debug.Printf("%s: New user attempt", req.Code) - if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText) { + if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText, false) { app.info.Printf("%s: New user failed: Captcha Incorrect", req.Code) respond(400, "errorCaptcha", gc) return diff --git a/api.go b/api.go index 1b926e0..5c9a709 100644 --- a/api.go +++ b/api.go @@ -114,6 +114,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { var req ResetPasswordDTO gc.BindJSON(&req) validation := app.validator.validate(req.Password) + captcha := app.config.Section("captcha").Key("enabled").MustBool(false) valid := true for _, val := range validation { if !val { @@ -121,12 +122,18 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { } } if !valid || req.PIN == "" { - // 200 bcs idk what i did in js app.info.Printf("%s: Password reset failed: Invalid password", req.PIN) gc.JSON(400, validation) return } isInternal := false + + if captcha && !app.verifyCaptcha(req.PIN, req.PIN, req.CaptchaText, true) { + app.info.Printf("%s: PWR Failed: Captcha Incorrect", req.PIN) + respond(400, "errorCaptcha", gc) + return + } + var userID, username string if reset, ok := app.internalPWRs[req.PIN]; ok { isInternal = true @@ -138,6 +145,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { } userID = reset.ID username = reset.Username + status, err := app.jf.ResetPasswordAdmin(userID) if !(status == 200 || status == 204) || err != nil { app.err.Printf("Password Reset failed (%d): %v", status, err) diff --git a/html/form-base.html b/html/form-base.html index ba8c318..7f46a30 100644 --- a/html/form-base.html +++ b/html/form-base.html @@ -34,8 +34,12 @@ {{ if .passwordReset }} + {{ else }} +{{ end }} {{ if .reCAPTCHA }} {{ end }} {{ end }} -{{ end }} diff --git a/main.go b/main.go index 52c96c8..3128893 100644 --- a/main.go +++ b/main.go @@ -120,6 +120,7 @@ type appContext struct { proxyTransport *http.Transport proxyConfig easyproxy.ProxyConfig internalPWRs map[string]InternalPWR + pwrCaptchas map[string]Captcha ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request confirmationKeysLock sync.Mutex } diff --git a/models.go b/models.go index ba46f7b..2e17211 100644 --- a/models.go +++ b/models.go @@ -332,8 +332,9 @@ type MatrixLoginDTO struct { } type ResetPasswordDTO struct { - PIN string `json:"pin"` - Password string `json:"password"` + PIN string `json:"pin"` + Password string `json:"password"` + CaptchaText string `json:"captcha_text"` } type AdminPasswordResetDTO struct { diff --git a/ts/form.ts b/ts/form.ts index b263481..ef4b555 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -4,7 +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"; +import { Captcha, GreCAPTCHA } from "./modules/captcha.js"; interface formWindow extends Window { invalidPassword: string; @@ -173,7 +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 captcha = new Captcha(window.code, window.captcha, window.reCAPTCHA); +let captcha = new Captcha(window.code, window.captcha, window.reCAPTCHA, false); function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void { if (window.emailRequired) { @@ -203,20 +203,7 @@ function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: bool let baseValidator = captcha.baseValidatorWrapper(_baseValidator); -interface GreCAPTCHA { - render: (container: HTMLDivElement, parameters: { - sitekey?: string, - theme?: string, - size?: string, - tabindex?: number, - "callback"?: () => void, - "expired-callback"?: () => void, - "error-callback"?: () => void - }) => void; - getResponse: (opt_widget_id?: HTMLDivElement) => string; -} - -declare var grecaptcha: GreCAPTCHA +declare var grecaptcha: GreCAPTCHA; let validatorConf: ValidatorConf = { passwordField: passwordField, diff --git a/ts/modules/captcha.ts b/ts/modules/captcha.ts index ae9e5fc..97dbd8d 100644 --- a/ts/modules/captcha.ts +++ b/ts/modules/captcha.ts @@ -1,6 +1,7 @@ import { _get, _post } from "./common.js"; export class Captcha { + isPWR = false; enabled = true; verified = false; captchaID = ""; @@ -27,7 +28,7 @@ export class Captcha { }; }; - verify = (callback: () => void) => _post("/captcha/verify/" + this.code + "/" + this.captchaID + "/" + this.input.value, null, (req: XMLHttpRequest) => { + verify = (callback: () => void) => _post("/captcha/verify/" + this.code + "/" + this.captchaID + "/" + this.input.value + (this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 204) { this.checkbox.innerHTML = ``; @@ -44,21 +45,37 @@ export class Captcha { } }); - generate = () => _get("/captcha/gen/"+this.code, null, (req: XMLHttpRequest) => { + generate = () => _get("/captcha/gen/"+this.code+(this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 200) { - this.captchaID = req.response["id"]; + this.captchaID = this.isPWR ? this.code : req.response["id"]; + // the Math.random() appearance below is used for PWRs, since they don't have a unique captchaID. The parameter is ignored by the server, but tells the browser to reload the image. document.getElementById("captcha-img").innerHTML = ` - + `; this.input.value = ""; } } }); - constructor(code: string, enabled: boolean, reCAPTCHA: boolean) { + constructor(code: string, enabled: boolean, reCAPTCHA: boolean, isPWR: boolean) { this.code = code; this.enabled = enabled; this.reCAPTCHA = reCAPTCHA; + this.isPWR = isPWR; } } + +export interface GreCAPTCHA { + render: (container: HTMLDivElement, parameters: { + sitekey?: string, + theme?: string, + size?: string, + tabindex?: number, + "callback"?: () => void, + "expired-callback"?: () => void, + "error-callback"?: () => void + }) => void; + getResponse: (opt_widget_id?: HTMLDivElement) => string; +} + diff --git a/ts/pwr.ts b/ts/pwr.ts index 7cdaee6..b59de72 100644 --- a/ts/pwr.ts +++ b/ts/pwr.ts @@ -2,6 +2,7 @@ import { Modal } from "./modules/modal.js"; import { Validator, ValidatorConf } from "./modules/validator.js"; import { _post, addLoader, removeLoader } from "./modules/common.js"; import { loadLangSelector } from "./modules/lang.js"; +import { Captcha, GreCAPTCHA } from "./modules/captcha.js"; interface formWindow extends Window { invalidPassword: string; @@ -31,6 +32,7 @@ interface formWindow extends Window { captcha: boolean; reCAPTCHA: boolean; reCAPTCHASiteKey: string; + pwrPIN: string; } loadLangSelector("pwr"); @@ -45,11 +47,26 @@ const rePasswordField = document.getElementById("create-reenter-password") as HT window.successModal = new Modal(document.getElementById("modal-success"), true); +function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void { + if (window.captcha && !window.reCAPTCHA && !captchaValid) { + oncomplete(false); + return; + } + oncomplete(true); +} + +let captcha = new Captcha(window.pwrPIN, window.captcha, window.reCAPTCHA, true); + +declare var grecaptcha: GreCAPTCHA; + +let baseValidator = captcha.baseValidatorWrapper(_baseValidator); + let validatorConf: ValidatorConf = { passwordField: passwordField, rePasswordField: rePasswordField, submitInput: submitInput, - submitButton: submitSpan + submitButton: submitSpan, + validatorFunc: baseValidator }; var validator = new Validator(validatorConf); @@ -58,6 +75,13 @@ var requirements = validator.requirements; interface sendDTO { pin: string; password: string; + captcha_text?: string; +} + +if (window.captcha && !window.reCAPTCHA) { + captcha.generate(); + (document.getElementById("captcha-regen") as HTMLSpanElement).onclick = captcha.generate; + captcha.input.onkeyup = validator.validate; } form.onsubmit = (event: Event) => { @@ -68,12 +92,31 @@ form.onsubmit = (event: Event) => { pin: params.get("pin"), password: passwordField.value }; + if (window.captcha) { + if (window.reCAPTCHA) { + send.captcha_text = grecaptcha.getResponse(); + } else { + send.captcha_text = captcha.input.value; + } + } _post("/reset", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { removeLoader(submitSpan); if (req.status == 400) { - for (let type in req.response) { - if (requirements[type]) { requirements[type].valid = req.response[type] as boolean; } + if (req.response["error"] as string) { // FIXME: Show captcha error + const old = submitSpan.textContent; + submitSpan.textContent = window.messages[req.response["error"]]; + submitSpan.classList.add("~critical"); + submitSpan.classList.remove("~urge"); + setTimeout(() => { + submitSpan.classList.add("~urge"); + submitSpan.classList.remove("~critical"); + submitSpan.textContent = old; + }, 2000); + } else { + for (let type in req.response) { + if (requirements[type]) { requirements[type].valid = req.response[type] as boolean; } + } } return; } else if (req.status != 200) { diff --git a/views.go b/views.go index 4896707..bacb51e 100644 --- a/views.go +++ b/views.go @@ -299,6 +299,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) { 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("") + data["pwrPIN"] = pin gcHTML(gc, http.StatusOK, "form-loader.html", data) return } @@ -396,20 +397,28 @@ func (app *appContext) ResetPassword(gc *gin.Context) { // @Router /captcha/img/{code}/{captchaID} [get] func (app *appContext) GetCaptcha(gc *gin.Context) { code := gc.Param("invCode") + isPWR := gc.Query("pwr") == "true" captchaID := gc.Param("captchaID") - inv, ok := app.storage.GetInvitesKey(code) - if !ok { - gcHTML(gc, 404, "invalidCode.html", gin.H{ - "urlBase": app.getURLBase(gc), - "cssClass": app.cssClass, - "cssVersion": cssVersion, - "contactMessage": app.config.Section("ui").Key("contact_message").String(), - }) - } + var inv Invite var capt Captcha - ok = true - if inv.Captchas != nil { - capt, ok = inv.Captchas[captchaID] + ok := true + if !isPWR { + inv, ok = app.storage.GetInvitesKey(code) + if !ok { + gcHTML(gc, 404, "invalidCode.html", gin.H{ + "urlBase": app.getURLBase(gc), + "cssClass": app.cssClass, + "cssVersion": cssVersion, + "contactMessage": app.config.Section("ui").Key("contact_message").String(), + }) + } + if inv.Captchas != nil { + capt, ok = inv.Captchas[captchaID] + } else { + ok = false + } + } else { + capt, ok = app.pwrCaptchas[code] } if !ok { respondBool(400, false, gc) @@ -428,7 +437,13 @@ func (app *appContext) GetCaptcha(gc *gin.Context) { // @tags Users func (app *appContext) GenCaptcha(gc *gin.Context) { code := gc.Param("invCode") - inv, ok := app.storage.GetInvitesKey(code) + isPWR := gc.Query("pwr") == "true" + var inv Invite + ok := true + if !isPWR { + inv, ok = app.storage.GetInvitesKey(code) + } + if !ok { gcHTML(gc, 404, "invalidCode.html", gin.H{ "urlBase": app.getURLBase(gc), @@ -443,7 +458,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) { respondBool(500, false, gc) return } - if inv.Captchas == nil { + if !isPWR && inv.Captchas == nil { inv.Captchas = map[string]Captcha{} } captchaID := genAuthToken() @@ -453,26 +468,43 @@ func (app *appContext) GenCaptcha(gc *gin.Context) { respondBool(500, false, gc) return } - inv.Captchas[captchaID] = Captcha{ - Answer: capt.Text, - Image: buf.Bytes(), - Generated: time.Now(), + if isPWR { + if app.pwrCaptchas == nil { + app.pwrCaptchas = map[string]Captcha{} + } + app.pwrCaptchas[code] = Captcha{ + Answer: capt.Text, + Image: buf.Bytes(), + Generated: time.Now(), + } + } else { + inv.Captchas[captchaID] = Captcha{ + Answer: capt.Text, + Image: buf.Bytes(), + Generated: time.Now(), + } + app.storage.SetInvitesKey(code, inv) } - app.storage.SetInvitesKey(code, inv) gc.JSON(200, genCaptchaDTO{captchaID}) return } -func (app *appContext) verifyCaptcha(code, id, text string) bool { +func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool { reCAPTCHA := app.config.Section("captcha").Key("recaptcha").MustBool(false) if !reCAPTCHA { // internal CAPTCHA - inv, ok := app.storage.GetInvitesKey(code) - if !ok || inv.Captchas == nil { - app.debug.Printf("Couldn't find invite \"%s\"", code) - return false + var c Captcha + ok := true + if !isPWR { + inv, ok := app.storage.GetInvitesKey(code) + if !ok || (!isPWR && inv.Captchas == nil) { + app.debug.Printf("Couldn't find invite \"%s\"", code) + return false + } + c, ok = inv.Captchas[id] + } else { + c, ok = app.pwrCaptchas[code] } - c, ok := inv.Captchas[id] if !ok { app.debug.Printf("Couldn't find Captcha \"%s\"", id) return false @@ -532,21 +564,30 @@ func (app *appContext) verifyCaptcha(code, id, text string) bool { // @Router /captcha/verify/{code}/{captchaID}/{text} [get] func (app *appContext) VerifyCaptcha(gc *gin.Context) { code := gc.Param("invCode") + isPWR := gc.Query("pwr") == "true" captchaID := gc.Param("captchaID") text := gc.Param("text") - inv, ok := app.storage.GetInvitesKey(code) - if !ok { - gcHTML(gc, 404, "invalidCode.html", gin.H{ - "urlBase": app.getURLBase(gc), - "cssClass": app.cssClass, - "cssVersion": cssVersion, - "contactMessage": app.config.Section("ui").Key("contact_message").String(), - }) - return - } + var inv Invite var capt Captcha - if inv.Captchas != nil { - capt, ok = inv.Captchas[captchaID] + var ok bool + if !isPWR { + inv, ok = app.storage.GetInvitesKey(code) + if !ok { + gcHTML(gc, 404, "invalidCode.html", gin.H{ + "urlBase": app.getURLBase(gc), + "cssClass": app.cssClass, + "cssVersion": cssVersion, + "contactMessage": app.config.Section("ui").Key("contact_message").String(), + }) + return + } + if inv.Captchas != nil { + capt, ok = inv.Captchas[captchaID] + } else { + ok = false + } + } else { + capt, ok = app.pwrCaptchas[code] } if !ok { respondBool(400, false, gc) From 49d8c6f8e452ba34173e729019d380c9f0451a55 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sat, 23 Dec 2023 20:18:16 +0000 Subject: [PATCH 3/3] pwr: add captcha daemon --- daemon.go | 15 +++++++++++++++ ts/pwr.ts | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/daemon.go b/daemon.go index 245800d..19a3f97 100644 --- a/daemon.go +++ b/daemon.go @@ -74,6 +74,17 @@ func (app *appContext) clearTelegram() { } } +func (app *appContext) clearPWRCaptchas() { + app.debug.Println("Housekeeping: Clearing old PWR Captchas") + captchas := map[string]Captcha{} + for k, capt := range app.pwrCaptchas { + if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) { + captchas[k] = capt + } + } + app.pwrCaptchas = captchas +} + func (app *appContext) clearActivities() { app.debug.Println("Housekeeping: Cleaning up Activity log...") keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000) @@ -136,6 +147,7 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false) clearTelegram := app.config.Section("telegram").Key("require_unique").MustBool(false) clearMatrix := app.config.Section("matrix").Key("require_unique").MustBool(false) + clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false) if clearEmail || clearDiscord || clearTelegram || clearMatrix { daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() }) @@ -153,6 +165,9 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo if clearMatrix { daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() }) } + if clearPWR { + daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearPWRCaptchas() }) + } return &daemon } diff --git a/ts/pwr.ts b/ts/pwr.ts index b59de72..175e755 100644 --- a/ts/pwr.ts +++ b/ts/pwr.ts @@ -103,7 +103,7 @@ form.onsubmit = (event: Event) => { if (req.readyState == 4) { removeLoader(submitSpan); if (req.status == 400) { - if (req.response["error"] as string) { // FIXME: Show captcha error + if (req.response["error"] as string) { const old = submitSpan.textContent; submitSpan.textContent = window.messages[req.response["error"]]; submitSpan.classList.add("~critical");