# MPS-8: User S3 Bucket + Artifact Storage

## Problem

Sandbox mode (MPS-7) lets visitors export filtered images and videos, but
exports download to the user's local device. Creators want a way to
automatically upload sandbox artifacts to their own cloud storage — a personal
S3 bucket where filtered content accumulates without manual file management.

## Solution

### User-level S3 credentials

Add S3-compatible storage credentials to the User model (not Shop — a user may
own multiple shops but uses one storage bucket):

| Column | Type | Example |
|--------|------|---------|
| `s3_endpoint` | `Unicode(256)` | `"https://nyc3.digitaloceanspaces.com"` |
| `s3_region` | `Unicode(64)` | `"nyc3"` |
| `s3_bucket` | `Unicode(128)` | `"my-sandbox-exports"` |
| `s3_access_key` | `Unicode(128)` | `"DO00..."` |
| `s3_secret_key` | `Unicode(128)` | `"wJalr..."` |

Property `has_s3_bucket` returns True when endpoint, bucket, access_key, and
secret_key are all non-empty.

### Storage settings form

New section on user settings page (`/u/settings`) with an "Artifact Storage"
form that POSTs to `/u/settings/storage`. Fields: endpoint URL, region
(optional), bucket name, access key, secret key (password field).

### Presigned upload endpoint

`POST /u/sandbox/upload` — accepts `filename` and `content_type` parameters,
generates a presigned POST using the user's stored S3 credentials. Returns
`{presigned: {url, fields}, key}`. The browser uploads the blob directly to the
user's bucket — the file never touches the MPS server.

S3 key format: `sandbox/{user_id}/{timestamp}-{filename}`

Upload limit: 50MB per file (enforced via presigned POST conditions).

### Toolbar integration

When a user has `has_s3_bucket = True`, the sandbox toolbar in `base.j2`
renders with `data-has-bucket="1"`. The `sandbox.js` upload-to-bucket button
appears only when this attribute is present. The upload flow:

1. User exports image/video (canvas toBlob)
2. User clicks "Upload to Bucket"
3. JS POSTs to `/u/sandbox/upload` with filename + content_type
4. Server returns presigned POST URL + fields
5. JS uploads blob directly to user's S3 bucket via FormData POST

### Supported services

Any S3-compatible endpoint: DigitalOcean Spaces, AWS S3, MinIO, Backblaze B2,
Cloudflare R2.

### Security

Credentials stored as plain Unicode columns (same pattern as Stripe/PayPal/Adyen
keys on Shop model). Secret key field uses `type="password"` in the form.
Credentials are only used server-side to generate presigned URLs — they are
never exposed to the browser.

## Shop-Level S3 Mirror (MPS-9)

A separate but related feature: shops can configure their own S3 bucket as a
**mirror** of the MPS main bucket. Every file uploaded to a shop (product files,
thumbnails, shop assets) is automatically copied to the shop's mirror bucket in
a background thread. See [MPS-9](mps-9.md).

## Files Changed

| File | Change |
|------|--------|
| `models/user.py` | S3 credential columns + `has_s3_bucket` property |
| `views/user.py` | Storage settings POST handler + S3 fields in settings dict |
| `views/user_sandbox.py` | Presigned upload endpoint |
| `templates/user_settings.j2` | Artifact Storage form |
| `templates/base.j2` | `data-has-bucket` attribute on sandbox toolbar |
| `static/js/sandbox.js` | Upload-to-bucket button + presigned POST flow |
| `routes.py` | `user_storage_settings`, `user_sandbox_upload` routes |
| `scripts/alembic/versions/f898ba460612_*.py` | Migration: user S3 columns |
| `tests/test_functional.py` | S3 bucket save/clear, presigned upload, toolbar attribute tests |

## Depends On

MPS-7 (sandbox mode — toolbar must exist to add upload button)
