HotTRDealsBackend/docs/frontend-api.md
2026-01-29 00:45:52 +00:00

7.8 KiB

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.