feat(sync): rate limit sync/backfill endpoints to 10 per 15 minutes

This commit is contained in:
null 2026-06-06 15:08:33 -05:00
parent 7d42d119c0
commit 80b5d56010
2 changed files with 18 additions and 3 deletions

View File

@ -63,6 +63,18 @@ const demoDataLimiter = makeLimiter(
'Too many demo data clear operations. Please try again in 15 minutes.', 'Too many demo data clear operations. Please try again in 15 minutes.',
); );
// 10 sync/backfill requests per 15 minutes per user — prevents SimpleFIN hammering
const syncLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => req.user?.id?.toString() || req.ip,
handler(req, res) {
res.status(429).json({ error: 'Too many sync requests. Please try again in 15 minutes.' });
},
});
// ── Export all limiters plus reset function ──────────────────────────────────── // ── Export all limiters plus reset function ────────────────────────────────────
const allLimiters = [ const allLimiters = [
loginLimiter, loginLimiter,
@ -73,6 +85,7 @@ const allLimiters = [
oidcLimiter, oidcLimiter,
backupOperationLimiter, backupOperationLimiter,
demoDataLimiter, demoDataLimiter,
syncLimiter,
]; ];
function resetStores() { function resetStores() {
@ -92,5 +105,6 @@ module.exports = {
oidcLimiter, oidcLimiter,
backupOperationLimiter, backupOperationLimiter,
demoDataLimiter, demoDataLimiter,
syncLimiter,
resetStores, resetStores,
}; };

View File

@ -7,6 +7,7 @@ const { decorateDataSource, ensureManualDataSource } = require('../services/tran
const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService'); const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService');
const { sanitizeErrorMessage } = require('../services/simplefinService'); const { sanitizeErrorMessage } = require('../services/simplefinService');
const { getBankSyncConfig } = require('../services/bankSyncConfigService'); const { getBankSyncConfig } = require('../services/bankSyncConfigService');
const { syncLimiter } = require('../middleware/rateLimiter');
const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']); const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']);
const VALID_STATUSES = new Set(['active', 'inactive', 'error']); const VALID_STATUSES = new Set(['active', 'inactive', 'error']);
@ -207,7 +208,7 @@ router.put('/:sourceId/accounts/:accountId', (req, res) => {
// ─── POST /api/data-sources/:id/sync ───────────────────────────────────────── // ─── POST /api/data-sources/:id/sync ─────────────────────────────────────────
router.post('/:id/sync', async (req, res) => { router.post('/:id/sync', syncLimiter, async (req, res) => {
if (!getBankSyncConfig().enabled) { if (!getBankSyncConfig().enabled) {
return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED'));
} }
@ -230,7 +231,7 @@ router.post('/:id/sync', async (req, res) => {
// ─── POST /api/data-sources/sync-all ───────────────────────────────────────── // ─── POST /api/data-sources/sync-all ─────────────────────────────────────────
// Syncs every SimpleFIN source for the current user. Returns aggregated stats. // Syncs every SimpleFIN source for the current user. Returns aggregated stats.
router.post('/sync-all', async (req, res) => { router.post('/sync-all', syncLimiter, async (req, res) => {
if (!getBankSyncConfig().enabled) { if (!getBankSyncConfig().enabled) {
return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED'));
} }
@ -284,7 +285,7 @@ router.post('/sync-all', async (req, res) => {
// ─── POST /api/data-sources/:id/backfill ───────────────────────────────────── // ─── POST /api/data-sources/:id/backfill ─────────────────────────────────────
router.post('/:id/backfill', async (req, res) => { router.post('/:id/backfill', syncLimiter, async (req, res) => {
if (!getBankSyncConfig().enabled) { if (!getBankSyncConfig().enabled) {
return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED'));
} }