diff --git a/docs/s3_cors_presign.md b/docs/s3_cors_presign.md new file mode 100644 index 0000000..f7a2cba --- /dev/null +++ b/docs/s3_cors_presign.md @@ -0,0 +1,56 @@ +# Bucket CORS for Presigned Asset Fetches + +The frontend loads certain assets (e.g. OCR text) with `fetch()` against their presigned URLs +(see `frontend/src/preview/DocumentViewerPanel.jsx`). Browsers will block that request unless +the storage bucket sends CORS headers that allow the frontend origin. Configure a rule that +includes: + +* the list of allowed origins (your production, staging, or local domains) +* `GET` (and optionally other methods you expose) +* permissive request headers (usually `"*"` is fine for presigned URLs) +* exposed response headers if the frontend needs them (`etag`, `content-length`, etc.) + +## Example CORS document + +```json +{ + "CORSRules": [ + { + "AllowedOrigins": ["https://app.example"], + "AllowedMethods": ["GET"], + "AllowedHeaders": ["*"], + "ExposeHeaders": ["etag", "content-length", "content-type"], + "MaxAgeSeconds": 300 + } + ] +} +``` + +Replace `https://app.example` with each domain that must fetch presigned assets. Add additional +rules if different origins require different methods. + +## Applying the rule + +### AWS S3 CLI +```bash +aws s3api put-bucket-cors \ + --bucket \ + --cors-configuration file://cors.json \ + [--endpoint-url ] +``` +Save the JSON payload as `cors.json`. When targeting S3-compatible providers (e.g. Hetzner, Ceph RGW), +pass their endpoint via `--endpoint-url`. + +### s3cmd (Ceph RGW / generic S3) +```bash +s3cmd setcors cors.json s3:// +``` + +### MinIO Client (`mc`) +```bash +mc alias set storage +mc anonymous set-json storage/ cors.json +``` + +Most dashboards expose a similar form—paste the JSON rule into the CORS section for the bucket. +Once the rule is active, browsers will allow the frontend to read presigned assets with fetch(). diff --git a/frontend/src/preview/DocumentViewerPanel.jsx b/frontend/src/preview/DocumentViewerPanel.jsx index 5ef7e35..57f5ab9 100644 --- a/frontend/src/preview/DocumentViewerPanel.jsx +++ b/frontend/src/preview/DocumentViewerPanel.jsx @@ -108,14 +108,14 @@ const DocumentViewerPanel = ({ setActiveTab('details'); }, [document?.id, hasOcr, metadataPayload]); - const [ocrUrl, setOcrUrl] = useState(null); + const [ocrContent, setOcrContent] = useState(null); const [ocrLoading, setOcrLoading] = useState(false); const [ocrError, setOcrError] = useState(null); useEffect(() => { let cancelled = false; if (!document || !hasOcr || typeof getDocumentAsset !== 'function') { - setOcrUrl(null); + setOcrContent(null); setOcrLoading(false); setOcrError(null); return () => { @@ -147,8 +147,41 @@ const DocumentViewerPanel = ({ } } + let textContent = null; + if (!cancelled && url) { + const controller = new AbortController(); + + try { + const response = await fetch(url, { + method: 'GET', + mode: 'cors', + credentials: 'omit', + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(`Unexpected status: ${response.status}`); + } + + textContent = await response.text(); + } catch (error) { + if (!cancelled) { + console.error('[OCR] Failed to fetch text', error); + setOcrError('Unable to load OCR content.'); + } + } + + if (!cancelled) { + setOcrContent(textContent); + } + + controller.abort(); + } + if (!cancelled) { - setOcrUrl(url); + if (!textContent) { + setOcrContent(null); + } setOcrLoading(false); } }; @@ -248,12 +281,10 @@ const DocumentViewerPanel = ({
{ocrError}
- ) : ocrUrl ? ( -