d7fb7b7a7b
Build & Deploy / Build & Push Docker Image (push) Failing after 47s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Failing after 4m55s
Test / Admin Unit Tests (push) Successful in 9m37s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Successful in 9m54s
576 lines
26 KiB
TypeScript
576 lines
26 KiB
TypeScript
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
|