# Frontend API Guide (HotTRDeals)

This file is a frontend-focused summary of current backend routes, auth, and response models.
Server entry: `server.js`.

## Base URL and CORS
- Local API base: `http://localhost:3000`
- All routes below are relative to `/api`.
- CORS in dev allows `http://localhost:5173` and `credentials: true`.

## Auth summary
- Access token is returned in response body: `{ token, user }`.
- Send access token via header: `Authorization: Bearer <token>`.
- Refresh token is stored in httpOnly cookie named `rt`.
- For `/auth/refresh` and `/auth/logout`, the frontend must send cookies (`withCredentials: true`).
- Cookie options:
  - Dev: `secure=false`, `sameSite=lax`
  - Prod: `secure=true`, `sameSite=none`

## Roles
- Roles: `USER`, `MOD`, `ADMIN`.
- `requireRole("MOD")` means only MOD and ADMIN can access.

## Common response shapes
- Errors are inconsistent:
  - Some endpoints return `{ error: "..." }`
  - Some endpoints return `{ message: "..." }`
- Expect both in frontend error handling.

## Common models (from adapters/contracts)

### PublicUserSummary
```
{
  id: number,
  username: string,
  avatarUrl: string | null
}
```

### PublicUserDetails
```
{
  id: number,
  username: string,
  avatarUrl: string | null,
  email: string,
  createdAt: string (ISO)
}
```

### AuthUser
```
{
  id: number,
  username: string,
  email: string,
  role: "USER" | "MOD" | "ADMIN",
  avatarUrl: string | null
}
```

### DealCard
```
{
  id: number,
  title: string,
  description: string,
  price: number | null,
  originalPrice?: number | null,
  shippingPrice?: number | null,
  score: number,
  commentsCount: number,
  url: string | null,
  status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED",
  saleType: "ONLINE" | "OFFLINE" | "CODE",
  affiliateType: "AFFILIATE" | "NON_AFFILIATE" | "USER_AFFILIATE",
  myVote: -1 | 0 | 1,
  createdAt: string (ISO),
  updatedAt: string (ISO),
  user: PublicUserSummary,
  seller: { name: string, url: string | null },
  imageUrl: string
}
```

### DealDetail
```
{
  id: number,
  title: string,
  description: string,
  url: string | null,
  price: number | null,
  originalPrice?: number | null,
  shippingPrice?: number | null,
  score: number,
  commentsCount: number,
  status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED",
  saleType: "ONLINE" | "OFFLINE" | "CODE",
  affiliateType: "AFFILIATE" | "NON_AFFILIATE" | "USER_AFFILIATE",
  createdAt: string (ISO),
  updatedAt: string (ISO),
  user: PublicUserSummary,
  seller: { id: number, name: string, url: string | null },
  images: [{ id: number, imageUrl: string, order: number }],
  comments: [{ id: number, text: string, createdAt: string, parentId?: number | null, user: PublicUserSummary }],
  breadcrumb: [{ id: number, name: string, slug: string }],
  notice: {
    id: number,
    dealId: number,
    title: string,
    body: string | null,
    severity: "INFO" | "WARNING" | "DANGER" | "SUCCESS",
    isActive: boolean,
    createdBy: number,
    createdAt: string,
    updatedAt: string
  } | null,
  similarDeals: [{ id: number, title: string, price: number | null, score: number, imageUrl: string, sellerName: string, createdAt: string | null }]
}
```

### DealListResponse
```
{
  page: number,
  total: number,
  totalPages: number,
  results: DealCard[]
}
```

### Comment (DealComment)
```
{
  id: number,
  text: string,
  createdAt: string,
  parentId?: number | null,
  user: PublicUserSummary
}
```

### UserProfile
```
{
  user: PublicUserDetails,
  stats: { totalLikes: number, totalShares: number, totalComments: number, totalDeals: number },
  deals: DealCard[],
  comments: [{ ...Comment, deal: { id: number, title: string } }]
}
```

### VoteResponse
```
{
  dealId: number,
  voteType: -1 | 0 | 1,
  delta: number,
  score: number | null
}
```

### VoteListResponse
```
{
  votes: [{ id, dealId, userId, voteType, createdAt, lastVotedAt }]
}
```

## Endpoints

### Auth (`/api/auth`)
- `POST /register`
  - Body: `{ username, email, password }`
  - Response: `{ token, user: AuthUser }` and sets `rt` cookie if refresh token exists.

