first commit

This commit is contained in:
Anurag Jangid 2026-10-08 02:52:58 +05:30
commit 34ea15a54d
246 changed files with 18660 additions and 0 deletions

586
docs/API_SPEC.md Normal file
View file

@ -0,0 +1,586 @@
# API Specification — Desi Clans
**Version:** 1.0
**Base URL:** `https://api.desiclans.com` (prod) / `http://localhost:8000` (dev)
**Format:** JSON unless noted
**Auth:** `Authorization: Bearer <access_token>` for protected routes
---
## 1. Conventions
| Item | Rule |
|------|------|
| Errors | `{ "detail": "message" }` or validation array from FastAPI |
| Dates | ISO 8601 UTC in JSON |
| Pagination | `?limit=20&offset=0` where added later; v1 lists unbounded with sane caps in service |
| IDs | Integer primary keys in v1 |
| Slugs | Lowercase, hyphenated, unique per edition |
---
## 2. Platform — `/api`
### `GET /health`
Liveness and PostgreSQL connectivity check.
**Response 200** — database reachable
```json
{
"status": "ok",
"service": "desi-clans-api",
"database": "connected"
}
```
**Response 503** — database unreachable
```json
{
"status": "degraded",
"service": "desi-clans-api",
"database": "unavailable"
}
```
---
### `GET /events`
List catalog events (published by default).
**Query**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `category` | `holi` \| `garba` \| `dandiya` \| `bhajan_clubbing` \| `special` | — | Filter by category |
| `status` | `draft` \| `published` \| `cancelled` | `published` | Filter by status |
| `upcoming` | boolean | `true` | If true, only `starts_at >= now` (UTC) |
**Response 200** — `EventSummary[]`
```json
[
{
"id": 1,
"slug": "rang-barse-palace-grounds-2026",
"title": "Rang Barse — Palace Grounds",
"category": "holi",
"status": "published",
"starts_at": "2026-03-09T10:00:00Z",
"ends_at": "2026-03-09T18:00:00Z",
"is_featured": true,
"cover_image_url": "https://…",
"cover_image_alt": "…",
"venue": {
"id": 1,
"slug": "palace-grounds",
"name": "Palace Grounds",
"area": "Vasanth Nagar",
"city": "Bengaluru",
"address": "…",
"map_url": "https://…",
"description": "…"
}
}
]
```
---
### `GET /events/{slug}`
**Response 200** — `EventDetail` (includes `description`, `meta_description`, `media[]`, `ticket_types[]`)
**Response 404** — unknown slug
---
### `GET /events/{id}/tickets`
List purchasable ticket types for a **published** event (numeric event id).
**Response 200** — `EventTicketTypePublic[]`
```json
[
{
"id": 1,
"event_id": 1,
"name": "General admission",
"description": "Full-day access",
"price_paise": 149900,
"status": "active",
"sort_order": 0,
"remaining_quantity": 42
}
]
```
**Response 404** — unknown or unpublished event
---
### `GET /venues`
**Response 200** — `VenuePublic[]`
---
### `GET /venues/{id}`
**Response 200** — `VenuePublic`
**Response 404** — unknown id
---
## 3. Public — `/api/public` (legacy, planned)
Legacy public routes will mount under this prefix when business routers are enabled.
---
### `GET /config` (not yet mounted)
Site-wide config: platform links, feature flags (future).
**Response 200** — `SiteConfigPublic`
```json
{
"site_name": "Desi Clans",
"city": "Bengaluru",
"links": [
{ "id": 1, "kind": "instagram", "label": "Instagram", "url": "https://...", "sort_order": 0 }
]
}
```
---
### `GET /editions`
List published editions.
**Query**
| Param | Type | Description |
|-------|------|-------------|
| `event_type` | string | Optional enum filter |
| `featured` | bool | Optional |
**Response 200** — `EventEditionPublic[]`
---
### `GET /editions/{slug}`
Single published edition.
**Response 200** — `EventEditionPublic`
**Response 404** — not found or unpublished
**`EventEditionPublic` (shape)**
```json
{
"id": 1,
"title": "Holi at Palace Grounds",
"slug": "holi-palace-grounds-2026",
"event_type": "holi",
"city": "Bengaluru",
"venue_name": "Palace Grounds",
"event_date": "2026-03-10T10:00:00Z",
"description": "...",
"is_featured": true,
"cover_image_url": "/media/...",
"cover_thumb_url": "/media/.../thumb",
"tickets": {
"bookmyshow": "https://...",
"district": null,
"instagram": null
},
"meta_description": "...",
"media": [
{ "id": 1, "url": "...", "thumb_url": "...", "caption": null, "sort_order": 0 }
]
}
```
*Future:* `ticketing_mode`, `tiers[]` when native ticketing is enabled.
---
### `GET /go`
Outbound redirect with click tracking.
**Query**
| Param | Required | Description |
|-------|----------|-------------|
| `kind` | yes | e.g. `bookmyshow`, `district`, `instagram` |
| `edition_id` | no | Edition context |
**Response 302** → destination URL
---
### `POST /subscribe`
**Body** — `SubscribeRequest`
```json
{ "email": "user@example.com", "neighborhood": "HSR Layout" }
```
**Response 201** — created
**Response 409** — email already subscribed
---
### `GET /editions/{slug}/calendar.ics`
**Response 200** — `text/calendar` body
---
## 3. Share (crawler HTML)
### `GET /share/events/{slug}`
Also available under public router duplicate path per deployment.
**Response 200** — HTML with Open Graph tags (no auth).
---
## 4. Auth — `/api/auth`
Attendee accounts (bcrypt-hashed passwords, JWT bearer tokens). Admin auth remains separate under `/api/admin` when legacy routers are mounted.
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/register` | — | Create account |
| POST | `/login` | — | Issue access token |
| GET | `/me` | Bearer | Current user profile |
### `POST /register`
**Body** — `RegisterRequest`
```json
{
"email": "guest@example.com",
"password": "minimum-8-chars",
"full_name": "Optional Name"
}
```
**Response 201** — `Token`
**Response 409** — email already registered
**Response 422** — validation error (password &lt; 8 characters, invalid email)
---
### `POST /login`
**Body** — `LoginRequest`
```json
{ "email": "guest@example.com", "password": "********" }
```
**Response 200** — `Token`
```json
{ "access_token": "...", "token_type": "bearer" }
```
**Response 401** — invalid credentials
JWT claims: `sub` (user id), `exp`, `type: "user"`. Lifetime from `ACCESS_TOKEN_EXPIRE_MINUTES` (server env).
---
### `GET /me`
**Headers** — `Authorization: Bearer <access_token>`
**Response 200** — `UserPublic`
```json
{
"id": 1,
"email": "guest@example.com",
"full_name": "Optional Name",
"is_active": true,
"created_at": "2026-04-04T12:00:00Z"
}
```
**Response 401** — missing, invalid, or expired token
Protected routes use the `CurrentUser` dependency (`get_current_user` in `app/core/deps.py`).
---
## 5. Orders & tickets — `/api/orders`, `/api/tickets`
Requires attendee Bearer token. Prices are **always** computed server-side from `ticket_types.price_paise`; the client sends only `ticket_type_id` and `quantity`.
Checkout reserves inventory while `status: pending`. Tickets are issued only after **verified** payment (checkout signature or webhook). Never trust client-side “payment success” flags.
**Provider:** Razorpay in production (`PAYMENT_PROVIDER=razorpay`). Local dev uses `mock` (no secrets in React).
Optional header: `Idempotency-Key` on `POST /orders` (unique per checkout attempt).
### `POST /orders`
**Body** — `OrderCreate`
```json
{
"event_id": 1,
"items": [{ "ticket_type_id": 2, "quantity": 2 }]
}
```
**Response 201** — `OrderCheckoutResponse` (`status: pending`, `payment` block with public `key_id`, `provider_order_id`, `amount_paise`, `expires_at`)
**Response 400** — invalid event, wrong event/tier pairing, empty cart
**Response 401** — not authenticated
**Response 409** — insufficient inventory
---
### `POST /orders/{id}/payment/verify`
Server-side verification of Razorpay (or mock) checkout signature. Marks order `paid` and issues tickets.
**Body** — `PaymentVerifyRequest`
```json
{
"provider_order_id": "order_…",
"provider_payment_id": "pay_…",
"signature": "…"
}
```
**Response 200** — `OrderPublic` (`status: paid`)
**Response 400** — invalid signature or amount mismatch
**Response 409** — order not pending
---
### `POST /orders/{id}/cancel`
User abandoned checkout. Releases reserved inventory. **Pending orders only.**
**Response 200** — `OrderPublic` (`status: cancelled`)
---
### `POST /orders/{id}/payment/mock-complete`
Development only (`ENVIRONMENT=development`, `PAYMENT_PROVIDER=mock`). Server completes mock payment without client-held secrets.
---
### Webhooks — `/api/webhooks`
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/razorpay` | `X-Razorpay-Signature` | Razorpay events (`payment.captured`, `payment.failed`) |
| POST | `/mock` | `X-Mock-Signature` | HMAC body signature (tests / local) |
Webhook processing is **idempotent** (`payment_webhook_events.provider_event_id` unique). Duplicate deliveries return `{ "status": "duplicate_ignored" }`.
**Env (server only):** `RAZORPAY_KEY_SECRET`, `RAZORPAY_WEBHOOK_SECRET` — never expose to the frontend. `RAZORPAY_KEY_ID` is public and returned in checkout payloads for Razorpay.js.
Pending orders expire after `ORDER_PAYMENT_TIMEOUT_MINUTES` (inventory released, `status: failed`).
---
### `GET /orders/{id}`
**Response 200** — `OrderPublic` (owner only)
**Response 404** — not found or not your order
---
### `GET /tickets/my`
**Response 200** — `TicketPublic[]` for the current user
---
### `GET /tickets/{id}`
Authenticated owner only. Returns full digital pass payload including server-generated QR PNG (base64).
**Response 200** — `TicketDetailPublic` (event, venue, attendee, ticket type, `code`, `qr_payload`, `qr_image_base64`, `qr_token`)
**Response 404** — not found or not your ticket
---
### `POST /tickets/verify`
Gate / check-in verification (server-side). Marks ticket `used` atomically — **QR reuse rejected**.
**Headers:** `X-Check-In-Key` (required in production via `TICKET_VERIFY_API_KEY`)
**Body** — `TicketVerifyRequest`
```json
{ "qr_token": "…" }
```
**Response 200** — `TicketVerifyResponse` (`valid`, `status`, `message`, `ticket_code`, `event_title`)
---
**`TicketPublic.status` / `TicketDetailPublic.status`:** `valid` | `used` | `cancelled`
---
## 6. Admin — `/api/admin`
Requires admin JWT. `editor` vs `super_admin` enforced per endpoint.
### `GET /me`
Current admin profile.
**Response 200** — `AdminUserPublic`
---
### Platform links
| Method | Path | Role | Description |
|--------|------|------|-------------|
| GET | `/links` | editor+ | List active links |
| POST | `/links` | super_admin | Create |
| PATCH | `/links/{link_id}` | super_admin | Update |
| DELETE | `/links/{link_id}` | super_admin | Delete |
---
### Editions
| Method | Path | Role | Description |
|--------|------|------|-------------|
| GET | `/editions` | editor+ | All editions (incl. draft) |
| POST | `/editions` | editor+ | Create |
| PATCH | `/editions/{edition_id}` | editor+ | Update fields, publish flags |
| POST | `/editions/{edition_id}/cover` | editor+ | Multipart cover upload |
| POST | `/editions/{edition_id}/media` | editor+ | Gallery upload |
| PATCH | `/media/{media_id}` | editor+ | Caption, publish flag |
| POST | `/editions/{edition_id}/media/reorder` | editor+ | Body: ordered media ids |
| DELETE | `/media/{media_id}` | editor+ | Remove asset |
**`EventEditionAdmin`** extends public fields with `is_published`, `sort_order`, raw URLs, etc.
---
### Users & subscribers
| Method | Path | Role | Description |
|--------|------|------|-------------|
| GET | `/users` | super_admin | List admins |
| POST | `/users` | super_admin | Create admin |
| GET | `/subscribers` | editor+ | Newsletter list |
---
## 7. Analytics — `/api/admin/analytics`
### `GET /clicks/summary`
**Response 200** — `ClickSummaryItem[]`
```json
[{ "link_kind": "bookmyshow", "count": 42 }]
```
### `GET /clicks/recent`
**Response 200** — `OutboundClickPublic[]`
---
## 8. Future — Check-in & enhancements
### Catalog — `/api/public/editions/{slug}/tiers`
**Response** — active tiers with `remaining_quantity` (or sold out flag).
### Orders (extensions)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/` | User | Create pending order |
| GET | `/` | User | My orders |
| GET | `/{order_id}` | User | Order detail + tickets |
| POST | `/webhooks/payment` | HMAC | Provider callback (no JWT) |
### Check-in — `/api/check-in`
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/scan` | Staff | Body: `{ "qr": "..." }` → ticket status |
---
## 8. Enums
### `EventType`
`holi` | `garba` | `dandiya` | `bhajan_clubbing` | `special`
### `PlatformKind`
`bookmyshow` | `district` | `instagram` | `highape` | `youtube` | `whatsapp` | `website` | `other`
### `AdminRole`
`super_admin` | `editor`
### `OrderStatus`
`pending` | `paid` | `cancelled` | `failed`
### `TicketStatus` (issued ticket)
`valid` | `used` | `cancelled`
---
## 9. CORS
Allowed origins from `CORS_ORIGINS` env (comma-separated). Credentials enabled for admin SPA origin.
---
## 10. Static media
- **Local dev:** `GET /media/{path}`
- **Prod:** CDN URL built from stored key in `file_path`
---
## 11. Versioning
Breaking changes require `/api/v2` prefix or negotiated migration. v1 remains unversioned path until scale demands it.
---
## 12. Related
- [DATABASE.md](./DATABASE.md)
- [ARCHITECTURE.md](./ARCHITECTURE.md)

275
docs/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,275 @@
# Architecture — Desi Clans
**Version:** 1.0
**Last updated:** 2026-04-04
---
## 1. System context
```
┌─────────────┐ HTTPS ┌──────────────┐ SQL ┌────────────┐
│ Browser │ ──────────────► │ FastAPI API │ ◄──────────► │ PostgreSQL │
│ (React) │ │ + static │ └────────────┘
└─────────────┘ │ media │
│ └──────┬───────┘
│ │
│ optional ▼
└──────────────────────► S3 / CDN (prod media)
```
- **Monorepo:** `frontend/`, `backend/`, `docs/`.
- **Deployment unit:** API container + static frontend (CDN or same origin) + managed Postgres.
- **No microservices in v1.** One API process, clear router boundaries for future split.
---
## 2. Architectural principles
1. **API-first content** — Public UI never hardcodes event copy or images.
2. **Thin client** — Business rules (inventory, check-in) live in the backend.
3. **Explicit phases** — Ship discovery + admin before native payments if needed.
4. **Boring tech** — Stack locked to PRD; no extra frameworks without ADR.
5. **Security defaults** — JWT for auth, parameterized SQL via SQLAlchemy, validated Pydantic I/O.
---
## 3. Repository layout (target)
```
DESI-CLANS/
├── frontend/
│ ├── public/
│ ├── src/
│ │ ├── api/ # Axios clients, types mirroring API_SPEC
│ │ ├── components/ # UI primitives + composites
│ │ ├── hooks/
│ │ ├── layouts/
│ │ ├── pages/ # Route-level views
│ │ ├── styles/ # Tailwind entry, tokens
│ │ ├── utils/ # seo, dates, formatters
│ │ ├── animations/ # GSAP contexts, ScrollTrigger setup
│ │ ├── App.jsx
│ │ └── main.jsx
│ ├── index.html
│ ├── vite.config.js
│ ├── tailwind.config.js
│ └── package.json
├── backend/
│ ├── alembic/
│ ├── app/
│ │ ├── routers/ # public, auth, admin, share, analytics, orders (future)
│ │ ├── services/ # domain logic
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── auth.py
│ │ ├── config.py
│ │ ├── database.py
│ │ └── main.py
│ ├── scripts/ # seed, migrate helpers
│ ├── tests/
│ ├── Dockerfile
│ └── requirements.txt
├── docs/
└── README.md
```
---
## 4. Backend layers
| Layer | Responsibility |
|-------|----------------|
| **Routers** | HTTP, status codes, dependency injection, no business logic |
| **Services** | Transactions, orchestration (editions, media, tickets, email) |
| **Models** | SQLAlchemy ORM, enums |
| **Schemas** | Pydantic request/response; never expose hashed passwords |
**Cross-cutting**
- `config.py` — settings from environment (see `.env.example` when added).
- `auth.py` — JWT create/verify, `get_current_admin`, future `get_current_user`.
- Migrations — Alembic only; avoid `create_all` in production (dev convenience may exist temporarily).
---
## 5. Frontend layers
| Layer | Responsibility |
|-------|----------------|
| **Pages** | Data loading per route, SEO hooks, layout composition |
| **Components** | Presentational; accept props, no direct Axios in leaf components |
| **api/** | Base Axios instance, interceptors for admin token |
| **animations/** | GSAP timelines registered once; respect `prefers-reduced-motion` |
**Routing (React Router)**
- Public: `/`, `/events`, `/events/:slug`, `/subscribe` (optional dedicated page)
- Admin: `/admin/login`, `/admin/*` (protected)
- Account (phase): `/account`, `/account/tickets/:id`
- Check-in (phase): `/admin/check-in` or staff-only route
**SEO**
- Vite SPA: use `react-helmet-async` **only if** needed; prefer minimal approach:
- Default meta in `index.html`
- Per-route `document.title` + meta updates in page `useEffect`
- Backend `/share/events/{slug}` HTML for crawlers (already planned in API)
---
## 6. API boundaries
| Namespace | Auth | Purpose |
|-----------|------|---------|
| `/api/public/*` | None | Read editions, config, subscribe, outbound redirect, calendar |
| `/api/auth/*` | Credentials | Admin login (attendee login added with ticketing) |
| `/api/admin/*` | Admin JWT | CRUD editions, media, links, users, subscribers |
| `/api/admin/analytics/*` | Admin JWT | Click summaries |
| `/share/events/{slug}` | None | OG HTML for social crawlers |
| `/api/orders/*` (future) | User JWT | Checkout, my tickets |
| `/api/check-in/*` (future) | Staff JWT | Validate QR, mark checked in |
Public reads are cache-friendly (CDN `Cache-Control` on GET editions list with short TTL).
---
## 7. Media pipeline
1. Admin uploads via multipart → service stores file.
2. **Local:** `upload_dir` served at `/media/...`
3. **Production:** S3 bucket + public or signed URLs; store `file_path` key in DB.
4. Optional thumbnails generated on upload (service layer); expose `thumb_url` in API.
---
## 8. Ticketing architecture (target)
```
EventEdition ──< TicketTier
│
Order ──────────────┼──< OrderLine (tier, qty, unit_price)
│
└──< Ticket (code, qr_secret, status, checked_in_at)
```
**Checkout flow (native)**
1. Client POST order intent with tier + quantity.
2. Server validates window, inventory; creates `pending` order; returns payment session (phase B).
3. Webhook confirms payment → status `paid` → issue tickets with unique codes.
4. Email service sends QR (PNG or link to ticket page).
**QR validation**
- Payload: `ticket_id` + HMAC signature (server secret).
- Scan endpoint: verify signature, edition match, status `issued` → `checked_in`.
**Outbound mode (per edition flag)**
- `ticketing_mode: outbound | native`
- Outbound uses existing `bookmyshow_url`, `district_url`, etc., via `/api/public/go` click tracking.
---
## 9. Security
- Passwords: bcrypt via passlib.
- JWT: HS256, `ACCESS_TOKEN_EXPIRE_MINUTES` configurable.
- CORS: explicit origin list from env.
- Rate limit: add at reverse proxy (nginx/Cloudflare) for public POST subscribe and auth login.
- Admin actions audited via structured logs (user id, action, entity id).
---
## 10. Performance requirements
| Area | Target |
|------|--------|
| Public GET editions | p95 &lt; 300ms @ warm DB |
| Image delivery | WebP/AVIF where supported; width variants |
| Frontend bundle | Route-based code splitting for admin |
| DB | Indexes on `slug`, `event_type`, `is_published`, `event_date` |
| Animation | GSAP only on hero/key sections; disable heavy ScrollTrigger on mobile low power |
---
## 11. Observability
- Health: `GET /api/public/health`
- Structured JSON logs (request id, path, status, duration)
- Production: disable OpenAPI UI (`openapi_url = None` when `environment=production`)
---
## 12. Coding conventions
### Python (backend)
- Python 3.11+
- Type hints on public functions
- `ruff` or `flake8` + `black` (pick one formatter in CI)
- Router functions: max ~30 lines; delegate to services
- Enums in `models.py` for DB enums; mirror in Pydantic where needed
### JavaScript (frontend)
- React 18 functional components only
- Named exports for components; default export for pages optional
- Tailwind for styling; no CSS-in-JS library
- Axios instance with `baseURL` from `import.meta.env.VITE_API_URL`
- Env vars prefixed `VITE_` only for client-safe values
### Git
- Conventional commits encouraged: `feat:`, `fix:`, `docs:`
- One Alembic revision per schema change
---
## 13. Implementation phases
### Phase 0 — Constitution (current)
Documentation only; no new application features.
### Phase 1 — Public experience + admin content
- Vite + React + Tailwind + Router
- Wire public API: home, list, detail, subscribe, outbound go links
- Admin: login, edition CRUD, media upload
- Share previews + basic SEO
- **Aligns with partial backend already in repo**
### Phase 2 — Polish & performance
- GSAP hero + scroll sections with reduced-motion guard
- Image lazy loading, skeleton states
- Accessibility pass (focus, contrast, aria labels)
### Phase 3 — Native ticketing
- DB: tiers, orders, tickets
- Attendee auth
- Checkout + payment webhook
- Ticket page + QR generation
- Staff check-in UI
### Phase 4 — Hardening
- E2E smoke tests, load test on read paths
- CDN, backup strategy, runbooks
---
## 14. ADR policy
Significant deviations (new dependency, new service, auth model change) require a short `docs/adr/NNN-title.md` before implementation.
---
## 15. Related documents
- [API_SPEC.md](./API_SPEC.md)
- [DATABASE.md](./DATABASE.md)
- [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md)

210
docs/DATABASE.md Normal file
View file

@ -0,0 +1,210 @@
# Database Schema — Desi Clans
**Version:** 1.2
**Engine:** PostgreSQL 15+
**ORM:** SQLAlchemy 2.x
**Migrations:** Alembic
---
## 1. Overview
The database supports:
1. **Catalog (Phase 6)** — `venues`, `events`, `event_media`, `ticket_types`
2. **Legacy content** — `event_editions`, `media_assets` (admin-era tables)
3. **Marketing** — platform links, newsletter subscribers, outbound clicks
4. **Admin** — admin users and roles
5. **Accounts** — attendee `users` (Phase 8)
6. **Ticketing (Phase 9)** — orders, order items, issued tickets
---
## 2. Catalog ERD (implemented)
```
users ──< orders ──< order_items ──< tickets
│ │
│ └──> ticket_types
└──> events ──< event_media
│
└──< ticket_types
```
---
## 3. Tables — catalog
### `venues`
| Column | Type | Constraints |
|--------|------|-------------|
| id | SERIAL | PK |
| slug | VARCHAR(120) | UNIQUE, NOT NULL |
| name | VARCHAR(200) | NOT NULL |
| area | VARCHAR(120) | NOT NULL |
| city | VARCHAR(80) | NOT NULL, default Bengaluru |
| address | VARCHAR(512) | NOT NULL |
| map_url | VARCHAR(512) | NULL |
| description | TEXT | NULL |
| created_at | TIMESTAMPTZ | DEFAULT now() |
| updated_at | TIMESTAMPTZ | DEFAULT now() |
---
### `events`
| Column | Type | Constraints |
|--------|------|-------------|
| id | SERIAL | PK |
| slug | VARCHAR(200) | UNIQUE, NOT NULL |
| title | VARCHAR(240) | NOT NULL |
| category | eventtype ENUM | NOT NULL, INDEX |
| status | eventstatus ENUM | NOT NULL, default draft, INDEX |
| venue_id | INT | FK → venues.id RESTRICT, INDEX |
| description | TEXT | NULL |
| starts_at | TIMESTAMPTZ | NOT NULL, INDEX |
| ends_at | TIMESTAMPTZ | NULL |
| is_featured | BOOLEAN | default false |
| cover_image_url | VARCHAR(1024) | NULL |
| cover_image_alt | VARCHAR(300) | NULL |
| meta_description | VARCHAR(320) | NULL |
| created_at | TIMESTAMPTZ | DEFAULT now() |
| updated_at | TIMESTAMPTZ | DEFAULT now() |
**Indexes:** `(status, starts_at)`, `(category, status)`
**`eventstatus`:** `draft`, `published`, `cancelled`
---
### `event_media`
| Column | Type | Constraints |
|--------|------|-------------|
| id | SERIAL | PK |
| event_id | INT | FK → events.id CASCADE, INDEX |
| image_url | VARCHAR(1024) | NOT NULL |
| alt_text | VARCHAR(300) | NULL |
| sort_order | INT | default 0 |
| created_at | TIMESTAMPTZ | DEFAULT now() |
---
### `ticket_types`
| Column | Type | Constraints |
|--------|------|-------------|
| id | SERIAL | PK |
| event_id | INT | FK → events.id CASCADE, INDEX |
| name | VARCHAR(120) | NOT NULL |
| description | VARCHAR(512) | NULL |
| price_paise | INT | NOT NULL |
| quantity_total | INT | NOT NULL |
| quantity_sold | INT | default 0 |
| status | tickettypestatus ENUM | default active |
| sort_order | INT | default 0 |
| created_at | TIMESTAMPTZ | DEFAULT now() |
| updated_at | TIMESTAMPTZ | DEFAULT now() |
**`tickettypestatus`:** `active`, `sold_out`, `inactive`
**Check:** `quantity_sold <= quantity_total` (migration `20260404_0007`)
Inventory updates use row-level locks (`SELECT … FOR UPDATE`) during checkout.
---
### `users` (attendees)
| Column | Type | Constraints |
|--------|------|-------------|
| id | SERIAL | PK |
| email | VARCHAR(255) | UNIQUE, NOT NULL |
| hashed_password | VARCHAR(255) | NOT NULL (bcrypt) |
| full_name | VARCHAR(120) | NULL |
| is_active | BOOLEAN | default true |
| created_at | TIMESTAMPTZ | DEFAULT now() |
---
### `orders`
| Column | Type | Constraints |
|--------|------|-------------|
| id | SERIAL | PK |
| user_id | INT | FK → users.id RESTRICT, INDEX |
| event_id | INT | FK → events.id RESTRICT, INDEX |
| status | orderstatus ENUM | NOT NULL, INDEX |
| total_paise | INT | NOT NULL (server-calculated) |
| currency | VARCHAR(3) | default INR |
| payment_reference | VARCHAR(64) | NULL (mock `mock_*` until gateway) |
| paid_at | TIMESTAMPTZ | NULL |
| created_at | TIMESTAMPTZ | DEFAULT now() |
| updated_at | TIMESTAMPTZ | DEFAULT now() |
**`orderstatus`:** `pending`, `paid`, `cancelled`, `failed`
Checkout currently completes in one transaction with mock payment → `paid`.
---
### `order_items`
| Column | Type | Constraints |
|--------|------|-------------|
| id | SERIAL | PK |
| order_id | INT | FK → orders.id CASCADE, INDEX |
| ticket_type_id | INT | FK → ticket_types.id RESTRICT, INDEX |
| quantity | INT | NOT NULL, > 0 |
| unit_price_paise | INT | NOT NULL (snapshot at purchase) |
| line_total_paise | INT | NOT NULL |
---
### `tickets` (issued passes)
| Column | Type | Constraints |
|--------|------|-------------|
| id | SERIAL | PK |
| code | VARCHAR(32) | UNIQUE, NOT NULL (e.g. `DC-…`) |
| order_id | INT | FK → orders.id CASCADE, INDEX |
| order_item_id | INT | FK → order_items.id CASCADE, INDEX |
| ticket_type_id | INT | FK → ticket_types.id RESTRICT, INDEX |
| event_id | INT | FK → events.id RESTRICT, INDEX |
| user_id | INT | FK → users.id RESTRICT, INDEX |
| status | ticketstatus ENUM | NOT NULL, INDEX |
| created_at | TIMESTAMPTZ | DEFAULT now() |
**`ticketstatus`:** `valid`, `cancelled`, `redeemed`
**Index:** `(user_id, status)`
---
## 4. Seed data
```bash
python scripts/seed_catalog.py
```
Creates Bengaluru venues (Palace Grounds, KTPO Whitefield, HSR Layout) and published events for **Holi**, **Garba**, and **Dandiya** with media URLs and ticket tiers.
---
## 5. Legacy tables
See prior revisions for `event_editions`, `media_assets`, `admin_users`, `platform_links`, `newsletter_subscribers`, `outbound_clicks`, `alert_dispatches`.
---
## 6. Migration policy
- Revision `20260404_0005` adds catalog tables.
- Revision `20260404_0006` adds attendee `users`.
- Revision `20260404_0007` adds `orders`, `order_items`, `tickets`.
- If the DB was created via `create_all` before Alembic history existed, run `alembic stamp 20260404_0004` once, then `alembic upgrade head`.
---
## 7. Related
- [API_SPEC.md](./API_SPEC.md)

188
docs/DESIGN_SYSTEM.md Normal file
View file

@ -0,0 +1,188 @@
# Design System — Desi Clans
**Version:** 1.0
**Last updated:** 2026-04-04
---
## 1. Brand intent
Desi Clans should read as **premium editorial culture**, not a discount ticket site.
- **Cinematic:** Full-bleed imagery, deliberate negative space, restrained color.
- **Warm, not loud:** Celebratory without carnival clutter.
- **Bengaluru-rooted:** Urban sophistication + festival energy.
- **Trust:** Clear dates, venues, and pricing; no dark patterns on checkout.
---
## 2. Design principles
1. **Content leads** — Photography and event title dominate; UI chrome stays quiet.
2. **Mobile first** — Design at 390px width; scale up with fluid typography.
3. **Motion with purpose** — GSAP for hero reveals and section entrances; never block interaction.
4. **Accessible by default** — Color is not the only signal; focus states are always visible.
5. **Performance is UX** — Prefer CSS transforms for animation; lazy-load below-fold media.
---
## 3. Color (Tailwind tokens)
Define in `tailwind.config.js` under `theme.extend.colors`:
| Token | Role | Suggested value | Usage |
|-------|------|-----------------|--------|
| `ink` | Primary text | `#0F0E0C` | Headlines, body |
| `ink-muted` | Secondary text | `#5C574F` | Meta, captions |
| `surface` | Page background | `#FAF8F5` | Default canvas |
| `surface-elevated` | Cards | `#FFFFFF` | Cards, modals |
| `accent` | Primary CTA | `#C45C26` | Buttons, links (terracotta) |
| `accent-hover` | CTA hover | `#A34A1E` | Hover states |
| `gold` | Highlight | `#C9A227` | Featured badges, dividers (sparingly) |
| `festival` | Seasonal accent | `#8B2E5A` | Holi/Garba campaign strips (optional per edition) |
| `border` | Dividers | `#E8E4DD` | Hairlines |
| `success` | Valid ticket | `#2D6A4F` | Check-in success |
| `danger` | Errors | `#B42318` | Form errors |
**Dark sections:** Hero and footer may use `ink` background with `surface` text for contrast blocks—use for one section per page max.
---
## 4. Typography
Use **two families** only (load via Google Fonts or self-host):
| Role | Family | Tailwind class | Notes |
|------|--------|----------------|-------|
| Display | *Fraunces* or *Playfair Display* | `font-display` | H1–H2, editorial pull quotes |
| UI / body | *DM Sans* or *Inter* | `font-sans` | Body, buttons, labels |
**Scale (mobile-first)**
| Element | Size | Weight | Tracking |
|---------|------|--------|----------|
| H1 | `text-4xl` → `md:text-6xl` | 600 | tight |
| H2 | `text-3xl` → `md:text-4xl` | 600 | tight |
| H3 | `text-xl` | 600 | normal |
| Body | `text-base` | 400 | normal |
| Small / meta | `text-sm` | 400 | `text-ink-muted` |
| Button | `text-sm` uppercase | 600 | `tracking-wide` |
**Line length:** Prose blocks `max-w-prose` (~65ch).
---
## 5. Spacing & layout
- **Grid:** 4px base; use Tailwind spacing scale consistently.
- **Page gutters:** `px-4` mobile, `px-6` tablet, `px-8` desktop; `max-w-7xl mx-auto` for content.
- **Section rhythm:** `py-16` md:`py-24` between major sections.
- **Cards:** `rounded-2xl`, subtle shadow `shadow-sm`, border `border-border`.
---
## 6. Components (catalog)
Build in `frontend/src/components/` with consistent naming:
| Component | Description |
|-----------|-------------|
| `Button` | Primary, secondary (outline), ghost; min height 44px touch target |
| `Badge` | Event type, featured, sold out |
| `EventCard` | Cover, title, date, venue, CTA |
| `EventHero` | Full-width cover + gradient scrim + title |
| `MediaGallery` | Grid + lightbox (keyboard trap, focus return) |
| `TicketCTA` | Sticky on mobile for detail page |
| `Navbar` | Logo, Events, Subscribe; drawer on mobile |
| `Footer` | Links, social, newsletter |
| `Input`, `Label`, `FormError` | Accessible form primitives |
| `Skeleton` | Loading placeholders matching card layout |
| `AdminShell` | Sidebar + content (separate visual tone: denser, neutral) |
**Icons:** Lucide React only; size `20` inline, `24` in buttons.
---
## 7. Motion (GSAP + ScrollTrigger)
**Setup**
- Register `ScrollTrigger` once in `animations/gsap.js`.
- Export `prefersReducedMotion()` helper; if true, skip timelines and set final state.
**Patterns**
| Pattern | Use | Duration |
|---------|-----|----------|
| Hero fade + y | Home hero | 0.8–1.2s |
| Stagger cards | Event grid on scroll | 0.4s stagger |
| Parallax (subtle) | Hero image only | scrub 0.5 |
**Rules**
- No infinite loops on main content.
- `will-change` only during active animation.
- Do not animate `width`/`height`; use `transform` and `opacity`.
---
## 8. Photography & imagery
- **Aspect ratios:** Hero 16:9 or 3:2; cards 4:5 or 3:2.
- **Treatment:** Slight gradient overlay (`from-ink/60`) for text on images.
- **Alt text:** Required; describe the scene, not "event image".
- **Placeholders:** Neutral `surface` with shimmer skeleton, not gray boxes.
---
## 9. Accessibility
- Minimum touch target **44×44px**.
- Focus ring: `ring-2 ring-accent ring-offset-2 ring-offset-surface`.
- Color contrast: body text ≥ 4.5:1; large text ≥ 3:1.
- `prefers-reduced-motion`: disable ScrollTrigger scrubbing.
- Forms: associate labels, `aria-invalid`, `aria-describedby` for errors.
- Skip link: "Skip to main content" as first focusable element.
---
## 10. Admin UI tone
Admin is **utilitarian**, not marketing:
- Neutral `surface` background, compact table density.
- Same `font-sans`; skip display font in admin except page titles.
- Primary actions still use `accent` for consistency.
---
## 11. Content voice
- **Headlines:** Specific ("Garba Under the Lights — KTPO Whitefield") not generic ("Amazing Night").
- **CTAs:** "Get tickets", "View edition", "Join the list" — avoid "Click here".
- **Venues:** Always city + venue name when known.
---
## 12. Tailwind configuration checklist
- `content: ['./index.html', './src/**/*.{js,jsx}']`
- Extend `colors`, `fontFamily`, `borderRadius` as above
- No `@apply` abuse; prefer utility classes in JSX for clarity
- `plugins`: only `@tailwindcss/forms` if needed for input resets
---
## 13. Do not use
- Neon gradients, glassmorphism stacks, or concert-poster clutter.
- More than two font families.
- Auto-playing video with sound.
- Icon-only buttons without `aria-label`.
---
## 14. References
- [PRD.md](./PRD.md) — product scope
- [ARCHITECTURE.md](./ARCHITECTURE.md) — frontend structure

