feat(sync): rate limit sync/backfill endpoints to 10 per 15 minutes
This commit is contained in:
parent
7d42d119c0
commit
80b5d56010
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue