# Pryojon Backend — API Reference

**Base URL (local):** `http://localhost:4000`
**Base URL (prod):** `https://api.your-domain.com`
**Auth:** `Authorization: Bearer <JWT>` (obtained from `/api/auth/login`)
**Content-Type:** `application/json` (except `/api/uploads/*` which uses `multipart/form-data`)

## Conventions

- All timestamps are ISO 8601 UTC.
- Role gating: `public` (no token), `auth` (any logged-in user), `seller` (role=seller), `admin` (role=admin).
- Errors: `{ "error": "message" }` with HTTP 4xx/5xx.
- Pagination (where supported): `?page=1&limit=20`.

## Health

### `GET /api/health`
Public. Returns `{ "ok": true, "ts": <epoch_ms> }`.

```bash
curl http://localhost:4000/api/health
```

---

## Auth — `/api/auth`

### `POST /api/auth/signup` — public
Body:
```json
{ "email": "user@example.com", "password": "secret123", "display_name": "Jane", "phone": "+8801..." }
```
Response 201:
```json
{
  "access_token": "eyJhbGciOi...",
  "token_type": "bearer",
  "user": { "id": "uuid", "email": "user@example.com" },
  "profile": { "id": "uuid", "displayName": "Jane", "phone": "...", "email": "..." },
  "roles": ["customer"]
}
```
Errors: `409` email exists, `400` validation.

### `POST /api/auth/login` — public
Body: `{ "email": "...", "password": "..." }`
Response: same shape as signup. `401` on bad credentials.

### `GET /api/auth/me` — auth
Returns `{ user, profile, roles }` for the logged-in user.

### `POST /api/auth/logout` — auth
Returns `{ "ok": true }`. JWT is stateless; client discards the token.

### `POST /api/auth/forgot-password` — public
Body: `{ "email": "..." }`. Always returns `{ "ok": true }` (does not leak existence). If the user exists, a reset email is sent via SMTP (configure `SMTP_*` env vars).

### `POST /api/auth/reset-password` — public
Body: `{ "token": "<raw token from email>", "password": "newpass" }`. `400` if token invalid/expired/used.

---

## Profiles — `/api/profiles`

| Method | Path | Role |
|---|---|---|
| GET | `/:id` | public |
| PATCH | `/:id` | auth (must own profile) |

Body fields: `displayName`, `phone`, `email`, `avatarUrl`, `bio`.

---

## User Roles — `/api/user-roles`

| Method | Path | Role | Notes |
|---|---|---|---|
| GET | `/` | auth | returns the caller's roles |
| POST | `/` | admin | body: `{ userId, role: "admin" \| "seller" \| "customer" }` |
| DELETE | `/:id` | admin | |
| POST | `/claim-first-admin` | auth | bootstrap: becomes admin only if no admin exists yet |

---

## Categories — `/api/categories`

| Method | Path | Role |
|---|---|---|
| GET | `/` | public — query: `?parentId=`, `?active=true` |
| GET | `/:id` | public |
| POST | `/` | admin — `{ name, slug, icon, parentId?, sortOrder?, isActive? }` |
| PATCH | `/:id` | admin |
| DELETE | `/:id` | admin |

---

## Category Pricing — `/api/category-pricing`

CRUD for per-category package price overrides.
- `GET /` (public, `?categoryId=`), `POST /` (admin), `PATCH /:id` (admin), `DELETE /:id` (admin).

---

## Packages — `/api/packages`

| Method | Path | Role |
|---|---|---|
| GET | `/` | public — query `?active=true` |
| POST | `/` | admin — `{ name, durationDays, price, features: string[], isActive? }` |
| PATCH | `/:id` | admin |
| DELETE | `/:id` | admin |

---

## Services — `/api/services`

| Method | Path | Role |
|---|---|---|
| GET | `/` | public — query: `categoryId, districtId, thanaId, sellerId, status, search, page, limit` |
| GET | `/:id` | public |
| POST | `/` | auth (seller) — `{ title, description, categoryId, districtId, thanaId, price, packageId, ... }` |
| PATCH | `/:id` | auth (owner) or admin |
| POST | `/:id/increment-views` | public |
| DELETE | `/:id` | admin or seller (owner) |

