From df2349b197445f69e8890b1373bdf54777ead4bf Mon Sep 17 00:00:00 2001 From: ddrayko Date: Sat, 20 Jun 2026 16:47:45 +0200 Subject: [PATCH 01/17] Bump version to v0.9.7 BETA --- package.json | 2 +- public/js/app.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d8631e5..7b80abf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zerohost-dashboard", - "version": "1.0.0", + "version": "0.9.7", "private": true, "type": "module", "scripts": { diff --git a/public/js/app.js b/public/js/app.js index d11f187..315958a 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -398,7 +398,7 @@ async function renderDashboard() {
-
v0.9.6 BETA
+
v0.9.7 BETA
From 3499996420417096250efc46a782398d78cbd6d2 Mon Sep 17 00:00:00 2001 From: ddrayko Date: Sat, 20 Jun 2026 18:16:21 +0200 Subject: [PATCH 02/17] Replace Cloudflare Turnstile with self-hosted Cap CAPTCHA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add config/cap.js with Cap server-side verification - Remove config/turnstile.js - Update client-side: replace Turnstile widget with cap-widget web component - Update routes: rename cfTurnstile → capToken, verifyTurnstile → verifyCap - Update .env.example with Cap configuration --- .env.example | 5 +++-- config/cap.js | 35 +++++++++++++++++++++++++++++++++ config/turnstile.js | 30 ---------------------------- public/index.html | 2 +- public/js/app.js | 48 ++++++++++----------------------------------- routes/auth.js | 12 ++++++------ routes/servers.js | 6 +++--- 7 files changed, 58 insertions(+), 80 deletions(-) create mode 100644 config/cap.js delete mode 100644 config/turnstile.js diff --git a/.env.example b/.env.example index 04b120c..0037729 100644 --- a/.env.example +++ b/.env.example @@ -20,8 +20,9 @@ PTERO_API_KEY=change_me # Encryption key for sensitive data — Generate with: openssl rand -hex 32 ENCRYPTION_KEY=change_me_to_a_random_hex_string -# Cloudflare Turnstile -TURNSTILE_SECRET=change_me +# Cap CAPTCHA (self-hosted alternative to Turnstile) +CAP_ENDPOINT=https://cap.zero-host.org/f6c8171b08/ +CAP_SECRET=change_me # Session & Cookie COOKIE_SECRET=change_me_to_a_random_string diff --git a/config/cap.js b/config/cap.js new file mode 100644 index 0000000..cb79d0f --- /dev/null +++ b/config/cap.js @@ -0,0 +1,35 @@ +const CAP_SECRET = process.env.CAP_SECRET; +const CAP_ENDPOINT = process.env.CAP_ENDPOINT; + +if (!CAP_SECRET) { + console.error('Missing CAP_SECRET environment variable'); +} + +if (!CAP_ENDPOINT) { + console.error('Missing CAP_ENDPOINT environment variable'); +} + +async function fetchWithTimeout(url, options = {}, timeout = 10000) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeout); + try { + return await fetch(url, { ...options, signal: controller.signal }); + } finally { + clearTimeout(timer); + } +} + +export async function verifyCap(token) { + if (!token) return false; + try { + const res = await fetchWithTimeout(`${CAP_ENDPOINT}siteverify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ secret: CAP_SECRET, response: token }), + }); + const data = await res.json(); + return data.success === true; + } catch { + return false; + } +} diff --git a/config/turnstile.js b/config/turnstile.js deleted file mode 100644 index e470372..0000000 --- a/config/turnstile.js +++ /dev/null @@ -1,30 +0,0 @@ -const TURNSTILE_SECRET = process.env.TURNSTILE_SECRET; - -if (!TURNSTILE_SECRET) { - console.error('Missing TURNSTILE_SECRET environment variable'); -} - -async function fetchWithTimeout(url, options = {}, timeout = 10000) { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeout); - try { - return await fetch(url, { ...options, signal: controller.signal }); - } finally { - clearTimeout(timer); - } -} - -export async function verifyTurnstile(token) { - if (!token) return false; - try { - const res = await fetchWithTimeout('https://challenges.cloudflare.com/turnstile/v0/siteverify', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ secret: TURNSTILE_SECRET, response: token }), - }); - const data = await res.json(); - return data.success === true; - } catch { - return false; - } -} diff --git a/public/index.html b/public/index.html index 7c1c6d6..689ea10 100644 --- a/public/index.html +++ b/public/index.html @@ -9,7 +9,7 @@ - + diff --git a/public/js/app.js b/public/js/app.js index 315958a..4f80f51 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -24,7 +24,7 @@ function renderCookieBanner() { banner.innerHTML = ` @@ -150,29 +150,7 @@ function hideError(form) { if (errorEl) errorEl.classList.remove('show'); } -const turnstileWidgets = {}; - -function initTurnstile(selector) { - const el = typeof selector === 'string' ? document.querySelector(selector) : selector; - if (!el) return; - const tryRender = () => { - if (typeof turnstile !== 'undefined') { - if (el.querySelector('iframe')) return; - const widgetId = turnstile.render(el, { sitekey: '0x4AAAAAADjivxHTaDDdYR8W', theme: 'dark' }); - turnstileWidgets[selector] = widgetId; - } else { - setTimeout(tryRender, 200); - } - }; - tryRender(); -} -function resetTurnstile(selector) { - const id = turnstileWidgets[selector]; - if (typeof turnstile !== 'undefined' && id !== undefined) { - turnstile.reset(id); - } -} // ===== AUTH PAGES ===== function renderLoginPage() { @@ -196,7 +174,7 @@ function renderLoginPage() { -
+ @@ -210,7 +188,6 @@ function renderLoginPage() { `; $('#login-form').addEventListener('submit', handleLogin); - initTurnstile('#login-turnstile'); $('#go-register').addEventListener('click', (e) => { e.preventDefault(); renderRegisterPage(); @@ -248,7 +225,7 @@ function renderRegisterPage() { I agree to the privacy policy and consent to the processing of my personal data (email, username, IP address) for account management purposes. * -
+ @@ -262,7 +239,6 @@ function renderRegisterPage() { `; $('#register-form').addEventListener('submit', handleRegister); - initTurnstile('#register-turnstile'); $('#go-login').addEventListener('click', (e) => { e.preventDefault(); renderLoginPage(); @@ -277,13 +253,13 @@ async function handleLogin(e) { btn.innerHTML = ' Signing in...'; try { - const turnstileToken = document.querySelector('#login-turnstile')?.querySelector('[name="cf-turnstile-response"]')?.value || ''; + const capToken = document.querySelector('[name="cap-token"]')?.value || ''; const data = await api('/auth/login', { method: 'POST', body: JSON.stringify({ email: $('#login-email').value, password: $('#login-password').value, - cfTurnstile: turnstileToken, + capToken, }), }); state.token = data.token; @@ -293,7 +269,6 @@ async function handleLogin(e) { renderDashboard(); } catch (err) { showError(e.target, err.message); - resetTurnstile('#login-turnstile'); } finally { btn.disabled = false; btn.innerHTML = 'Sign In'; @@ -315,14 +290,14 @@ async function handleRegister(e) { btn.innerHTML = ' Creating...'; try { - const turnstileToken = document.querySelector('#register-turnstile')?.querySelector('[name="cf-turnstile-response"]')?.value || ''; + const capToken = document.querySelector('[name="cap-token"]')?.value || ''; const data = await api('/auth/register', { method: 'POST', body: JSON.stringify({ email: $('#reg-email').value, username: $('#reg-username').value, password: $('#reg-password').value, - cfTurnstile: turnstileToken, + capToken, rgpdConsent: true, }), }); @@ -333,7 +308,6 @@ async function handleRegister(e) { renderDashboard(); } catch (err) { showError(e.target, err.message); - resetTurnstile('#register-turnstile'); } finally { btn.disabled = false; btn.innerHTML = 'Create Account'; @@ -763,7 +737,7 @@ async function renderCreateServer() { 3 GB Disk -
+ @@ -225,7 +225,7 @@ function renderRegisterPage() { I agree to the privacy policy and consent to the processing of my personal data (email, username, IP address) for account management purposes. * - + @@ -738,7 +738,7 @@ async function renderCreateServer() {
- +
@@ -228,7 +254,6 @@ function renderRegisterPage() { I agree to the privacy policy and consent to the processing of my personal data (email, username, IP address) for account management purposes. * - @@ -256,7 +281,7 @@ async function handleLogin(e) { btn.innerHTML = ' Signing in...'; try { - const capToken = document.querySelector('[name="cap-token"]')?.value || ''; + const capToken = await showCapModal(); const data = await api('/auth/login', { method: 'POST', body: JSON.stringify({ @@ -293,7 +318,7 @@ async function handleRegister(e) { btn.innerHTML = ' Creating...'; try { - const capToken = document.querySelector('[name="cap-token"]')?.value || ''; + const capToken = await showCapModal(); const data = await api('/auth/register', { method: 'POST', body: JSON.stringify({ From 927063f2a3a46843f88f4f191234cec21c113a98 Mon Sep 17 00:00:00 2001 From: ddrayko Date: Mon, 22 Jun 2026 03:09:23 +0200 Subject: [PATCH 17/17] Fix cap-widget not rendering in modal: set explicit width, use createElement instead of innerHTML --- public/css/style.css | 10 ++++++++-- public/js/app.js | 45 ++++++++++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index aac0216..f0f148c 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -9,7 +9,7 @@ --bg-secondary: #1c1917; --bg-card: #1c1917; --bg-card-hover: #292524; - --bg-sidebar: #000; + --bg-sidebar: #0d0d0d; --border: rgba(238, 129, 50, 0.15); --border-hover: rgba(238, 129, 50, 0.3); --accent-1: #ee8132; @@ -1413,11 +1413,17 @@ tbody tr:hover { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; - padding: 32px; + padding: 24px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); animation: capScaleIn 0.2s ease; } +.cap-modal cap-widget { + display: block !important; + width: 300px !important; + min-width: unset !important; +} + .cap-modal-overlay.cap-modal-fadeout .cap-modal { animation: capScaleOut 0.3s ease forwards; } diff --git a/public/js/app.js b/public/js/app.js index 6170649..0e87ff3 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -154,28 +154,41 @@ function hideError(form) { function showCapModal() { return new Promise((resolve) => { + if (!customElements.get('cap-widget')) { + resolve(''); + return; + } + const capApiEndpoint = 'https://cap.zero-host.org/f6c8171b08/'; const overlay = document.createElement('div'); overlay.className = 'cap-modal-overlay'; - overlay.innerHTML = ` -
- -
- `; + + const modal = document.createElement('div'); + modal.className = 'cap-modal'; + + const widget = document.createElement('cap-widget'); + widget.setAttribute('data-cap-api-endpoint', capApiEndpoint); + widget.setAttribute('theme', 'dark'); + + modal.appendChild(widget); + overlay.appendChild(modal); document.body.appendChild(overlay); - const check = setInterval(() => { - const input = overlay.querySelector('[name="cap-token"]'); - if (input && input.value) { - clearInterval(check); - overlay.classList.add('cap-modal-fadeout'); - setTimeout(() => { - overlay.remove(); - resolve(input.value); - }, 300); - } - }, 200); + setTimeout(() => { + const check = setInterval(() => { + const hiddenInput = widget.querySelector('[name="cap-token"]'); + const token = widget.token || (hiddenInput && hiddenInput.value) || ''; + if (token) { + clearInterval(check); + overlay.classList.add('cap-modal-fadeout'); + setTimeout(() => { + overlay.remove(); + resolve(token); + }, 300); + } + }, 200); + }, 100); }); }