# 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 `. - 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.