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
72 lines
2.1 KiB
TypeScript
72 lines
2.1 KiB
TypeScript
import { Router, Request, Response, NextFunction } from 'express';
|
|
import { validate } from '../../middleware/validate';
|
|
import { paymentCheckoutRateLimit } from '../../middleware/rate-limit';
|
|
import { requirePaymentsEnabled } from './payment-settings.service';
|
|
import { donationPagesService } from './donation-pages.service';
|
|
import { donationsService } from './donations.service';
|
|
import { donationPageCheckoutSchema } from './donation-pages.schemas';
|
|
|
|
const router = Router();
|
|
|
|
// GET /api/donation-pages — list active pages (with stats)
|
|
router.get('/', async (_req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const pages = await donationPagesService.findActivePages();
|
|
res.json(pages);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
// GET /api/donation-pages/:slug — get active page by slug
|
|
router.get('/:slug', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const page = await donationPagesService.findBySlugPublic(req.params.slug as string);
|
|
res.json(page);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
// POST /api/donation-pages/:slug/donate — create Stripe checkout for this page
|
|
router.post(
|
|
'/:slug/donate',
|
|
requirePaymentsEnabled,
|
|
paymentCheckoutRateLimit,
|
|
validate(donationPageCheckoutSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const page = await donationPagesService.findBySlugPublic(req.params.slug as string);
|
|
|
|
const { amountCents, email, name, message, isAnonymous } = req.body;
|
|
|
|
// Validate against page-specific minimum
|
|
if (amountCents < page.minimumAmount) {
|
|
res.status(400).json({
|
|
error: {
|
|
message: `Minimum donation is $${(page.minimumAmount / 100).toFixed(2)}`,
|
|
code: 'MINIMUM_NOT_MET',
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const result = await donationsService.createDonationCheckout(
|
|
amountCents,
|
|
email,
|
|
name,
|
|
message,
|
|
isAnonymous,
|
|
page.id,
|
|
page.slug,
|
|
page.title,
|
|
);
|
|
res.json(result);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
},
|
|
);
|
|
|
|
export { router as donationPagesPublicRouter };
|