Files
accounted/lib/auth/rate-limit-http.ts
T
Mattsson a53a119a2e Fix/vat parent accounts (#438)
* feat(enable-banking): add support for account selection and syncing

- Updated StoredAccount interface to include an 'enabled' flag for account syncing preferences.
- Enhanced ensureFiscalPeriod function to handle overlapping fiscal periods with posted entries and opening balances.
- Added tests for fiscal period validation and account syncing logic.
- Implemented AccountPickerDialog component for user account selection.
- Created API routes for PATCH /accounts and POST /sync to manage account syncing.
- Introduced 'pending_selection' status for bank connections to allow user account selection before syncing.
- Updated database migration to support new connection status and backfill existing accounts with enabled=true.

* feat(enable-banking): implement account selection and consent event logging

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:12:00 +02:00

77 lines
2.4 KiB
TypeScript

import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
import { NextResponse } from 'next/server'
let redis: Redis | null = null
function getRedis(): Redis | null {
if (redis) return redis
const url = process.env.UPSTASH_REDIS_REST_URL
const token = process.env.UPSTASH_REDIS_REST_TOKEN
if (!url || !token) return null
redis = new Redis({ url, token })
return redis
}
const limiters = new Map<string, Ratelimit>()
function getLimiter(prefix: string, maxRequests: number, windowMs: number): Ratelimit | null {
const key = `${prefix}:${maxRequests}:${windowMs}`
const cached = limiters.get(key)
if (cached) return cached
const client = getRedis()
if (!client) return null
const limiter = new Ratelimit({
redis: client,
limiter: Ratelimit.slidingWindow(maxRequests, `${windowMs} ms`),
prefix,
analytics: false,
})
limiters.set(key, limiter)
return limiter
}
export interface RateLimitOptions {
prefix: string
identifier: string
maxRequests: number
windowMs: number
}
export interface RateLimitResult {
ok: boolean
response?: NextResponse
}
/**
* HTTP rate limit check using Upstash Ratelimit (sliding window).
*
* Returns `{ ok: true }` when the request is allowed.
* Returns `{ ok: false, response }` with a 429 NextResponse when blocked.
*
* No-ops (allows the request) when Upstash env vars are not configured —
* intentional so local dev and self-hosted deployments without Redis still work.
* Production hosted deployments must set UPSTASH_REDIS_REST_URL/TOKEN for the
* limit to be enforced; absence is logged once at startup by other call sites.
*/
export async function checkRateLimit(opts: RateLimitOptions): Promise<RateLimitResult> {
const limiter = getLimiter(opts.prefix, opts.maxRequests, opts.windowMs)
if (!limiter) return { ok: true }
const { success, reset, limit, remaining } = await limiter.limit(opts.identifier)
if (success) return { ok: true }
const retryAfterSec = Math.max(1, Math.ceil((reset - Date.now()) / 1000))
const response = NextResponse.json(
{ error: 'För många förfrågningar. Försök igen om en stund.' },
{ status: 429 }
)
response.headers.set('Retry-After', String(retryAfterSec))
response.headers.set('X-RateLimit-Limit', String(limit))
response.headers.set('X-RateLimit-Remaining', String(remaining))
response.headers.set('X-RateLimit-Reset', String(Math.ceil(reset / 1000)))
return { ok: false, response }
}