Addresses 11 original findings (1 critical, 3 high, 4 medium, 3 low) plus 4 additional findings from security review: - Mask secrets in PUT /settings response (was leaking decrypted keys) - Add paymentCheckoutRateLimit (10/hr/IP) to all 5 checkout endpoints - Implement durable audit logging to payment_audit_log table - Pin Stripe API version to 2026-01-28.clover (SDK v20.3.1) - Add charge.dispute.created/closed webhook handlers with DISPUTED status - Restore tickets on dispute won, handle charge_refunded closure - Guard against sentinel passthrough corrupting stored Stripe keys - Wrap refund DB updates in try/catch with webhook reconciliation fallback - Add $transaction for product maxPurchases race condition - Remove dead Payment model lookup from handleChargeRefunded - Cap donation amount at $100k in both schemas - Add requirePaymentsEnabled middleware on all checkout routes - Remove Stripe internal IDs from CSV exports - Add Cache-Control: no-store on admin settings responses Bunker Admin
73 lines
2.3 KiB
TypeScript
73 lines
2.3 KiB
TypeScript
import { Router, Request, Response, NextFunction } from 'express';
|
|
import { campaignsService } from './campaigns.service';
|
|
import { listModerationQueueSchema, moderateCampaignSchema } from './campaigns.schemas';
|
|
import { validate } from '../../../middleware/validate';
|
|
import { authenticate } from '../../../middleware/auth.middleware';
|
|
import { requireRole } from '../../../middleware/rbac.middleware';
|
|
import { INFLUENCE_ROLES } from '../../../utils/roles';
|
|
import { eventBus } from '../../../services/event-bus.service';
|
|
|
|
const router = Router();
|
|
|
|
router.use(authenticate);
|
|
router.use(requireRole(...INFLUENCE_ROLES));
|
|
|
|
// GET /api/campaigns/moderation/queue — list moderation queue
|
|
router.get(
|
|
'/moderation/queue',
|
|
validate(listModerationQueueSchema, 'query'),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const result = await campaignsService.findModerationQueue(req.query as any);
|
|
res.json(result);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
}
|
|
);
|
|
|
|
// GET /api/campaigns/moderation/stats — moderation stats
|
|
router.get(
|
|
'/moderation/stats',
|
|
async (_req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const stats = await campaignsService.getModerationStats();
|
|
res.json(stats);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
}
|
|
);
|
|
|
|
// PATCH /api/campaigns/moderation/:id — moderate a campaign
|
|
router.patch(
|
|
'/moderation/:id',
|
|
validate(moderateCampaignSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const id = req.params.id as string;
|
|
const before = await campaignsService.findById(id);
|
|
const campaign = await campaignsService.moderateCampaign(id, req.body, req.user!);
|
|
eventBus.publish('campaign.status.changed', {
|
|
campaignId: campaign.id,
|
|
title: campaign.title,
|
|
slug: campaign.slug,
|
|
oldStatus: before.moderationStatus ?? 'PENDING',
|
|
newStatus: campaign.moderationStatus ?? 'PENDING',
|
|
});
|
|
if (campaign.status === 'ACTIVE' && before.status !== 'ACTIVE') {
|
|
eventBus.publish('campaign.published', {
|
|
campaignId: campaign.id,
|
|
title: campaign.title,
|
|
slug: campaign.slug,
|
|
});
|
|
}
|
|
res.json(campaign);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
}
|
|
);
|
|
|
|
export { router as campaignModerationRouter };
|