diff --git a/api-users.go b/api-users.go
index b13b2f2..b5a47ad 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 c1fea0a..c9dde9c 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/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/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 949d91a..3d96872 100644
--- a/main.go
+++ b/main.go
@@ -122,6 +122,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 b7f5f7b..f1873f4 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 f278d86..ef4b555 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, GreCAPTCHA } 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, false);
function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void {
if (window.emailRequired) {
@@ -228,20 +201,9 @@ function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: bool
oncomplete(true);
}
-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;
-}
+let baseValidator = captcha.baseValidatorWrapper(_baseValidator);
-declare var grecaptcha: GreCAPTCHA
+declare var grecaptcha: GreCAPTCHA;
let validatorConf: ValidatorConf = {
passwordField: passwordField,
@@ -273,29 +235,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 +278,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..97dbd8d
--- /dev/null
+++ b/ts/modules/captcha.ts
@@ -0,0 +1,81 @@
+import { _get, _post } from "./common.js";
+
+export class Captcha {
+ isPWR = false;
+ 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 + (this.isPWR ? "?pwr=true" : ""), 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+(this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ if (req.status == 200) {
+ 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, 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 d5a40a0..175e755 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;
@@ -28,6 +29,10 @@ interface formWindow extends Window {
userExpiryHours: number;
userExpiryMinutes: number;
userExpiryMessage: string;
+ captcha: boolean;
+ reCAPTCHA: boolean;
+ reCAPTCHASiteKey: string;
+ pwrPIN: string;
}
loadLangSelector("pwr");
@@ -42,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);
@@ -55,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) => {
@@ -65,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) {
+ 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 ff1131d..c330b7c 100644
--- a/views.go
+++ b/views.go
@@ -296,6 +296,10 @@ 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("")
+ data["pwrPIN"] = pin
gcHTML(gc, http.StatusOK, "form-loader.html", data)
return
}
@@ -393,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)
@@ -425,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),
@@ -440,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()
@@ -450,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
@@ -529,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)