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 page = Number(req.query.page || 1) const status = req.query.status const dealId = req.query.dealId const userId = req.query.userId const payload = await dealReportService.listDealReports({ page, status, dealId, userId, }) 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