1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 17:10:10 +00:00

pwr: functioning captcha/recaptcha

This commit is contained in:
Harvey Tindall 2023-12-23 19:56:21 +00:00
parent ab05c07469
commit 278588ca39
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
9 changed files with 167 additions and 66 deletions

View File

@ -503,7 +503,7 @@ func (app *appContext) NewUser(gc *gin.Context) {
var req newUserDTO var req newUserDTO
gc.BindJSON(&req) gc.BindJSON(&req)
app.debug.Printf("%s: New user attempt", req.Code) 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) app.info.Printf("%s: New user failed: Captcha Incorrect", req.Code)
respond(400, "errorCaptcha", gc) respond(400, "errorCaptcha", gc)
return return

10
api.go
View File

@ -114,6 +114,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
var req ResetPasswordDTO var req ResetPasswordDTO
gc.BindJSON(&req) gc.BindJSON(&req)
validation := app.validator.validate(req.Password) validation := app.validator.validate(req.Password)
captcha := app.config.Section("captcha").Key("enabled").MustBool(false)
valid := true valid := true
for _, val := range validation { for _, val := range validation {
if !val { if !val {
@ -121,12 +122,18 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
} }
} }
if !valid || req.PIN == "" { if !valid || req.PIN == "" {
// 200 bcs idk what i did in js
app.info.Printf("%s: Password reset failed: Invalid password", req.PIN) app.info.Printf("%s: Password reset failed: Invalid password", req.PIN)
gc.JSON(400, validation) gc.JSON(400, validation)
return return
} }
isInternal := false 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 var userID, username string
if reset, ok := app.internalPWRs[req.PIN]; ok { if reset, ok := app.internalPWRs[req.PIN]; ok {
isInternal = true isInternal = true
@ -138,6 +145,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
} }
userID = reset.ID userID = reset.ID
username = reset.Username username = reset.Username
status, err := app.jf.ResetPasswordAdmin(userID) status, err := app.jf.ResetPasswordAdmin(userID)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Password Reset failed (%d): %v", status, err) app.err.Printf("Password Reset failed (%d): %v", status, err)

View File

@ -34,8 +34,12 @@
</script> </script>
{{ if .passwordReset }} {{ if .passwordReset }}
<script src="js/pwr.js" type="module"></script> <script src="js/pwr.js" type="module"></script>
<script>
window.pwrPIN = "{{ .pwrPIN }}";
</script>
{{ else }} {{ else }}
<script src="js/form.js" type="module"></script> <script src="js/form.js" type="module"></script>
{{ end }}
{{ if .reCAPTCHA }} {{ if .reCAPTCHA }}
<script> <script>
var reCAPTCHACallback = () => { var reCAPTCHACallback = () => {
@ -49,4 +53,3 @@
<script src="https://www.google.com/recaptcha/api.js?onload=reCAPTCHACallback&render=explicit" async defer></script> <script src="https://www.google.com/recaptcha/api.js?onload=reCAPTCHACallback&render=explicit" async defer></script>
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ end }}

View File

@ -120,6 +120,7 @@ type appContext struct {
proxyTransport *http.Transport proxyTransport *http.Transport
proxyConfig easyproxy.ProxyConfig proxyConfig easyproxy.ProxyConfig
internalPWRs map[string]InternalPWR internalPWRs map[string]InternalPWR
pwrCaptchas map[string]Captcha
ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request
confirmationKeysLock sync.Mutex confirmationKeysLock sync.Mutex
} }

View File

