mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 18:27:19 +00:00
Merge pull request #1965 from Infisical/feat/allow-custom-rate-limits
feat: allow custom rate limits
This commit is contained in:
Generated
+15
@@ -36,6 +36,7 @@
|
|||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^5.4.2",
|
"bullmq": "^5.4.2",
|
||||||
"cassandra-driver": "^4.7.2",
|
"cassandra-driver": "^4.7.2",
|
||||||
|
"cron": "^3.1.7",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"fastify": "^4.26.0",
|
"fastify": "^4.26.0",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
@@ -4806,6 +4807,11 @@
|
|||||||
"long": "*"
|
"long": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/luxon": {
|
||||||
|
"version": "3.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||||
|
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
@@ -6689,6 +6695,15 @@
|
|||||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/cron": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/luxon": "~3.4.0",
|
||||||
|
"luxon": "~3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cron-parser": {
|
"node_modules/cron-parser": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^5.4.2",
|
"bullmq": "^5.4.2",
|
||||||
"cassandra-driver": "^4.7.2",
|
"cassandra-driver": "^4.7.2",
|
||||||
|
"cron": "^3.1.7",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"fastify": "^4.26.0",
|
"fastify": "^4.26.0",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
|
|||||||
Vendored
+2
@@ -48,6 +48,7 @@ import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env
|
|||||||
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
||||||
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||||
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
|
import { TRateLimitServiceFactory } from "@app/services/rate-limit/rate-limit-service";
|
||||||
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
|
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
|
||||||
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
@@ -147,6 +148,7 @@ declare module "fastify" {
|
|||||||
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
|
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
|
||||||
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
|
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
|
||||||
secretSharing: TSecretSharingServiceFactory;
|
secretSharing: TSecretSharingServiceFactory;
|
||||||
|
rateLimit: TRateLimitServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
|||||||
Vendored
+4
@@ -149,6 +149,9 @@ import {
|
|||||||
TProjectUserMembershipRoles,
|
TProjectUserMembershipRoles,
|
||||||
TProjectUserMembershipRolesInsert,
|
TProjectUserMembershipRolesInsert,
|
||||||
TProjectUserMembershipRolesUpdate,
|
TProjectUserMembershipRolesUpdate,
|
||||||
|
TRateLimit,
|
||||||
|
TRateLimitInsert,
|
||||||
|
TRateLimitUpdate,
|
||||||
TSamlConfigs,
|
TSamlConfigs,
|
||||||
TSamlConfigsInsert,
|
TSamlConfigsInsert,
|
||||||
TSamlConfigsUpdate,
|
TSamlConfigsUpdate,
|
||||||
@@ -343,6 +346,7 @@ declare module "knex/types/tables" {
|
|||||||
TSecretFolderVersionsUpdate
|
TSecretFolderVersionsUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.SecretSharing]: Knex.CompositeTableType<TSecretSharing, TSecretSharingInsert, TSecretSharingUpdate>;
|
[TableName.SecretSharing]: Knex.CompositeTableType<TSecretSharing, TSecretSharingInsert, TSecretSharingUpdate>;
|
||||||
|
[TableName.RateLimit]: Knex.CompositeTableType<TRateLimit, TRateLimitInsert, TRateLimitUpdate>;
|
||||||
[TableName.SecretTag]: Knex.CompositeTableType<TSecretTags, TSecretTagsInsert, TSecretTagsUpdate>;
|
[TableName.SecretTag]: Knex.CompositeTableType<TSecretTags, TSecretTagsInsert, TSecretTagsUpdate>;
|
||||||
[TableName.SecretImport]: Knex.CompositeTableType<TSecretImports, TSecretImportsInsert, TSecretImportsUpdate>;
|
[TableName.SecretImport]: Knex.CompositeTableType<TSecretImports, TSecretImportsInsert, TSecretImportsUpdate>;
|
||||||
[TableName.Integration]: Knex.CompositeTableType<TIntegrations, TIntegrationsInsert, TIntegrationsUpdate>;
|
[TableName.Integration]: Knex.CompositeTableType<TIntegrations, TIntegrationsInsert, TIntegrationsUpdate>;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.RateLimit))) {
|
||||||
|
await knex.schema.createTable(TableName.RateLimit, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.integer("readRateLimit").defaultTo(600).notNullable();
|
||||||
|
t.integer("writeRateLimit").defaultTo(200).notNullable();
|
||||||
|
t.integer("secretsRateLimit").defaultTo(60).notNullable();
|
||||||
|
t.integer("authRateLimit").defaultTo(60).notNullable();
|
||||||
|
t.integer("inviteUserRateLimit").defaultTo(30).notNullable();
|
||||||
|
t.integer("mfaRateLimit").defaultTo(20).notNullable();
|
||||||
|
t.integer("creationLimit").defaultTo(30).notNullable();
|
||||||
|
t.integer("publicEndpointLimit").defaultTo(30).notNullable();
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.RateLimit);
|
||||||
|
|
||||||
|
// create init rate limit entry with defaults
|
||||||
|
await knex(TableName.RateLimit).insert({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.RateLimit);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.RateLimit);
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ export * from "./project-roles";
|
|||||||
export * from "./project-user-additional-privilege";
|
export * from "./project-user-additional-privilege";
|
||||||
export * from "./project-user-membership-roles";
|
export * from "./project-user-membership-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
|
export * from "./rate-limit";
|
||||||
export * from "./saml-configs";
|
export * from "./saml-configs";
|
||||||
export * from "./scim-tokens";
|
export * from "./scim-tokens";
|
||||||
export * from "./secret-approval-policies";
|
export * from "./secret-approval-policies";
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export enum TableName {
|
|||||||
IncidentContact = "incident_contacts",
|
IncidentContact = "incident_contacts",
|
||||||
UserAction = "user_actions",
|
UserAction = "user_actions",
|
||||||
SuperAdmin = "super_admin",
|
SuperAdmin = "super_admin",
|
||||||
|
RateLimit = "rate_limit",
|
||||||
ApiKey = "api_keys",
|
ApiKey = "api_keys",
|
||||||
Project = "projects",
|
Project = "projects",
|
||||||
ProjectBot = "project_bots",
|
ProjectBot = "project_bots",
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const RateLimitSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
readRateLimit: z.number().default(600),
|
||||||
|
writeRateLimit: z.number().default(200),
|
||||||
|
secretsRateLimit: z.number().default(60),
|
||||||
|
authRateLimit: z.number().default(60),
|
||||||
|
inviteUserRateLimit: z.number().default(30),
|
||||||
|
mfaRateLimit: z.number().default(20),
|
||||||
|
creationLimit: z.number().default(30),
|
||||||
|
publicEndpointLimit: z.number().default(30),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TRateLimit = z.infer<typeof RateLimitSchema>;
|
||||||
|
export type TRateLimitInsert = Omit<z.input<typeof RateLimitSchema>, TImmutableDBKeys>;
|
||||||
|
export type TRateLimitUpdate = Partial<Omit<z.input<typeof RateLimitSchema>, TImmutableDBKeys>>;
|
||||||
@@ -17,6 +17,8 @@ import { Logger } from "pino";
|
|||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { TQueueServiceFactory } from "@app/queue";
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { rateLimitDALFactory } from "@app/services/rate-limit/rate-limit-dal";
|
||||||
|
import { rateLimitServiceFactory } from "@app/services/rate-limit/rate-limit-service";
|
||||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
||||||
@@ -69,8 +71,12 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
|
|||||||
|
|
||||||
// Rate limiters and security headers
|
// Rate limiters and security headers
|
||||||
if (appCfg.isProductionMode) {
|
if (appCfg.isProductionMode) {
|
||||||
|
const rateLimitDAL = rateLimitDALFactory(db);
|
||||||
|
const rateLimitService = rateLimitServiceFactory({ rateLimitDAL });
|
||||||
|
await rateLimitService.syncRateLimitConfiguration();
|
||||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
||||||
}
|
}
|
||||||
|
|
||||||
await server.register(helmet, { contentSecurityPolicy: false });
|
await server.register(helmet, { contentSecurityPolicy: false });
|
||||||
|
|
||||||
await server.register(maintenanceMode);
|
await server.register(maintenanceMode);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-lim
|
|||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { getRateLimiterConfig } from "@app/services/rate-limit/rate-limit-service";
|
||||||
|
|
||||||
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -21,14 +22,14 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
|||||||
// GET endpoints
|
// GET endpoints
|
||||||
export const readLimit: RateLimitOptions = {
|
export const readLimit: RateLimitOptions = {
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 600,
|
max: () => getRateLimiterConfig().readLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
// POST, PATCH, PUT, DELETE endpoints
|
// POST, PATCH, PUT, DELETE endpoints
|
||||||
export const writeLimit: RateLimitOptions = {
|
export const writeLimit: RateLimitOptions = {
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 200, // (too low, FA having issues so increasing it - maidul)
|
max: () => getRateLimiterConfig().writeLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,25 +37,25 @@ export const writeLimit: RateLimitOptions = {
|
|||||||
export const secretsLimit: RateLimitOptions = {
|
export const secretsLimit: RateLimitOptions = {
|
||||||
// secrets, folders, secret imports
|
// secrets, folders, secret imports
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 60,
|
max: () => getRateLimiterConfig().secretsLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authRateLimit: RateLimitOptions = {
|
export const authRateLimit: RateLimitOptions = {
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 60,
|
max: () => getRateLimiterConfig().authRateLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
export const inviteUserRateLimit: RateLimitOptions = {
|
export const inviteUserRateLimit: RateLimitOptions = {
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 30,
|
max: () => getRateLimiterConfig().inviteUserRateLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mfaRateLimit: RateLimitOptions = {
|
export const mfaRateLimit: RateLimitOptions = {
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 20,
|
max: () => getRateLimiterConfig().mfaRateLimit,
|
||||||
keyGenerator: (req) => {
|
keyGenerator: (req) => {
|
||||||
return req.headers.authorization?.split(" ")[1] || req.realIp;
|
return req.headers.authorization?.split(" ")[1] || req.realIp;
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ export const mfaRateLimit: RateLimitOptions = {
|
|||||||
export const creationLimit: RateLimitOptions = {
|
export const creationLimit: RateLimitOptions = {
|
||||||
// identity, project, org
|
// identity, project, org
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 30,
|
max: () => getRateLimiterConfig().creationLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,6 +72,6 @@ export const creationLimit: RateLimitOptions = {
|
|||||||
export const publicEndpointLimit: RateLimitOptions = {
|
export const publicEndpointLimit: RateLimitOptions = {
|
||||||
// Shared Secrets
|
// Shared Secrets
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 30,
|
max: () => getRateLimiterConfig().publicEndpointLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CronJob } from "cron";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -121,6 +122,8 @@ import { projectMembershipServiceFactory } from "@app/services/project-membershi
|
|||||||
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
|
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
|
||||||
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
||||||
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
|
import { rateLimitDALFactory } from "@app/services/rate-limit/rate-limit-dal";
|
||||||
|
import { rateLimitServiceFactory } from "@app/services/rate-limit/rate-limit-service";
|
||||||
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
|
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
|
||||||
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
import { secretQueueFactory } from "@app/services/secret/secret-queue";
|
import { secretQueueFactory } from "@app/services/secret/secret-queue";
|
||||||
@@ -185,6 +188,7 @@ export const registerRoutes = async (
|
|||||||
const incidentContactDAL = incidentContactDALFactory(db);
|
const incidentContactDAL = incidentContactDALFactory(db);
|
||||||
const orgRoleDAL = orgRoleDALFactory(db);
|
const orgRoleDAL = orgRoleDALFactory(db);
|
||||||
const superAdminDAL = superAdminDALFactory(db);
|
const superAdminDAL = superAdminDALFactory(db);
|
||||||
|
const rateLimitDAL = rateLimitDALFactory(db);
|
||||||
const apiKeyDAL = apiKeyDALFactory(db);
|
const apiKeyDAL = apiKeyDALFactory(db);
|
||||||
|
|
||||||
const projectDAL = projectDALFactory(db);
|
const projectDAL = projectDALFactory(db);
|
||||||
@@ -444,6 +448,9 @@ export const registerRoutes = async (
|
|||||||
orgService,
|
orgService,
|
||||||
keyStore
|
keyStore
|
||||||
});
|
});
|
||||||
|
const rateLimitService = rateLimitServiceFactory({
|
||||||
|
rateLimitDAL
|
||||||
|
});
|
||||||
const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL });
|
const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL });
|
||||||
|
|
||||||
const secretScanningQueue = secretScanningQueueFactory({
|
const secretScanningQueue = secretScanningQueueFactory({
|
||||||
@@ -862,6 +869,7 @@ export const registerRoutes = async (
|
|||||||
secret: secretService,
|
secret: secretService,
|
||||||
secretReplication: secretReplicationService,
|
secretReplication: secretReplicationService,
|
||||||
secretTag: secretTagService,
|
secretTag: secretTagService,
|
||||||
|
rateLimit: rateLimitService,
|
||||||
folder: folderService,
|
folder: folderService,
|
||||||
secretImport: secretImportService,
|
secretImport: secretImportService,
|
||||||
projectBot: projectBotService,
|
projectBot: projectBotService,
|
||||||
@@ -900,6 +908,11 @@ export const registerRoutes = async (
|
|||||||
secretSharing: secretSharingService
|
secretSharing: secretSharingService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cronJobs: CronJob[] = [];
|
||||||
|
if (appCfg.isProductionMode) {
|
||||||
|
cronJobs.push(rateLimitService.initializeBackgroundSync());
|
||||||
|
}
|
||||||
|
|
||||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||||
user: userDAL
|
user: userDAL
|
||||||
});
|
});
|
||||||
@@ -954,6 +967,7 @@ export const registerRoutes = async (
|
|||||||
await server.register(registerV3Routes, { prefix: "/api/v3" });
|
await server.register(registerV3Routes, { prefix: "/api/v3" });
|
||||||
|
|
||||||
server.addHook("onClose", async () => {
|
server.addHook("onClose", async () => {
|
||||||
|
cronJobs.forEach((job) => job.stop());
|
||||||
await telemetryService.flushAll();
|
await telemetryService.flushAll();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { registerProjectEnvRouter } from "./project-env-router";
|
|||||||
import { registerProjectKeyRouter } from "./project-key-router";
|
import { registerProjectKeyRouter } from "./project-key-router";
|
||||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
|
import { registerRateLimitRouter } from "./rate-limit-router";
|
||||||
import { registerSecretFolderRouter } from "./secret-folder-router";
|
import { registerSecretFolderRouter } from "./secret-folder-router";
|
||||||
import { registerSecretImportRouter } from "./secret-import-router";
|
import { registerSecretImportRouter } from "./secret-import-router";
|
||||||
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
||||||
@@ -43,6 +44,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||||
await server.register(registerOrgRouter, { prefix: "/organization" });
|
await server.register(registerOrgRouter, { prefix: "/organization" });
|
||||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||||
|
await server.register(registerRateLimitRouter, { prefix: "/rate-limit" });
|
||||||
await server.register(registerUserRouter, { prefix: "/user" });
|
await server.register(registerUserRouter, { prefix: "/user" });
|
||||||
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { RateLimitSchema } from "@app/db/schemas";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
rateLimit: RateLimitSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async () => {
|
||||||
|
const rateLimit = await server.services.rateLimit.getRateLimits();
|
||||||
|
if (!rateLimit) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
name: "Get Rate Limit Error",
|
||||||
|
message: "Rate limit configuration does not exist."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { rateLimit };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PUT",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
readRateLimit: z.number(),
|
||||||
|
writeRateLimit: z.number(),
|
||||||
|
secretsRateLimit: z.number(),
|
||||||
|
authRateLimit: z.number(),
|
||||||
|
inviteUserRateLimit: z.number(),
|
||||||
|
mfaRateLimit: z.number(),
|
||||||
|
creationLimit: z.number(),
|
||||||
|
publicEndpointLimit: z.number()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
rateLimit: RateLimitSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const rateLimit = await server.services.rateLimit.updateRateLimit(req.body);
|
||||||
|
return { rateLimit };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TRateLimitDALFactory = ReturnType<typeof rateLimitDALFactory>;
|
||||||
|
|
||||||
|
export const rateLimitDALFactory = (db: TDbClient) => ormify(db, TableName.RateLimit, {});
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { CronJob } from "cron";
|
||||||
|
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TRateLimitDALFactory } from "./rate-limit-dal";
|
||||||
|
import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
|
||||||
|
|
||||||
|
let rateLimitMaxConfiguration = {
|
||||||
|
readLimit: 60,
|
||||||
|
publicEndpointLimit: 30,
|
||||||
|
writeLimit: 200,
|
||||||
|
secretsLimit: 60,
|
||||||
|
authRateLimit: 60,
|
||||||
|
inviteUserRateLimit: 30,
|
||||||
|
mfaRateLimit: 20,
|
||||||
|
creationLimit: 30
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.freeze(rateLimitMaxConfiguration);
|
||||||
|
|
||||||
|
export const getRateLimiterConfig = () => {
|
||||||
|
return rateLimitMaxConfiguration;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TRateLimitServiceFactoryDep = {
|
||||||
|
rateLimitDAL: TRateLimitDALFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRateLimitServiceFactory = ReturnType<typeof rateLimitServiceFactory>;
|
||||||
|
|
||||||
|
export const rateLimitServiceFactory = ({ rateLimitDAL }: TRateLimitServiceFactoryDep) => {
|
||||||
|
const DEFAULT_RATE_LIMIT_CONFIG_ID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
const getRateLimits = async (): Promise<TRateLimit | undefined> => {
|
||||||
|
let rateLimit: TRateLimit;
|
||||||
|
|
||||||
|
try {
|
||||||
|
rateLimit = await rateLimitDAL.findOne({ id: DEFAULT_RATE_LIMIT_CONFIG_ID });
|
||||||
|
if (!rateLimit) {
|
||||||
|
// rate limit might not exist
|
||||||
|
rateLimit = await rateLimitDAL.create({
|
||||||
|
// @ts-expect-error id is kept as fixed because there should only be one rate limit config per instance
|
||||||
|
id: DEFAULT_RATE_LIMIT_CONFIG_ID
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rateLimit;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Error fetching rate limits %o", err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRateLimit = async (updates: TRateLimitUpdateDTO): Promise<TRateLimit> => {
|
||||||
|
return rateLimitDAL.updateById(DEFAULT_RATE_LIMIT_CONFIG_ID, updates);
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncRateLimitConfiguration = async () => {
|
||||||
|
try {
|
||||||
|
const rateLimit = await getRateLimits();
|
||||||
|
if (rateLimit) {
|
||||||
|
const newRateLimitMaxConfiguration: typeof rateLimitMaxConfiguration = {
|
||||||
|
readLimit: rateLimit.readRateLimit,
|
||||||
|
publicEndpointLimit: rateLimit.publicEndpointLimit,
|
||||||
|
writeLimit: rateLimit.writeRateLimit,
|
||||||
|
secretsLimit: rateLimit.secretsRateLimit,
|
||||||
|
authRateLimit: rateLimit.authRateLimit,
|
||||||
|
inviteUserRateLimit: rateLimit.inviteUserRateLimit,
|
||||||
|
mfaRateLimit: rateLimit.mfaRateLimit,
|
||||||
|
creationLimit: rateLimit.creationLimit
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
|
||||||
|
Object.freeze(newRateLimitMaxConfiguration);
|
||||||
|
rateLimitMaxConfiguration = newRateLimitMaxConfiguration;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error syncing rate limit configurations: %o`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeBackgroundSync = () => {
|
||||||
|
// sync rate limits configuration every 10 minutes
|
||||||
|
const job = new CronJob("*/10 * * * *", syncRateLimitConfiguration);
|
||||||
|
job.start();
|
||||||
|
|
||||||
|
return job;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getRateLimits,
|
||||||
|
updateRateLimit,
|
||||||
|
initializeBackgroundSync,
|
||||||
|
syncRateLimitConfiguration
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export type TRateLimitUpdateDTO = {
|
||||||
|
readRateLimit: number;
|
||||||
|
writeRateLimit: number;
|
||||||
|
secretsRateLimit: number;
|
||||||
|
authRateLimit: number;
|
||||||
|
inviteUserRateLimit: number;
|
||||||
|
mfaRateLimit: number;
|
||||||
|
creationLimit: number;
|
||||||
|
publicEndpointLimit: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRateLimit = {
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
} & TRateLimitUpdateDTO;
|
||||||
@@ -17,6 +17,7 @@ export * from "./keys";
|
|||||||
export * from "./ldapConfig";
|
export * from "./ldapConfig";
|
||||||
export * from "./organization";
|
export * from "./organization";
|
||||||
export * from "./projectUserAdditionalPrivilege";
|
export * from "./projectUserAdditionalPrivilege";
|
||||||
|
export * from "./rateLimit";
|
||||||
export * from "./roles";
|
export * from "./roles";
|
||||||
export * from "./scim";
|
export * from "./scim";
|
||||||
export * from "./secretApproval";
|
export * from "./secretApproval";
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useUpdateRateLimit } from "./mutation";
|
||||||
|
export { useGetRateLimit } from "./queries";
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { rateLimitQueryKeys } from "./queries";
|
||||||
|
import { TRateLimit } from "./types";
|
||||||
|
|
||||||
|
export const useUpdateRateLimit = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<TRateLimit, {}, TRateLimit>({
|
||||||
|
mutationFn: async (opt) => {
|
||||||
|
const { data } = await apiRequest.put<{ rateLimit: TRateLimit }>("/api/v1/rate-limit", opt);
|
||||||
|
return data.rateLimit;
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData(rateLimitQueryKeys.rateLimit(), data);
|
||||||
|
queryClient.invalidateQueries(rateLimitQueryKeys.rateLimit());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TRateLimit } from "./types";
|
||||||
|
|
||||||
|
export const rateLimitQueryKeys = {
|
||||||
|
rateLimit: () => ["rate-limit"] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRateLimit = async () => {
|
||||||
|
const { data } = await apiRequest.get<{ rateLimit: TRateLimit }>("/api/v1/rate-limit");
|
||||||
|
return data.rateLimit;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetRateLimit = ({
|
||||||
|
options = {}
|
||||||
|
}: {
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TRateLimit,
|
||||||
|
unknown,
|
||||||
|
TRateLimit,
|
||||||
|
ReturnType<typeof rateLimitQueryKeys.rateLimit>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>;
|
||||||
|
} = {}) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: rateLimitQueryKeys.rateLimit(),
|
||||||
|
queryFn: fetchRateLimit,
|
||||||
|
...options,
|
||||||
|
enabled: options?.enabled ?? true
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export type TRateLimit = {
|
||||||
|
readRateLimit: number;
|
||||||
|
writeRateLimit: number;
|
||||||
|
secretsRateLimit: number;
|
||||||
|
authRateLimit: number;
|
||||||
|
inviteUserRateLimit: number;
|
||||||
|
mfaRateLimit: number;
|
||||||
|
creationLimit: number;
|
||||||
|
publicEndpointLimit: number;
|
||||||
|
};
|
||||||
@@ -18,12 +18,16 @@ import {
|
|||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
Tabs} from "@app/components/v2";
|
Tabs
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useServerConfig, useUser } from "@app/context";
|
import { useOrganization, useServerConfig, useUser } from "@app/context";
|
||||||
import { useUpdateServerConfig } from "@app/hooks/api";
|
import { useUpdateServerConfig } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { RateLimitPanel } from "./RateLimitPanel";
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
Settings = "settings"
|
Settings = "settings",
|
||||||
|
RateLimit = "rate-limit"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SignUpModes {
|
enum SignUpModes {
|
||||||
@@ -117,6 +121,7 @@ export const AdminDashboardPage = () => {
|
|||||||
<TabList>
|
<TabList>
|
||||||
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||||
<Tab value={TabSections.Settings}>General</Tab>
|
<Tab value={TabSections.Settings}>General</Tab>
|
||||||
|
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={TabSections.Settings}>
|
<TabPanel value={TabSections.Settings}>
|
||||||
@@ -233,6 +238,9 @@ export const AdminDashboardPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.RateLimit}>
|
||||||
|
<RateLimitPanel />
|
||||||
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useGetRateLimit, useUpdateRateLimit } from "@app/hooks/api";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
readRateLimit: z.number(),
|
||||||
|
writeRateLimit: z.number(),
|
||||||
|
secretsRateLimit: z.number(),
|
||||||
|
authRateLimit: z.number(),
|
||||||
|
inviteUserRateLimit: z.number(),
|
||||||
|
mfaRateLimit: z.number(),
|
||||||
|
creationLimit: z.number(),
|
||||||
|
publicEndpointLimit: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TRateLimitForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const RateLimitPanel = () => {
|
||||||
|
const { data: rateLimit, isLoading } = useGetRateLimit();
|
||||||
|
const { mutateAsync: updateRateLimit } = useUpdateRateLimit();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = useForm<TRateLimitForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
values: {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
readRateLimit: rateLimit?.readRateLimit ?? 600,
|
||||||
|
writeRateLimit: rateLimit?.writeRateLimit ?? 200,
|
||||||
|
secretsRateLimit: rateLimit?.secretsRateLimit ?? 60,
|
||||||
|
authRateLimit: rateLimit?.authRateLimit ?? 60,
|
||||||
|
inviteUserRateLimit: rateLimit?.inviteUserRateLimit ?? 30,
|
||||||
|
mfaRateLimit: rateLimit?.mfaRateLimit ?? 20,
|
||||||
|
creationLimit: rateLimit?.creationLimit ?? 30,
|
||||||
|
publicEndpointLimit: rateLimit?.publicEndpointLimit ?? 30
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onRateLimitFormSubmit = async (formData: TRateLimitForm) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
readRateLimit,
|
||||||
|
writeRateLimit,
|
||||||
|
secretsRateLimit,
|
||||||
|
authRateLimit,
|
||||||
|
inviteUserRateLimit,
|
||||||
|
mfaRateLimit,
|
||||||
|
creationLimit,
|
||||||
|
publicEndpointLimit
|
||||||
|
} = formData;
|
||||||
|
|
||||||
|
await updateRateLimit({
|
||||||
|
readRateLimit,
|
||||||
|
writeRateLimit,
|
||||||
|
secretsRateLimit,
|
||||||
|
authRateLimit,
|
||||||
|
inviteUserRateLimit,
|
||||||
|
mfaRateLimit,
|
||||||
|
creationLimit,
|
||||||
|
publicEndpointLimit
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
text: "Rate limits have been successfully updated. Please allow at least 10 minutes for the changes to take effect.",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update rate limiting setting."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return isLoading ? (
|
||||||
|
<ContentLoader />
|
||||||
|
) : (
|
||||||
|
<form
|
||||||
|
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
onSubmit={handleSubmit(onRateLimitFormSubmit)}
|
||||||
|
>
|
||||||
|
<div className="mb-8 flex flex-col justify-start">
|
||||||
|
<div className="mb-4 text-xl font-semibold text-mineshaft-100">
|
||||||
|
Configure rate limits
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="readRateLimit"
|
||||||
|
defaultValue={300}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Global read requests per minute"
|
||||||
|
className="w-72"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={300}
|
||||||
|
name="writeRateLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Global write requests per minute"
|
||||||
|
className="w-72"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={300}
|
||||||
|
name="secretsRateLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret requests per minute"
|
||||||
|
className="w-72"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={300}
|
||||||
|
name="authRateLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Auth requests per minute"
|
||||||
|
className="w-72"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={300}
|
||||||
|
name="inviteUserRateLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="User invitation requests per minute"
|
||||||
|
className="w-72"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={300}
|
||||||
|
name="mfaRateLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Multi factor auth requests per minute"
|
||||||
|
className="w-72"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={300}
|
||||||
|
name="creationLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="New resource creation requests per minute"
|
||||||
|
className="w-72"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={300}
|
||||||
|
name="publicEndpointLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret sharing requests per minute"
|
||||||
|
className="w-72"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting || !isDirty}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user