Response includes `serviceCode` (auto-generated, e.g. `PJV001`), `expiresAt`, and embedded `images`, `category`, `seller`.

---

## Service Images — `/api/service-images`

| Method | Path | Role |
|---|---|---|
| GET | `/?serviceId=` | public |
| POST | `/` | auth — `{ serviceId, url, sortOrder? }` |
| DELETE | `/:id` | auth (owner) |

Use `/api/uploads/service-images` first to obtain `url`.

---

## Sellers — `/api/sellers`

| Method | Path | Role |
|---|---|---|
| GET | `/` | admin — list/search all sellers |
| GET | `/me` | auth | current user's seller record |
| GET | `/:id` | public |
| POST | `/` | auth — create seller application |
| PATCH | `/:id` | auth (owner) or admin (for `approvalStatus`) |

Seller fields: `businessName`, `nidNumber`, `address`, `districtId`, `thanaId`, `approvalStatus` (`pending`/`approved`/`rejected`).

---

## Seller Documents — `/api/seller-documents`

| Method | Path | Role |
|---|---|---|
| GET | `/?sellerId=` | auth (owner or admin) |
| POST | `/` | auth — `{ sellerId, docType, url }` |
| PATCH | `/:id` | admin — `{ status: "approved"\|"rejected" }` |
| DELETE | `/:id` | auth (owner) |

---

## Payments — `/api/payments`

| Method | Path | Role |
|---|---|---|
| GET | `/` | auth — own payments; admin sees all (`?all=true`) |
| POST | `/` | auth — `{ serviceId, packageId, amount, method, txnRef }` |
| PATCH | `/:id` | admin — `{ status: "approved"\|"rejected" }` (approval auto-extends service `expiresAt`) |

---

## Reviews — `/api/reviews`

| Method | Path | Role |
|---|---|---|
| GET | `/?serviceId=` or `?sellerId=` | public |
| POST | `/` | public (or auth if you flip the policy) — `{ serviceId, rating(1-5), comment, reviewerName }` |
| PATCH | `/:id` | auth (author) |
| DELETE | `/:id` | admin |

Posting/updating/deleting recalculates the parent service's `avgRating` and `reviewCount`.

---

## Banners — `/api/banners`

| Method | Path | Role |
|---|---|---|
| GET | `/?slot=` | public |
| POST | `/` | admin — `{ slot, imageUrl, linkUrl?, sortOrder?, isActive? }` |
| PATCH | `/:id` | admin |
| DELETE | `/:id` | admin |

---

## Site Pages — `/api/site-pages`

| Method | Path | Role |
|---|---|---|
| GET | `/` | public (list) |
| GET | `/:slug` | public |
| POST | `/` | admin — `{ slug, title, contentHtml }` |
| PATCH | `/:id` | admin |
| DELETE | `/:id` | admin |

---

## Districts & Thanas — `/api/districts`, `/api/thanas`

Standard CRUD. `GET` is public. `POST/PATCH/DELETE` require admin.
- District: `{ name, nameBn? }`
- Thana: `{ name, nameBn?, districtId }` — `GET /?districtId=` filters by district.

---

## Uploads — `/api/uploads`

Replaces Supabase Storage. Files are saved under `<UPLOAD_DIR>/<bucket>/` and served at `/uploads/<bucket>/<filename>`.

Valid buckets: `banners`, `category-images`, `service-images`, `seller-docs`, `avatars`.

### `POST /api/uploads/:bucket` — auth
`multipart/form-data` with field `file`.

```bash
curl -X POST http://localhost:4000/api/uploads/service-images \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@./photo.jpg"
```
Response:
```json
{ "url": "http://localhost:4000/uploads/service-images/1719000000-photo.jpg", "filename": "1719000000-photo.jpg" }
```

### `DELETE /api/uploads/:bucket/:filename` — auth

---

## Error reference

| Code | Meaning |
|---|---|
| 400 | Validation error (zod) |
| 401 | Missing/invalid JWT or bad credentials |
| 403 | Authenticated but role not allowed |
| 404 | Not found |
| 409 | Conflict (e.g., duplicate email) |
| 429 | Rate limit (auth endpoints: 50 req / 15 min / IP) |
| 500 | Server error |