@ -332,8 +332,9 @@ type MatrixLoginDTO struct {
} }
type ResetPasswordDTO struct { type ResetPasswordDTO struct {
PIN string `json:"pin"` PIN string `json:"pin"`
Password string `json:"password"` Password string `json:"password"`
CaptchaText string `json:"captcha_text"`
} }
type AdminPasswordResetDTO struct { type AdminPasswordResetDTO struct {

View File

@ -4,7 +4,7 @@ import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from
import { loadLangSelector } from "./modules/lang.js"; import { loadLangSelector } from "./modules/lang.js";
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js"; import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.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 { interface formWindow extends Window {
invalidPassword: string; invalidPassword: string;
@ -173,7 +173,7 @@ if (!window.usernameEnabled) { usernameField.parentElement.remove(); usernameFie
const passwordField = document.getElementById("create-password") as HTMLInputElement; const passwordField = document.getElementById("create-password") as HTMLInputElement;
const rePasswordField = document.getElementById("create-reenter-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 { function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void {
if (window.emailRequired) { if (window.emailRequired) {
@ -203,20 +203,7 @@ function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: bool
let baseValidator = captcha.baseValidatorWrapper(_baseValidator); let baseValidator = captcha.baseValidatorWrapper(_baseValidator);
interface GreCAPTCHA { declare var grecaptcha: 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
let validatorConf: ValidatorConf = { let validatorConf: ValidatorConf = {
passwordField: passwordField, passwordField: passwordField,

View File

@ -1,6 +1,7 @@
import { _get, _post } from "./common.js"; import { _get, _post } from "./common.js";
export class Captcha { export class Captcha {
isPWR = false;
enabled = true; enabled = true;
verified = false; verified = false;
captchaID = ""; 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.readyState == 4) {
if (req.status == 204) { if (req.status == 204) {
this.checkbox.innerHTML = `<i class="ri-check-line"></i>`; this.checkbox.innerHTML = `<i class="ri-check-line"></i>`;
@ -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.readyState == 4) {
if (req.status == 200) { 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 = ` document.getElementById("captcha-img").innerHTML = `
<img class="w-100" src="${window.location.toString().substring(0, window.location.toString().lastIndexOf("/invite"))}/captcha/img/${this.code}/${this.captchaID}"></img> <img class="w-100" src="${window.location.toString().substring(0, window.location.toString().lastIndexOf("/invite"))}/captcha/img/${this.code}/${this.isPWR ? Math.random() : this.captchaID}${this.isPWR ? "?pwr=true" : ""}"></img>
`; `;
this.input.value = ""; this.input.value = "";
} }
} }
}); });
constructor(code: string, enabled: boolean, reCAPTCHA: boolean) { constructor(code: string, enabled: boolean, reCAPTCHA: boolean, isPWR: boolean) {
this.code = code; this.code = code;
this.enabled = enabled; this.enabled = enabled;
this.reCAPTCHA = reCAPTCHA; 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;
}

View File

@ -2,6 +2,7 @@ import { Modal } from "./modules/modal.js";
import { Validator, ValidatorConf } from "./modules/validator.js"; import { Validator, ValidatorConf } from "./modules/validator.js";
import { _post, addLoader, removeLoader } from "./modules/common.js"; import { _post, addLoader, removeLoader } from "./modules/common.js";
import { loadLangSelector } from "./modules/lang.js"; import { loadLangSelector } from "./modules/lang.js";
import { Captcha, GreCAPTCHA } from "./modules/captcha.js";
interface formWindow extends Window { interface formWindow extends Window {
invalidPassword: string; invalidPassword: string;
@ -31,6 +32,7 @@ interface formWindow extends Window {
captcha: boolean; captcha: boolean;
reCAPTCHA: boolean; reCAPTCHA: boolean;
reCAPTCHASiteKey: string; reCAPTCHASiteKey: string;
pwrPIN: string;
} }
loadLangSelector("pwr"); loadLangSelector("pwr");
@ -45,11 +47,26 @@ const rePasswordField = document.getElementById("create-reenter-password") as HT
window.successModal = new Modal(document.getElementById("modal-success"), true); 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 = { let validatorConf: ValidatorConf = {
passwordField: passwordField, passwordField: passwordField,
rePasswordField: rePasswordField, rePasswordField: rePasswordField,
submitInput: submitInput, submitInput: submitInput,
submitButton: submitSpan submitButton: submitSpan,
validatorFunc: baseValidator
}; };
var validator = new Validator(validatorConf); var validator = new Validator(validatorConf);
@ -58,6 +75,13 @@ var requirements = validator.requirements;
interface sendDTO { interface sendDTO {
pin: string; pin: string;
password: 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) => { form.onsubmit = (event: Event) => {
@ -68,12 +92,31 @@ form.onsubmit = (event: Event) => {
pin: params.get("pin"), pin: params.get("pin"),
password: passwordField.value 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) => { _post("/reset", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
removeLoader(submitSpan); removeLoader(submitSpan);
if (req.status == 400) { if (req.status == 400) {
for (let type in req.response) { if (req.response["error"] as string) { // FIXME: Show captcha error
if (requirements[type]) { requirements[type].valid = req.response[type] as boolean; } 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; return;
} else if (req.status != 200) { } else if (req.status != 200) {

115
views.go
View File

@ -299,6 +299,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
data["captcha"] = app.config.Section("captcha").Key("enabled").MustBool(false) data["captcha"] = app.config.Section("captcha").Key("enabled").MustBool(false)
data["reCAPTCHA"] = app.config.Section("captcha").Key("recaptcha").MustBool(false) data["reCAPTCHA"] = app.config.Section("captcha").Key("recaptcha").MustBool(false)
data["reCAPTCHASiteKey"] = app.config.Section("captcha").Key("recaptcha_site_key").MustString("") data["reCAPTCHASiteKey"] = app.config.Section("captcha").Key("recaptcha_site_key").MustString("")
data["pwrPIN"] = pin
gcHTML(gc, http.StatusOK, "form-loader.html", data) gcHTML(gc, http.StatusOK, "form-loader.html", data)
return return
} }
@ -396,20 +397,28 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
// @Router /captcha/img/{code}/{captchaID} [get] // @Router /captcha/img/{code}/{captchaID} [get]
func (app *appContext) GetCaptcha(gc *gin.Context) { func (app *appContext) GetCaptcha(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
isPWR := gc.Query("pwr") == "true"
captchaID := gc.Param("captchaID") captchaID := gc.Param("captchaID")
inv, ok := app.storage.GetInvitesKey(code) var inv Invite
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 capt Captcha var capt Captcha
ok = true ok := true
if inv.Captchas != nil { if !isPWR {
capt, ok = inv.Captchas[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(),
})
}
if inv.Captchas != nil {
capt, ok = inv.Captchas[captchaID]
} else {
ok = false
}
} else {
capt, ok = app.pwrCaptchas[code]
} }
if !ok { if !ok {
respondBool(400, false, gc) respondBool(400, false, gc)
@ -428,7 +437,13 @@ func (app *appContext) GetCaptcha(gc *gin.Context) {
// @tags Users // @tags Users
func (app *appContext) GenCaptcha(gc *gin.Context) { func (app *appContext) GenCaptcha(gc *gin.Context) {
code := gc.Param("invCode") 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 { if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{ gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc), "urlBase": app.getURLBase(gc),
@ -443,7 +458,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
respondBool(500, false, gc) respondBool(500, false, gc)
return return
} }
if inv.Captchas == nil { if !isPWR && inv.Captchas == nil {
inv.Captchas = map[string]Captcha{} inv.Captchas = map[string]Captcha{}
} }
captchaID := genAuthToken() captchaID := genAuthToken()
@ -453,26 +468,43 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
respondBool(500, false, gc) respondBool(500, false, gc)
return return
} }
inv.Captchas[captchaID] = Captcha{ if isPWR {
Answer: capt.Text, if app.pwrCaptchas == nil {
Image: buf.Bytes(), app.pwrCaptchas = map[string]Captcha{}
Generated: time.Now(), }
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}) gc.JSON(200, genCaptchaDTO{captchaID})
return 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) reCAPTCHA := app.config.Section("captcha").Key("recaptcha").MustBool(false)
if !reCAPTCHA { if !reCAPTCHA {
// internal CAPTCHA // internal CAPTCHA
inv, ok := app.storage.GetInvitesKey(code) var c Captcha
if !ok || inv.Captchas == nil { ok := true
app.debug.Printf("Couldn't find invite \"%s\"", code) if !isPWR {
return false 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 { if !ok {
app.debug.Printf("Couldn't find Captcha \"%s\"", id) app.debug.Printf("Couldn't find Captcha \"%s\"", id)
return false return false
@ -532,21 +564,30 @@ func (app *appContext) verifyCaptcha(code, id, text string) bool {
// @Router /captcha/verify/{code}/{captchaID}/{text} [get] // @Router /captcha/verify/{code}/{captchaID}/{text} [get]
func (app *appContext) VerifyCaptcha(gc *gin.Context) { func (app *appContext) VerifyCaptcha(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
isPWR := gc.Query("pwr") == "true"
captchaID := gc.Param("captchaID") captchaID := gc.Param("captchaID")
text := gc.Param("text") text := gc.Param("text")
inv, ok := app.storage.GetInvitesKey(code) var inv Invite
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 capt Captcha var capt Captcha
if inv.Captchas != nil { var ok bool
capt, ok = inv.Captchas[captchaID] 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 { if !ok {
respondBool(400, false, gc) respondBool(400, false, gc)