Skip to content

Latest commit

 

History

History
206 lines (157 loc) · 9.48 KB

File metadata and controls

206 lines (157 loc) · 9.48 KB

Dart — API Contract

The HTTP surface between the browser app and the FastAPI backend. The ad is rendered in the browser; the backend persists the result, proxies images, and imports store catalogues.

Base URL (local dev): http://localhost:8000


Conventions

  • IDs are UUID strings; timestamps are ISO-8601 UTC.
  • Errors use the error model with an appropriate HTTP status.
  • Money is integer cents. cost_cents is always 0 (rendering is free); price_cents carries the merchant's product price when set.
  • The public endpoints (/proxy-image, /store-products, /store-icons, /save-ad) are per-IP rate limited; over the limit returns 429 rate_limited.

POST /save-ad — persist a browser-rendered ad

multipart/form-data. Stores the rendered video + product image (+ optional logo) in Supabase Storage and writes the library row with the service-role key, scoped to the user the token identifies. Safari's sRGB-tagged mp4s are losslessly re-tagged to BT.709 on the way in (needs ffmpeg on the host; a no-op otherwise).

Form fields

Field Type Notes
video file the rendered MP4 (or WebM)
token string the user's Supabase access token (validated via /auth/v1/user)
id string client UUID; sanitised to [A-Za-z0-9_-] before use in any path
product_title string optional
target_audience string optional
aspect_ratio string 16:9 | 1:1 | 4:5 | 9:16 (default 16:9)
duration_sec int 3–20 (default 10)
resolution string 1080p
cost_cents int 0
price_cents int product price in cents; 0 when none
brand_accent string optional hex accent used for the ad
logo_knockout bool whether the saved logo is a knockout-safe cutout
image file optional product image
logo file optional brand logo (best-effort; a failed logo never fails the save)

Response 200

{ "video_url": "https://<project>.supabase.co/storage/v1/object/public/dart-videos/<uid>/<id>.mp4",
  "image_url": "https://.../img-<id>.png" }

401 invalid/expired token · 403 the id belongs to another user · 413 upload too large · 429 rate limited · 500 server missing Supabase config · 502 upload failed.


GET /proxy-image?url= — same-origin image proxy

Re-serves an external product image so the browser can draw it onto the render canvas without tainting it. SSRF-guarded: scheme + host are validated, and redirects are followed manually with every hop re-checked against private/loopback/link-local ranges. The body is streamed and capped (25 MB). The response is served nosniff with a locked-down CSP.

