319 lines
7.8 KiB
Markdown
319 lines
7.8 KiB
Markdown
# 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.
|
|
|