Files
2026-07-13 13:32:57 +08:00

70 lines
2.1 KiB
TypeScript

import { z } from "zod";
import {
RateLimitTokenBucketConfig,
RateLimiterConfig,
} from "~/services/authorizationRateLimitMiddleware.server";
import { parseDurationToMs, type EffectiveRateLimit } from "./RateLimitSection";
export type RateLimitDomain = {
intent: string;
systemDefault: () => RateLimiterConfig;
apply: (orgId: string, next: RateLimitTokenBucketConfig, adminUserId: string) => Promise<void>;
};
export function resolveEffectiveRateLimit(
override: unknown,
domain: RateLimitDomain
): EffectiveRateLimit {
if (override == null) {
return { source: "default", config: domain.systemDefault() };
}
const parsed = RateLimiterConfig.safeParse(override);
if (parsed.success) {
return { source: "override", config: parsed.data };
}
// Column holds malformed JSON — fall back silently. Admin must investigate
// at the DB level; this UI can't recover it.
return { source: "default", config: domain.systemDefault() };
}
export type RateLimitActionResult =
| { ok: true }
| { ok: false; errors: Record<string, string[] | undefined> };
export async function handleRateLimitAction(
formData: FormData,
orgId: string,
adminUserId: string,
domain: RateLimitDomain
): Promise<RateLimitActionResult> {
const schema = z.object({
intent: z.literal(domain.intent),
refillRate: z.coerce.number().int().min(1),
interval: z
.string()
.trim()
.refine((v) => parseDurationToMs(v) > 0, {
message: "Must be a duration like 10s, 1m, 500ms.",
}),
maxTokens: z.coerce.number().int().min(1),
});
const submission = schema.safeParse(Object.fromEntries(formData));
if (!submission.success) {
return { ok: false, errors: submission.error.flatten().fieldErrors };
}
const built = RateLimitTokenBucketConfig.safeParse({
type: "tokenBucket",
refillRate: submission.data.refillRate,
interval: submission.data.interval,
maxTokens: submission.data.maxTokens,
});
if (!built.success) {
return { ok: false, errors: built.error.flatten().fieldErrors };
}
await domain.apply(orgId, built.data, adminUserId);
return { ok: true };
}