11import { z } from "zod" ;
2- import { prisma } from "~/db.server" ;
3- import { env } from "~/env.server" ;
42import {
53 RateLimitTokenBucketConfig ,
64 RateLimiterConfig ,
75} from "~/services/authorizationRateLimitMiddleware.server" ;
8- import { logger } from "~/services/logger.server" ;
9- import { type Duration } from "~/services/rateLimiter.server" ;
106import {
117 parseDurationToMs ,
12- RATE_LIMIT_INTENT ,
138 type EffectiveRateLimit ,
149} from "./RateLimitSection" ;
1510
16- function systemDefaultRateLimit ( ) {
17- return {
18- type : "tokenBucket" as const ,
19- refillRate : env . API_RATE_LIMIT_REFILL_RATE ,
20- interval : env . API_RATE_LIMIT_REFILL_INTERVAL as Duration ,
21- maxTokens : env . API_RATE_LIMIT_MAX ,
22- } ;
23- }
11+ export type RateLimitDomain = {
12+ intent : string ;
13+ systemDefault : ( ) => RateLimiterConfig ;
14+ apply : (
15+ orgId : string ,
16+ next : RateLimitTokenBucketConfig ,
17+ adminUserId : string
18+ ) => Promise < void > ;
19+ } ;
2420
2521export function resolveEffectiveRateLimit (
26- override : unknown
22+ override : unknown ,
23+ domain : RateLimitDomain
2724) : EffectiveRateLimit {
2825 if ( override == null ) {
29- return { source : "default" , config : systemDefaultRateLimit ( ) } ;
26+ return { source : "default" , config : domain . systemDefault ( ) } ;
3027 }
3128 const parsed = RateLimiterConfig . safeParse ( override ) ;
3229 if ( parsed . success ) {
3330 return { source : "override" , config : parsed . data } ;
3431 }
3532 // Column holds malformed JSON — fall back silently. Admin must investigate
3633 // at the DB level; this UI can't recover it.
37- return { source : "default" , config : systemDefaultRateLimit ( ) } ;
34+ return { source : "default" , config : domain . systemDefault ( ) } ;
3835}
3936
40- const SetRateLimitSchema = z . object ( {
41- intent : z . literal ( RATE_LIMIT_INTENT ) ,
42- refillRate : z . coerce . number ( ) . int ( ) . min ( 1 ) ,
43- interval : z
44- . string ( )
45- . trim ( )
46- . refine ( ( v ) => parseDurationToMs ( v ) > 0 , {
47- message : "Must be a duration like 10s, 1m, 500ms." ,
48- } ) ,
49- maxTokens : z . coerce . number ( ) . int ( ) . min ( 1 ) ,
50- } ) ;
51-
5237export type RateLimitActionResult =
5338 | { ok : true }
5439 | { ok : false ; errors : Record < string , string [ ] | undefined > } ;
5540
5641export async function handleRateLimitAction (
5742 formData : FormData ,
5843 orgId : string ,
59- adminUserId : string
44+ adminUserId : string ,
45+ domain : RateLimitDomain
6046) : Promise < RateLimitActionResult > {
61- const submission = SetRateLimitSchema . safeParse ( Object . fromEntries ( formData ) ) ;
47+ const schema = z . object ( {
48+ intent : z . literal ( domain . intent ) ,
49+ refillRate : z . coerce . number ( ) . int ( ) . min ( 1 ) ,
50+ interval : z
51+ . string ( )
52+ . trim ( )
53+ . refine ( ( v ) => parseDurationToMs ( v ) > 0 , {
54+ message : "Must be a duration like 10s, 1m, 500ms." ,
55+ } ) ,
56+ maxTokens : z . coerce . number ( ) . int ( ) . min ( 1 ) ,
57+ } ) ;
58+
59+ const submission = schema . safeParse ( Object . fromEntries ( formData ) ) ;
6260 if ( ! submission . success ) {
6361 return { ok : false , errors : submission . error . flatten ( ) . fieldErrors } ;
6462 }
6563
66- const existing = await prisma . organization . findFirst ( {
67- where : { id : orgId } ,
68- select : { apiRateLimiterConfig : true } ,
69- } ) ;
70- if ( ! existing ) {
71- throw new Response ( null , { status : 404 } ) ;
72- }
73-
7464 const built = RateLimitTokenBucketConfig . safeParse ( {
7565 type : "tokenBucket" ,
7666 refillRate : submission . data . refillRate ,
@@ -81,17 +71,6 @@ export async function handleRateLimitAction(
8171 return { ok : false , errors : built . error . flatten ( ) . fieldErrors } ;
8272 }
8373
84- await prisma . organization . update ( {
85- where : { id : orgId } ,
86- data : { apiRateLimiterConfig : built . data as any } ,
87- } ) ;
88-
89- logger . info ( "admin.backOffice.rateLimit" , {
90- adminUserId,
91- orgId,
92- previous : existing . apiRateLimiterConfig ,
93- next : built . data ,
94- } ) ;
95-
74+ await domain . apply ( orgId , built . data , adminUserId ) ;
9675 return { ok : true } ;
9776}
0 commit comments