HotTRDealsBackend/routes/deal.routes.js
2026-02-04 06:39:10 +00:00

485 lines
15 KiB
JavaScript

// routes/deals.js
const express = require("express")
const router = express.Router()
const requireAuth = require("../middleware/requireAuth")
const requireNotRestricted = require("../middleware/requireNotRestricted")
const optionalAuth = require("../middleware/optionalAuth")
const { upload } = require("../middleware/upload.middleware")
const { validate } = require("../middleware/validate.middleware")
const { endpoints } = require("@shared/contracts")
const requireApiKey = require("../middleware/requireApiKey")
const userDB = require("../db/user.db")
const {
getDeals,
getDealById,
createDeal,
getDealEngagement,
getDealSuggestions,
getBestWidgetDeals,
} = require("../services/deal.service")
const dealSaveService = require("../services/dealSave.service")
const dealReportService = require("../services/dealReport.service")
const { mapCreateDealRequestToDealCreateData } = require("../adapters/requests/dealCreate.adapter")
const { mapDealToDealDetailResponse } = require("../adapters/responses/dealDetail.adapter")
const { mapDealToDealCardResponse, mapPaginatedDealsToDealCardResponse } = require("../adapters/responses/dealCard.adapter")
const { getClientIp } = require("../utils/requestInfo")
const {
queueDealImpressions,
queueDealView,
queueDealClick,
} = require("../services/redis/dealAnalytics.service")
const { getOrCacheDeal } = require("../services/redis/dealCache.service")
const { enqueueAuditFromRequest, buildAuditMeta } = require("../services/audit.service")
const { AUDIT_ACTIONS } = require("../services/auditActions")
const { deals, users } = endpoints
function parsePage(value) {
const num = Number(value)
if (!Number.isInteger(num) || num < 1) return 1
return num
}
const listQueryValidator = validate(deals.dealsListRequestSchema, "query", "validatedDealListQuery")
const buildViewer = (req) =>
req.auth ? { userId: req.auth.userId, role: req.auth.role } : null
function createListHandler(preset) {
return async (req, res) => {
try {
const viewer = buildViewer(req)
const { q, page, limit, hotListId, trendingListId } = req.validatedDealListQuery
const payload = await getDeals({
preset,
q,
page,
limit,
viewer,
filters: req.query,
hotListId,
trendingListId,
})
const response = deals.dealsListResponseSchema.parse(
mapPaginatedDealsToDealCardResponse(payload)
)
const dealIds = payload?.results?.map((deal) => deal.id) || []
queueDealImpressions({
dealIds,
userId: req.auth?.userId ?? null,
ip: getClientIp(req),
}).catch((err) => console.error("Deal impression queue failed:", err?.message || err))
res.json(response)
} catch (err) {
console.error(err)
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
}
// Public deals of a user (viewer optional; self profile => "MY" else "USER_PUBLIC")
router.get(
"/users/:userName/deals",
optionalAuth,
validate(users.userProfileRequestSchema, "params", "validatedUserProfile"),
listQueryValidator,
async (req, res) => {
try {
const { userName } = req.validatedUserProfile
const targetUser = await userDB.findUser(
{ username: userName },
{ select: { id: true } }
)
if (!targetUser) return res.status(404).json({ error: "Kullanici bulunamadi" })
const { q, page, limit, hotListId, trendingListId } = req.validatedDealListQuery
const viewer = buildViewer(req)
const isSelfProfile = viewer?.userId === targetUser.id
const preset = isSelfProfile ? "MY" : "USER_PUBLIC"
const payload = await getDeals({
preset,
q,
page,
limit,
targetUserId: targetUser.id,
viewer,
filters: req.query,
hotListId,
trendingListId,
})
const response = deals.dealsListResponseSchema.parse(
mapPaginatedDealsToDealCardResponse(payload)
)
const dealIds = payload?.results?.map((deal) => deal.id) || []
queueDealImpressions({
dealIds,
userId: req.auth?.userId ?? null,
ip: getClientIp(req),
}).catch((err) => console.error("Deal impression queue failed:", err?.message || err))
res.json(response)
} catch (err) {
console.error(err)
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
// My deals (auth required)
router.get(
"/me/deals",
requireAuth,
listQueryValidator,
createListHandler("MY")
)
router.get("/new", requireApiKey, optionalAuth, listQueryValidator, createListHandler("NEW"))
router.get("/hot", requireApiKey, optionalAuth, listQueryValidator, createListHandler("HOT"))
router.get("/trending", requireApiKey, optionalAuth, listQueryValidator, createListHandler("TRENDING"))
router.get("/search/suggest", optionalAuth, async (req, res) => {
try {
const q = String(req.query.q || "").trim()
const limit = Number(req.query.limit || 8)
const payload = await getDealSuggestions({ q, limit, viewer: buildViewer(req) })
const response = mapPaginatedDealsToDealCardResponse({
page: 1,
total: payload.results.length,
totalPages: 1,
results: payload.results,
})
res.json(response.results)
} catch (err) {
console.error(err)
res.status(500).json({ error: "Sunucu hatasi" })
}
})
// Resolve deal URL (SSR uses api key; user token optional)
router.post("/url", requireApiKey, optionalAuth, async (req, res) => {
try {
const dealId = Number(req.body?.dealId)
if (!Number.isInteger(dealId) || dealId <= 0) {
return res.status(400).json({ error: "dealId invalid" })
}
const deal = await getOrCacheDeal(dealId, { ttlSeconds: 15 * 60 })
if (!deal) return res.status(404).json({ error: "Deal bulunamadi" })
if (deal.status === "PENDING" || deal.status === "REJECTED") {
const isOwner = req.auth?.userId && Number(deal.userId) === Number(req.auth.userId)
const isMod = req.auth?.role === "MOD" || req.auth?.role === "ADMIN"
if (!isOwner && !isMod) return res.status(404).json({ error: "Deal bulunamadi" })
}
const userId = req.auth?.userId ?? null
const ip = getClientIp(req)
queueDealClick({ dealId, userId, ip }).catch((err) =>
console.error("Deal click queue failed:", err?.message || err)
)
res.json({ url: deal.url ?? null })
} catch (err) {
console.error(err)
res.status(500).json({ error: "Sunucu hatasi" })
}
})
// Report deal (auth required)
router.post("/:id/report", requireAuth, async (req, res) => {
try {
const id = Number(req.params.id)
const reason = req.body?.reason
const note = req.body?.note
const result = await dealReportService.createDealReport({
dealId: id,
userId: req.auth.userId,
reason,
note,
})
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.DEAL.REPORT_CREATE,
buildAuditMeta({
entityType: "DEAL",
entityId: id,
extra: { reason: reason ?? null },
})
)
res.json(result)
} catch (err) {
console.error(err)
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Report basarisiz" })
}
})
// Saved deals (auth required)
router.post("/:id/save", requireAuth, async (req, res) => {
try {
const id = Number(req.params.id)
const result = await dealSaveService.saveDealForUser({
userId: req.auth.userId,
dealId: id,
})
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.DEAL.SAVE,
buildAuditMeta({
entityType: "DEAL",
entityId: id,
})
)
res.json(result)
} catch (err) {
console.error(err)
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Kaydetme basarisiz" })
}
})
router.delete("/:id/save", requireAuth, async (req, res) => {
try {
const id = Number(req.params.id)
await dealSaveService.removeSavedDealForUser({
userId: req.auth.userId,
dealId: id,
})
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.DEAL.UNSAVE,
buildAuditMeta({
entityType: "DEAL",
entityId: id,
})
)
res.sendStatus(200)
} catch (err) {
console.error(err)
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Silme basarisiz" })
}
})
router.get("/saved", requireAuth, async (req, res) => {
try {
const page = parsePage(req.query.page)
const payload = await dealSaveService.listSavedDeals({
userId: req.auth.userId,
page,
})
const response = deals.dealsListResponseSchema.parse(
mapPaginatedDealsToDealCardResponse(payload)
)
res.json(response)
} catch (err) {
console.error(err)
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Kaydedilenler alinamadi" })
}
})
// Best deals widget: hot day/week/year top 5
router.get("/widgets/best", requireApiKey, optionalAuth, async (req, res) => {
try {
const viewer = buildViewer(req)
const limitRaw = Number(req.query.limit ?? 5)
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(20, limitRaw)) : 5
const payload = await getBestWidgetDeals({ viewer, limit })
const hotDay = payload.hotDay.map(mapDealToDealCardResponse)
const hotWeek = payload.hotWeek.map(mapDealToDealCardResponse)
const hotMonth = payload.hotMonth.map(mapDealToDealCardResponse)
const dealIds = [...payload.hotDay, ...payload.hotWeek, ...payload.hotMonth].map((d) => d.id)
if (dealIds.length) {
queueDealImpressions({
dealIds,
userId: req.auth?.userId ?? null,
ip: getClientIp(req),
}).catch((err) => console.error("Deal impression queue failed:", err?.message || err))
}
res.json({ hotDay, hotWeek, hotMonth })
} catch (err) {
console.error(err)
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.get(
"/search",
optionalAuth,
listQueryValidator,
async (req, res) => {
try {
const { q, page, limit, hotListId, trendingListId } = req.validatedDealListQuery
if (!q || !q.trim()) {
return res.json({ results: [], total: 0, totalPages: 0, page })
}
const payload = await getDeals({
preset: "NEW",
q,
page,
limit,
viewer: buildViewer(req),
filters: req.query,
baseWhere: { status: "ACTIVE" },
hotListId,
trendingListId,
useRedisSearch: true,
})
const response = deals.dealsListResponseSchema.parse(
mapPaginatedDealsToDealCardResponse(payload)
)
const dealIds = payload?.results?.map((deal) => deal.id) || []
queueDealImpressions({
dealIds,
userId: req.auth?.userId ?? null,
ip: getClientIp(req),
}).catch((err) => console.error("Deal impression queue failed:", err?.message || err))
res.json(response)
} catch (err) {
console.error(err)
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
// TOP deals (daily/weekly/monthly) - viewer optional
router.get("/top", optionalAuth, async (req, res) => {
try {
const viewer = buildViewer(req)
const range = String(req.query.range || "day")
const limitRaw = Number(req.query.limit ?? 6)
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(20, limitRaw)) : 6
let preset = "HOT_DAY"
if (range === "week") preset = "HOT_WEEK"
else if (range === "month") preset = "HOT_MONTH"
else if (range !== "day") return res.status(400).json({ error: "range invalid" })
const payload = await getDeals({
preset,
q: null,
page: 1,
limit,
viewer,
filters: req.query,
})
const response = deals.dealsListResponseSchema.parse(
mapPaginatedDealsToDealCardResponse(payload)
)
const dealIds = payload?.results?.map((deal) => deal.id) || []
queueDealImpressions({
dealIds,
userId: req.auth?.userId ?? null,
ip: getClientIp(req),
}).catch((err) => console.error("Deal impression queue failed:", err?.message || err))
// frontend DealCard[] bekliyor
res.json(response.results)
} catch (err) {
console.error(err)
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.post(
"/engagement",
requireAuth,
validate(deals.dealEngagementRequestSchema, "body", "validatedEngagement"),
async (req, res) => {
try {
const { ids } = req.validatedEngagement
const viewer = buildViewer(req)
const engagement = await getDealEngagement(ids, viewer)
res.json(deals.dealEngagementResponseSchema.parse(engagement))
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.get(
"/:id",
optionalAuth,
validate(deals.dealDetailRequestSchema, "params", "validatedDealId"),
async (req, res) => {
try {
const { id } = req.validatedDealId
const deal = await getDealById(id, buildViewer(req))
if (!deal) return res.status(404).json({ error: "Deal bulunamadi" })
queueDealView({
dealId: deal.id,
userId: req.auth?.userId ?? null,
ip: getClientIp(req),
}).catch((err) => console.error("Deal view queue failed:", err?.message || err))
const mapped = mapDealToDealDetailResponse(deal)
res.json(deals.dealDetailResponseSchema.parse(mapped))
} catch (err) {
console.error(err)
res.status(500).json({ error: "Sunucu hatasi" })
}
}
)
// Create deal (auth required)
router.post(
"/",
requireAuth,
requireNotRestricted({ checkSuspend: true }),
upload.array("images", 5),
validate(deals.dealCreateRequestSchema, "body", "validatedDealPayload"),
async (req, res) => {
try {
const userId = req.auth.userId
const dealCreateData = mapCreateDealRequestToDealCreateData(
req.validatedDealPayload,
userId
)
const deal = await createDeal(dealCreateData, req.files || [])
const mapped = mapDealToDealDetailResponse(deal)
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.DEAL.CREATE,
buildAuditMeta({
entityType: "DEAL",
entityId: deal.id,
after: { title: deal.title },
})
)
res.json(deals.dealCreateResponseSchema.parse(mapped))
} catch (err) {
console.error(err)
res.status(500).json({ error: "Sunucu hatasi" })
}
}
)
module.exports = router