diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 0c387164..37d16874 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -49,7 +49,6 @@ export const app = new Hono() return sessionCors(c, next); }) - .use('*', rateLimiter) .use('*', async (c, next) => { if (isPublicCorsPath(c.req.path)) { c.set('user', null); @@ -88,6 +87,7 @@ export const app = new Hono() await next(); }) + .use('*', rateLimiter) // Routes .get('/', (c) => diff --git a/apps/api/src/middlewares/rate-limiter.ts b/apps/api/src/middlewares/rate-limiter.ts index fba69161..3a27b200 100644 --- a/apps/api/src/middlewares/rate-limiter.ts +++ b/apps/api/src/middlewares/rate-limiter.ts @@ -5,12 +5,23 @@ import { RATE_LIMIT_HEADERS } from '@api/constants/rate-limiter'; -import type { MiddlewareHandler } from 'hono'; +import type { Context, MiddlewareHandler } from 'hono'; import { RedisRateLimiter } from '@api/utils/redis/limiter'; import { userKeyGenerator } from '../utils/redis/key-generators'; import { env } from '@api/config/env'; import { logRedisUnavailableOnce, redis } from '@api/utils/redis/redis'; +/** Dashboard SSR and other internal callers authenticate with PRIVATE_SERVER_KEY. */ +export function isTrustedServerApiKeyRequest(c: Context): boolean { + const expectedKey = env.PRIVATE_SERVER_KEY; + if (!expectedKey) return false; + + const authHeader = c.req.header('Authorization'); + if (!authHeader?.startsWith('Bearer ')) return false; + + return authHeader.slice('Bearer '.length).trim() === expectedKey; +} + export interface RateLimiterOptions { windowMs?: number; maxRequests?: number | ((c: any) => number); @@ -86,7 +97,21 @@ export const createRateLimiter = (options: RateLimiterOptions = {}): MiddlewareH }; }; +function defaultMaxRequests(c: Context): number { + // Org lookup runs on every SSR layout load; keep it out of the tight global bucket. + if (c.req.method === 'GET' && c.req.path === '/organization') return 500; + + return DEFAULT_RATE_LIMITER_OPTIONS.maxRequests; +} + +function shouldSkipRateLimit(c: Context): boolean { + if (isTrustedServerApiKeyRequest(c)) return true; + + return c.req.path === '/api/auth/get-session'; +} + // Default rate limiter middleware with standard configuration export default createRateLimiter({ - maxRequests: (c) => (c.req.path === '/api/auth/get-session' ? 1000 : DEFAULT_RATE_LIMITER_OPTIONS.maxRequests) + maxRequests: defaultMaxRequests, + skip: shouldSkipRateLimit });