- `POST /login`
  - Body: `{ email, password }`
  - Response: `{ token, user: AuthUser }` and sets `rt` cookie.

- `POST /refresh`
  - Cookie required: `rt`
  - Response: `{ token, user: AuthUser }` and rotates `rt` cookie.

- `POST /logout`
  - Cookie optional: `rt`
  - Response: `204 No Content`, clears `rt` cookie.

- `GET /me`
  - Auth: required
  - Response: `AuthUser`
  - Note: current implementation reads `req.user.userId` in adapter, while auth middleware sets `req.auth`. If `req.user` is undefined, this endpoint can fail.

### Account (`/api/account`)
- `POST /avatar`
  - Auth: required
  - Multipart: `file` (single)
  - Validation: JPEG only, max 2MB
  - Response: `{ message, user: PublicUserSummary }`

- `GET /me`
  - Auth: required
  - Response: `PublicUserDetails`

### Deals (`/api/deals`)
List query params (used in list endpoints):
- `q` (string, default "")
- `page` (int, default 1)
- `limit` (int, default 10, max 100)

Endpoints:
- `GET /users/:userName/deals`
  - Auth: optional
  - Response: `DealListResponse`

- `GET /me/deals`
  - Auth: required
  - Response: `DealListResponse`

- `GET /new`
- `GET /hot`
- `GET /trending`
- `GET /` (same as `/new`)
  - Auth: optional
  - Response: `DealListResponse`

- `GET /search`
  - Auth: optional
  - If `q` is empty: `{ results: [], total: 0, totalPages: 0, page }`
  - Else: `DealListResponse`

- `GET /top`
  - Auth: optional
  - Query: `range=day|week|month` (default `day`), `limit` (default 6, max 20)
  - Response: `DealCard[]` (array only)

- `GET /:id`
  - Response: `DealDetail`

- `POST /`
  - Auth: required
  - Multipart: `images` (array, max 5)
  - Limits: 10MB per file (upload middleware)
  - Body fields: `{ title, description?, url?, price?, sellerName? }`
  - Response: `DealDetail`

### Category (`/api/category`)
- `GET /:slug`
  - Response: `{ id, name, slug, description, breadcrumb }`

- `GET /:slug/deals`
  - Auth: optional
  - Query: `page`, `limit`, plus filters from query
  - Response: `DealCard[]` (array only)

### Seller (`/api/seller`)
- `POST /from-link`
  - Auth: required
  - Body: `{ url }`
  - Response: `{ found: boolean, seller: { id, name, url } | null }`

- `GET /:sellerName`
  - Response: `{ id, name, url, logoUrl }`

- `GET /:sellerName/deals`
  - Auth: optional
  - Query: `page`, `limit`, `q`
  - Response: `DealCard[]` (array only)

### Comments (`/api/comments`)
- `GET /:dealId`
  - Response: `Comment[]`

- `POST /`
  - Auth: required
  - Body: `{ dealId, text, parentId? }`
  - Response: `Comment`

- `DELETE /:id`
  - Auth: required
  - Response: `{ message: "Yorum silindi." }`

### Votes (`/api/vote` and `/api/deal-votes`)
- `POST /`
  - Auth: required
  - Body: `{ dealId, voteType }` where voteType is -1, 0, or 1
  - Response: `VoteResponse`

- `GET /:dealId`
  - Response: `VoteListResponse`

### Users (`/api/user` and `/api/users`)
- `GET /:userName`
  - Response: `UserProfile`

### Mod (`/api/mod`) (MOD or ADMIN)
- `GET /deals/pending`
  - Auth + Role: MOD
  - Query: `page`, `limit`, `q` plus filters
  - Response: `DealCard[]` (array only)

- `POST /deals/:id/approve`
- `POST /deals/:id/reject`
- `POST /deals/:id/expire`
- `POST /deals/:id/unexpire`
  - Auth + Role: MOD
  - Response: `{ id, status }`

## Notes for frontend
- Some list endpoints return full pagination object, some return only `results` array. Treat them separately:
  - Full: `/api/deals` list endpoints, `/api/deals/search`, `/api/deals/users/:userName/deals`, `/api/deals/me/deals`
  - Array only: `/api/deals/top`, `/api/category/:slug/deals`, `/api/seller/:sellerName/deals`, `/api/mod/deals/pending`
- Optional-auth endpoints return `401` if a token is provided but invalid.
- MyVote is only filled when Authorization header is present.
- There are duplicate route prefixes for users and votes. Frontend can use either, but pick one for consistency.

