import { Router } from 'express' import { requireAdminAuth, requireAdminRole, requireFreshAdmin2FA } from '../../middleware/requireAdminAuth' import { parseBody, parseQuery, parseParams } from '../../http/validate' import { ok, created } from '../../http/respond' import { setSessionCookie, clearSessionCookie } from '../../security/sessionCookies' import * as service from './admin.service' import * as subService from '../subscriptions/subscription.service' import * as menuService from '../menu/menu.service' import { presentAdminUser } from './admin.presenter' import { loginSchema, forgotPasswordSchema, resetPasswordSchema, totpVerifySchema, companiesQuerySchema, rentersQuerySchema, auditLogQuerySchema, billingQuerySchema, notificationsQuerySchema, invoicesQuerySchema, adminCompanyUpdateSchema, companyStatusSchema, createAdminSchema, adminRoleSchema, adminPermissionsSchema, updateAdminSchema, homepageUpdateSchema, idParamSchema, companyIdParamSchema, billingAccountIdParamSchema, invoiceIdParamSchema, pricingUpdateSchema, planFeatureCreateSchema, planFeatureUpdateSchema, planFeatureIdParamSchema, promotionCreateSchema, promotionUpdateSchema, promotionIdParamSchema, billingAccountUpdateSchema, createBillingInvoiceSchema, payBillingInvoiceSchema, retryBillingInvoiceSchema, billingReasonSchema, billingCreditNoteSchema, billingRefundSchema, menuItemSchema, menuItemStatusSchema, menuPlanAssignmentsSchema, menuCompanyAssignmentsSchema, menuPreviewSchema, menuAuditLogQuerySchema, menuPlanParamSchema, menuCompanyParamSchema, } from './admin.schemas' import { z } from 'zod' const adminSubOverrideSchema = z.object({ reason: z.string().min(1).max(500), }) const adminExtendTrialSchema = z.object({ extraDays: z.number().int().positive(), reason: z.string().min(1).max(500), }) const adminImpersonationSchema = z.object({ reason: z.string().min(5).max(500), durationMinutes: z.number().int().min(1).max(30).default(15), }) const subIdParamSchema = z.object({ subscriptionId: z.string() }) const router = Router() // ─── Auth (public) ───────────────────────────────────────────── router.post('/auth/login', async (req, res, next) => { try { const { email, password, totpCode, recoveryCode } = parseBody(loginSchema, req) const result = await service.login(email, password, totpCode, recoveryCode) if (!result) return res.status(401).json({ error: 'invalid_credentials', message: 'Invalid email or password', statusCode: 401 }) if ('totpRequired' in result) return res.status(401).json({ error: 'totp_required', message: '2FA code required', statusCode: 401 }) if ('invalidTotp' in result) return res.status(401).json({ error: 'invalid_totp', message: 'Invalid 2FA code', statusCode: 401 }) setSessionCookie(res, 'admin', result.token, 8 * 60 * 60 * 1000) ok(res, result) } catch (err) { next(err) } }) router.post('/auth/logout', (_req, res) => { clearSessionCookie(res, 'admin') ok(res, { success: true }) }) router.post('/auth/forgot-password', async (req, res, next) => { try { const { email } = parseBody(forgotPasswordSchema, req) await service.forgotPassword(email) ok(res, { message: 'If that email is registered, a reset link has been sent.' }) } catch (err) { next(err) } }) router.post('/auth/reset-password', async (req, res, next) => { try { const { token, password } = parseBody(resetPasswordSchema, req) const ok2 = await service.resetPassword(token, password) if (!ok2) return res.status(400).json({ error: 'invalid_token', message: 'Reset link is invalid or has expired.', statusCode: 400 }) ok(res, { message: 'Password updated successfully. You can now sign in.' }) } catch (err) { next(err) } }) // ─── Auth (protected) ────────────────────────────────────────── router.get('/auth/me', requireAdminAuth, (req, res) => { ok(res, presentAdminUser(req.admin as any)) }) router.post('/auth/2fa/setup', requireAdminAuth, async (req, res, next) => { try { ok(res, await service.setupTotp(req.admin.id, req.admin.email)) } catch (err) { next(err) } }) router.post('/auth/2fa/verify', requireAdminAuth, async (req, res, next) => { try { const { code } = parseBody(totpVerifySchema, req) const result = await service.verifyTotp(req.admin.id, code) if (!result) return res.status(400).json({ error: 'invalid_code', message: 'Invalid 2FA code', statusCode: 400 }) setSessionCookie(res, 'admin', result.token, 8 * 60 * 60 * 1000) ok(res, { success: true, admin: result.admin, recoveryCodes: result.recoveryCodes }) } catch (err) { next(err) } }) router.post('/auth/2fa/recovery-codes/regenerate', requireAdminAuth, requireFreshAdmin2FA, async (req, res, next) => { try { ok(res, await service.regenerateRecoveryCodes(req.admin.id)) } catch (err) { next(err) } }) // ─── Companies ───────────────────────────────────────────────── router.get('/companies', requireAdminAuth, async (req, res, next) => { try { ok(res, await service.listCompanies(parseQuery(companiesQuerySchema, req))) } catch (err) { next(err) } }) router.get('/companies/:id', requireAdminAuth, async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) ok(res, await service.getCompany(id)) } catch (err) { next(err) } }) router.patch('/companies/:id', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) const body = parseBody(adminCompanyUpdateSchema, req) ok(res, await service.updateCompany(id, body, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.patch('/companies/:id/status', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) const { status, reason } = parseBody(companyStatusSchema, req) ok(res, await service.setCompanyStatus(id, status, reason, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.delete('/companies/:id', requireAdminAuth, requireAdminRole('ADMIN'), requireFreshAdmin2FA, async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) await service.deleteCompany(id, req.admin.id, req.ip) ok(res, { success: true }) } catch (err) { next(err) } }) router.post('/companies/:id/impersonate', requireAdminAuth, requireAdminRole('SUPER_ADMIN'), requireFreshAdmin2FA, async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) const { reason, durationMinutes } = parseBody(adminImpersonationSchema, req) ok(res, await service.impersonateCompany(id, req.admin.id, req.ip, reason, durationMinutes)) } catch (err) { next(err) } }) // ─── Menu management ────────────────────────────────────────── router.get('/menu-items', requireAdminAuth, requireAdminRole('SUPPORT'), async (_req, res, next) => { try { ok(res, await menuService.listMenuItems()) } catch (err) { next(err) } }) router.post('/menu-items', requireAdminAuth, requireAdminRole('ADMIN'), async (req, res, next) => { try { created(res, { data: await menuService.createMenuItem(parseBody(menuItemSchema, req), req.admin) }) } catch (err) { next(err) } }) router.get('/menu-items/:id', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) ok(res, await menuService.getMenuItem(id)) } catch (err) { next(err) } }) router.put('/menu-items/:id', requireAdminAuth, requireAdminRole('ADMIN'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) ok(res, await menuService.updateMenuItem(id, parseBody(menuItemSchema, req), req.admin)) } catch (err) { next(err) } }) router.patch('/menu-items/:id/status', requireAdminAuth, requireAdminRole('ADMIN'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) const { isActive } = parseBody(menuItemStatusSchema, req) ok(res, await menuService.setMenuItemStatus(id, isActive, req.admin)) } catch (err) { next(err) } }) router.delete('/menu-items/:id', requireAdminAuth, requireAdminRole('ADMIN'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) await menuService.deleteMenuItem(id, req.admin) ok(res, { success: true }) } catch (err) { next(err) } }) router.post('/menu-items/:id/subscriptions', requireAdminAuth, requireAdminRole('ADMIN'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) const { assignments } = parseBody(menuPlanAssignmentsSchema, req) ok(res, await menuService.assignMenuItemToPlans(id, assignments, req.admin)) } catch (err) { next(err) } }) router.delete('/menu-items/:id/subscriptions/:plan', requireAdminAuth, requireAdminRole('ADMIN'), async (req, res, next) => { try { const { id, plan } = parseParams(menuPlanParamSchema, req) ok(res, await menuService.removeMenuItemFromPlan(id, plan, req.admin)) } catch (err) { next(err) } }) router.post('/menu-items/:id/companies', requireAdminAuth, requireAdminRole('ADMIN'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) const { assignments } = parseBody(menuCompanyAssignmentsSchema, req) ok(res, await menuService.assignMenuItemToCompanies(id, assignments, req.admin)) } catch (err) { next(err) } }) router.delete('/menu-items/:id/companies/:companyId', requireAdminAuth, requireAdminRole('ADMIN'), async (req, res, next) => { try { const { id, companyId } = parseParams(menuCompanyParamSchema, req) ok(res, await menuService.removeMenuItemFromCompany(id, companyId, req.admin)) } catch (err) { next(err) } }) router.post('/menu-preview', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => { try { ok(res, await menuService.previewCompanyMenu(parseBody(menuPreviewSchema, req))) } catch (err) { next(err) } }) router.get('/menu-audit-logs', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => { try { ok(res, await menuService.listMenuAuditLogs(parseQuery(menuAuditLogQuerySchema, req))) } catch (err) { next(err) } }) // ─── Renters ─────────────────────────────────────────────────── router.get('/renters', requireAdminAuth, async (req, res, next) => { try { ok(res, await service.listRenters(parseQuery(rentersQuerySchema, req))) } catch (err) { next(err) } }) router.post('/renters/:id/block', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) await service.setRenterActive(id, false) ok(res, { success: true }) } catch (err) { next(err) } }) router.post('/renters/:id/unblock', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) await service.setRenterActive(id, true) ok(res, { success: true }) } catch (err) { next(err) } }) // ─── Metrics ─────────────────────────────────────────────────── router.get('/metrics', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { ok(res, await service.getPlatformMetrics()) } catch (err) { next(err) } }) // ─── Notifications ───────────────────────────────────────────── router.get('/notifications', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => { try { ok(res, await service.listNotifications(parseQuery(notificationsQuerySchema, req))) } catch (err) { next(err) } }) // ─── Audit logs ──────────────────────────────────────────────── router.get('/audit-logs', requireAdminAuth, requireAdminRole('ADMIN'), async (req, res, next) => { try { ok(res, await service.getAuditLogs(parseQuery(auditLogQuerySchema, req))) } catch (err) { next(err) } }) // ─── Admin users ─────────────────────────────────────────────── router.get('/admins', requireAdminAuth, requireAdminRole('SUPER_ADMIN'), async (req, res, next) => { try { ok(res, await service.listAdmins()) } catch (err) { next(err) } }) router.post('/admins', requireAdminAuth, requireAdminRole('SUPER_ADMIN'), requireFreshAdmin2FA, async (req, res, next) => { try { created(res, { data: await service.createAdmin(parseBody(createAdminSchema, req)) }) } catch (err) { next(err) } }) router.patch('/admins/:id', requireAdminAuth, requireAdminRole('SUPER_ADMIN'), requireFreshAdmin2FA, async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) ok(res, await service.updateAdmin(id, parseBody(updateAdminSchema, req))) } catch (err) { next(err) } }) router.patch('/admins/:id/role', requireAdminAuth, requireAdminRole('SUPER_ADMIN'), requireFreshAdmin2FA, async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) const { role } = parseBody(adminRoleSchema, req) await service.updateAdminRole(id, role) ok(res, { success: true }) } catch (err) { next(err) } }) router.patch('/admins/:id/permissions', requireAdminAuth, requireAdminRole('SUPER_ADMIN'), requireFreshAdmin2FA, async (req, res, next) => { try { const { id } = parseParams(idParamSchema, req) const { permissions } = parseBody(adminPermissionsSchema, req) ok(res, await service.updateAdminPermissions(id, permissions)) } catch (err) { next(err) } }) // ─── Billing ─────────────────────────────────────────────────── router.get('/billing', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { ok(res, await service.getBilling(parseQuery(billingQuerySchema, req))) } catch (err) { next(err) } }) router.get('/billing/invoices/:invoiceId/pdf', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { invoiceId } = parseParams(invoiceIdParamSchema, req) const { pdfBuffer, invoiceNumber } = await service.getInvoicePdf(invoiceId) res.setHeader('Content-Type', 'application/pdf') res.setHeader('Content-Disposition', `attachment; filename="${invoiceNumber}.pdf"`) res.setHeader('Content-Length', pdfBuffer.length) res.end(pdfBuffer) } catch (err) { next(err) } }) router.get('/billing/:companyId', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { companyId } = parseParams(companyIdParamSchema, req) ok(res, await service.getBillingAccountDetail(companyId)) } catch (err) { next(err) } }) router.get('/billing/:companyId/invoices', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { companyId } = parseParams(companyIdParamSchema, req) ok(res, await service.getCompanyInvoices(companyId, parseQuery(invoicesQuerySchema, req))) } catch (err) { next(err) } }) router.patch('/billing/accounts/:billingAccountId', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { billingAccountId } = parseParams(billingAccountIdParamSchema, req) ok(res, await service.updateBillingAccount(billingAccountId, parseBody(billingAccountUpdateSchema, req), req.admin.id, req.ip)) } catch (err) { next(err) } }) router.post('/billing/accounts/:billingAccountId/pause-dunning', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { billingAccountId } = parseParams(billingAccountIdParamSchema, req) ok(res, await service.setDunningPaused(billingAccountId, true, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.post('/billing/accounts/:billingAccountId/resume-dunning', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { billingAccountId } = parseParams(billingAccountIdParamSchema, req) ok(res, await service.setDunningPaused(billingAccountId, false, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.post('/billing/accounts/:billingAccountId/invoices', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { billingAccountId } = parseParams(billingAccountIdParamSchema, req) created(res, await service.createBillingInvoice(billingAccountId, parseBody(createBillingInvoiceSchema, req), req.admin.id, req.ip)) } catch (err) { next(err) } }) router.post('/billing/invoices/:invoiceId/finalize', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { invoiceId } = parseParams(invoiceIdParamSchema, req) ok(res, await service.finalizeBillingInvoice(invoiceId, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.post('/billing/invoices/:invoiceId/pay', requireAdminAuth, requireAdminRole('FINANCE'), requireFreshAdmin2FA, async (req, res, next) => { try { const { invoiceId } = parseParams(invoiceIdParamSchema, req) ok(res, await service.payBillingInvoice(invoiceId, parseBody(payBillingInvoiceSchema, req), req.admin.id, req.ip)) } catch (err) { next(err) } }) router.post('/billing/invoices/:invoiceId/retry-payment', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { invoiceId } = parseParams(invoiceIdParamSchema, req) ok(res, await service.retryBillingInvoicePayment(invoiceId, parseBody(retryBillingInvoiceSchema, req), req.admin.id, req.ip)) } catch (err) { next(err) } }) router.post('/billing/invoices/:invoiceId/void', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { invoiceId } = parseParams(invoiceIdParamSchema, req) const { reason } = parseBody(billingReasonSchema, req) ok(res, await service.voidBillingInvoice(invoiceId, reason, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.post('/billing/invoices/:invoiceId/uncollectible', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { invoiceId } = parseParams(invoiceIdParamSchema, req) const { reason } = parseBody(billingReasonSchema, req) ok(res, await service.markBillingInvoiceUncollectible(invoiceId, reason, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.post('/billing/invoices/:invoiceId/credit-notes', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { const { invoiceId } = parseParams(invoiceIdParamSchema, req) ok(res, await service.issueBillingCreditNote(invoiceId, parseBody(billingCreditNoteSchema, req), req.admin.id, req.ip)) } catch (err) { next(err) } }) router.post('/billing/invoices/:invoiceId/refunds', requireAdminAuth, requireAdminRole('FINANCE'), requireFreshAdmin2FA, async (req, res, next) => { try { const { invoiceId } = parseParams(invoiceIdParamSchema, req) ok(res, await service.issueBillingRefund(invoiceId, parseBody(billingRefundSchema, req), req.admin.id, req.ip)) } catch (err) { next(err) } }) // ─── Pricing config ──────────────────────────────────────────── router.get('/pricing', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { ok(res, await service.getPricingConfigs()) } catch (err) { next(err) } }) router.patch('/pricing', requireAdminAuth, requireAdminRole('FINANCE'), requireFreshAdmin2FA, async (req, res, next) => { try { const { entries } = parseBody(pricingUpdateSchema, req) ok(res, await service.updatePricingConfigs(entries, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.get('/pricing/features', requireAdminAuth, requireAdminRole('FINANCE'), async (_req, res, next) => { try { ok(res, await service.listPlanFeatures()) } catch (err) { next(err) } }) router.post('/pricing/features', requireAdminAuth, requireAdminRole('FINANCE'), requireFreshAdmin2FA, async (req, res, next) => { try { const data = parseBody(planFeatureCreateSchema, req) created(res, await service.createPlanFeature(data, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.patch('/pricing/features/:featureId', requireAdminAuth, requireAdminRole('FINANCE'), requireFreshAdmin2FA, async (req, res, next) => { try { const { featureId } = parseParams(planFeatureIdParamSchema, req) const data = parseBody(planFeatureUpdateSchema, req) ok(res, await service.updatePlanFeature(featureId, data, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.delete('/pricing/features/:featureId', requireAdminAuth, requireAdminRole('FINANCE'), requireFreshAdmin2FA, async (req, res, next) => { try { const { featureId } = parseParams(planFeatureIdParamSchema, req) await service.deletePlanFeature(featureId, req.admin.id, req.ip) ok(res, { success: true }) } catch (err) { next(err) } }) // ─── Promotions ──────────────────────────────────────────────────────────── router.get('/pricing/promotions', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => { try { ok(res, await service.listPromotions()) } catch (err) { next(err) } }) router.post('/pricing/promotions', requireAdminAuth, requireAdminRole('FINANCE'), requireFreshAdmin2FA, async (req, res, next) => { try { const data = parseBody(promotionCreateSchema, req) created(res, await service.createPromotion(data as any, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.patch('/pricing/promotions/:promotionId', requireAdminAuth, requireAdminRole('FINANCE'), requireFreshAdmin2FA, async (req, res, next) => { try { const { promotionId } = parseParams(promotionIdParamSchema, req) const data = parseBody(promotionUpdateSchema, req) ok(res, await service.updatePromotion(promotionId, data as any, req.admin.id, req.ip)) } catch (err) { next(err) } }) router.delete('/pricing/promotions/:promotionId', requireAdminAuth, requireAdminRole('FINANCE'), requireFreshAdmin2FA, async (req, res, next) => { try { const { promotionId } = parseParams(promotionIdParamSchema, req) await service.deletePromotion(promotionId, req.admin.id, req.ip) ok(res, { success: true }) } catch (err) { next(err) } }) // ─── Subscription admin overrides ───────────────────────────── router.get('/subscriptions/:subscriptionId/events', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => { try { const { subscriptionId } = parseParams(subIdParamSchema, req) ok(res, await subService.getEventsBySubscriptionId(subscriptionId)) } catch (err) { next(err) } }) router.post('/subscriptions/:subscriptionId/extend-trial', requireAdminAuth, requireAdminRole('SUPPORT'), requireFreshAdmin2FA, async (req, res, next) => { try { const { subscriptionId } = parseParams(subIdParamSchema, req) const { extraDays, reason } = parseBody(adminExtendTrialSchema, req) ok(res, await subService.adminExtendTrial(subscriptionId, extraDays, req.admin.id, reason)) } catch (err) { next(err) } }) router.post('/subscriptions/:subscriptionId/extend-grace-period', requireAdminAuth, requireAdminRole('SUPPORT'), requireFreshAdmin2FA, async (req, res, next) => { try { const { subscriptionId } = parseParams(subIdParamSchema, req) const { reason } = parseBody(adminSubOverrideSchema, req) ok(res, await subService.adminExtendGracePeriod(subscriptionId, 7, req.admin.id, reason)) } catch (err) { next(err) } }) router.post('/subscriptions/:subscriptionId/suspend', requireAdminAuth, requireAdminRole('SUPPORT'), requireFreshAdmin2FA, async (req, res, next) => { try { const { subscriptionId } = parseParams(subIdParamSchema, req) const { reason } = parseBody(adminSubOverrideSchema, req) ok(res, await subService.adminSuspendSubscription(subscriptionId, req.admin.id, reason)) } catch (err) { next(err) } }) router.post('/subscriptions/:subscriptionId/reactivate', requireAdminAuth, requireAdminRole('SUPPORT'), requireFreshAdmin2FA, async (req, res, next) => { try { const { subscriptionId } = parseParams(subIdParamSchema, req) const { reason } = parseBody(adminSubOverrideSchema, req) ok(res, await subService.adminReactivateSubscription(subscriptionId, req.admin.id, reason)) } catch (err) { next(err) } }) router.post('/subscriptions/:subscriptionId/cancel', requireAdminAuth, requireAdminRole('SUPPORT'), requireFreshAdmin2FA, async (req, res, next) => { try { const { subscriptionId } = parseParams(subIdParamSchema, req) const { reason } = parseBody(adminSubOverrideSchema, req) ok(res, await subService.adminCancelSubscription(subscriptionId, req.admin.id, reason)) } catch (err) { next(err) } }) // ─── Site config ─────────────────────────────────────────────── router.get('/site-config/marketplace-homepage', requireAdminAuth, async (req, res, next) => { try { ok(res, await service.getMarketplaceHomepage()) } catch (err) { next(err) } }) router.patch('/site-config/marketplace-homepage', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => { try { const { homepage } = parseBody(homepageUpdateSchema, req) ok(res, await service.updateMarketplaceHomepage(homepage, req.admin.id, req.ip)) } catch (err) { next(err) } }) export default router