HotTRDealsBackend/routes/mod.routes.js
2026-02-07 22:42:02 +00:00

803 lines
25 KiB
JavaScript

const express = require("express")
const router = express.Router()
const requireAuth = require("../middleware/requireAuth")
const requireRole = require("../middleware/requireRole")
const { validate } = require("../middleware/validate.middleware")
const { endpoints } = require("@shared/contracts")
const {
getPendingDeals,
approveDeal,
rejectDeal,
expireDeal,
unexpireDeal,
getDealDetailForMod,
updateDealForMod,
} = require("../services/mod.service")
const { mapPaginatedDealsToDealCardResponse } = require("../adapters/responses/dealCard.adapter")
const { mapDealToDealDetailResponse } = require("../adapters/responses/dealDetail.adapter")
const dealReportService = require("../services/dealReport.service")
const badgeService = require("../services/badge.service")
const { setBadgeInRedis } = require("../services/redis/badgeCache.service")
const { attachTagsToDeal, removeTagsFromDeal, replaceTagsForDeal } = require("../services/tag.service")
const { updateDealInRedis, getOrCacheDealForModeration } = require("../services/redis/dealCache.service")
const { queueDealUpdate } = require("../services/redis/dbSync.service")
const moderationService = require("../services/moderation.service")
const adminService = require("../services/admin.service")
const adminMetricsService = require("../services/adminMetrics.service")
const { deleteCommentAsMod } = require("../services/comment.service")
const { enqueueAuditFromRequest, buildAuditMeta } = require("../services/audit.service")
const { AUDIT_ACTIONS } = require("../services/auditActions")
const { deals, mod } = endpoints
const listQueryValidator = validate(deals.dealsListRequestSchema, "query", "validatedDealListQuery")
const modUpdateValidator = validate(mod.modDealUpdateRequestSchema, "body", "validatedDealUpdate")
const modDealIdValidator = validate(mod.modDealUpdateParamsSchema, "params", "validatedDealId")
const modBadgeCreateValidator = validate(mod.modBadgeCreateRequestSchema, "body", "validatedBadgeCreate")
const modBadgeUpdateParamsValidator = validate(mod.modBadgeUpdateParamsSchema, "params", "validatedBadgeId")
const modBadgeUpdateValidator = validate(mod.modBadgeUpdateRequestSchema, "body", "validatedBadgeUpdate")
const modBadgeAssignValidator = validate(mod.modBadgeAssignRequestSchema, "body", "validatedBadgeAssign")
const modBadgeRemoveValidator = validate(mod.modBadgeRemoveRequestSchema, "body", "validatedBadgeRemove")
const buildViewer = (req) =>
req.auth ? { userId: req.auth.userId, role: req.auth.role } : null
const formatDateAsString = (value) =>
value instanceof Date ? value.toISOString() : value ?? null
function parseTagsFromBody(req, { allowEmpty = false } = {}) {
const tags = Array.isArray(req.body?.tags) ? req.body.tags : []
if (!allowEmpty && !tags.length) {
const err = new Error("Tag listesi gerekli")
err.statusCode = 400
throw err
}
return tags
}
const ALLOWED_DEAL_STATUSES = new Set(["PENDING", "ACTIVE", "REJECTED", "EXPIRED"])
function normalizeDealStatus(value) {
const normalized = String(value || "").trim().toUpperCase()
return ALLOWED_DEAL_STATUSES.has(normalized) ? normalized : null
}
router.get("/deals/pending", requireAuth, requireRole("MOD"), listQueryValidator, async (req, res) => {
try {
const { q, page, limit } = req.validatedDealListQuery
const payload = await getPendingDeals({
page,
limit,
filters: { ...req.query, q },
viewer: buildViewer(req),
})
const response = mapPaginatedDealsToDealCardResponse(payload)
res.json(response.results)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.post(
"/deals/:id/approve",
requireAuth,
requireRole("MOD"),
validate(deals.dealDetailRequestSchema, "params", "validatedDealId"),
async (req, res) => {
try {
const { id } = req.validatedDealId
const updated = await approveDeal(id)
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.DEAL_APPROVE,
buildAuditMeta({
entityType: "DEAL",
entityId: Number(id),
after: { status: updated.status },
})
)
res.json({ id: updated.id, status: updated.status })
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.post(
"/deals/:id/reject",
requireAuth,
requireRole("MOD"),
validate(deals.dealDetailRequestSchema, "params", "validatedDealId"),
async (req, res) => {
try {
const { id } = req.validatedDealId
const updated = await rejectDeal(id)
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.DEAL_REJECT,
buildAuditMeta({
entityType: "DEAL",
entityId: Number(id),
after: { status: updated.status },
})
)
res.json({ id: updated.id, status: updated.status })
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.post(
"/deals/:id/expire",
requireAuth,
requireRole("MOD"),
validate(deals.dealDetailRequestSchema, "params", "validatedDealId"),
async (req, res) => {
try {
const { id } = req.validatedDealId
const updated = await expireDeal(id)
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.DEAL_EXPIRE,
buildAuditMeta({
entityType: "DEAL",
entityId: Number(id),
after: { status: updated.status },
})
)
res.json({ id: updated.id, status: updated.status })
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.post(
"/deals/:id/unexpire",
requireAuth,
requireRole("MOD"),
validate(deals.dealDetailRequestSchema, "params", "validatedDealId"),
async (req, res) => {
try {
const { id } = req.validatedDealId
const updated = await unexpireDeal(id)
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.DEAL_UNEXPIRE,
buildAuditMeta({
entityType: "DEAL",
entityId: Number(id),
after: { status: updated.status },
})
)
res.json({ id: updated.id, status: updated.status })
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.get(
"/deals/:id/detail",
requireAuth,
requireRole("MOD"),
validate(deals.dealDetailRequestSchema, "params", "validatedDealId"),
async (req, res) => {
try {
const { id } = req.validatedDealId
const { deal, aiReview } = await getDealDetailForMod(id, buildViewer(req))
const mapped = mapDealToDealDetailResponse(deal)
const response = {
...mapped,
aiReview: aiReview
? {
dealId: aiReview.dealId,
bestCategoryId: aiReview.bestCategoryId,
categoryBreadcrumb: aiReview.categoryBreadcrumb || [],
needsReview: aiReview.needsReview,
hasIssue: aiReview.hasIssue,
issueType: aiReview.issueType,
issueReason: aiReview.issueReason ?? null,
tags: Array.isArray(aiReview.tags) ? aiReview.tags : [],
createdAt: formatDateAsString(aiReview.createdAt),
}
: null,
}
res.json(response)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.patch(
"/deals/:id",
requireAuth,
requireRole("MOD"),
modDealIdValidator,
modUpdateValidator,
async (req, res) => {
try {
const { id } = req.validatedDealId
const updated = await updateDealForMod(id, req.validatedDealUpdate, buildViewer(req))
const mapped = mapDealToDealDetailResponse(updated)
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.DEAL_UPDATE,
buildAuditMeta({
entityType: "DEAL",
entityId: Number(id),
extra: { fields: Object.keys(req.validatedDealUpdate || {}) },
})
)
res.json(mapped)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.post(
"/deals/:id/tags",
requireAuth,
requireRole("MOD"),
validate(deals.dealDetailRequestSchema, "params", "validatedDealId"),
async (req, res) => {
try {
const { id } = req.validatedDealId
const tags = parseTagsFromBody(req)
const result = await attachTagsToDeal(id, tags)
await updateDealInRedis(id, { tags: result.tags }, { updatedAt: new Date() })
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.DEAL_TAG_ADD,
buildAuditMeta({
entityType: "DEAL",
entityId: Number(id),
after: { tags: result.tags },
})
)
res.json({ tags: result.tags })
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.delete(
"/deals/:id/tags",
requireAuth,
requireRole("MOD"),
validate(deals.dealDetailRequestSchema, "params", "validatedDealId"),
async (req, res) => {
try {
const { id } = req.validatedDealId
const tags = parseTagsFromBody(req)
const result = await removeTagsFromDeal(id, tags)
await updateDealInRedis(id, { tags: result.tags }, { updatedAt: new Date() })
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.DEAL_TAG_REMOVE,
buildAuditMeta({
entityType: "DEAL",
entityId: Number(id),
after: { tags: result.tags },
})
)
res.json({ tags: result.tags })
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.put(
"/deals/:id/tags",
requireAuth,
requireRole("MOD"),
validate(deals.dealDetailRequestSchema, "params", "validatedDealId"),
async (req, res) => {
try {
const { id } = req.validatedDealId
const tags = parseTagsFromBody(req, { allowEmpty: true })
const result = await replaceTagsForDeal(id, tags)
await updateDealInRedis(id, { tags: result.tags }, { updatedAt: new Date() })
res.json({ tags: result.tags })
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.delete(
"/comments/:id",
requireAuth,
requireRole("MOD"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedCommentId"),
async (req, res) => {
try {
const { id } = req.validatedCommentId
const result = await deleteCommentAsMod(id)
res.json(result)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.post(
"/users/:id/mute",
requireAuth,
requireRole("MOD"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedUserId"),
async (req, res) => {
try {
const { id } = req.validatedUserId
const durationDays = Number(req.body?.durationDays || 7)
const result = await moderationService.muteUser(id, { durationDays })
res.json({
userId: result.id,
mutedUntil: result.mutedUntil ? new Date(result.mutedUntil).toISOString() : null,
})
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.delete(
"/users/:id/mute",
requireAuth,
requireRole("MOD"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedUserId"),
async (req, res) => {
try {
const { id } = req.validatedUserId
const result = await moderationService.clearMute(id)
res.json({ userId: result.id, mutedUntil: null })
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.post(
"/users/:id/notes",
requireAuth,
requireRole("MOD"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedUserId"),
async (req, res) => {
try {
const { id } = req.validatedUserId
const note = String(req.body?.note || "").trim()
const result = await moderationService.addUserNote({
userId: id,
createdById: req.auth.userId,
note,
})
res.json(result)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.get(
"/users/:id/notes",
requireAuth,
requireRole("MOD"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedUserId"),
async (req, res) => {
try {
const { id } = req.validatedUserId
const page = Number(req.query.page || 1)
const limit = Number(req.query.limit || 20)
const result = await moderationService.listUserNotes({ userId: id, page, limit })
res.json(result)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.patch(
"/deals/reports/:id",
requireAuth,
requireRole("MOD"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedReportId"),
async (req, res) => {
try {
const { id } = req.validatedReportId
const status = req.body?.status
const result = await dealReportService.updateDealReportStatus({
reportId: id,
status,
})
res.json(result)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.post(
"/users/:id/disable",
requireAuth,
requireRole("ADMIN"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedUserId"),
async (req, res) => {
try {
const { id } = req.validatedUserId
const result = await moderationService.disableUser(id)
res.json({
userId: result.id,
disabledAt: result.disabledAt ? new Date(result.disabledAt).toISOString() : null,
})
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.delete(
"/users/:id/disable",
requireAuth,
requireRole("ADMIN"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedUserId"),
async (req, res) => {
try {
const { id } = req.validatedUserId
const result = await moderationService.enableUser(id)
res.json({ userId: result.id, disabledAt: null })
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.patch(
"/users/:id/role",
requireAuth,
requireRole("ADMIN"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedUserId"),
async (req, res) => {
try {
const { id } = req.validatedUserId
const role = req.body?.role
if (String(role || "").toUpperCase() === "ADMIN") {
return res.status(400).json({ error: "ADMIN rolü verilemez" })
}
const result = await moderationService.updateUserRole(id, role)
res.json({ userId: result.id, role: result.role })
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.post("/categories", requireAuth, requireRole("ADMIN"), async (req, res) => {
try {
const category = await adminService.createCategory(req.body || {})
res.json(category)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.patch(
"/categories/:id",
requireAuth,
requireRole("ADMIN"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedCategoryId"),
async (req, res) => {
try {
const { id } = req.validatedCategoryId
const category = await adminService.updateCategory(id, req.body || {})
res.json(category)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.post("/sellers", requireAuth, requireRole("ADMIN"), async (req, res) => {
try {
if (req.body?.id) {
const seller = await adminService.updateSeller(req.body.id, req.body || {}, {
createdById: req.auth.userId,
})
return res.json(seller)
}
const seller = await adminService.createSeller(req.body || {}, {
createdById: req.auth.userId,
})
res.json(seller)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.patch(
"/sellers/:id",
requireAuth,
requireRole("ADMIN"),
validate(mod.modDealUpdateParamsSchema, "params", "validatedSellerId"),
async (req, res) => {
try {
const { id } = req.validatedSellerId
const seller = await adminService.updateSeller(id, req.body || {}, { createdById: req.auth.userId })
res.json(seller)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.get("/admin/categories", requireAuth, requireRole("ADMIN"), async (req, res) => {
try {
const categories = await adminService.listCategoriesCached()
res.json(categories)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.get("/admin/sellers", requireAuth, requireRole("ADMIN"), async (req, res) => {
try {
const sellers = await adminService.listSellersCached()
res.json(sellers)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.get("/admin/metrics", requireAuth, requireRole("ADMIN"), async (req, res) => {
try {
const metrics = await adminMetricsService.getAdminMetrics()
res.json(metrics)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.patch(
"/deals/:id/override",
requireAuth,
requireRole("ADMIN"),
validate(deals.dealDetailRequestSchema, "params", "validatedDealId"),
async (req, res) => {
try {
const { id } = req.validatedDealId
const status = req.body?.status
const userId = req.body?.userId
const normalizedStatus = status !== undefined ? normalizeDealStatus(status) : null
if (status !== undefined && !normalizedStatus) {
return res.status(400).json({ error: "Gecersiz status" })
}
const normalizedUserId =
userId !== undefined && userId !== null ? Number(userId) : undefined
if (normalizedUserId !== undefined) {
if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
return res.status(400).json({ error: "Gecersiz userId" })
}
}
const { deal } = await getOrCacheDealForModeration(id)
if (!deal) return res.status(404).json({ error: "Deal bulunamadi" })
const patch = {}
if (status !== undefined) patch.status = normalizedStatus
if (normalizedUserId !== undefined) patch.userId = normalizedUserId
if (!Object.keys(patch).length) {
return res.status(400).json({ error: "Guncellenecek alan yok" })
}
const updatedAt = new Date()
const updated = await updateDealInRedis(id, patch, { updatedAt })
queueDealUpdate({
dealId: Number(id),
data: patch,
updatedAt: updatedAt.toISOString(),
}).catch((err) => console.error("DB sync deal override failed:", err?.message || err))
res.json({
id: Number(id),
status: updated?.status ?? patch.status ?? deal.status,
userId: updated?.userId ?? patch.userId ?? deal.userId,
})
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.get("/sellers", requireAuth, requireRole("MOD"), async (req, res) => {
try {
const sellers = await adminService.listSellersCached()
const payload = sellers.map((seller) => ({
id: seller.id,
name: seller.name,
url: seller.url ?? "",
sellerLogo: seller.sellerLogo ?? "",
isActive: seller.isActive ?? true,
}))
res.json(mod.modSellerListResponseSchema.parse(payload))
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.get("/categories", requireAuth, requireRole("MOD"), async (req, res) => {
try {
const categories = await adminService.listCategoriesCached()
const payload = categories.map((category) => ({
id: category.id,
name: category.name,
parentId: category.parentId ?? null,
}))
res.json(mod.modCategoryListResponseSchema.parse(payload))
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.get("/deals/reports", requireAuth, requireRole("MOD"), async (req, res) => {
try {
const payload = await dealReportService.listDealReports({
})
res.json(payload)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.get("/deals/reports/pending", requireAuth, requireRole("MOD"), async (req, res) => {
try {
const page = Number(req.query.page || 1)
const payload = await dealReportService.getPendingReports({ page })
res.json(payload)
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.post("/badges", requireAuth, requireRole("MOD"), modBadgeCreateValidator, async (req, res) => {
try {
const badge = await badgeService.createBadge(req.validatedBadgeCreate)
await setBadgeInRedis(badge)
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.BADGE_CREATE,
buildAuditMeta({
entityType: "BADGE",
entityId: badge.id,
after: { name: badge.name },
})
)
res.json(mod.modBadgeResponseSchema.parse(badge))
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
})
router.patch(
"/badges/:id",
requireAuth,
requireRole("MOD"),
modBadgeUpdateParamsValidator,
modBadgeUpdateValidator,
async (req, res) => {
try {
const { id } = req.validatedBadgeId
const badge = await badgeService.updateBadge(id, req.validatedBadgeUpdate)
await setBadgeInRedis(badge)
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.BADGE_UPDATE,
buildAuditMeta({
entityType: "BADGE",
entityId: badge.id,
extra: { fields: Object.keys(req.validatedBadgeUpdate || {}) },
})
)
res.json(mod.modBadgeResponseSchema.parse(badge))
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.post(
"/badges/assign",
requireAuth,
requireRole("MOD"),
modBadgeAssignValidator,
async (req, res) => {
try {
const assigned = await badgeService.assignBadgeToUser(req.validatedBadgeAssign)
const response = {
userId: assigned.userId,
badgeId: assigned.badgeId,
earnedAt: assigned.earnedAt instanceof Date ? assigned.earnedAt.toISOString() : assigned.earnedAt,
}
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.BADGE_ASSIGN,
buildAuditMeta({
entityType: "USER",
entityId: response.userId,
extra: { badgeId: response.badgeId, earnedAt: response.earnedAt },
})
)
res.json(mod.modBadgeAssignResponseSchema.parse(response))
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
router.delete(
"/badges/assign",
requireAuth,
requireRole("MOD"),
modBadgeRemoveValidator,
async (req, res) => {
try {
await badgeService.removeBadgeFromUser(req.validatedBadgeRemove)
enqueueAuditFromRequest(
req,
AUDIT_ACTIONS.MOD.BADGE_REMOVE,
buildAuditMeta({
entityType: "USER",
entityId: req.validatedBadgeRemove.userId,
extra: { badgeId: req.validatedBadgeRemove.badgeId },
})
)
res.json(mod.modBadgeRemoveResponseSchema.parse({ removed: true }))
} catch (err) {
const status = err.statusCode || 500
res.status(status).json({ error: err.message || "Sunucu hatasi" })
}
}
)
module.exports = router