Response 200 — the image bytes (Content-Type: image/*). 400 bad/disallowed URL or non-image · 429 rate limited · 502 fetch failed / too large.


GET /store-products?url= — import a store catalogue

Pulls a merchant's public Shopify products feed (/products.json) so the app can batch-generate an ad per product — no OAuth, no key. A product-page link (/products/<handle>) imports just that product; a bare store URL imports the catalogue (up to 100). Falls back to reading a single product page's JSON-LD / OpenGraph when it isn't a Shopify feed. SSRF-guarded and rate limited like the image proxy.

Response 200

{ "products": [ { "title": "", "image": "https://…", "price": "$45.00" } ],
  "logo": "https://…/apple-touch-icon.png" }

400 missing/invalid store URL · 429 rate limited · 502 couldn't read the link.


GET /store-icons?url= — a store's own declared icons

Reads the store's homepage <head> for <link rel="icon"|"shortcut icon"|"apple-touch-icon"| "apple-touch-icon-precomposed"> and a <link rel="manifest">'s icons array, so the app can try the merchant's own (often much higher-resolution) artwork before falling back to a generic favicon-service guess. SVG sources are skipped (the frontend needs a raster it can read alpha from). SSRF-guarded and rate limited like the image proxy. Never errors on a store with nothing declared or an unreachable one — just returns an empty list, so the frontend's existing icon.horse/Google fallback chain still runs.

Response 200

{ "icons": [ "https://…/apple-touch-icon.png", "https://…/icon-192.png" ] }

Largest declared size first; [] when nothing was found. 400 missing/invalid store URL · 429 rate limited.


POST /auth/signup/* + POST /auth/reset/* — emailed-code auth flows

Dart emails the 6-digit codes itself (Brevo). Signup creates the Supabase account (admin API, pre-confirmed) only after the code verifies — an unverified signup never exists as an account. Password reset runs the same code flow for accounts that do exist, then sets the new password via the admin API. Sign-in is plain Supabase email+password and never needs a code.

/auth/signup/code { "email": "you@x.com" }{ "sent": true, "request": "…" } 400 invalid_input bad email · 409 conflict email already has an account · 429 rate_limited per-IP limit or 60s per-address cooldown · 502 email send failed · 503 Brevo not configured.

/auth/signup/verify { "email", "code", "password", "request" }{ "created": true } 400 invalid_input bad shape or the password fails Supabase's policy · 400 invalid_code wrong/expired (5 attempts max) or wrong request · 409 conflict already exists · 429 rate_limited.

/auth/reset/code { "email": "you@x.com" }{ "sent": true, "request": "…" } Same errors as signup's /code, except 404 not_found when the email has no account.

/auth/reset/check { "email", "code", "request" }{ "valid": true } Validates the code without consuming it (the UI only asks for a new password after this passes). Wrong guesses count against the same 5-attempt cap as /verify.

/auth/reset/verify { "email", "code", "password", "request" }{ "reset": true } Same errors as signup's /verify, plus 404 not_found when the account no longer exists.

request is an opaque token returned by /code to the browser that asked — a guess without it is rejected before the attempt counter, so third parties can't burn the owner's 5 attempts. Codes are stored hashed (peppered, purpose-scoped — a signup code can't verify as a reset code) in auth_codes with a 10-minute TTL.


POST /auth/password + POST /auth/delete-account — signed-in account management

Both take the session token in the body and re-confirm the password (GoTrue password grant), so an unattended open tab can't silently change or destroy an account.

/auth/password/code { "token", "current_password" }{ "sent": true, "request": "…" } Password + session check out → a 6-digit code goes to the account email, so a leaked password + stolen session still can't change the password.

/auth/password { "token", "current_password", "new_password", "code", "request" }{ "updated": true } 400 invalid_input wrong current password / bad shape / Supabase policy · 400 invalid_code wrong/expired code · 401 unauthorized bad session · 429 rate_limited.

/auth/delete-account { "token", "password" }{ "deleted": true } Deletes the library rows and stored files first, then the account (a failure partway leaves the account intact and retryable). Same errors as /auth/password.

/auth/email/code { "token", "password", "new_email" }{ "sent": true, "request": "…" } Emails a 6-digit code to the new address (purpose-scoped, same machinery as signup). 400 wrong password / already your email · 409 conflict address taken · 429 cooldown.

/auth/email/verify { "token", "new_email", "code", "request" }{ "updated": true } Switches the account's login email once the code verifies (admin API, pre-confirmed). Same code errors as the signup/reset verifies.

/auth/overview { "token" }{ "storage_bytes": 12345678 } Read-only stats for the account page (no password confirmation needed). 401 unauthorized bad session · 429 rate_limited.


GET /health

{ "status": "ok",
  "save_ad_ready": true,
  "video_retag_ready": true,
  "signup_email_ready": true,
  "monitoring_ready": true,
  "providers": { "scraper": "...", "script": "...", "video": "..." } }

video_retag_ready is true only when ffmpeg is present and the re-tag is enabled; signup_email_ready when Brevo is configured (BREVO_API_KEY + AUTH_EMAIL_FROM); monitoring_ready when Sentry is configured (SENTRY_DSN).


Error model

{ "error": { "code": "invalid_url", "message": "Host is not allowed.", "retryable": false } }

Codes in use: invalid_url, invalid_input, invalid_code, conflict, scrape_failed, unauthorized, rate_limited, not_found, internal.


Legacy /jobs + /settings (retained, unused by the app)

An earlier server-side pipeline (POST /jobs, GET /jobs/{id}, GET /jobs, POST /jobs/{id}/regenerate, POST /jobs/{id}/export, GET/POST /settings*) still exists behind the same app and is covered by tests with all-mock providers, but the shipped product does not call it — generation and rendering happen in the browser, and persistence goes through /save-ad. The /jobs routes require a Supabase login when SUPABASE_URL is set; the /settings routes instead require the operator's SETTINGS_ADMIN_KEY (via an X-Admin-Key header) and return 404 when that key is unset. All are rate limited.