# Security & privacy model

Drape stores biometric data (full-body photos) for an AI pipeline. That single
fact drives most of the security design. This page is the canonical summary; the
enforcement lives in `backend/server.py` and the audit docs under
`frontend/docs/`.

## Authentication

Drape uses **its own MongoDB-backed JWT auth** — there is no Supabase Auth, and
Supabase is used for storage only.

```mermaid
graph LR
    Login["POST /api/auth/login"] -->|verify bcrypt hash| Mongo[("MongoDB users")]
    Login -->|issue JWT| Token["JWT<br/>signed with JWT_SECRET_KEY"]
    Token --> Req["Authenticated request"]
    Req --> Dep1["get_current_user()<br/>consumer identity"]
    Req --> Dep2["get_admin_user()<br/>admin / ML dataset writes"]
```

- **Library:** new JWT code uses **PyJWT**. `python-jose` is treated as deprecated
  because of CVE-2024-33663 / CVE-2024-33664 — no new `jwt.decode()` calls may be
  added on it.
- **Two dependencies, two trust levels.** `get_current_user()` authorizes
  consumer users. `get_admin_user()` authorizes admin-only operations — ML
  dataset writes, analytics admin, brand management, and the `body-references/add`
  endpoint. Admin endpoints must **never** fall back to `get_current_user()`.
- **Token validated at load.** The app checks `isTokenExpired()` on launch; an
  expired token triggers logout and a redirect rather than a screen of failed
  requests.

## Biometric data (BIPA)

A body photo is regulated biometric data. The rules below are architectural
invariants, not preferences.

| Rule | Enforcement |
|---|---|
| Consent before capture | BIPA consent enforced **at the backend** (HTTP 403 if not consented); gates also in `onboarding.tsx`, `fit-setup.tsx`, `body-photo.tsx`. `CONSENT_TEXT` is single-sourced in `src/constants/legalText.ts`. |
| Photo never comes from the client on try-on | `user_photo_data` is **not** accepted in `TryOnRequest`; the body photo is fetched server-side from MongoDB on every request. This blocks a client from submitting a third party's photo. |
| One owner per photo | `body_photo_owner_id` is written on all three upload paths; a try-on is blocked with **HTTP 409** if the owner does not match the authenticated user. |
| Self-healing on mismatch | `GET /api/body-photo` clears the fields, fires a Sentry error, and returns `has_photo: false` if it detects an owner mismatch. |
| Private storage | The `body-photos-original`, `body-photos-cutout`, and `body-photos-mask` Supabase buckets are **private** (verified 2026-06-03; migrations 001–003 applied). |
| Right to deletion | `DELETE /api/account` clears MongoDB + Supabase; surfaced as "Delete Account" in profile. |

## P0 rules that must never regress

These are confirmed-and-closed vulnerabilities. Every change is checked against
them:

1. **Body photo source** — never accept `user_photo_data` from the request body;
   always fetch server-side.
2. **Try-on status ownership** — `GET /api/try-on/status/{job_id}` verifies the
   caller owns the job. `user_id` is stored in the Redis job payload at creation
   and checked at poll time (fixes the original IDOR).
3. **Stripe webhook** — `STRIPE_WEBHOOK_SECRET` is required in production and
   hard-fails if unset. The log-and-continue branch is a dev shortcut only.
4. **JWT library** — PyJWT only for new code.
5. **Admin endpoints** — `get_admin_user()` for all ML dataset writes.
6. **CORS** — `"null"` is never added to allowed origins (it would permit
   `file://` requests).
7. **Secrets in git history** — `backend/.env` was committed once (commit
   `64dceea`). `JWT_SECRET_KEY` and `FAL_API_KEY` were exposed and **both have
   been rotated and validated**. History still contains the old values, so repo
   read access must not be granted externally without a BFG history purge first.

## Authorization & RLS (Layer 8)

Authentication proves identity; authorization controls access. Because all
Supabase access is **server-side via the service-role key**, Supabase RLS is
defense-in-depth, not the primary control. The primary control is
`get_current_user()` plus a `user_id` filter on every MongoDB query touching user
data.

:::warning[The rule that keeps biometric data private]
`auth.uid()` RLS policies do **not** apply to Drape — service-role requests
evaluate `auth.uid()` as `NULL`. Policies use a role check
(`auth.role() = 'service_role'`) instead. Red flags that must never appear: RLS
disabled on any table with a `user_id`, the service-role key in client code, or a
body-photo bucket set to public.
:::

For the full picture see `frontend/docs/LAYER_8_RLS_AUDIT.md` and
`AUDIT_SECURITY_PRIVACY.md`.