73
docs/PERFORMANCE.md Normal file
View file

@ -0,0 +1,73 @@
# Production performance — Desi Clans (public frontend)
**Last audit:** 2026-04-04
**Build:** `cd frontend && npm run build`
## Summary
| Area | Finding | Action taken |
|------|---------|--------------|
| **Bundle** | Admin + GSAP + Three on critical path | Route-level `React.lazy`, dynamic `loadGsap()`, isolated `hero-atmosphere` chunk |
| **Initial JS** | Single large entry | Split: `vendor-react`, `vendor-http`, `gsap`, `three`, per-route pages |
| **Images** | Full-width Unsplash URLs everywhere | `withImageWidth` / `srcSet` on hero, cards, featured, galleries |
| **Video** | None in app | N/A — no `<video>` on marketing site |
| **Fonts** | Google Fonts (2 families, limited weights) | `preconnect`, `display=swap` in CSS URL; no JS font loader |
| **API** | Home blocked on 2nd request for featured copy | List-only first paint; featured detail deferred via `requestIdleCallback` |
| **Caching** | Refetch on every navigation | In-memory TTL cache (60s) for events list, slug detail, venues |
| **GSAP** | Static import bloated entry | `loadGsap()` dynamic import on home + event detail only |
| **WebGL** | Large Three chunk | Desktop-only, idle mount, separate chunk; mobile = no download |
| **React** | Navbar scroll → many setState | `requestAnimationFrame` throttling |
| **CLS** | Hero / cards | Explicit `width`/`height` on hero; reserved route fallback shell |
## Production chunks (2026-04-04 build)
| Chunk | gzip (approx.) | When loaded |
|-------|----------------|-------------|
| `index-*.js` | **6.9 KB** | First paint (shell + router) |
| `vendor-react-*.js` | 79 KB | First paint |
| `vendor-http-*.js` | 19 KB | First API call |
| `HomePage-*.js` | 5.8 KB | Home route |
| `gsap-*.js` | 45 KB | After `loadGsap()` on home / event detail |
| `hero-atmosphere-*.js` | 124 KB | Idle + desktop only |
| `AdminRoutes-*.js` | 4.4 KB | `/admin` only |
**Before route split:** main `index` JS was ~**44 KB** gzip in one bundle with admin + pages.
**After:** entry shell ~**7 KB** gzip; admin/GSAP/Three not on marketing critical path.
Run `npm run build` and inspect `dist/assets/` for current gzip sizes.
## Lazy loading
| Asset | Strategy |
|-------|----------|
| Hero photo | `fetchpriority="high"`, preload 1600w, responsive `srcSet` |
| Event cards, categories, gallery, featured | `loading="lazy"`, `decoding="async"`, `sizes` |
| Event detail cover | `fetchPriority="high"` |
| Routes | `React.lazy` + `Suspense` |
| GSAP / WebGL | Dynamic import + idle callback |
## Mobile
- WebGL atmosphere **disabled** &lt; 1024px
- GSAP scroll reveals skipped when `prefers-reduced-motion`
- Smaller hero `srcSet` widths selected by browser
- Sticky ticket bar uses safe-area padding
## Lighthouse (preview)
Measure against production build:
```bash
cd frontend
npm run build && npm run preview -- --host 127.0.0.1 --port 4173
npx lighthouse http://127.0.0.1:4173/ --only-categories=performance,accessibility --chrome-flags="--headless"
```
Prior baseline (pre-split): ~86 performance / 96 a11y on home. Re-run after deploy; scores vary with network and Unsplash latency.
## Future (not in scope)
- CDN cache headers for `/api` and `/media` (backend)
- `short_description` on `EventSummary` to avoid featured detail fetch
- Self-hosted fonts subset for Fraunces + DM Sans
- Service worker / offline (PWA)

