Skip to content

quotes: store uploaded files in our own R2 bucket, not litterbox.catbox.moe - #34

Merged
Aswinmcw merged 1 commit into
mainfrom
feat/quote-uploads-r2
Sep 3, 2026
Merged

quotes: store uploaded files in our own R2 bucket, not litterbox.catbox.moe#34
Aswinmcw merged 1 commit into
mainfrom
feat/quote-uploads-r2

Conversation

@Aswinmcw

@Aswinmcw Aswinmcw commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

The quote form's file upload went straight from the browser to litterbox.catbox.moe — an anonymous public file host with a 72-hour expiry. Anyone holding the URL could download the customer's part design; the file disappeared after three days whether or not the quote had been answered; and privacy.html did not mention that a third party received uploads at all.

Uploads now go to a private R2 bucket owned by this account:

  • POST /api/quote/upload (new, in src/uploads.js) takes the file as the raw request body with the name in X-File-Name, and writes it to the UPLOADS bucket under quotes/<year>/<uuid>/<safe-name>. Extension allowlist mirrors the form's accept= (STL/OBJ/3MF/STEP/STP/JPG/PNG/PDF); 100 MB cap (the form already says "Max 100MB", and it is the Workers body limit); rate-limited by the same RL_QUOTE limiter as the form.
  • POST /api/quote now accepts file_key (validated against the exact key shape the uploader mints) instead of a browser-supplied file_url. The Worker stores file_url = https://…/api/admin/quotes/file?key=…, so the owner email's Download button and the dashboard's attachment link are unchanged, and both now require the owner session.
  • GET /api/admin/quotes/file?key=… streams the object back as Content-Disposition: attachment with nosniff and no-store. Sits under /api/admin/ so the existing owner gate covers it; unauthenticated → 401.
  • CSP connect-src drops litterbox.catbox.moe. Privacy policy now says uploads are stored privately on our Cloudflare account and never posted to a third-party host.
  • Existing quote rows with litterbox URLs are untouched (they have expired anyway).

Before merging / deploying

  1. Create the bucket once: npx wrangler r2 bucket create 3dprints-uploads. The Worker returns a clear 503 on upload if the binding is missing rather than failing the quote.
  2. Optionally set a lifecycle rule on the bucket (Cloudflare dashboard → R2 → bucket → Settings) e.g. delete quotes/ objects after 180 days. There is no default expiry; files are kept.

Test plan

  • npm test green; new test/uploads.mjs (44 checks): key shape, filename sanitising (paths, unicode, header-breaking characters), extension refusals (.html, .exe, .svg, .stl.js), empty body, 413 over cap, 503 without binding, download headers, 400/404 paths, and that no storefront/Worker file references a catbox URL
  • wrangler dev with local R2: 300 KB .stl uploads and returns a key; .html is refused; unauthenticated download is 401; a full quote-form submission through the browser POSTs to /api/quote/upload then /api/quote, and the D1 row's file_url is the owner-only route
  • After deploy: submit a quote with a file from the live site, open the Download link from the owner email while signed in to /shop

Made with Cursor

@Aswinmcw
Aswinmcw requested review from a team and Aswin-coder as code owners September 3, 2026 09:31
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
3d-printing ab3d927 Commit Preview URL

Branch Preview URL
Sep 03 2026, 09:48 AM

@Aswincloud-Bot Aswincloud-Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved: @Aswinmcw is a member of @Aswincloud/admins.

…ox.moe

The quote form posted the customer's file — an STL of their part, a photo, a
drawing — from the browser to an anonymous public host with a 72-hour expiry.
Anyone with the URL could fetch it, it vanished whether or not the job had
been priced, and the privacy policy said nothing about a third party.

- POST /api/quote/upload takes the raw bytes and writes them to the UPLOADS
  bucket under quotes/<year>/<uuid>/<safe-name>. Extension allowlist matching
  the form's accept=, 100 MB cap, same RL_QUOTE limiter as the form.
- The quote carries file_key; the Worker turns it into the owner-only route
  GET /api/admin/quotes/file?key=… and stores that as file_url, so the email
  "Download" button and the dashboard link work unchanged and both require
  the owner to be signed in. Served as an attachment with nosniff.
- CSP connect-src drops litterbox.catbox.moe. Privacy policy says where files
  go. wrangler.toml gains the r2_buckets binding.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Aswinmcw
Aswinmcw force-pushed the feat/quote-uploads-r2 branch from 8c38b35 to ab3d927 Compare September 3, 2026 09:48
@Aswinmcw
Aswinmcw added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit 48ce415 Sep 3, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants