Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions backend/socket-handlers/main-socket-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,34 @@ import { Settings } from "../settings";
import fs, { promises as fsAsync } from "fs";
import path from "path";

async function verifyTurnstileToken(token: string, clientIP: string, secretKey: string): Promise<boolean> {
if (!token) {
console.error("Token is not provided.");
return false;
}

const url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const result = await fetch(url, {
body: JSON.stringify({
secret: secretKey,
response: token,
remoteip: clientIP
}),
method: "POST",
headers: {
"Content-Type": "application/json"
}
});
const outcome = await result.json();
if (outcome && outcome.success) {
console.log("Token verified successfully:", outcome);
return true;
}
console.error("Token verification failed:", outcome["error-codes"]);
return false;

}

export class MainSocketHandler extends SocketHandler {
create(socket : DockgeSocket, server : DockgeServer) {

Expand Down Expand Up @@ -62,6 +90,37 @@ export class MainSocketHandler extends SocketHandler {
}
});

// New event to fetch the Turnstile site key
socket.on("getTurnstileSiteKey", async (callback) => {
try {
const siteKey = process.env.TURNSTILE_SITE_KEY || "";
console.log("Turnstile site key from env:", siteKey);
// Checking
if (typeof callback !== "function") {
return;
}
if (!siteKey) {
log.warn("auth", "Turnstile site key is not configured in the environment.");
callback({
ok: false,
msg: "Turnstile site key is not configured.",
});
return;
}

callback({
ok: true,
siteKey: siteKey,
});
} catch (error) {
log.error("Error fetching Turnstile site key:", error);
callback({
ok: false,
msg: "Failed to fetch Turnstile site key.",
});
}
});

// Login by token
socket.on("loginByToken", async (token, callback) => {
const clientIP = await server.getClientIP(socket);
Expand Down Expand Up @@ -126,6 +185,21 @@ export class MainSocketHandler extends SocketHandler {

log.info("auth", `Login by username + password. IP=${clientIP}`);

const siteKey = process.env.TURNSTILE_SITE_KEY || "";
const secretKey = process.env.TURNSTILE_SECRET_KEY || "";
if (siteKey && secretKey) {
// Verify Turnstile token
const isCaptchaValid = await verifyTurnstileToken(data.captchaToken, clientIP, secretKey);
if (!isCaptchaValid) {
return callback({
ok: false,
msg: "Invalid CAPTCHA"
});
}
} else {
log.warn("auth", "Turnstile keys are not configured. Skipping CAPTCHA verification.");
}

// Checking
if (typeof callback !== "function") {
return;
Expand Down
75 changes: 74 additions & 1 deletion frontend/src/components/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
</label>
</div>
</div>
<div v-if="siteKey" id="turnstile-widget" ref="turnstile" class="mt-3 mb-3"></div>

<button class="w-100 btn btn-primary" type="submit" :disabled="processing">
{{ $t("Login") }}
</button>
Expand All @@ -52,10 +54,14 @@ export default {
token: "",
res: null,
tokenRequired: false,
captchaToken: "",
siteKey: "",
};
},
mounted() {
this.getTurnstileSiteKey();
document.title += " - Login";
},
Expand All @@ -71,17 +77,84 @@ export default {
submit() {
this.processing = true;
this.$root.login(this.username, this.password, this.token, (res) => {
if (this.siteKey && !this.captchaToken) {
console.error("CAPTCHA token is missing or invalid.");
this.processing = false;
this.res = { ok: false,
msg: "Invalid CAPTCHA!" };
this.resetTurnstile(); // Reset the widget
return;
}
this.$root.login(this.username, this.password, this.token, this.captchaToken, (res) => {
this.processing = false;
if (res.tokenRequired) {
this.tokenRequired = true;
} else if (!res.ok) {
this.res = res;
this.resetTurnstile();
} else {
this.res = res;
}
});
},
resetTurnstile() {
if (window.turnstile && this.$refs.turnstile) {
console.log("Resetting Turnstile widget...");
window.turnstile.reset(this.$refs.turnstile);
this.captchaToken = ""; // Clear the old token
}
},
getTurnstileSiteKey() {
this.$root.getTurnstileSiteKey((res) => {
if (res.ok) {
this.siteKey = res.siteKey;
if (this.siteKey) {
console.log("Turnstile site key is provided. Loading Turnstile script...");
const script = document.createElement("script");
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
script.async = true;
script.defer = true;
script.onload = () => {
console.log("Turnstile script loaded successfully.");
this.initializeTurnstile();
};
script.onerror = () => {
console.error("Failed to load Turnstile script.");
};
document.head.appendChild(script);
console.log("Turnstile script loaded...");
} else {
console.warn("Turnstile site key is not provided. Widget will not be rendered.");
}
} else {
console.error("Failed to fetch Turnstile site key from socket:", res.msg);
}
});
},
/**
* Initialize the Turnstile widget
*/
initializeTurnstile() {
if (window.turnstile) {
console.log("Initializing Turnstile widget...");
window.turnstile.render(this.$refs.turnstile, {
sitekey: this.siteKey,
callback: (token) => {
this.captchaToken = token; // Save the token
},
"error-callback": () => {
console.error("Turnstile error occurred");
this.captchaToken = null;
},
});
}
},
},
};
</script>
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/mixins/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,19 +318,31 @@ export default defineComponent({
return undefined;
},

/**
* Get configured Cloudflare Turnstile Site Key from backend
* @param {loginCB} callback Callback to call with result
*/
getTurnstileSiteKey(callback) {
this.getSocket().emit("getTurnstileSiteKey", (res) => {
callback(res);
});
},

/**
* Send request to log user in
* @param {string} username Username to log in with
* @param {string} password Password to log in with
* @param {string} token User token
* @param {string} captchaToken captcha token
* @param {loginCB} callback Callback to call with result
* @returns {void}
*/
login(username : string, password : string, token : string, callback) {
login(username : string, password : string, token : string, captchaToken : string, callback) {
this.getSocket().emit("login", {
username,
password,
token,
captchaToken,
}, (res) => {
if (res.tokenRequired) {
callback(res);
Expand Down
Loading