185
docs/PRD.md Normal file
View file

@ -0,0 +1,185 @@
# Product Requirements Document — Desi Clans
**Version:** 1.0
**Status:** Constitution (pre-build)
**Last updated:** 2026-04-04
**Owner:** Product / Engineering
---
## 1. Vision
Desi Clans is a **premium, Bengaluru-first** platform for discovering and attending **Indian cultural celebrations**—Holi, Garba, Dandiya, and related festivities. The experience should feel **cinematic and editorial**, not like a generic ticket aggregator or concert listing site.
**In scope:** Cultural events, curated editions, venues across Bengaluru (Palace Grounds, KTPO Whitefield, HSR Layout, and similar).
**Out of scope:** Concerts, generic nightlife listings, multi-city expansion beyond Bengaluru in v1.
---
## 2. Goals
| Goal | Success signal |
|------|----------------|
| Premium brand perception | Editorial layouts, strong typography, intentional motion |
| Mobile-first attendance | ≥60% traffic on mobile; core flows usable one-handed |
| Fast | LCP &lt; 2.5s on 4G; API p95 &lt; 300ms for public reads |
| Accessible | WCAG 2.1 AA for public UI; keyboard + screen reader paths |
| SEO | Indexable event URLs, share previews, structured data |
| Operable | Admin can publish editions, media, and ticket inventory without deploys |
| Trustworthy ticketing | Clear pricing, issued tickets with QR, reliable check-in |
---
## 3. Personas
### 3.1 Attendee (primary)
- Discovers events via search, social, or word of mouth.
- Needs date, venue, vibe, gallery, and a frictionless path to buy or hold tickets.
- Expects mobile ticket (QR) at the gate.
### 3.2 Organizer / Desi Clans ops
- Publishes event editions, hero media, copy, and SEO fields.
- Manages ticket tiers, capacity, and sales window.
- Monitors outbound or native sales and gate check-in.
### 3.3 Gate staff
- Scans QR codes quickly; sees valid / already used / invalid states.
- Works on mobile browsers in variable network conditions.
### 3.4 Platform admin
- Manages admin users (roles), site links, newsletter subscribers, and analytics.
---
## 4. User stories (v1)
### Discovery & content
- As an attendee, I can browse **published** event editions filtered by type and date.
- As an attendee, I can open an **event detail page** with cover, gallery, venue, calendar add, and ticket CTA.
- As an attendee, I can share an event; recipients see a **rich preview** (title, image, description).
### Accounts (ticketing phase)
- As an attendee, I can register / log in to purchase and view **my tickets**.
- As an attendee, I receive a **confirmation** with ticket QR(s) after purchase.
### Ticketing
- As ops, I define **ticket tiers** (name, price, quantity, sale window) per edition.
- As an attendee, I can **purchase** tickets (native checkout in target architecture; see phases).
- As gate staff, I can **scan** a QR and mark entry once per ticket.
### Admin
- As an editor, I can CRUD editions, upload cover and gallery media, set publish/featured flags.
- As super admin, I can manage admin users and view click / sales summaries.
- As ops, I can export subscriber list and send alerts (email) for new editions (batch, not real-time chat).
### Newsletter
- As a visitor, I can subscribe with email and optional neighborhood.
---
## 5. Functional requirements
### 5.1 Public site
- Home: featured edition(s), upcoming list, brand story strip.
- Event listing with filters: `event_type`, date range, featured.
- Event detail: slug URL, `meta_description`, Open Graph via share route or SSR meta injection.
- Outbound ticket links (BookMyShow, District, etc.) **or** native checkout when enabled per edition.
- Calendar download (`.ics`) for edition start time.
- Newsletter signup.
### 5.2 Authentication
- **Admin:** JWT (access token), bcrypt passwords, role-based access (`super_admin`, `editor`).
- **Attendees (ticketing phase):** JWT for account + order history; separate from admin namespace.
### 5.3 Admin dashboard
- Secure login; session via Bearer token stored in memory (not localStorage for admin if avoidable; httpOnly cookie acceptable future enhancement).
- Edition editor, media manager, platform links, subscribers, analytics (outbound clicks; order stats when native ticketing live).
- Check-in view for authorized staff (role or edition-scoped permission).
### 5.4 Ticketing model (target)
- **Edition** → one or more **ticket tiers** → **orders** → **tickets** (unique code + QR payload).
- Inventory enforced at order creation ( pessimistic row lock or atomic decrement ).
- QR encodes signed token or opaque ticket id verified server-side (no PII in QR).
- States: `issued`, `checked_in`, `cancelled`, `refunded` (refund workflow optional v1.1).
### 5.5 Payments (phased)
- **Phase A:** Outbound links only (no payment on Desi Clans).
- **Phase B:** Native orders with payment provider webhook (e.g. Razorpay)—documented in architecture, implemented only when product enables it.
### 5.6 Non-functional
- API version prefix: `/api/...`
- PostgreSQL as system of record; Alembic migrations required for schema changes.
- Media: local disk in dev; S3-compatible object storage in production (optional boto3—already present in environment).
- No concert content types in taxonomy.
---
## 6. Event taxonomy
| `event_type` | Description |
|--------------|-------------|
| `holi` | Holi celebrations |
| `garba` | Garba nights |
| `dandiya` | Dandiya events |
| `bhajan_clubbing` | Curated devotional / club fusion (brand-specific) |
| `special` | Other Indian cultural celebrations (not concerts) |
---
## 7. Out of scope (v1)
- Multi-language UI (English only v1).
- Native mobile apps.
- Seat maps / reserved seating.
- Secondary resale marketplace.
- Live streaming.
- User-generated reviews or forums.
- Concert and non-cultural listings.
---
## 8. Risks & mitigations
| Risk | Mitigation |
|------|------------|
| Double ticket sale | DB transactions, unique constraints, idempotent payment webhooks |
| QR forgery | Signed payloads, server validation, rate limits on scan |
| Admin token theft | Short JWT TTL, HTTPS only, minimal CORS origins |
| Heavy media hurting LCP | Responsive images, lazy load, CDN for production assets |
---
## 9. Acceptance criteria (MVP public site)
- [ ] Published editions render from API on home, list, and detail routes.
- [ ] Event pages include correct meta for SEO and social sharing.
- [ ] Mobile layout passes manual QA on iOS Safari and Chrome Android.
- [ ] Admin can publish an edition with cover and gallery without code changes.
- [ ] Lighthouse accessibility score ≥ 90 on event detail (lab).
**MVP ticketing (later phase):** purchase flow, email with QR, successful check-in scan.
---
## 10. References
- [ARCHITECTURE.md](./ARCHITECTURE.md) — system design and phases
- [API_SPEC.md](./API_SPEC.md) — endpoints
- [DATABASE.md](./DATABASE.md) — entities
- [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md) — UI principles
- [PROGRESS.md](./PROGRESS.md) — implementation status

