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)