Add support for more CAPTCHA providers

This commit is contained in:
MDMCK10 2024-09-30 22:42:52 +02:00
parent 576bdfb13c
commit 92a1d492c1
6 changed files with 185 additions and 12 deletions

View file

@ -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",

View file

@ -10,7 +10,7 @@
<script src="../../node_modules/@fortawesome/fontawesome-free/js/all.min.js" crossorigin="anonymous"></script>
<link rel="icon" href="../assets/favicon.ico">
<meta name="description" content="A website that lets you take turns controlling online virtual machines with complete strangers!"/>
<!-- Opengraph shit -->
<!-- Opengraph -->
<meta property="og:type" content="website"/>
<meta property="og:title" content="CollabVM"/>
<meta property="og:url" content="https://computernewb.com/collab-vm/"/>
@ -97,6 +97,8 @@
<label for="accountLoginPassword" id="accountLoginPasswordLabel"></label><br/>
<input id="accountLoginPassword" type="password" class="form-control" name="password" required><br>
<div id="accountLoginCaptcha"></div>
<div id="accountLoginReCaptcha"></div>
<div id="accountLoginTurnstile"></div>
<button type="submit" class="btn btn-primary" id="accountModalLoginBtn"></button> <button type="button" class="btn btn-secondary" id="accountForgotPasswordButton"></button>
</form>
</div>
@ -113,6 +115,8 @@
<label for="accountRegisterDateOfBirth" id="accountRegisterDateOfBirthLabel"></label><br/>
<input id="accountRegisterDateOfBirth" type="date" class="form-control" name="dateofbirth" required><br/>
<div id="accountRegisterCaptcha"></div>
<div id="accountRegisterReCaptcha"></div>
<div id="accountRegisterTurnstile"></div>
<button type="submit" class="btn btn-primary" id="accountModalRegisterBtn"></button>
</form>
</div>
@ -152,6 +156,8 @@
<label for="accountResetPasswordUsername" id="accountResetPasswordUsernameLabel"></label>
<input id="accountResetPasswordUsername" type="text" class="form-control" name="username" required/><br/>
<div id="accountResetPasswordCaptcha"></div>
<div id="accountResetPasswordReCaptcha"></div>
<div id="accountResetPasswordTurnstile"></div>
<button type="submit" class="btn btn-primary" id="accountResetPasswordBtn"></button>
</div>
<div id="accountResetPasswordVerifySection">
@ -306,7 +312,9 @@
</div>
</div>
</div>
<script src="https://js.hcaptcha.com/1/api.js"></script>
<script src="https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off"></script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
<script src="https://www.google.com/recaptcha/api.js?render=explicit"></script>
<script type="module" src="../ts/main.ts" type="application/javascript"></script>
</body>
</html>

View file

@ -18,10 +18,12 @@ export default class AuthManager {
})
}
login(username : string, password : string, captchaToken : string | undefined) : Promise<AccountLoginResult> {
login(username : string, password : string, captchaToken : string | undefined, turnstileToken : string | undefined, recaptchaToken : string | undefined) : Promise<AccountLoginResult> {
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<AccountRegisterResult> {
register(username : string, password : string, email : string, dateOfBirth : dayjs.Dayjs, captchaToken : string | undefined, turnstileToken: string | undefined, recaptchaToken : string | undefined) : Promise<AccountRegisterResult> {
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<PasswordResetResult>(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 {

View file

@ -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;

View file

@ -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. */

View file

@ -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"