diff --git a/backend/socket-handlers/main-socket-handler.ts b/backend/socket-handlers/main-socket-handler.ts index 9e8886c0..5607c90f 100644 --- a/backend/socket-handlers/main-socket-handler.ts +++ b/backend/socket-handlers/main-socket-handler.ts @@ -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 { + 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) { @@ -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); @@ -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; diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue index 9667d7a7..ceda7461 100644 --- a/frontend/src/components/Login.vue +++ b/frontend/src/components/Login.vue @@ -30,6 +30,8 @@ +
+ @@ -52,10 +54,14 @@ export default { token: "", res: null, tokenRequired: false, + captchaToken: "", + siteKey: "", }; }, mounted() { + + this.getTurnstileSiteKey(); document.title += " - Login"; }, @@ -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; + }, + }); + } + }, + }, }; diff --git a/frontend/src/mixins/socket.ts b/frontend/src/mixins/socket.ts index b789ff6c..a43ad86b 100644 --- a/frontend/src/mixins/socket.ts +++ b/frontend/src/mixins/socket.ts @@ -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);