414
docs/PROGRESS.md Normal file
View file

@ -0,0 +1,414 @@
# Implementation Progress — Desi Clans
**Last updated:** 2026-04-04 (Phase 13 visual refinement)
This file tracks delivery against [ARCHITECTURE.md](./ARCHITECTURE.md) phases.
---
## Legend
| Symbol | Meaning |
|--------|---------|
| ✅ | Done |
| 🟡 | Partial |
| ⬜ | Not started |
---
## Phase 0 — Constitution
| Item | Status | Notes |
|------|--------|-------|
| PRD.md | ✅ | v1.0 |
| ARCHITECTURE.md | ✅ | v1.0 |
| DESIGN_SYSTEM.md | ✅ | v1.0 |
| API_SPEC.md | ✅ | v1.0 |
| DATABASE.md | ✅ | v1.0 |
| PROGRESS.md | ✅ | This file |
| README.md | ✅ | Root readme |
---
## Phase 1a — Project foundation
### Monorepo & tooling
| Item | Status | Notes |
|------|--------|-------|
| Root `.gitignore` | ✅ | Node, Python, env, uploads |
| `docker-compose.yml` | ✅ | PostgreSQL 16 for local dev |
| Dev scripts (`scripts/*.ps1`, `*.sh`) | ✅ | Backend + frontend |
| Root README setup | ✅ | Quick start |
### Backend foundation
| Item | Status | Notes |
|------|--------|-------|
| FastAPI app entry | ✅ | CORS, optional `/media` mount |
| `app/config.py` (Pydantic Settings) | ✅ | `.env` driven |
| `app/database.py` | ✅ | SQLAlchemy engine + session |
| `app/storage.py` | ✅ | Local + S3 backends |
| `requirements.txt` | ✅ | Pinned ranges |
| `backend/.env.example` | ✅ | Documented variables |
| `backend/README.md` | ✅ | Setup + scripts |
| Alembic + migrations | ✅ | Existing revisions (models in repo) |
| Health endpoint only in `main` | ✅ | `GET /api/public/health` |
| `tests/test_health.py` | ✅ | FastAPI TestClient |
| `create_all` removed from `main` | ✅ | Use Alembic for schema |
### Frontend foundation
| Item | Status | Notes |
|------|--------|-------|
| Vite + React (JSX) | ✅ | `frontend/` |
| Tailwind CSS v3 | ✅ | Design tokens in config |
| React Router | ✅ | Home + 404, `AppLayout` |
| Axios client | ✅ | `src/api/client.js` |
| GSAP setup | ✅ | `src/animations/gsap.js` + hero fade |
| Lucide React | ✅ | Home page icon |
| Folder structure | ✅ | api, components, hooks, layouts, pages, styles, utils, animations |
| `frontend/.env.example` | ✅ | `VITE_API_URL` |
| Vite dev proxy `/api` | ✅ | `vite.config.js` |
| `frontend/README.md` | ✅ | |
### Business features (deferred — next step)
| Item | Status | Notes |
|------|--------|-------|
| Mount public/admin/auth routers | ⬜ | Code exists under `app/routers/` |
| Public event pages (UI) | ⬜ | |
| Admin dashboard (UI) | ⬜ | |
| Wire Axios to editions API | ⬜ | |
---
## Phase 1b — Public site + admin content
### Backend (routers to enable)
| Item | Status | Notes |
|------|--------|-------|
| JWT admin auth | 🟡 | `app/auth.py` + `routers/auth.py` |
| Public editions API | 🟡 | `routers/public.py` |
| Admin CRUD + media | 🟡 | `routers/admin.py` |
| Analytics, share, subscribers | 🟡 | Routers present, not mounted |
### Frontend
| Item | Status | Notes |
|------|--------|-------|
| Events list + detail pages | ⬜ | |
| Admin dashboard UI | ⬜ | |
| SEO meta per route | 🟡 | Default in `index.html` only |
### DevOps
| Item | Status | Notes |
|------|--------|-------|
| Docker backend image | 🟡 | `backend/Dockerfile` |
| CI lint/test | ⬜ | |
---
## Phase 2 — Design system (UI primitives)
| Item | Status | Notes |
|------|--------|-------|
| Tailwind tokens (color, type, spacing, radius, shadow) | ✅ | `tailwind.config.js` |
| Typography components (`Heading`, `Text`, `Eyebrow`) | ✅ | Fluid display scale |
| `Button`, `Badge`, `Card`, `Container` | ✅ | `src/components/ui/` |
| `Section`, `SectionHeader`, `SectionSplit` | ✅ | Editorial layouts |
| `Navbar`, `NavLink`, `SiteFooter` | ✅ | Mobile menu + sticky header |
| `EventCard` | ✅ | Image-led, minimal radius |
| `Skeleton`, `Spinner`, `EmptyState` | ✅ | Loading & empty patterns |
| Barrel exports | ✅ | `src/components/index.js` |
| Showcase route `/system` | ✅ | Responsive verification + breakpoint hook |
| `npm run build` | ✅ | Verified |
---
## Phase 3 — Public homepage (mock data)
| Item | Status | Notes |
|------|--------|-------|
| `MarketingLayout` + overlay nav | ✅ | Scroll-solid header, anchor links |
| Cinematic `HomeHero` | ✅ | Full-viewport, parallax image, GSAP entrance |
| Upcoming events + `EventCard` grid | ✅ | `src/data/homeMock.js` |
| Featured edition section | ✅ | Editorial split layout |
| Cultural categories | ✅ | Image tiles, ink section |
| Bengaluru venues list | ✅ | `#venues` |
| Immersive gallery | ✅ | Horizontal snap mobile, grid desktop |
| Brand story | ✅ | `SectionSplit` + pull quote |
| Final CTA + mock subscribe form | ✅ | No API |
| Premium `MarketingFooter` | ✅ | |
| GSAP ScrollTrigger reveals | ✅ | `useHomeScrollAnimations`, gallery stagger |
| `prefers-reduced-motion` | ✅ | Animations skipped |
| `overflow-x-hidden` / responsive gutters | ✅ | |
| `npm run build` | ✅ | |
**Not in scope:** backend API, ticketing, auth.
---
## Phase 4 — Event details (mock data)
| Item | Status | Notes |
|------|--------|-------|
| Route `/events/:slug` | ✅ | `EventDetailsPage` |
| Mock catalog `eventsMock.js` + `getEventBySlug` | ✅ | 5 editions incl. long title / no cover |
| Reusable `components/event/*` sections (10 blocks) | ✅ | Hero → CTA |
| Invalid slug empty state | ✅ | Tested `/events/does-not-exist` |
| Homepage cards → event routes | ✅ | `EventCard` `to` prop |
| Mobile sticky ticket bar | ✅ | Hidden `lg+` |
| Document title per edition | ✅ | `useDocumentTitle` |
| Payments / checkout | ⬜ | Deferred |
| API-driven editions | ⬜ | Replace mock |
---
## Phase 5 — FastAPI backend foundation
| Item | Status | Notes |
|------|--------|-------|
| `app/core/` config, deps, errors | ✅ | Pydantic Settings, DI, handlers |
| `app/db/` session + Base | ✅ | PostgreSQL via `DATABASE_URL` |
| `app/models/` package | ✅ | Entities for Alembic |
| `app/schemas/` package | ✅ | `HealthResponse` + domain schemas |
| `app/api/routes/health.py` | ✅ | `GET /api/health` + DB ping |
| `app/main.py` factory | ✅ | CORS, static media, router mount |
| Legacy `app/routers/` | 🟡 | Not mounted yet |
| `pytest` health test | ✅ | 200 or 503 with DB fields |
| API_SPEC.md | ✅ | `/api/health` documented |
---
## Phase 6 — Event & venue data layer
| Item | Status | Notes |
|------|--------|-------|
| Models: `Venue`, `Event`, `EventMedia`, `TicketType` | ✅ | `app/models/catalog.py` |
| Alembic `20260404_0005` | ✅ | FKs, indexes, enums |
| `GET /api/events` + filters | ✅ | category, status, upcoming |
| `GET /api/events/{slug}` | ✅ | Detail + media + tickets |
| `GET /api/venues`, `/api/venues/{id}` | ✅ | |
| Pydantic `schemas/catalog.py` | ✅ | |
| `scripts/seed_catalog.py` | ✅ | Holi, Garba, Dandiya |
| API tests `test_catalog_api.py` | ✅ | 8 pytest total passing |
| DATABASE.md / API_SPEC.md | ✅ | |
| Frontend wired to API | ✅ | Phase 7 — see below |
---
## Phase 7 — Frontend API integration
| Item | Status | Notes |
|------|--------|-------|
| `src/services/api.js` | ✅ | Axios client, `ApiError`, health |
| `src/services/events.js` | ✅ | List, slug, view mappers |
| `src/services/venues.js` | ✅ | List + row mapper |
| `useAsyncQuery` + domain hooks | ✅ | `useEventsList`, `useEventDetail`, `useVenuesList` |
| Homepage → events + venues API | ✅ | Loading / error / empty / retry |
| Event details → slug API | ✅ | 404 empty, error retry |
| Presentation vs data separation | ✅ | Sections receive props; no fetch in UI |
| `RequestState` / skeletons | ✅ | Shared loading & error UI |
| Vitest service tests | ✅ | Success, network, empty, 404 slug |
| `npm run build` | ✅ | Production bundle |
| Static hero/brand/categories | 🟡 | Still `homeMock.js` (non-catalog) |
---
## Phase 8 — Authentication
| Item | Status | Notes |
|------|--------|-------|
| `User` model + Alembic `20260404_0006` | ✅ | `users` table, bcrypt hashes only |
| `POST /api/auth/register` | ✅ | Validation, 409 on duplicate |
| `POST /api/auth/login` | ✅ | JWT `type: user` |
| `GET /api/auth/me` | ✅ | `CurrentUser` dependency |
| `app/core/security.py` | ✅ | Hash, verify, encode/decode JWT |
| Config: `SECRET_KEY`, token TTL | ✅ | Production strength checks |
| Frontend `services/auth.js` + `token.js` | ✅ | Bearer interceptor on API client |
| `AuthProvider` + `useAuth` | ✅ | Bootstrap via `/me` |
| Login / register / account pages | ✅ | Marketing layout |
| `ProtectedRoute` (`/account`) | ✅ | Redirect to `/login` |
| Navbar sign in / sign out | ✅ | |
| Backend tests `test_auth`, `test_security` | ✅ | |
| Frontend vitest `auth.test.js` | ✅ | |
| Admin RBAC | ⬜ | Deferred — legacy routers not mounted |
---
## Phase 9 — Ticket inventory & orders
| Item | Status | Notes |
|------|--------|-------|
| `Order`, `OrderItem`, `Ticket` + migration `0007` | ✅ | Inventory reservation, unique codes |
| Catalog `TicketType` inventory | ✅ | `FOR UPDATE`, oversell checks |
| `GET /api/events/{id}/tickets` | ✅ | |
| `POST /api/orders`, `GET` order/tickets | ✅ | Server-side pricing |
| Concurrency tests | ✅ | `test_orders.py` |
---
## Phase 10 — Payment integration
| Item | Status | Notes |
|------|--------|-------|
| Razorpay server SDK + env secrets | ✅ | `razorpay` package |
| `PAYMENT_PROVIDER` mock / razorpay | ✅ | Mock for local & CI |
| Pending order → verified `paid` | ✅ | Tickets issued after confirm |
| `POST …/payment/verify` | ✅ | Signature verification |
| Webhooks + idempotency table | ✅ | `0008`, duplicate safe |
| Cancel / timeout / failed payment | ✅ | Inventory release |
| `Idempotency-Key` on checkout | ✅ | |
| Frontend checkout (no secrets) | ✅ | `useCheckout`, Razorpay.js |
| Tests `test_payments.py` | ✅ | verify, webhook, cancel |
| Garba category image fix | ✅ | `homeMock` + seed alt media |
---
## Phase 12 — Admin dashboard
| Item | Status | Notes |
|------|--------|-------|
| Admin JWT (`type: admin`) separate from attendees | ✅ | `decode_admin_access_token` |
| `POST /api/admin/auth/login`, `GET /api/admin/auth/me` | ✅ | |
| RBAC `editor` / `super_admin` deps | ✅ | `EditorAdmin`, `SuperAdmin` |
| Dashboard metrics API | ✅ | Revenue, tickets, orders, attendance |
| Events / venues / ticket types CRUD | ✅ | Admin catalog services |
| Orders & payments lists | ✅ | |
| Admin ticket verify | ✅ | Reuses gate logic |
| Event statistics | ✅ | Per-event breakdown |
| Admin SPA `/admin/*` | ✅ | Tables, forms, filters |
| `scripts/seed_admin.py` | ✅ | |
| `test_admin_api.py` | ✅ | Attendee blocked |
---
## Phase 11 — Digital tickets
| Item | Status | Notes |
|------|--------|-------|
| `qr_token` + QR PNG generation | ✅ | Issued on payment; migration `0009` |
| `GET /api/tickets/{id}` detail | ✅ | Owner-only digital pass |
| `POST /api/tickets/verify` | ✅ | Check-in key, atomic `used` |
| Ticket states `valid` / `used` / `cancelled` | ✅ | Renamed from `redeemed` |
| React `DigitalTicket` + `/tickets/:id` | ✅ | Branding, QR, metadata |
| Account ticket list | ✅ | Links to pass |
| Tests `test_digital_tickets.py` | ✅ | QR detail + reuse prevention |
---
## Phase 15 — Security audit
| Item | Status | Notes |
|------|--------|-------|
| `docs/SECURITY.md` | ✅ | Assumptions, controls, prod checklist |
| Production settings validation | ✅ | Gate key, Razorpay, CORS, admin password |
| Mock webhook gated to development | ✅ | |
| Admin verify requires gate key | ✅ | Constant-time key compare |
| Auth rate limits (in-process) | ✅ | login/register/verify/payment |
| Login password min length aligned | ✅ | 8 chars attendee + admin |
---
## Phase 14 — Production performance audit
| Item | Status | Notes |
|------|--------|-------|
| Route code splitting (marketing, admin, auth) | ✅ | `App.jsx` lazy routes |
| Dynamic GSAP load | ✅ | `loadGsap.js` |
| API cache + deferred featured detail | ✅ | `queryCache.js`, idle fetch |
| Responsive Unsplash `srcSet` | ✅ | `utils/mediaUrl.js` |
| Navbar scroll rAF throttle | ✅ | |
| `docs/PERFORMANCE.md` | ✅ | Audit + targets |
---
## Phase 13b — Hero WebGL atmosphere (single 3D touchpoint)
| Item | Status | Notes |
|------|--------|-------|
| Home hero festival dust (Three.js) | ✅ | `src/webgl/hero-atmosphere/` only |
| Lazy load + idle defer | ✅ | `React.lazy`, `requestIdleCallback`, separate chunk |
| Desktop / WebGL / reduced-motion gates | ✅ | `eligibility.js` + vitest |
| Mobile & no-WebGL fallback | ✅ | Layer not mounted; hero unchanged |
| Offscreen pause (IntersectionObserver) | ✅ | Stops rAF when hero scrolls away |
---
## Phase 13 — Visual refinement (public site)
| Item | Status | Notes |
|------|--------|-------|
| Typography, spacing, cards (`rounded-2xl`, shadows) | ✅ | Buttons, `EventCard`, tiers, ticket pass |
| Nav / hover / section rhythm | ✅ | Editorial nav underline, fade into content |
| GSAP: home stagger groups + event detail reveals | ✅ | `useEventDetailAnimations`, parallax heroes |
| `prefers-reduced-motion` CSS + GSAP guards | ✅ | `index.css`, hooks skip motion |
| Image cropping (`object` focal) + `sizes` | ✅ | Hero, gallery, cards |
| Vite chunk split (GSAP) | ✅ | `manualChunks` |
| Lighthouse pass (local preview) | 🟡 | Home perf ~86, a11y 96 (preview build); LCP tuned via hero preload + no opacity-hiding H1 |
---
## Phase 2b — Polish & performance
| Item | Status |
|------|--------|
| GSAP ScrollTrigger sections | ✅ | Phase 13 |
| `prefers-reduced-motion` (beyond GSAP init) | ✅ | Phase 13 |
| Image lazy load on real event pages | ✅ | Lazy below fold; hero `fetchPriority` |
| Accessibility audit (WCAG AA) | ⬜ |
| Lighthouse performance pass | 🟡 | Chunking + LCP hints; audit ongoing |
---
## Phase 3 — Native ticketing
| Item | Status |
|------|--------|
| DB: ticket_tiers, orders, tickets | ✅ | Phase 9 |
| Attendee users + auth | ✅ | Phase 8 |
| Checkout + payment webhook | ✅ | Phase 10 |
| QR generation + ticket page | ✅ | Phase 11 |
| Staff check-in API + UI | ⬜ |
| Email ticket delivery | ⬜ |
---
## Phase 4 — Hardening
| Item | Status |
|------|--------|
| E2E tests (critical paths) | ⬜ |
| Rate limiting / WAF | ⬜ |
| Production CDN for media | ⬜ |
| Runbooks + monitoring | ⬜ |
---
## Current focus (recommended)
1. Mount existing API routers in `app/main.py` and verify against PostgreSQL.
2. Add admin SPA routes and auth token handling.
3. GSAP polish, a11y audit, Lighthouse pass (Phase 2b).
---
## Changelog
| Date | Change |
|------|--------|
| 2026-04-04 | Initial constitution docs |
| 2026-04-04 | Phase 1a foundation: frontend scaffold, backend config/database/storage, health API, docker-compose, dev scripts |
| 2026-04-04 | Phase 2 design system: Tailwind tokens, React primitives, `/system` showcase |
| 2026-04-04 | Phase 3 public homepage: mock data, marketing sections, GSAP motion |
| 2026-04-04 | Phase 4 event details: `/events/:slug`, reusable sections, mock catalog |
| 2026-04-04 | Phase 5 backend: layered app layout, `/api/health`, PostgreSQL check |
| 2026-04-04 | Phase 6 catalog: venues/events/media/tickets API + seed |
| 2026-04-04 | Phase 7: services layer, hooks, homepage + event detail API wiring, vitest |
| 2026-04-04 | Phase 8: attendee auth API, JWT, React auth flow, tests |
| 2026-04-04 | Phase 9–10: orders, Razorpay/mock payments, webhooks, checkout UI |
| 2026-04-04 | Phase 11: QR digital passes, verify API, ticket UI |
| 2026-04-04 | Phase 13: design-system visual polish, motion, perf chunking |

