Skip to content

Commit 84c23f7

Browse files
committed
Add Cloudflare Turnstile bot protection to download modal
1 parent 2abab21 commit 84c23f7

File tree

3 files changed

+82
-4
lines changed

3 files changed

+82
-4
lines changed

src/components/DownloadModal.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,18 @@
362362
text-align: center;
363363
}
364364

365+
/* Turnstile widget */
366+
.turnstile-section {
367+
display: flex;
368+
flex-direction: column;
369+
align-items: center;
370+
gap: 8px;
371+
margin-top: 16px;
372+
}
373+
374+
.turnstile-hint {
375+
font-size: 12px;
376+
color: #666;
377+
text-align: center;
378+
}
379+

src/components/DownloadModal.jsx

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,57 @@
33
* Displays in the center of the map with band/format selection options
44
*/
55

6-
import { useState, useEffect } from 'react';
6+
import { useState, useEffect, useRef, useCallback } from 'react';
77
import { downloadTile, triggerDownload, checkBackendHealth } from '../utils/downloadApi';
88
import { getCollection } from '../config/collections';
99
import './DownloadModal.css';
1010

11+
const TURNSTILE_SITE_KEY = import.meta.env.VITE_TURNSTILE_SITE_KEY || '0x4AAAAAACW5aVJzaeavtikq';
12+
1113
function DownloadModal({ item, collection, onClose }) {
1214
const [selectedAsset, setSelectedAsset] = useState(null);
1315
const [selectedFormat, setSelectedFormat] = useState('geotiff');
1416
const [isDownloading, setIsDownloading] = useState(false);
1517
const [error, setError] = useState(null);
1618
const [backendAvailable, setBackendAvailable] = useState(null);
1719
const [abortController, setAbortController] = useState(null);
20+
const [turnstileToken, setTurnstileToken] = useState(null);
21+
const turnstileRef = useRef(null);
22+
const turnstileWidgetId = useRef(null);
23+
24+
// Load Turnstile script and render widget
25+
useEffect(() => {
26+
// Load Turnstile script if not already loaded
27+
if (!window.turnstile) {
28+
const script = document.createElement('script');
29+
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
30+
script.async = true;
31+
script.onload = () => renderTurnstile();
32+
document.head.appendChild(script);
33+
} else {
34+
renderTurnstile();
35+
}
36+
37+
return () => {
38+
// Cleanup widget on unmount
39+
if (turnstileWidgetId.current && window.turnstile) {
40+
window.turnstile.remove(turnstileWidgetId.current);
41+
}
42+
};
43+
}, []);
44+
45+
const renderTurnstile = useCallback(() => {
46+
if (turnstileRef.current && window.turnstile && !turnstileWidgetId.current) {
47+
turnstileWidgetId.current = window.turnstile.render(turnstileRef.current, {
48+
sitekey: TURNSTILE_SITE_KEY,
49+
callback: (token) => setTurnstileToken(token),
50+
'expired-callback': () => setTurnstileToken(null),
51+
'error-callback': () => setTurnstileToken(null),
52+
theme: 'light',
53+
size: 'normal',
54+
});
55+
}
56+
}, []);
1857

1958
// Get collection config for available bands
2059
const collectionConfig = getCollection(collection);
@@ -44,6 +83,11 @@ function DownloadModal({ item, collection, onClose }) {
4483
return;
4584
}
4685

86+
if (!turnstileToken) {
87+
setError('Please complete the security check');
88+
return;
89+
}
90+
4791
setIsDownloading(true);
4892
setError(null);
4993

@@ -67,6 +111,7 @@ function DownloadModal({ item, collection, onClose }) {
67111
format: selectedFormat,
68112
rescale: bandConfig.rescale || null,
69113
colormap: bandConfig.colormap || null,
114+
turnstileToken: turnstileToken,
70115
}, filename, controller.signal);
71116

72117
// Trigger download and close
@@ -82,6 +127,11 @@ function DownloadModal({ item, collection, onClose }) {
82127
}
83128
setIsDownloading(false);
84129
setAbortController(null);
130+
// Reset turnstile for retry
131+
setTurnstileToken(null);
132+
if (turnstileWidgetId.current && window.turnstile) {
133+
window.turnstile.reset(turnstileWidgetId.current);
134+
}
85135
}
86136
};
87137

@@ -164,7 +214,11 @@ function DownloadModal({ item, collection, onClose }) {
164214
{/* Backend status warning */}
165215
{backendAvailable === false && (
166216
<div className="backend-warning">
167-
Download backend is not available. Please try again later.
217+
<strong>Download Service Unavailable</strong>
218+
<p style={{ margin: '8px 0 0 0', fontSize: '12px' }}>
219+
The download backend is currently offline. You can still explore satellite imagery through the map viewer.
220+
This may be due to maintenance or budget limits.
221+
</p>
168222
</div>
169223
)}
170224

@@ -227,6 +281,14 @@ function DownloadModal({ item, collection, onClose }) {
227281
</div>
228282
)}
229283

284+
{/* Turnstile widget */}
285+
<div className="download-section turnstile-section">
286+
<div ref={turnstileRef}></div>
287+
{!turnstileToken && !isDownloading && (
288+
<div className="turnstile-hint">Please complete the security check above</div>
289+
)}
290+
</div>
291+
230292
{/* Error display */}
231293
{error && (
232294
<div className="download-error">
@@ -246,7 +308,7 @@ function DownloadModal({ item, collection, onClose }) {
246308
<button
247309
className="download-button-primary"
248310
onClick={handleDownload}
249-
disabled={isDownloading || backendAvailable === false || !selectedAsset || downloadsDisabled}
311+
disabled={isDownloading || backendAvailable === false || !selectedAsset || downloadsDisabled || !turnstileToken}
250312
>
251313
{isDownloading ? (
252314
<>

src/utils/downloadApi.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export async function getCollectionAssets(collectionId) {
3434
* @param {AbortSignal} [signal] - AbortSignal for request cancellation
3535
* @returns {Promise<Blob>} Downloaded file as Blob
3636
*/
37-
export async function downloadTile({ collection, itemId, assetKey, bbox, format, rescale, colormap }, onProgress, signal) {
37+
export async function downloadTile({ collection, itemId, assetKey, bbox, format, rescale, colormap, turnstileToken }, onProgress, signal) {
3838
const response = await fetch(`${BACKEND_URL}/download`, {
3939
method: 'POST',
4040
headers: {
@@ -48,6 +48,7 @@ export async function downloadTile({ collection, itemId, assetKey, bbox, format,
4848
format: format,
4949
rescale: rescale || null,
5050
colormap: colormap || null,
51+
turnstile_token: turnstileToken || null,
5152
}),
5253
signal: signal,
5354
});

0 commit comments

Comments
 (0)