incognitolm commited on
Commit ·
caef4d9
1
Parent(s): a78b2c4
Turnstile Addition
Browse files- public/css/modals.css +15 -0
- public/index.html +18 -0
- public/js/turnstile.js +38 -0
- public/oauth-callback.html +9 -0
- server/index.js +38 -1
- server/wsHandler.js +19 -0
public/css/modals.css
CHANGED
|
@@ -195,3 +195,18 @@
|
|
| 195 |
.limit-icon { font-size: 40px; margin-bottom: 12px; }
|
| 196 |
.limit-title { font-size: 20px; font-weight: 600; margin-bottom: 6px; }
|
| 197 |
.limit-desc { font-size: 14px; color: var(--text-dim); margin-bottom: 20px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
.limit-icon { font-size: 40px; margin-bottom: 12px; }
|
| 196 |
.limit-title { font-size: 20px; font-weight: 600; margin-bottom: 6px; }
|
| 197 |
.limit-desc { font-size: 14px; color: var(--text-dim); margin-bottom: 20px; }
|
| 198 |
+
|
| 199 |
+
/* ── Turnstile verification overlay ─────────────────────────────────────── */
|
| 200 |
+
.turnstile-overlay { display: flex; align-items: center; justify-content: center; }
|
| 201 |
+
.turnstile-box {
|
| 202 |
+
background: rgba(17,17,19,0.96);
|
| 203 |
+
border-radius: var(--radius-lg);
|
| 204 |
+
padding: 20px;
|
| 205 |
+
width: 100%; max-width: 420px;
|
| 206 |
+
box-shadow: 0 8px 30px rgba(0,0,0,0.6);
|
| 207 |
+
text-align: center; color: var(--text);
|
| 208 |
+
}
|
| 209 |
+
.turnstile-message { margin-bottom: 12px; color: var(--text-dim); font-size: 14px; }
|
| 210 |
+
|
| 211 |
+
/* make the rest of the page dim and inert while overlay visible */
|
| 212 |
+
.page-faded { filter: blur(2px) brightness(0.6); pointer-events: none; user-select: none; }
|
public/index.html
CHANGED
|
@@ -14,6 +14,12 @@
|
|
| 14 |
<link rel="stylesheet" href="/css/modals.css" />
|
| 15 |
<link rel="stylesheet" href="/css/input.css" />
|
| 16 |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
</head>
|
| 18 |
<body>
|
| 19 |
<!-- Share import overlay -->
|
|
@@ -160,6 +166,16 @@
|
|
| 160 |
<div id="modal-box" class="modal-box"></div>
|
| 161 |
</div>
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
<!-- Notification area -->
|
| 164 |
<div id="notifications" class="notifications"></div>
|
| 165 |
|
|
@@ -171,6 +187,8 @@
|
|
| 171 |
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
| 172 |
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>
|
| 173 |
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
|
|
|
|
|
|
|
| 174 |
<script type="module" src="/js/app.js"></script>
|
| 175 |
<div
|
| 176 |
style="
|
|
|
|
| 14 |
<link rel="stylesheet" href="/css/modals.css" />
|
| 15 |
<link rel="stylesheet" href="/css/input.css" />
|
| 16 |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
| 17 |
+
<script
|
| 18 |
+
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
|
| 19 |
+
async
|
| 20 |
+
defer
|
| 21 |
+
></script>
|
| 22 |
+
<link rel="preconnect" href="https://challenges.cloudflare.com" />
|
| 23 |
</head>
|
| 24 |
<body>
|
| 25 |
<!-- Share import overlay -->
|
|
|
|
| 166 |
<div id="modal-box" class="modal-box"></div>
|
| 167 |
</div>
|
| 168 |
|
| 169 |
+
<!-- Turnstile verification overlay (shows until user verifies) -->
|
| 170 |
+
<div id="turnstile-overlay" class="modal-overlay turnstile-overlay">
|
| 171 |
+
<div class="turnstile-box" id="turnstile-box">
|
| 172 |
+
<div class="turnstile-message">Please verify you are human to continue using the chat.</div>
|
| 173 |
+
<div id="turnstile-widget">
|
| 174 |
+
<div class="cf-turnstile" data-sitekey="0x4AAAAAAC1ZXKIhZ9Kdz8j9" data-callback="onTurnstileSuccess"></div>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
<!-- Notification area -->
|
| 180 |
<div id="notifications" class="notifications"></div>
|
| 181 |
|
|
|
|
| 187 |
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
| 188 |
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>
|
| 189 |
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
|
| 190 |
+
<!-- Turnstile widget handler (shows verification overlay) -->
|
| 191 |
+
<script type="module" src="/js/turnstile.js"></script>
|
| 192 |
<script type="module" src="/js/app.js"></script>
|
| 193 |
<div
|
| 194 |
style="
|
public/js/turnstile.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// turnstile.js — show overlay and handle verification
|
| 2 |
+
(function() {
|
| 3 |
+
function hasTurnstileCookie() {
|
| 4 |
+
return document.cookie.split(';').some(c => c.trim().startsWith('turnstile='));
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
const overlay = document.getElementById('turnstile-overlay');
|
| 8 |
+
const pageRoot = document.getElementById('app') || document.body;
|
| 9 |
+
|
| 10 |
+
function hideOverlay() {
|
| 11 |
+
if (overlay) overlay.style.display = 'none';
|
| 12 |
+
if (pageRoot) pageRoot.classList.remove('page-faded');
|
| 13 |
+
}
|
| 14 |
+
function showOverlay() {
|
| 15 |
+
if (overlay) overlay.style.display = 'flex';
|
| 16 |
+
if (pageRoot) pageRoot.classList.add('page-faded');
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Global callback for Cloudflare Turnstile
|
| 20 |
+
window.onTurnstileSuccess = async function(token) {
|
| 21 |
+
try {
|
| 22 |
+
// Try REST verify first (sets cookie)
|
| 23 |
+
await fetch('/api/turnstile', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }) });
|
| 24 |
+
} catch (e) {
|
| 25 |
+
console.error('Turnstile REST verify failed', e);
|
| 26 |
+
}
|
| 27 |
+
// Also notify server over websocket (if connected)
|
| 28 |
+
try {
|
| 29 |
+
if (window.ws?.send) window.ws.send({ type: 'turnstile:verify', token });
|
| 30 |
+
} catch (e) { /* ignore */ }
|
| 31 |
+
|
| 32 |
+
// Hide overlay
|
| 33 |
+
hideOverlay();
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
// Initialize visibility
|
| 37 |
+
if (hasTurnstileCookie()) hideOverlay(); else showOverlay();
|
| 38 |
+
})();
|
public/oauth-callback.html
CHANGED
|
@@ -78,5 +78,14 @@
|
|
| 78 |
msgEl.textContent = 'No token received. You can close this window.';
|
| 79 |
}
|
| 80 |
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
</body>
|
| 82 |
</html>
|
|
|
|
| 78 |
msgEl.textContent = 'No token received. You can close this window.';
|
| 79 |
}
|
| 80 |
</script>
|
| 81 |
+
<!-- Cloudflare Turnstile -->
|
| 82 |
+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
| 83 |
+
<div id="turnstile-overlay" style="position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);z-index:9999;">
|
| 84 |
+
<div style="background:#0d0e11;padding:18px;border-radius:10px;text-align:center;max-width:420px;width:90%;">
|
| 85 |
+
<div style="color:#d7d7d7;margin-bottom:12px;">Please verify you are human to continue.</div>
|
| 86 |
+
<div class="cf-turnstile" data-sitekey="0x4AAAAAAC1ZXKIhZ9Kdz8j9" data-callback="onTurnstileSuccess"></div>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
<script type="module" src="/js/turnstile.js"></script>
|
| 90 |
</body>
|
| 91 |
</html>
|
server/index.js
CHANGED
|
@@ -23,6 +23,14 @@ const app = express();
|
|
| 23 |
|
| 24 |
app.use(express.static(path.join(__dirname, '..', 'public')));
|
| 25 |
app.use(express.json({ limit: '10mb' }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
app.get('/health', (_req, res) => res.json({ ok: true }));
|
| 27 |
|
| 28 |
app.get('/api/share/:token', async (req, res) => {
|
|
@@ -40,6 +48,31 @@ app.get('/api/share/:token', async (req, res) => {
|
|
| 40 |
} catch { res.status(500).json({ error: 'Server error' }); }
|
| 41 |
});
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
app.get('*', (req, res) => {
|
| 44 |
if (!req.path.startsWith('/api/'))
|
| 45 |
res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
|
|
@@ -54,7 +87,11 @@ wss.on('connection', (ws, req) => {
|
|
| 54 |
const ip = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
| 55 |
|| req.socket.remoteAddress || 'unknown';
|
| 56 |
const userAgent = req.headers['user-agent'] || 'unknown';
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
ws.on('message', async raw => {
|
| 60 |
try { await handleWsMessage(ws, JSON.parse(raw.toString()), wsClients); }
|
|
|
|
| 23 |
|
| 24 |
app.use(express.static(path.join(__dirname, '..', 'public')));
|
| 25 |
app.use(express.json({ limit: '10mb' }));
|
| 26 |
+
// Require turnstile cookie for API routes (except the verification endpoint itself)
|
| 27 |
+
app.use('/api', (req, res, next) => {
|
| 28 |
+
const exempt = ['/turnstile', '/health'];
|
| 29 |
+
if (exempt.includes(req.path)) return next();
|
| 30 |
+
const cookieHeader = req.headers.cookie || '';
|
| 31 |
+
if (cookieHeader.includes('turnstile=1')) return next();
|
| 32 |
+
return res.status(403).json({ error: 'turnstile:required' });
|
| 33 |
+
});
|
| 34 |
app.get('/health', (_req, res) => res.json({ ok: true }));
|
| 35 |
|
| 36 |
app.get('/api/share/:token', async (req, res) => {
|
|
|
|
| 48 |
} catch { res.status(500).json({ error: 'Server error' }); }
|
| 49 |
});
|
| 50 |
|
| 51 |
+
// Turnstile verification endpoint - accepts token and verifies with Cloudflare
|
| 52 |
+
app.post('/api/turnstile', async (req, res) => {
|
| 53 |
+
try {
|
| 54 |
+
const token = req.body?.token;
|
| 55 |
+
const secret = process.env.TURNSTILE_SECRET_KEY;
|
| 56 |
+
if (!token || !secret) return res.status(400).json({ error: 'Missing token or server not configured' });
|
| 57 |
+
|
| 58 |
+
const params = new URLSearchParams();
|
| 59 |
+
params.append('secret', secret);
|
| 60 |
+
params.append('response', token);
|
| 61 |
+
if (req.ip) params.append('remoteip', req.ip);
|
| 62 |
+
|
| 63 |
+
const r = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
| 64 |
+
method: 'POST', body: params,
|
| 65 |
+
});
|
| 66 |
+
const j = await r.json();
|
| 67 |
+
if (j?.success) {
|
| 68 |
+
// Set a short-lived cookie to allow access to other endpoints
|
| 69 |
+
res.cookie('turnstile', '1', { maxAge: 24 * 3600 * 1000, path: '/', sameSite: 'lax' });
|
| 70 |
+
return res.json({ success: true });
|
| 71 |
+
}
|
| 72 |
+
return res.status(403).json({ error: 'Verification failed' });
|
| 73 |
+
} catch (e) { console.error('turnstile verify', e); return res.status(500).json({ error: 'Server error' }); }
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
app.get('*', (req, res) => {
|
| 77 |
if (!req.path.startsWith('/api/'))
|
| 78 |
res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
|
|
|
|
| 87 |
const ip = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
| 88 |
|| req.socket.remoteAddress || 'unknown';
|
| 89 |
const userAgent = req.headers['user-agent'] || 'unknown';
|
| 90 |
+
// Mark verified if request included a turnstile cookie
|
| 91 |
+
const cookies = (req.headers.cookie || '').split(';').map(s => s.trim()).filter(Boolean);
|
| 92 |
+
const cookieMap = Object.fromEntries(cookies.map(c => { const i = c.indexOf('='); return [c.slice(0, i), c.slice(i+1)]; }));
|
| 93 |
+
const verified = cookieMap.turnstile === '1';
|
| 94 |
+
wsClients.set(ws, { tempId: crypto.randomUUID(), ip, userAgent, userId: null, authenticated: false, verified });
|
| 95 |
|
| 96 |
ws.on('message', async raw => {
|
| 97 |
try { await handleWsMessage(ws, JSON.parse(raw.toString()), wsClients); }
|
server/wsHandler.js
CHANGED
|
@@ -15,6 +15,10 @@ const activeStreams = new Map();
|
|
| 15 |
|
| 16 |
export async function handleWsMessage(ws, msg, wsClients) {
|
| 17 |
const client = wsClients.get(ws); if (!client) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
const h = handlers[msg.type];
|
| 19 |
if (h) return h(ws, msg, client, wsClients);
|
| 20 |
safeSend(ws, { type: 'error', message: `Unknown: ${msg.type}` });
|
|
@@ -27,6 +31,21 @@ function bcast(wsClients, userId, data, excludeWs) {
|
|
| 27 |
const handlers = {
|
| 28 |
'ping': (ws) => { safeSend(ws, { type: 'pong' }); },
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
'auth:login': async (ws, msg, client, wsClients) => {
|
| 31 |
const { accessToken, tempId: clientTempId } = msg;
|
| 32 |
if (!accessToken) return safeSend(ws, { type: 'auth:error', message: 'Missing token' });
|
|
|
|
| 15 |
|
| 16 |
export async function handleWsMessage(ws, msg, wsClients) {
|
| 17 |
const client = wsClients.get(ws); if (!client) return;
|
| 18 |
+
// Require turnstile verification for most message types
|
| 19 |
+
if (!client.verified && msg.type !== 'ping' && msg.type !== 'turnstile:verify') {
|
| 20 |
+
return safeSend(ws, { type: 'error', message: 'turnstile:required' });
|
| 21 |
+
}
|
| 22 |
const h = handlers[msg.type];
|
| 23 |
if (h) return h(ws, msg, client, wsClients);
|
| 24 |
safeSend(ws, { type: 'error', message: `Unknown: ${msg.type}` });
|
|
|
|
| 31 |
const handlers = {
|
| 32 |
'ping': (ws) => { safeSend(ws, { type: 'pong' }); },
|
| 33 |
|
| 34 |
+
// Verify turnstile token sent over websocket
|
| 35 |
+
'turnstile:verify': async (ws, msg, client) => {
|
| 36 |
+
try {
|
| 37 |
+
const token = msg?.token;
|
| 38 |
+
const secret = process.env.TURNSTILE_SECRET_KEY;
|
| 39 |
+
if (!token || !secret) return safeSend(ws, { type: 'turnstile:error', message: 'Missing token or server not configured' });
|
| 40 |
+
const params = new URLSearchParams(); params.append('secret', secret); params.append('response', token);
|
| 41 |
+
if (client.ip) params.append('remoteip', client.ip);
|
| 42 |
+
const r = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: params });
|
| 43 |
+
const j = await r.json();
|
| 44 |
+
if (j?.success) { client.verified = true; return safeSend(ws, { type: 'turnstile:ok' }); }
|
| 45 |
+
return safeSend(ws, { type: 'turnstile:error', message: 'Verification failed' });
|
| 46 |
+
} catch (e) { console.error('ws turnstile verify', e); return safeSend(ws, { type: 'turnstile:error', message: 'Server error' }); }
|
| 47 |
+
},
|
| 48 |
+
|
| 49 |
'auth:login': async (ws, msg, client, wsClients) => {
|
| 50 |
const { accessToken, tempId: clientTempId } = msg;
|
| 51 |
if (!accessToken) return safeSend(ws, { type: 'auth:error', message: 'Missing token' });
|