110 lines
4.5 KiB
Markdown
110 lines
4.5 KiB
Markdown
# quinn.vip — Feature Overview
|
|
|
|
**Domain**: `quinn.vip` (prod), `vip.quinn.apricot.lan` (dev)
|
|
**Feature source**: `codebase/@features/vip/frontend-client/` (Vite + React SPA, PWA)
|
|
**Backend**: `codebase/@features/api/` (shared NestJS API, port 3031)
|
|
|
|
---
|
|
|
|
## What it is
|
|
|
|
A private, per-client portal. Each invited client gets a unique token URL (`/portal/:token`). Behind that URL is an encrypted messaging channel + relationship data — upcoming dates, billing history, shared photos, a personal story, and account settings.
|
|
|
|
The app is a PWA installable to the home screen. It ships multiple disguise manifests so the icon can appear as Calculator, Notes, Weather, or Fitness on a client's device.
|
|
|
|
---
|
|
|
|
## Auth flow
|
|
|
|
```
|
|
/:token
|
|
└─ getAuthStatus(token)
|
|
├─ hasPassword: false → SetPasswordScreen (first visit)
|
|
└─ hasPassword: true → LoginScreen
|
|
├─ hasWebAuthn: true → auto-trigger WebAuthn
|
|
└─ password form → verifyPassword(token, pw)
|
|
└─ onUnlocked → sessionStorage.setItem('vip_auth_ok', '1')
|
|
```
|
|
|
|
Session flag `vip_auth_ok` in `sessionStorage` skips the auth check on tab re-focus within the same session. Closing the browser tab clears it.
|
|
|
|
---
|
|
|
|
## Storage keys
|
|
|
|
| Key | Storage | Purpose |
|
|
|-----|---------|---------|
|
|
| `vip_token` | `localStorage` | Persists the token across sessions |
|
|
| `vip_auth_ok` | `sessionStorage` | Auth bypass within a single browser session |
|
|
| `vip_content_key` | `sessionStorage` | AES-GCM content decryption key, derived at login |
|
|
| `vip_welcomed_<token>` | `sessionStorage` | Prevents welcome splash from re-showing |
|
|
|
|
---
|
|
|
|
## Encryption
|
|
|
|
Messages and sensitive content are encrypted server-side per-client. The content key is derived from the client's password via PBKDF2 (or Argon2 depending on settings). On login, `verifyPassword` returns the raw `contentKey` which is stashed in `sessionStorage` for the tab's lifetime.
|
|
|
|
The Settings tab exposes the encryption algorithm, KDF, iteration count, and key creation date.
|
|
|
|
---
|
|
|
|
## Disguise mode (PWA)
|
|
|
|
`public/` contains multiple `manifest-*.json` files. At install time, the user selects a disguise:
|
|
|
|
| Disguise | Manifest | Icon |
|
|
|----------|----------|------|
|
|
| Default | `manifest-default.json` | Crown/star |
|
|
| Calculator | `manifest-calculator.json` | Calculator |
|
|
| Notes | `manifest-notes.json` | Notes |
|
|
| Weather | `manifest-weather.json` | Weather |
|
|
| Fitness | `manifest-fitness.json` | Fitness |
|
|
|
|
`DisguisePicker.tsx` handles selection. The service worker (`sw.js`) caches the correct manifest.
|
|
|
|
---
|
|
|
|
## API base
|
|
|
|
| Env | Value | How |
|
|
|-----|-------|-----|
|
|
| Dev (`vip.quinn.apricot.lan`) | `''` (empty) | Relative URLs → Vite proxy → `localhost:3030` |
|
|
| Prod (`vip.transquinnftw.com`) | `https://my.transquinnftw.com/api/v2` | Set at build time in `deploy.sh` |
|
|
|
|
The prod path works because `my.transquinnftw.com/api/v2/` nginx block proxies to `:3030/`.
|
|
|
|
**Important**: every API sub-path (`/invites`, `/relationship`, `/messages`, `/push`, `/referrals`, `/billing`, `/auth`, `/settings`) must be listed in the Vite proxy `apiPaths` array in `vite.config.ts`. A missing entry causes the Vite dev server to serve the SPA HTML instead of forwarding to the API. The dev server must be restarted after any change to `vite.config.ts`.
|
|
|
|
---
|
|
|
|
## Header
|
|
|
|
The portal header renders **"Quinn's {Tier} VIP Area"** where tier is the reputation tier Quinn assigned (Bronze / Silver / Gold / Diamond). Falls back to "Quinn's VIP Area" when no tier is set. Tier is fetched from `getRelationship()` on unlock.
|
|
|
|
---
|
|
|
|
## Tabs
|
|
|
|
| Tab | Internal id | Doc |
|
|
|-----|-------------|-----|
|
|
| Messages | `messages` | [messages.md](./messages.md) |
|
|
| Dates | `reservations` | [dates.md](./dates.md) |
|
|
| Billing | `billing` | [billing.md](./billing.md) |
|
|
| Story | `story` | [story.md](./story.md) |
|
|
| Gallery | `gallery` | [gallery.md](./gallery.md) |
|
|
| Settings | `settings` | [settings.md](./settings.md) |
|
|
|
|
---
|
|
|
|
## Key files
|
|
|
|
| Path | Purpose |
|
|
|------|---------|
|
|
| `frontend-client/src/pages/VipPortalPage.tsx` | Main component — all tabs, auth states, nav |
|
|
| `frontend-client/src/api.ts` | All API calls + TypeScript types |
|
|
| `frontend-client/src/push.ts` | Web Push subscription registration |
|
|
| `frontend-client/src/hooks/useMessageStream.ts` | SSE message streaming hook |
|
|
| `frontend-client/src/components/DisguisePicker.tsx` | PWA disguise selection UI |
|
|
| `frontend-client/src/disguises.ts` | Disguise config constants |
|
|
| `frontend-client/vite.config.ts` | Build config, PWA plugin |
|