incognitolm commited on
Commit
caef4d9
·
1 Parent(s): a78b2c4

Turnstile Addition

Browse files
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
- wsClients.set(ws, { tempId: crypto.randomUUID(), ip, userAgent, userId: null, authenticated: false });
 
 
 
 
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' });