diff --git a/package.json b/package.json index ade1262..3644fdf 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "devDependencies": { "@hcaptcha/types": "^1.0.3", "@types/bootstrap": "^5.2.10", + "@types/cloudflare-turnstile": "^0.2.2", "@types/dompurify": "^3.0.5", + "@types/grecaptcha": "^3.0.9", "@types/jest": "^29.5.12", "buffer": "^5.5.0||^6.0.0", "jest": "^29.7.0", diff --git a/src/html/index.html b/src/html/index.html index a6d9040..b48f1de 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -10,7 +10,7 @@ - + @@ -97,6 +97,8 @@

+
+
@@ -113,6 +115,8 @@

+
+
@@ -152,6 +156,8 @@
+
+
@@ -306,7 +312,9 @@
- + + + diff --git a/src/ts/AuthManager.ts b/src/ts/AuthManager.ts index d23422f..94a9a07 100644 --- a/src/ts/AuthManager.ts +++ b/src/ts/AuthManager.ts @@ -18,10 +18,12 @@ export default class AuthManager { }) } - login(username : string, password : string, captchaToken : string | undefined) : Promise { + login(username : string, password : string, captchaToken : string | undefined, turnstileToken : string | undefined, recaptchaToken : string | undefined) : Promise { return new Promise(async (res,rej) => { if (!this.info) throw new Error("Cannot login before fetching API information."); if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token."); + if (!turnstileToken && this.info.turnstile.required) throw new Error("This API requires a valid Turnstile token."); + if (!recaptchaToken && this.info.recaptcha.required) throw new Error("This API requires a valid reCAPTCHA token."); var data = await fetch(this.apiEndpoint + "/api/v1/login", { method: "POST", headers: { @@ -30,7 +32,9 @@ export default class AuthManager { body: JSON.stringify({ username: username, password: password, - captchaToken: captchaToken + captchaToken: captchaToken, + turnstileToken: turnstileToken, + recaptchaToken: recaptchaToken }) }); var json = await data.json() as AccountLoginResult; @@ -69,10 +73,12 @@ export default class AuthManager { }) } - register(username : string, password : string, email : string, dateOfBirth : dayjs.Dayjs, captchaToken : string | undefined) : Promise { + register(username : string, password : string, email : string, dateOfBirth : dayjs.Dayjs, captchaToken : string | undefined, turnstileToken: string | undefined, recaptchaToken : string | undefined) : Promise { return new Promise(async (res, rej) => { if (!this.info) throw new Error("Cannot login before fetching API information."); if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token."); + if (!turnstileToken && this.info.turnstile.required) throw new Error("This API requires a valid Turnstile token."); + if (!recaptchaToken && this.info.recaptcha.required) throw new Error("This API requires a valid reCAPTCHA token."); var data = await fetch(this.apiEndpoint + "/api/v1/register", { method: "POST", headers: { @@ -83,7 +89,9 @@ export default class AuthManager { password: password, email: email, dateOfBirth: dateOfBirth.format("YYYY-MM-DD"), - captchatoken: captchaToken + captchatoken: captchaToken, + turnstiletoken: turnstileToken, + recaptchaToken: recaptchaToken }) }); res(await data.json() as AccountRegisterResult); @@ -154,10 +162,12 @@ export default class AuthManager { }); } - sendPasswordResetEmail(username : string, email : string, captchaToken : string | undefined) { + sendPasswordResetEmail(username : string, email : string, captchaToken : string | undefined, turnstileToken : string | undefined, recaptchaToken : string | undefined) { return new Promise(async res => { if (!this.info) throw new Error("Cannot send password reset email without fetching API information."); if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token."); + if (!turnstileToken && this.info.turnstile.required) throw new Error("This API requires a valid Turnstile token."); + if (!recaptchaToken && this.info.recaptcha.required) throw new Error("This API requires a valid reCAPTCHA token."); var data = await fetch(this.apiEndpoint + "/api/v1/sendreset", { method: "POST", headers: { @@ -166,7 +176,9 @@ export default class AuthManager { body: JSON.stringify({ username: username, email: email, - captchaToken: captchaToken + captchaToken: captchaToken, + turnstileToken: turnstileToken, + recaptchaToken: recaptchaToken }) }); res(await data.json() as PasswordResetResult); @@ -198,6 +210,14 @@ export interface AuthServerInformation { required : boolean; siteKey : string | undefined; }; + turnstile : { + required : boolean; + siteKey : string | undefined; + }; + recaptcha : { + required : boolean; + siteKey : string | undefined; + } } export interface AccountRegisterResult { diff --git a/src/ts/main.ts b/src/ts/main.ts index 783ef35..69eecb4 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -97,7 +97,11 @@ const elements = { accountRegisterForm: document.getElementById("accountRegisterForm") as HTMLFormElement, accountVerifyEmailForm: document.getElementById("accountVerifyEmailForm") as HTMLFormElement, accountLoginCaptcha: document.getElementById("accountLoginCaptcha") as HTMLDivElement, + accountLoginRecaptcha: document.getElementById("accountLoginReCaptcha") as HTMLDivElement, + accountLoginTurnstile: document.getElementById("accountLoginTurnstile") as HTMLDivElement, accountRegisterCaptcha: document.getElementById("accountRegisterCaptcha") as HTMLDivElement, + accountRegisterRecaptcha: document.getElementById("accountRegisterReCaptcha") as HTMLDivElement, + accountRegisterTurnstile: document.getElementById("accountRegisterTurnstile") as HTMLDivElement, accountLoginUsername: document.getElementById("accountLoginUsername") as HTMLInputElement, accountLoginPassword: document.getElementById("accountLoginPassword") as HTMLInputElement, @@ -123,6 +127,8 @@ const elements = { accountResetPasswordEmail: document.getElementById("accountResetPasswordEmail") as HTMLInputElement, accountResetPasswordUsername: document.getElementById("accountResetPasswordUsername") as HTMLInputElement, accountResetPasswordCaptcha: document.getElementById("accountResetPasswordCaptcha") as HTMLDivElement, + accountResetPasswordRecaptcha: document.getElementById("accountResetPasswordReCaptcha") as HTMLDivElement, + accountResetPasswordTurnstile: document.getElementById("accountResetPasswordTurnstile") as HTMLDivElement, accountResetPasswordVerifySection: document.getElementById("accountResetPasswordVerifySection") as HTMLDivElement, accountVerifyPasswordResetText: document.getElementById("accountVerifyPasswordResetText") as HTMLParagraphElement, @@ -943,12 +949,45 @@ async function renderAuth() { elements.accountRegisterCaptcha.innerHTML = ""; elements.accountLoginCaptcha.innerHTML = ""; elements.accountResetPasswordCaptcha.innerHTML = ""; + elements.accountRegisterTurnstile.innerHTML = ""; + elements.accountLoginTurnstile.innerHTML = ""; + elements.accountResetPasswordTurnstile.innerHTML = ""; + elements.accountRegisterRecaptcha.innerHTML = ""; + elements.accountLoginRecaptcha.innerHTML = ""; + elements.accountResetPasswordRecaptcha.innerHTML = ""; if (auth!.info!.hcaptcha.required) { var hconfig = {sitekey: auth!.info!.hcaptcha.siteKey!}; hcaptcha.render(elements.accountRegisterCaptcha, hconfig); hcaptcha.render(elements.accountLoginCaptcha, hconfig); hcaptcha.render(elements.accountResetPasswordCaptcha, hconfig); } + + if(auth!.info?.turnstile.required) { + var turnstileconfig = {sitekey: auth!.info!.turnstile.siteKey!}; + + // hCaptcha does this automatically, but Turnstile doesn't, oh well. + var turnstileRegisterWidgetId = turnstile.render(elements.accountRegisterTurnstile, turnstileconfig); + var turnstileLoginWidgetId = turnstile.render(elements.accountLoginTurnstile, turnstileconfig); + var turnstileResetPasswordWidgetId = turnstile.render(elements.accountResetPasswordTurnstile, turnstileconfig); + + elements.accountRegisterTurnstile.children[0].setAttribute("data-turnstile-widget-id", turnstileRegisterWidgetId!); + elements.accountLoginTurnstile.children[0].setAttribute("data-turnstile-widget-id", turnstileLoginWidgetId!); + elements.accountResetPasswordTurnstile.children[0].setAttribute("data-turnstile-widget-id", turnstileResetPasswordWidgetId!); + } + + if(auth!.info?.recaptcha.required) { + var recaptchaconfig = {sitekey: auth!.info!.recaptcha.siteKey!}; + + // Same deal as with Turnstile + var RecaptchaRegisterWidgetId = grecaptcha.render(elements.accountRegisterRecaptcha, recaptchaconfig); + var RecaptchaLoginWidgetId = grecaptcha.render(elements.accountLoginRecaptcha, recaptchaconfig); + var RecaptchaResetPasswordWidgetId = grecaptcha.render(elements.accountResetPasswordRecaptcha, recaptchaconfig); + + elements.accountRegisterRecaptcha.children[0].setAttribute("data-recaptcha-widget-id", RecaptchaRegisterWidgetId!.toString()); + elements.accountLoginRecaptcha.children[0].setAttribute("data-recaptcha-widget-id", RecaptchaLoginWidgetId!.toString()); + elements.accountResetPasswordRecaptcha.children[0].setAttribute("data-recaptcha-widget-id", RecaptchaResetPasswordWidgetId!.toString()); + } + var token = localStorage.getItem("collabvm_session_" + new URL(auth!.apiEndpoint).host); if (token) { var result = await auth!.loadSession(token); @@ -1032,10 +1071,41 @@ elements.accountLoginForm.addEventListener('submit', async (e) => { } hcaptchaToken = response; } + + var turnstileToken = undefined; + var turnstileID = undefined; + + if (auth!.info!.turnstile.required) { + turnstileID = elements.accountLoginTurnstile.children[0].getAttribute("data-turnstile-widget-id")! + var response: string = turnstile.getResponse(turnstileID) || ""; + if (response === "") { + elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); + elements.accountModalError.style.display = "block"; + return false; + } + turnstileToken = response; + } + + var recaptchaToken = undefined; + var recaptchaID = undefined; + + if (auth!.info!.recaptcha.required) { + recaptchaID = parseInt(elements.accountLoginRecaptcha.children[0].getAttribute("data-recaptcha-widget-id")!) + var response = grecaptcha.getResponse(recaptchaID); + if (response === "") { + elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); + elements.accountModalError.style.display = "block"; + return false; + } + recaptchaToken = response; + } + var username = elements.accountLoginUsername.value; var password = elements.accountLoginPassword.value; - var result = await auth!.login(username, password, hcaptchaToken); + var result = await auth!.login(username, password, hcaptchaToken, turnstileToken, recaptchaToken); if (auth!.info!.hcaptcha.required) hcaptcha.reset(hcaptchaID); + if (auth!.info!.turnstile.required) turnstile.reset(turnstileID); + if (auth!.info!.recaptcha.required) grecaptcha.reset(recaptchaID); if (result.success) { elements.accountLoginUsername.value = ""; elements.accountLoginPassword.value = ""; @@ -1069,6 +1139,35 @@ elements.accountRegisterForm.addEventListener('submit', async (e) => { } hcaptchaToken = response; } + + var turnstileToken = undefined; + var turnstileID = undefined; + + if (auth!.info!.turnstile.required) { + turnstileID = elements.accountRegisterTurnstile.children[0].getAttribute("data-turnstile-widget-id")! + var response: string = turnstile.getResponse(turnstileID) || ""; + if (response === "") { + elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); + elements.accountModalError.style.display = "block"; + return false; + } + turnstileToken = response; + } + + var recaptchaToken = undefined; + var recaptchaID = undefined; + + if (auth!.info!.recaptcha.required) { + recaptchaID = parseInt(elements.accountRegisterRecaptcha.children[0].getAttribute("data-recaptcha-widget-id")!) + var response = grecaptcha.getResponse(recaptchaID); + if (response === "") { + elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); + elements.accountModalError.style.display = "block"; + return false; + } + recaptchaToken = response; + } + var username = elements.accountRegisterUsername.value; var password = elements.accountRegisterPassword.value; var email = elements.accountRegisterEmail.value; @@ -1078,8 +1177,10 @@ elements.accountRegisterForm.addEventListener('submit', async (e) => { elements.accountModalError.style.display = "block"; return false; } - var result = await auth!.register(username, password, email, dob, hcaptchaToken); + var result = await auth!.register(username, password, email, dob, hcaptchaToken, turnstileToken, recaptchaToken); if (auth!.info!.hcaptcha.required) hcaptcha.reset(hcaptchaID); + if (auth!.info!.turnstile.required) turnstile.reset(turnstileID); + if (auth!.info!.recaptcha.required) grecaptcha.reset(recaptchaID); if (result.success) { elements.accountRegisterUsername.value = ""; elements.accountRegisterEmail.value = ""; @@ -1182,10 +1283,41 @@ elements.accountResetPasswordForm.addEventListener('submit', async e => { } hcaptchaToken = response; } + + var turnstileToken = undefined; + var turnstileID = undefined; + + if (auth!.info!.turnstile.required) { + turnstileID = elements.accountResetPasswordTurnstile.children[0].getAttribute("data-turnstile-widget-id")! + var response: string = turnstile.getResponse(turnstileID) || ""; + if (response === "") { + elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); + elements.accountModalError.style.display = "block"; + return false; + } + turnstileToken = response; + } + + var recaptchaToken = undefined; + var recaptchaID = undefined; + + if (auth!.info!.recaptcha.required) { + recaptchaID = parseInt(elements.accountResetPasswordRecaptcha.children[0].getAttribute("data-recaptcha-widget-id")!) + var response = grecaptcha.getResponse(recaptchaID); + if (response === "") { + elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); + elements.accountModalError.style.display = "block"; + return false; + } + recaptchaToken = response; + } + var username = elements.accountResetPasswordUsername.value; var email = elements.accountResetPasswordEmail.value; - var result = await auth!.sendPasswordResetEmail(username, email, hcaptchaToken); + var result = await auth!.sendPasswordResetEmail(username, email, hcaptchaToken, turnstileToken, recaptchaToken); if (auth!.info!.hcaptcha.required) hcaptcha.reset(hcaptchaID); + if (auth!.info!.turnstile.required) turnstile.reset(turnstileID); + if (auth!.info!.recaptcha.required) grecaptcha.reset(recaptchaID); if (result.success) { resetPasswordUsername = username; resetPasswordEmail = email; diff --git a/tsconfig.json b/tsconfig.json index d8e2dae..cda64e7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,7 +32,8 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ "typeRoots": [ - "node_modules/@hcaptcha" + "node_modules/@hcaptcha", + "node_modules/@types" ], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ diff --git a/yarn.lock b/yarn.lock index 16b7cdf..1e79233 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1446,6 +1446,11 @@ dependencies: "@popperjs/core" "^2.9.2" +"@types/cloudflare-turnstile@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@types/cloudflare-turnstile/-/cloudflare-turnstile-0.2.2.tgz#3364d65b00f03376f4e555820db270173807a52c" + integrity sha512-3Yf7b1Glci+V2bFWwWBbZkRgTuegp7RDgNTOG4U0UNPB9RV4AWvwqg2/qqLff8G+SwKFNXoXvTkqaRBZrAFdKA== + "@types/dompurify@^3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7" @@ -1460,6 +1465,11 @@ dependencies: "@types/node" "*" +"@types/grecaptcha@^3.0.9": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@types/grecaptcha/-/grecaptcha-3.0.9.tgz#9f3b07ec06c8fff221aa6fc124fe5b8a0e2c3349" + integrity sha512-fFxMtjAvXXMYTzDFK5NpcVB7WHnrHVLl00QzEGpuFxSAC789io6M+vjcn+g5FTEamIJtJr/IHkCDsqvJxeWDyw== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"