194
docs/SECURITY.md Normal file
View file

@ -0,0 +1,194 @@
# Security — Desi Clans
**Last reviewed:** 2026-04-04
This document describes security assumptions, controls, and operational requirements for the Desi Clans monorepo (FastAPI API + React SPA).
---
## Threat model (summary)
| Asset | Primary risks |
|-------|----------------|
| User accounts | Credential stuffing, session theft |
| Admin panel | Privilege abuse, catalog tampering |
| Orders / payments | Price tampering, replay, webhook forgery |
| Digital tickets | QR leakage, double entry, scanner abuse |
| Media uploads | Malware, path traversal (legacy paths only) |
**Out of scope for v1:** DDoS at scale (use CDN/WAF in production), PCI DSS Level 1 (Razorpay.js handles card data).
---
## Authentication & JWT
### Attendees (`/api/auth/*`)
- Passwords hashed with **bcrypt** (passlib). Plain passwords never stored or returned.
- JWT **HS256**, claim `type: "user"`, `sub` = numeric user id.
- Register/login: Pydantic validation (email normalized, password **≥ 8** characters).
- **Rate limit:** 20 login / 10 register requests per IP per minute (in-process).
### Admins (`/api/admin/auth/*`)
- Separate JWT claim `type: "admin"`; attendee tokens **cannot** call admin routes (tested).
- Roles: `editor`, `super_admin` — all admin API routes currently require `editor` or above.
- **Rate limit:** 20 login attempts per IP per minute.
### Frontend token storage
- Attendee token: `localStorage` (`desiclans_access_token`).
- Admin token: separate key (`desiclans_admin_token`).
- **Assumption:** XSS on the marketing or admin origin equals token compromise. Mitigate with CSP, dependency hygiene, and short TTL if needed.
### Production configuration
- `SECRET_KEY` ≥ 32 chars, not on blocklist (`config.py`).
- `ACCESS_TOKEN_EXPIRE_MINUTES` bounded 5–20160.
---
## Authorization
| Endpoint class | Enforcement |
|----------------|-------------|
| Public catalog | No auth |
| Orders, tickets (owner) | `CurrentUser` + `user_id` match on orders/tickets |
| Gate verify | `X-Check-In-Key` (+ rate limit) |
| Admin CRUD | `EditorAdmin` JWT |
| Admin ticket verify | **Editor JWT + same gate key** as public verify |
**QR reuse:** `verify_ticket_at_gate` locks ticket row (`SELECT … FOR UPDATE`), sets `used` once; second scan returns `valid: false` (HTTP 200).
**Order manipulation:** Clients send `event_id` + line items only; **prices and totals** come from `TicketType` rows under row lock. Ticket types must belong to the event.
**Payment verify:** User must own order; `provider_order_id` must match order; signature verified via Razorpay/mock HMAC; amount/currency compared to `order.total_paise`.
---
## Payments & webhooks
| Provider | Use |
|----------|-----|
| `mock` | Local dev / CI only |
| `razorpay` | Production |
- **`POST /api/webhooks/mock`** returns **404** unless `ENVIRONMENT=development`.
- Razorpay webhooks: signature verified (`RAZORPAY_WEBHOOK_SECRET`).
- Webhook events stored with **idempotency** on `provider_event_id`.
- `POST …/payment/mock-complete` only when `development` + `mock` provider.
**Production startup validation** (`Settings.production_hardening`):
- `PAYMENT_PROVIDER=razorpay` with Razorpay secrets set.
- `TICKET_VERIFY_API_KEY` ≥ 16 characters.
- `ADMIN_PASSWORD` not default.
- `CORS_ORIGINS` non-empty.
---
## CORS
- Explicit origin list from `CORS_ORIGINS` (no `*`).
- `allow_credentials=True` — **do not** add untrusted origins in production.
---
## SQL injection
- Business logic uses **SQLAlchemy ORM** / parameterized queries.
- Raw SQL limited to health check `SELECT 1` and Alembic migrations.
---
## Input validation
- Request bodies use **Pydantic** schemas (`schemas/*`).
- Ticket verify: `qr_token` length bounds.
- Order lines: quantity 1–20, max 20 lines.
---
## File uploads
- **Active `/api` router:** no upload endpoints.
- Legacy `app/routers/admin.py` (not mounted): extension whitelist, 8MB cap, PIL re-encode to WebP.
- **Assumption:** Keep legacy routers unmounted or remove before exposing uploads on production API.
---
## Secrets & environment
| Variable | Notes |
|----------|--------|
| `SECRET_KEY` | JWT signing; rotate with forced re-login |
| `PAYMENT_MOCK_SECRET` | Dev only |
| `RAZORPAY_*` | Production payments |
| `TICKET_VERIFY_API_KEY` | Gate scanners; constant-time compare |
| `DATABASE_URL` | Never commit; least-privilege DB user |
See `backend/.env.example` and `frontend/.env.example`.
---
## API error leakage
- Production: `debug=False`, OpenAPI disabled, generic 500 body.
- `AppError` returns intentional `detail` strings only.
- Development may expose exception text on 500.
---
## Rate limiting
In-process sliding window (`app/core/rate_limit.py`) on:
- Auth login/register
- Admin login
- Ticket verify
- Payment verify
**Limitation:** Per-process only; use reverse-proxy or WAF limits for multi-instance deployments.
---
## QR codes
- Opaque `qr_token` (url-safe random); payload is JSON `{"v":1,"token":…}` without embedded HMAC.
- Security relies on **HTTPS**, token secrecy, and server-side verify.
- Ticket detail (`GET /api/tickets/{id}`) owner-only; includes QR image for valid paid orders.
---
## Frontend (checkout)
- Razorpay **key id** only in browser; secrets stay on server.
- No payment card data touches Desi Clans backend.
---
## Scanner / admin verify UI
- Staff UI calls `POST /api/tickets/verify` with optional `VITE_TICKET_VERIFY_API_KEY` → `X-Check-In-Key` header.
- In development, if `TICKET_VERIFY_API_KEY` is unset, verify is open (local convenience only).
---
## Checklist before production
1. Set strong `SECRET_KEY`, `TICKET_VERIFY_API_KEY`, Razorpay secrets, non-default `ADMIN_PASSWORD`.
2. `ENVIRONMENT=production`, `PAYMENT_PROVIDER=razorpay`.
3. Tight `CORS_ORIGINS` to real site origin(s).
4. TLS everywhere; HSTS at edge.
5. Do not mount legacy `app/routers/*` without security review.
6. Enable edge rate limiting and monitoring on auth/webhook paths.
---
## Related tests
- `tests/test_security.py` — JWT types, bcrypt
- `tests/test_security_hardening.py` — production settings, mock webhook
- `tests/test_payments.py` — verify + webhook idempotency
- `tests/test_digital_tickets.py` — QR reuse
- `tests/test_admin_api.py` — attendee blocked from admin