@@ -46,14 +46,34 @@ module.exports.dbExportHandler = async (req, res, next) => {
4646 const today = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
4747 const key = `project:${ projectId } :export_limit:${ today } ` ;
4848
49- const currentCount = await redis . get ( key ) ;
50- if ( currentCount && Number ( currentCount ) >= maxExports ) {
51- return next ( new AppError ( 429 , `Daily export limit reached (${ maxExports } /${ maxExports } ). Please try again tomorrow.` ) ) ;
49+ // Atomic check-and-increment: use Lua script to prevent TOCTOU race where
50+ // two concurrent requests both read the current count, both pass the limit
51+ // check, and both increment, exceeding the daily quota. The script ensures
52+ // only one request at a time sees a sub-limit count.
53+ const luaScript = `
54+ local current = redis.call('GET', KEYS[1])
55+ current = current and tonumber(current) or 0
56+ if current >= tonumber(ARGV[1]) then
57+ return {0, current}
58+ end
59+ local newCount = redis.call('INCR', KEYS[1])
60+ if newCount == 1 then
61+ redis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))
62+ end
63+ return {1, newCount}
64+ ` ;
65+
66+ let result ;
67+ try {
68+ result = await redis . eval ( luaScript , 1 , key , maxExports , 86400 ) ;
69+ } catch ( err ) {
70+ console . error ( "[Dashboard API] Redis Lua script error:" , err ) ;
71+ return next ( new AppError ( 500 , "Failed to check export quota." ) ) ;
5272 }
5373
54- const newCount = await redis . incr ( key ) ;
55- if ( newCount === 1 ) {
56- await redis . expire ( key , 86400 ) ; // Set expiry to 24 hours
74+ const [ allowed , newCount ] = result ;
75+ if ( ! allowed ) {
76+ return next ( new AppError ( 429 , `Daily export limit reached ( ${ maxExports } / ${ maxExports } ). Please try again tomorrow.` ) ) ;
5777 }
5878
5979 await exportQueue . add ( 'export-database' , { projectId, collectionName, userId, email } ) ;
0 commit comments