Compare commits

..

1 Commits

Author SHA1 Message Date
65b6f61b53 dedicated-instance-deploy 2025-02-25 16:21:20 +09:00
47 changed files with 4153 additions and 1149 deletions

2292
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -117,6 +117,7 @@
"vitest": "^1.2.2"
},
"dependencies": {
"@aws-sdk/client-cloudformation": "^3.750.0",
"@aws-sdk/client-elasticache": "^3.637.0",
"@aws-sdk/client-iam": "^3.525.0",
"@aws-sdk/client-kms": "^3.609.0",
@ -161,6 +162,7 @@
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-cdk-lib": "^2.180.0",
"aws-sdk": "^2.1553.0",
"axios": "^1.6.7",
"axios-retry": "^4.0.0",
@ -192,7 +194,6 @@
"nanoid": "^3.3.8",
"nodemailer": "^6.9.9",
"odbc": "^2.4.9",
"openai": "^4.85.4",
"openid-client": "^5.6.5",
"ora": "^7.0.1",
"oracledb": "^6.4.0",

View File

@ -19,6 +19,7 @@ import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/ser
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
import { TDedicatedInstanceServiceFactory } from "@app/ee/services/dedicated-instance/dedicated-instance-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
@ -45,7 +46,6 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TAutomatedSecurityServiceFactory } from "@app/services/automated-security/automated-security-service";
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
@ -229,7 +229,7 @@ declare module "fastify" {
secretSync: TSecretSyncServiceFactory;
kmip: TKmipServiceFactory;
kmipOperation: TKmipOperationServiceFactory;
automatedSecurity: TAutomatedSecurityServiceFactory;
dedicatedInstance: TDedicatedInstanceServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@ -29,9 +29,6 @@ import {
TAuthTokenSessionsUpdate,
TAuthTokensInsert,
TAuthTokensUpdate,
TAutomatedSecurityReports,
TAutomatedSecurityReportsInsert,
TAutomatedSecurityReportsUpdate,
TBackupPrivateKey,
TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate,
@ -116,9 +113,6 @@ import {
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate,
TIdentityProfile,
TIdentityProfileInsert,
TIdentityProfileUpdate,
TIdentityProjectAdditionalPrivilege,
TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate,
@ -936,15 +930,10 @@ declare module "knex/types/tables" {
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate
>;
[TableName.IdentityProfile]: KnexOriginal.CompositeTableType<
TIdentityProfile,
TIdentityProfileInsert,
TIdentityProfileUpdate
>;
[TableName.AutomatedSecurityReports]: KnexOriginal.CompositeTableType<
TAutomatedSecurityReports,
TAutomatedSecurityReportsInsert,
TAutomatedSecurityReportsUpdate
[TableName.DedicatedInstances]: KnexOriginal.CompositeTableType<
TDedicatedInstances,
TDedicatedInstancesInsert,
TDedicatedInstancesUpdate
>;
}
}

View File

@ -0,0 +1,56 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.DedicatedInstances);
if (!isTablePresent) {
await knex.schema.createTable(TableName.DedicatedInstances, (t) => {
t.uuid("id").primary().defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable();
t.string("instanceName").notNullable();
t.string("subdomain").notNullable().unique();
t.enum("status", ["RUNNING", "UPGRADING", "PROVISIONING", "FAILED"]).notNullable();
t.string("rdsInstanceType").notNullable();
t.string("elasticCacheType").notNullable();
t.integer("elasticContainerMemory").notNullable();
t.integer("elasticContainerCpu").notNullable();
t.string("region").notNullable();
t.string("version").notNullable();
t.integer("backupRetentionDays").defaultTo(7);
t.timestamp("lastBackupTime").nullable();
t.timestamp("lastUpgradeTime").nullable();
t.boolean("publiclyAccessible").defaultTo(false);
t.string("vpcId").nullable();
t.specificType("subnetIds", "text[]").nullable();
t.jsonb("tags").nullable();
t.boolean("multiAz").defaultTo(true);
t.integer("rdsAllocatedStorage").defaultTo(50);
t.integer("rdsBackupRetentionDays").defaultTo(7);
t.integer("redisNumCacheNodes").defaultTo(1);
t.integer("desiredContainerCount").defaultTo(1);
t.string("stackName").nullable();
t.text("rdsInstanceId").nullable();
t.text("redisClusterId").nullable();
t.text("ecsClusterArn").nullable();
t.text("ecsServiceArn").nullable();
t.specificType("securityGroupIds", "text[]").nullable();
t.text("error").nullable();
t.timestamps(true, true, true);
t.foreign("orgId")
.references("id")
.inTable(TableName.Organization)
.onDelete("CASCADE");
t.unique(["orgId", "instanceName"]);
});
}
await createOnUpdateTrigger(knex, TableName.DedicatedInstances);
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.DedicatedInstances);
await knex.schema.dropTableIfExists(TableName.DedicatedInstances);
}

View File

@ -0,0 +1,16 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// First drop the existing constraint
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} DROP CONSTRAINT IF EXISTS dedicated_instances_status_check`);
// Add the new constraint with updated enum values
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} ADD CONSTRAINT dedicated_instances_status_check CHECK (status IN ('RUNNING', 'UPGRADING', 'PROVISIONING', 'FAILED'))`);
}
export async function down(knex: Knex): Promise<void> {
// Revert back to original constraint
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} DROP CONSTRAINT IF EXISTS dedicated_instances_status_check`);
await knex.raw(`ALTER TABLE ${TableName.DedicatedInstances} ADD CONSTRAINT dedicated_instances_status_check CHECK (status IN ('RUNNING', 'UPGRADING', 'PROVISIONING'))`);
}

View File

@ -1,53 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityProfile))) {
await knex.schema.createTable(TableName.IdentityProfile, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("userId");
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("identityId");
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.text("temporalProfile").notNullable(); // access pattern or frequency
t.text("scopeProfile").notNullable(); // scope of usage - are they accessing development environment secrets. which paths? are they doing mainly admin work?
t.text("usageProfile").notNullable(); // method of usage
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.IdentityProfile);
}
if (!(await knex.schema.hasTable(TableName.AutomatedSecurityReports))) {
await knex.schema.createTable(TableName.AutomatedSecurityReports, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("profileId").notNullable();
t.foreign("profileId").references("id").inTable(TableName.IdentityProfile).onDelete("CASCADE");
t.jsonb("event").notNullable();
t.string("remarks").notNullable();
t.string("severity").notNullable();
t.string("status").notNullable();
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.AutomatedSecurityReports);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityProfile);
await knex.schema.dropTableIfExists(TableName.AutomatedSecurityReports);
}

View File

@ -1,25 +0,0 @@
// 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 AutomatedSecurityReportsSchema = z.object({
id: z.string().uuid(),
profileId: z.string().uuid(),
event: z.unknown(),
remarks: z.string(),
severity: z.string(),
status: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TAutomatedSecurityReports = z.infer<typeof AutomatedSecurityReportsSchema>;
export type TAutomatedSecurityReportsInsert = Omit<z.input<typeof AutomatedSecurityReportsSchema>, TImmutableDBKeys>;
export type TAutomatedSecurityReportsUpdate = Partial<
Omit<z.input<typeof AutomatedSecurityReportsSchema>, TImmutableDBKeys>
>;

View File

@ -0,0 +1,34 @@
// 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 DedicatedInstancesSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
instanceName: z.string(),
status: z.string(),
rdsInstanceType: z.string(),
elasticCacheType: z.string(),
elasticContainerMemory: z.number(),
elasticContainerCpu: z.number(),
region: z.string(),
version: z.string(),
backupRetentionDays: z.number().default(7).nullable().optional(),
lastBackupTime: z.date().nullable().optional(),
lastUpgradeTime: z.date().nullable().optional(),
publiclyAccessible: z.boolean().default(false).nullable().optional(),
vpcId: z.string().nullable().optional(),
subnetIds: z.string().array().nullable().optional(),
tags: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TDedicatedInstances = z.infer<typeof DedicatedInstancesSchema>;
export type TDedicatedInstancesInsert = Omit<z.input<typeof DedicatedInstancesSchema>, TImmutableDBKeys>;
export type TDedicatedInstancesUpdate = Partial<Omit<z.input<typeof DedicatedInstancesSchema>, TImmutableDBKeys>>;

View File

@ -1,24 +0,0 @@
// 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 IdentityProfileSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid().nullable().optional(),
identityId: z.string().uuid().nullable().optional(),
temporalProfile: z.string(),
scopeProfile: z.string(),
usageProfile: z.string(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityProfile = z.infer<typeof IdentityProfileSchema>;
export type TIdentityProfileInsert = Omit<z.input<typeof IdentityProfileSchema>, TImmutableDBKeys>;
export type TIdentityProfileUpdate = Partial<Omit<z.input<typeof IdentityProfileSchema>, TImmutableDBKeys>>;

View File

@ -7,7 +7,6 @@ export * from "./audit-log-streams";
export * from "./audit-logs";
export * from "./auth-token-sessions";
export * from "./auth-tokens";
export * from "./automated-security-reports";
export * from "./backup-private-key";
export * from "./certificate-authorities";
export * from "./certificate-authority-certs";
@ -36,7 +35,6 @@ export * from "./identity-kubernetes-auths";
export * from "./identity-metadata";
export * from "./identity-oidc-auths";
export * from "./identity-org-memberships";
export * from "./identity-profile";
export * from "./identity-project-additional-privilege";
export * from "./identity-project-membership-role";
export * from "./identity-project-memberships";

View File

@ -2,6 +2,7 @@ import { z } from "zod";
export enum TableName {
Users = "users",
Organization = "organizations",
SshCertificateAuthority = "ssh_certificate_authorities",
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
SshCertificateTemplate = "ssh_certificate_templates",
@ -29,7 +30,6 @@ export enum TableName {
AuthTokens = "auth_tokens",
AuthTokenSession = "auth_token_sessions",
BackupPrivateKey = "backup_private_key",
Organization = "organizations",
OrgMembership = "org_memberships",
OrgRoles = "org_roles",
OrgBot = "org_bots",
@ -80,8 +80,6 @@ export enum TableName {
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
// used by both identity and users
IdentityMetadata = "identity_metadata",
IdentityProfile = "identity_profile",
AutomatedSecurityReports = "automated_security_reports",
ResourceMetadata = "resource_metadata",
ScimToken = "scim_tokens",
AccessApprovalPolicy = "access_approval_policies",
@ -138,7 +136,8 @@ export enum TableName {
KmipClient = "kmip_clients",
KmipOrgConfig = "kmip_org_configs",
KmipOrgServerCertificates = "kmip_org_server_certificates",
KmipClientCertificates = "kmip_client_certificates"
KmipClientCertificates = "kmip_client_certificates",
DedicatedInstances = "dedicated_instances"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,141 @@
import { z } from "zod";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const DedicatedInstanceSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
instanceName: z.string().min(1),
subdomain: z.string().min(1),
status: z.enum(["RUNNING", "UPGRADING", "PROVISIONING", "FAILED"]),
rdsInstanceType: z.string(),
elasticCacheType: z.string(),
elasticContainerMemory: z.number(),
elasticContainerCpu: z.number(),
region: z.string(),
version: z.string(),
backupRetentionDays: z.number(),
lastBackupTime: z.date().nullable(),
lastUpgradeTime: z.date().nullable(),
publiclyAccessible: z.boolean(),
vpcId: z.string().nullable(),
subnetIds: z.array(z.string()).nullable(),
tags: z.record(z.string()).nullable(),
createdAt: z.date(),
updatedAt: z.date()
});
const CreateDedicatedInstanceSchema = z.object({
instanceName: z.string().min(1),
subdomain: z.string().min(1),
provider: z.literal('aws'), // Only allow 'aws' as provider
region: z.string(),
publiclyAccessible: z.boolean().default(false)
});
const DedicatedInstanceDetailsSchema = DedicatedInstanceSchema.extend({
stackStatus: z.string().optional(),
stackStatusReason: z.string().optional(),
error: z.string().nullable(),
events: z.array(
z.object({
timestamp: z.date().optional(),
logicalResourceId: z.string().optional(),
resourceType: z.string().optional(),
resourceStatus: z.string().optional(),
resourceStatusReason: z.string().optional()
})
).optional()
});
export const registerDedicatedInstanceRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:organizationId/dedicated-instances",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
organizationId: z.string().uuid()
}),
response: {
200: z.object({
instances: DedicatedInstanceSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const instances = await server.services.dedicatedInstance.listInstances({
orgId: req.params.organizationId
});
return { instances };
}
});
server.route({
method: "POST",
url: "/:organizationId/dedicated-instances",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
organizationId: z.string().uuid()
}),
body: CreateDedicatedInstanceSchema
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { organizationId } = req.params;
const { instanceName, subdomain, region, publiclyAccessible, provider} = req.body;
const instance = await server.services.dedicatedInstance.createInstance({
orgId: organizationId,
instanceName,
subdomain,
region,
publiclyAccessible,
provider: provider,
dryRun: false,
});
return instance;
}
});
server.route({
method: "GET",
url: "/:organizationId/dedicated-instances/:instanceId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
organizationId: z.string().uuid(),
instanceId: z.string().uuid()
}),
response: {
200: DedicatedInstanceDetailsSchema
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { organizationId, instanceId } = req.params;
const { instance, stackStatus, stackStatusReason, events } = await server.services.dedicatedInstance.getInstance({
orgId: organizationId,
instanceId
});
return {
...instance,
stackStatus,
stackStatusReason,
events
};
}
});
};

View File

@ -4,6 +4,7 @@ import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-rou
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDedicatedInstanceRouter } from "./dedicated-instance-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
@ -38,6 +39,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
await server.register(registerLicenseRouter, { prefix: "/organizations" });
await server.register(registerDedicatedInstanceRouter, { prefix: "/organizations" });
await server.register(
async (projectRouter) => {
await projectRouter.register(registerProjectRoleRouter);

View File

@ -0,0 +1,28 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TDedicatedInstanceDALFactory = ReturnType<typeof dedicatedInstanceDALFactory>;
export const dedicatedInstanceDALFactory = (db: TDbClient) => {
const dedicatedInstanceOrm = ormify(db, TableName.DedicatedInstances);
const findInstancesByOrgId = async (orgId: string, tx?: Knex) => {
try {
const instances = await (tx || db.replicaNode())(TableName.DedicatedInstances)
.where({ orgId })
.select("*");
return instances;
} catch (error) {
throw new DatabaseError({ error, name: "Find instances by org ID" });
}
};
return {
...dedicatedInstanceOrm,
findInstancesByOrgId
};
};

View File

@ -0,0 +1,470 @@
import { ForbiddenError } from "@casl/ability";
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as elasticache from 'aws-cdk-lib/aws-elasticache';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as iam from 'aws-cdk-lib/aws-iam';
import { randomBytes } from 'crypto';
import { CloudFormationClient, CreateStackCommand, DescribeStacksCommand, DescribeStackEventsCommand } from "@aws-sdk/client-cloudformation";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TDedicatedInstanceDALFactory } from "./dedicated-instance-dal";
type TDedicatedInstanceServiceFactoryDep = {
dedicatedInstanceDAL: TDedicatedInstanceDALFactory;
permissionService: TPermissionServiceFactory;
};
interface CreateInstanceParams {
orgId: string;
instanceName: string;
subdomain: string;
region: string;
provider: 'aws';
publiclyAccessible: boolean;
clusterSize: 'small' | 'medium' | 'large';
dryRun?: boolean;
}
interface GetInstanceParams {
orgId: string;
instanceId: string;
}
interface StackResource {
resourceType: string;
resourceStatus: string;
resourceStatusReason?: string;
}
interface StackEvent {
timestamp?: Date;
logicalResourceId?: string;
resourceType?: string;
resourceStatus?: string;
resourceStatusReason?: string;
}
interface InstanceDetails {
instance: Awaited<ReturnType<TDedicatedInstanceDALFactory["findById"]>>;
stackStatus?: string;
stackStatusReason?: string;
resources?: StackResource[];
events?: StackEvent[];
}
export type TDedicatedInstanceServiceFactory = ReturnType<typeof dedicatedInstanceServiceFactory>;
const CLUSTER_SIZES = {
small: {
containerCpu: 1024, // 1 vCPU
containerMemory: 2048, // 2GB
rdsInstanceType: 'db.t3.small',
elasticCacheType: 'cache.t3.micro',
desiredContainerCount: 1,
displayName: '1 vCPU, 2GB RAM'
},
medium: {
containerCpu: 2048, // 2 vCPU
containerMemory: 4096, // 4GB
rdsInstanceType: 'db.t3.medium',
elasticCacheType: 'cache.t3.small',
desiredContainerCount: 2,
displayName: '2 vCPU, 4GB RAM'
},
large: {
containerCpu: 4096, // 4 vCPU
containerMemory: 8192, // 8GB
rdsInstanceType: 'db.t3.large',
elasticCacheType: 'cache.t3.medium',
desiredContainerCount: 4,
displayName: '4 vCPU, 8GB RAM'
}
};
export const dedicatedInstanceServiceFactory = ({
dedicatedInstanceDAL,
permissionService
}: TDedicatedInstanceServiceFactoryDep) => {
const listInstances = async ({
orgId
}: {
orgId: string;
}) => {
const instances = await dedicatedInstanceDAL.findInstancesByOrgId(orgId);
return instances;
};
const createInstance = async (params: CreateInstanceParams) => {
const { orgId, instanceName, subdomain, region, publiclyAccessible, dryRun = false, clusterSize = 'small' } = params;
if (params.provider !== 'aws') {
throw new BadRequestError({ message: 'Only AWS provider is supported' });
}
const clusterConfig = CLUSTER_SIZES[clusterSize];
// Configure AWS SDK with environment variables
const awsConfig = {
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
region: region,
};
if (!awsConfig.credentials.accessKeyId || !awsConfig.credentials.secretAccessKey) {
throw new Error('AWS credentials not found in environment variables');
}
const internalTags = {
'managed-by': 'infisical',
'organization-id': orgId,
'instance-name': instanceName
};
// Create the instance record with expanded configuration
const instance = await dedicatedInstanceDAL.create({
orgId,
instanceName,
subdomain,
status: "PROVISIONING",
region,
rdsInstanceType: clusterConfig.rdsInstanceType,
elasticCacheType: clusterConfig.elasticCacheType,
elasticContainerMemory: clusterConfig.containerMemory,
elasticContainerCpu: clusterConfig.containerCpu,
publiclyAccessible,
tags: internalTags,
version: "1.0.0",
multiAz: true,
rdsAllocatedStorage: 50,
rdsBackupRetentionDays: 7,
redisNumCacheNodes: 1,
desiredContainerCount: clusterConfig.desiredContainerCount,
subnetIds: [],
securityGroupIds: []
});
// Generate unique names for resources
const stackName = `infisical-dedicated-${instance.id}`;
const dbPassword = randomBytes(32).toString('hex');
// Create CDK app and stack
const app = new cdk.App();
const stack = new cdk.Stack(app, stackName, {
env: { region },
tags: internalTags,
synthesizer: new cdk.DefaultStackSynthesizer({
generateBootstrapVersionRule: false,
})
});
// Create VPC
const vpc = new ec2.Vpc(stack, `${orgId}-${instanceName}-vpc`, {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
cidrMask: 24,
},
{
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
cidrMask: 24,
},
],
});
// Create RDS instance
const dbSecurityGroup = new ec2.SecurityGroup(stack, `${orgId}-${instanceName}-db-sg`, {
vpc,
description: `Security group for ${instanceName} RDS instance`,
});
const db = new rds.DatabaseInstance(stack, `${orgId}-${instanceName}-db`, {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_14,
}),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
securityGroups: [dbSecurityGroup],
credentials: rds.Credentials.fromPassword(
'postgres',
cdk.SecretValue.unsafePlainText(dbPassword)
),
multiAz: true,
allocatedStorage: 50,
backupRetention: cdk.Duration.days(7),
});
// Create Redis cluster
const redisSecurityGroup = new ec2.SecurityGroup(stack, `${orgId}-${instanceName}-redis-sg`, {
vpc,
description: `Security group for ${instanceName} Redis cluster`,
});
const redisSubnetGroup = new elasticache.CfnSubnetGroup(stack, `${orgId}-${instanceName}-redis-subnet`, {
subnetIds: vpc.privateSubnets.map((subnet: ec2.ISubnet) => subnet.subnetId),
description: `Subnet group for ${instanceName} Redis cluster`,
});
const redis = new elasticache.CfnCacheCluster(stack, `${orgId}-${instanceName}-redis`, {
engine: 'redis',
cacheNodeType: 'cache.t3.micro',
numCacheNodes: 1,
vpcSecurityGroupIds: [redisSecurityGroup.securityGroupId],
cacheSubnetGroupName: redisSubnetGroup.ref,
});
// Create ECS Fargate cluster and service
const cluster = new ecs.Cluster(stack, `${orgId}-${instanceName}-cluster`, { vpc });
// Create task execution role with permissions to read from Parameter Store
const executionRole = new iam.Role(stack, `${orgId}-${instanceName}-execution-role`, {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
executionRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')
);
// Create ECS task definition and service
const taskDefinition = new ecs.FargateTaskDefinition(stack, `${orgId}-${instanceName}-task-def`, {
memoryLimitMiB: clusterConfig.containerMemory,
cpu: clusterConfig.containerCpu,
executionRole,
});
taskDefinition.addContainer('infisical', {
image: ecs.ContainerImage.fromRegistry('infisical/infisical:latest-postgres'),
environment: {
NODE_ENV: 'production',
ENCRYPTION_KEY: randomBytes(16).toString('hex'),
AUTH_SECRET: randomBytes(32).toString('base64'),
DB_CONNECTION_URI: `postgresql://postgres:${dbPassword}@${db.instanceEndpoint.hostname}:5432/postgres?sslmode=no-verify`,
REDIS_URL: `redis://${redis.attrRedisEndpointAddress}:${redis.attrRedisEndpointPort}`,
},
logging: ecs.LogDrivers.awsLogs({ streamPrefix: stackName }),
});
const service = new ecs.FargateService(stack, `${orgId}-${instanceName}-service`, {
cluster,
taskDefinition,
desiredCount: clusterConfig.desiredContainerCount,
assignPublicIp: publiclyAccessible,
vpcSubnets: {
subnetType: publiclyAccessible ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
});
// Create security group for ECS service
const ecsSecurityGroup = new ec2.SecurityGroup(stack, `${orgId}-${instanceName}-ecs-sg`, {
vpc,
description: `Security group for ${instanceName} ECS service`,
allowAllOutbound: true,
});
// Update service to use the security group
service.connections.addSecurityGroup(ecsSecurityGroup);
// Configure security group rules for RDS access
dbSecurityGroup.addIngressRule(
ecsSecurityGroup,
ec2.Port.tcp(5432),
'Allow ECS tasks to connect to PostgreSQL'
);
// Configure security group rules for Redis access
redisSecurityGroup.addIngressRule(
ecsSecurityGroup,
ec2.Port.tcp(6379),
'Allow ECS tasks to connect to Redis'
);
// Add outputs for resource IDs
new cdk.CfnOutput(stack, 'vpcid', { value: vpc.vpcId });
new cdk.CfnOutput(stack, 'publicsubnet1id', { value: vpc.publicSubnets[0].subnetId });
new cdk.CfnOutput(stack, 'publicsubnet2id', { value: vpc.publicSubnets[1].subnetId });
new cdk.CfnOutput(stack, 'privatesubnet1id', { value: vpc.privateSubnets[0].subnetId });
new cdk.CfnOutput(stack, 'privatesubnet2id', { value: vpc.privateSubnets[1].subnetId });
new cdk.CfnOutput(stack, 'rdsinstanceid', { value: db.instanceIdentifier });
new cdk.CfnOutput(stack, 'redisclusterid', { value: redis.ref });
new cdk.CfnOutput(stack, 'ecsclusterarn', { value: cluster.clusterArn });
new cdk.CfnOutput(stack, 'ecsservicearn', { value: service.serviceArn });
new cdk.CfnOutput(stack, 'dbsecuritygroupid', { value: dbSecurityGroup.securityGroupId });
new cdk.CfnOutput(stack, 'redissecuritygroupid', { value: redisSecurityGroup.securityGroupId });
// After VPC creation, store subnet IDs
const subnetIds = [
...vpc.publicSubnets.map(subnet => subnet.subnetId),
...vpc.privateSubnets.map(subnet => subnet.subnetId)
];
// After security group creation, store security group IDs
const securityGroupIds = [
dbSecurityGroup.securityGroupId,
redisSecurityGroup.securityGroupId
];
// Update instance with all infrastructure details
await dedicatedInstanceDAL.updateById(instance.id, {
stackName,
status: "PROVISIONING",
// Remove the token values and update them after stack creation
rdsInstanceId: null,
redisClusterId: null,
ecsClusterArn: null,
ecsServiceArn: null,
vpcId: null,
subnetIds: null,
securityGroupIds: null
});
// Deploy the stack
const deployment = app.synth();
if (dryRun) {
console.log('Dry run - would create stack with template:', JSON.stringify(deployment.getStackArtifact(stackName).template, null, 2));
return instance;
}
// Deploy the CloudFormation stack
try {
const cfnClient = new CloudFormationClient(awsConfig);
const command = new CreateStackCommand({
StackName: stackName,
TemplateBody: JSON.stringify(deployment.getStackArtifact(stackName).template),
Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
Tags: Object.entries(internalTags).map(([Key, Value]) => ({ Key, Value }))
});
await cfnClient.send(command);
} catch (error) {
await dedicatedInstanceDAL.updateById(instance.id, {
status: "FAILED",
error: error instanceof Error ? error.message : 'Unknown error occurred'
});
throw error;
}
return instance;
};
const getInstance = async ({ orgId, instanceId }: GetInstanceParams): Promise<InstanceDetails> => {
const instance = await dedicatedInstanceDAL.findById(instanceId);
if (!instance) {
throw new NotFoundError({ message: "Instance not found" });
}
if (instance.orgId !== orgId) {
throw new BadRequestError({ message: "Not authorized to access this instance" });
}
// Get CloudFormation stack status
try {
const awsConfig = {
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
region: instance.region,
};
if (!awsConfig.credentials.accessKeyId || !awsConfig.credentials.secretAccessKey) {
throw new Error('AWS credentials not found in environment variables');
}
const cfnClient = new CloudFormationClient(awsConfig);
// Get stack status
const stackResponse = await cfnClient.send(new DescribeStacksCommand({
StackName: instance.stackName
}));
const stack = stackResponse.Stacks?.[0];
if (!stack) {
return { instance };
}
// Get stack events for progress tracking
const eventsResponse = await cfnClient.send(new DescribeStackEventsCommand({
StackName: instance.stackName
}));
const events = eventsResponse.StackEvents?.map(event => ({
timestamp: event.Timestamp,
logicalResourceId: event.LogicalResourceId,
resourceType: event.ResourceType,
resourceStatus: event.ResourceStatus,
resourceStatusReason: event.ResourceStatusReason
})).sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0)) || [];
const stackStatus = stack.StackStatus || '';
let updates: Record<string, any> = {};
// Process outputs when stack is complete
if (stackStatus === 'CREATE_COMPLETE') {
const outputs = stack.Outputs || [];
const getOutput = (key: string) => outputs.find(o => o.OutputKey?.toLowerCase() === key.toLowerCase())?.OutputValue;
updates = {
status: 'RUNNING',
vpcId: getOutput('vpcid'),
subnetIds: [
getOutput('publicsubnet1id'),
getOutput('publicsubnet2id'),
getOutput('privatesubnet1id'),
getOutput('privatesubnet2id')
].filter(Boolean) as string[],
rdsInstanceId: getOutput('rdsinstanceid'),
redisClusterId: getOutput('redisclusterid'),
ecsClusterArn: getOutput('ecsclusterarn'),
ecsServiceArn: getOutput('ecsservicearn'),
securityGroupIds: [
getOutput('dbsecuritygroupid'),
getOutput('redissecuritygroupid')
].filter(Boolean) as string[]
};
} else if (stackStatus.includes('FAILED')) {
updates = {
status: 'FAILED',
error: stack.StackStatusReason
};
}
// Update instance if we have changes
if (Object.keys(updates).length > 0) {
await dedicatedInstanceDAL.updateById(instance.id, updates);
}
return {
instance: {
...instance,
...updates
},
stackStatus: stack.StackStatus,
stackStatusReason: stack.StackStatusReason,
events
};
} catch (error) {
// Log the error but don't throw - we still want to return the instance details
console.error('Error fetching CloudFormation stack status:', error);
}
return { instance };
};
return {
listInstances,
createInstance,
getInstance
};
};

View File

@ -25,8 +25,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
customRateLimits: false,
customAlerts: false,
secretAccessInsights: false,
auditLogs: true,
auditLogsRetentionDays: 3,
auditLogs: false,
auditLogsRetentionDays: 0,
auditLogStreams: false,
auditLogStreamLimit: 3,
samlSSO: false,

View File

@ -12,7 +12,8 @@ export enum OrgPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
Delete = "delete",
Update = "update"
}
export enum OrgPermissionAppConnectionActions {
@ -50,7 +51,8 @@ export enum OrgPermissionSubjects {
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections",
Kmip = "kmip"
Kmip = "kmip",
DedicatedInstance = "dedicatedInstance"
}
export type AppConnectionSubjectFields = {
@ -81,7 +83,8 @@ export type OrgPermissionSet =
)
]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [OrgPermissionActions, OrgPermissionSubjects.DedicatedInstance];
const AppConnectionConditionSchema = z
.object({
@ -180,6 +183,10 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.DedicatedInstance).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
})
]);
@ -271,6 +278,11 @@ const buildAdminPermission = () => {
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
can(OrgPermissionActions.Create, OrgPermissionSubjects.DedicatedInstance);
can(OrgPermissionActions.Read, OrgPermissionSubjects.DedicatedInstance);
can(OrgPermissionActions.Update, OrgPermissionSubjects.DedicatedInstance);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.DedicatedInstance);
return rules;
};
@ -301,6 +313,8 @@ const buildMemberPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionActions.Read, OrgPermissionSubjects.DedicatedInstance);
return rules;
};

View File

@ -228,9 +228,7 @@ const envSchema = z
if (!val) return undefined;
return JSON.parse(val) as string[];
})
),
AI_API_KEY: zpStr(z.string().optional())
)
})
// To ensure that basic encryption is always possible.
.refine(

View File

@ -39,7 +39,6 @@ export enum QueueName {
DynamicSecretRevocation = "dynamic-secret-revocation",
CaCrlRotation = "ca-crl-rotation",
SecretReplication = "secret-replication",
AutomatedSecurity = "automated-security",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration",
AccessTokenStatusUpdate = "access-token-status-update",
@ -73,8 +72,7 @@ export enum QueueJobs {
SecretSyncSyncSecrets = "secret-sync-sync-secrets",
SecretSyncImportSecrets = "secret-sync-import-secrets",
SecretSyncRemoveSecrets = "secret-sync-remove-secrets",
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
ProfileIdentity = "profile-identity"
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications"
}
export type TQueueJobTypes = {
@ -197,10 +195,6 @@ export type TQueueJobTypes = {
};
};
};
[QueueName.AutomatedSecurity]: {
name: QueueJobs.ProfileIdentity;
payload: undefined;
};
[QueueName.AppConnectionSecretSync]:
| {
name: QueueJobs.SecretSyncSyncSecrets;

View File

@ -105,9 +105,6 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { automatedSecurityReportDALFactory } from "@app/services/automated-security/automated-security-report-dal";
import { automatedSecurityServiceFactory } from "@app/services/automated-security/automated-security-service";
import { identityProfileDALFactory } from "@app/services/automated-security/identity-profile-dal";
import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { certificateDALFactory } from "@app/services/certificate/certificate-dal";
import { certificateServiceFactory } from "@app/services/certificate/certificate-service";
@ -235,6 +232,8 @@ import { webhookDALFactory } from "@app/services/webhook/webhook-dal";
import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
import { workflowIntegrationDALFactory } from "@app/services/workflow-integration/workflow-integration-dal";
import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
import { dedicatedInstanceDALFactory } from "@app/ee/services/dedicated-instance/dedicated-instance-dal";
import { dedicatedInstanceServiceFactory } from "@app/ee/services/dedicated-instance/dedicated-instance-service";
import { injectAuditLogInfo } from "../plugins/audit-log";
import { injectIdentity } from "../plugins/auth/inject-identity";
@ -395,8 +394,6 @@ export const registerRoutes = async (
const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db);
const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db);
const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db);
const automatedSecurityReportDAL = automatedSecurityReportDALFactory(db);
const identityProfileDAL = identityProfileDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
@ -1247,7 +1244,6 @@ export const registerRoutes = async (
permissionService,
licenseService
});
const identityUaService = identityUaServiceFactory({
identityOrgMembershipDAL,
permissionService,
@ -1256,7 +1252,6 @@ export const registerRoutes = async (
identityUaDAL,
licenseService
});
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
identityKubernetesAuthDAL,
identityOrgMembershipDAL,
@ -1464,12 +1459,9 @@ export const registerRoutes = async (
permissionService
});
const automatedSecurityService = automatedSecurityServiceFactory({
auditLogDAL,
automatedSecurityReportDAL,
identityProfileDAL,
orgDAL,
queueService,
const dedicatedInstanceDAL = dedicatedInstanceDALFactory(db);
const dedicatedInstanceService = dedicatedInstanceServiceFactory({
dedicatedInstanceDAL,
permissionService
});
@ -1574,7 +1566,7 @@ export const registerRoutes = async (
secretSync: secretSyncService,
kmip: kmipService,
kmipOperation: kmipOperationService,
automatedSecurity: automatedSecurityService
dedicatedInstance: dedicatedInstanceService
});
const cronJobs: CronJob[] = [];

View File

@ -1,72 +0,0 @@
import z from "zod";
import { AutomatedSecurityReportsSchema } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerAutomatedSecurityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/trigger",
handler: async () => {
return server.services.automatedSecurity.processSecurityJob();
}
});
server.route({
method: "GET",
url: "/reports",
schema: {
response: {
200: AutomatedSecurityReportsSchema.extend({
userId: z.string().nullish(),
name: z.string().nullish()
}).array()
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.automatedSecurity.getReports(req.permission.orgId);
}
});
server.route({
method: "PATCH",
url: "/reports/:id/status",
schema: {
params: z.object({
id: z.string()
}),
response: {
200: AutomatedSecurityReportsSchema.extend({
userId: z.string().nullish(),
name: z.string().nullish()
}).array()
},
body: z.object({
status: z.enum(["ignored", "resolved"])
})
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.automatedSecurity.patchSecurityReportStatus(req.params.id, req.body.status);
}
});
server.route({
method: "POST",
url: "/project-permission/analyze",
schema: {
body: z.object({
projectId: z.string(),
userId: z.string()
}),
response: {
200: z.any()
}
},
handler: async (req) => {
return server.services.automatedSecurity.analyzeIdentityProjectPermission(req.body.userId, req.body.projectId);
}
});
};

View File

@ -8,7 +8,6 @@ import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/
import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router";
import { registerAutomatedSecurityRouter } from "./automated-security-router";
import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router";
import { registerCertRouter } from "./certificate-router";
@ -142,6 +141,4 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
},
{ prefix: "/secret-syncs" }
);
await server.register(registerAutomatedSecurityRouter, { prefix: "/automated-security" });
};

View File

@ -1,49 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TAutomatedSecurityReportDALFactory = ReturnType<typeof automatedSecurityReportDALFactory>;
export const automatedSecurityReportDALFactory = (db: TDbClient) => {
const automatedSecurityReportOrm = ormify(db, TableName.AutomatedSecurityReports);
const findByOrg = (orgId: string, status = "pending") => {
return db(TableName.AutomatedSecurityReports)
.join(
TableName.IdentityProfile,
`${TableName.IdentityProfile}.id`,
`${TableName.AutomatedSecurityReports}.profileId`
)
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.IdentityProfile}.userId`)
.where(`${TableName.IdentityProfile}.orgId`, "=", orgId)
.where(`${TableName.AutomatedSecurityReports}.status`, "=", status)
.select(
selectAllTableCols(TableName.AutomatedSecurityReports),
db.ref("userId").withSchema(TableName.IdentityProfile).as("userId"),
db.ref("email").withSchema(TableName.Users).as("name")
);
};
const findById = (id: string) => {
return db(TableName.AutomatedSecurityReports)
.join(
TableName.IdentityProfile,
`${TableName.IdentityProfile}.id`,
`${TableName.AutomatedSecurityReports}.profileId`
)
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.IdentityProfile}.userId`)
.where(`${TableName.AutomatedSecurityReports}.id`, "=", id)
.select(
selectAllTableCols(TableName.AutomatedSecurityReports),
selectAllTableCols(TableName.IdentityProfile),
db.ref("email").withSchema(TableName.Users).as("name")
)
.first();
};
return {
...automatedSecurityReportOrm,
findByOrg,
findById
};
};

View File

@ -1,540 +0,0 @@
import { OpenAI } from "openai";
import { zodResponseFormat } from "openai/helpers/zod";
import z from "zod";
import { ActionProjectType, TAuditLogs } from "@app/db/schemas";
import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { AuthMethod } from "../auth/auth-type";
import { TOrgDALFactory } from "../org/org-dal";
import { TAutomatedSecurityReportDALFactory } from "./automated-security-report-dal";
import { TIdentityProfileDALFactory } from "./identity-profile-dal";
type TAutomatedSecurityServiceFactoryDep = {
auditLogDAL: TAuditLogDALFactory;
automatedSecurityReportDAL: TAutomatedSecurityReportDALFactory;
permissionService: TPermissionServiceFactory;
identityProfileDAL: TIdentityProfileDALFactory;
queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById" | "stopRepeatableJob">;
orgDAL: TOrgDALFactory;
};
export type TAutomatedSecurityServiceFactory = ReturnType<typeof automatedSecurityServiceFactory>;
export const ProfileIdentityResponseSchema = z.object({
temporalProfile: z.string(),
scopeProfile: z.string(),
usageProfile: z.string()
});
enum AnomalySeverity {
LOW = "LOW",
MEDIUM = "MEDIUM",
HIGH = "HIGH",
NONE = "NONE"
}
export const AnomalyResponseSchema = z.object({
results: z
.object({
auditLogId: z.string(),
reason: z.string(),
severity: z.nativeEnum(AnomalySeverity),
isAnomalous: z.boolean()
})
.array()
});
export const CaslRuleAnalysisSchema = z.object({
results: z
.object({
rule: z.string(),
classification: z.string(),
justification: z.string()
})
.array()
});
export const automatedSecurityServiceFactory = ({
auditLogDAL,
automatedSecurityReportDAL,
identityProfileDAL,
permissionService,
queueService,
orgDAL
}: TAutomatedSecurityServiceFactoryDep) => {
const getReports = async (orgId: string) => {
const automatedReports = await automatedSecurityReportDAL.findByOrg(orgId, "pending");
return automatedReports;
};
const patchSecurityReportStatus = async (id: string, status: "resolved" | "ignored") => {
const appCfg = getConfig();
const securityReport = await automatedSecurityReportDAL.findById(id);
if (!securityReport) {
throw new NotFoundError({
message: "Cannot find security report"
});
}
if (status === "ignored") {
const openAiClient = new OpenAI({
apiKey: appCfg.AI_API_KEY
});
const profileResponse = await openAiClient.beta.chat.completions.parse({
model: "gpt-4o",
messages: [
{
role: "system",
content: `You are a security behavior analysis system that builds and maintains behavioral profiles.
Your task is to analyze security events and prior profile information to generate cumulative profiles.
Focus on three aspects:
1. Temporal patterns (WHEN/HOW OFTEN)
2. Scope patterns (WHAT/WHERE)
3. Usage patterns (HOW)
Build comprehensive profiles:
- Each profile should be 5-7 sentences to capture both established and emerging patterns
- Core patterns get 1-2 sentences
- Notable exceptions get 1-2 sentences
- Emerging behaviors get 1-2 sentences
- Key correlations get 1 sentence
Build profiles incrementally:
- ALWAYS retain existing profile information unless directly contradicted
- Add ANY new observed patterns, even from limited samples
- Mark patterns as "emerging" if based on few observations
- Use qualifiers like "occasionally" or "sometimes" for sparse patterns
- Never dismiss patterns as insignificant due to sample size
- Maintain history of both frequent and rare behaviors
When updating profiles:
- Keep all historical patterns unless explicitly contradicted
- Add new patterns with appropriate frequency qualifiers
- Note both common and occasional behaviors
- Use tentative language for new patterns ("appears to", "beginning to")
- Highlight any changes or new observations
Remember that dates being passed in are in ISO format`
},
{
role: "user",
content: `Current behavioral profiles:
${JSON.stringify(
{
temporalProfile: securityReport.temporalProfile,
scopeProfile: securityReport.scopeProfile,
usageProfile: securityReport.usageProfile
},
null,
2
)}
New security event to analyze:
${JSON.stringify(securityReport.event)}
Generate updated profiles that preserve ALL existing patterns and add new observations.
Use appropriate qualifiers for frequency but include all behaviors.
Return exactly in this format:
temporalProfile: "..."
scopeProfile: "..."
usageProfile: "..."`
}
],
response_format: zodResponseFormat(ProfileIdentityResponseSchema, "profile_identity_response_schema")
});
const parsedResponse = profileResponse.choices[0].message.parsed;
await identityProfileDAL.updateById(securityReport.profileId, {
temporalProfile: parsedResponse?.temporalProfile ?? "",
usageProfile: parsedResponse?.usageProfile ?? "",
scopeProfile: parsedResponse?.scopeProfile ?? ""
});
}
await automatedSecurityReportDAL.updateById(id, {
status
});
};
const processSecurityJob = async () => {
const appCfg = getConfig();
const orgs = await orgDAL.find({
id: "1edc8c6e-8a39-499e-89a2-5d26149149cb"
});
await Promise.allSettled(
orgs.map(async (org) => {
const orgUsers = await orgDAL.findAllOrgMembers(org.id);
const dateNow = new Date();
const startDate = new Date(dateNow);
startDate.setMinutes(dateNow.getMinutes() - 30);
await Promise.all(
orgUsers.map(async (orgUser) => {
try {
const openAiClient = new OpenAI({
apiKey: appCfg.AI_API_KEY
});
const auditLogEvents = await auditLogDAL.find({
actorId: orgUser.user.id,
orgId: org.id,
startDate: startDate.toISOString(),
limit: 100
});
if (!auditLogEvents.length) {
return;
}
let normalEvents: TAuditLogs[] = auditLogEvents;
const identityProfile = await identityProfileDAL.transaction(async (tx) => {
const profile = await identityProfileDAL.findOne(
{
userId: orgUser.user.id
},
tx
);
if (!profile) {
return identityProfileDAL.create(
{
userId: orgUser.user.id,
temporalProfile: "",
usageProfile: "",
scopeProfile: "",
orgId: org.id
},
tx
);
}
return profile;
});
if (identityProfile.usageProfile && identityProfile.scopeProfile && identityProfile.temporalProfile) {
logger.info("Checking for anomalies...");
const anomalyResponse = await openAiClient.beta.chat.completions.parse({
model: "gpt-4o",
messages: [
{
role: "system",
content: `
You are a security analysis system that uses behavioral profiles to identify potentially suspicious activity for an identity.
Understanding Identity Profiles:
- Temporal Profile: When and how frequently the identity normally operates
- Scope Profile: What resources and permissions the identity typically uses
- Usage Profile: How the identity normally interacts with systems
Security Analysis Rules:
HIGH Severity - Clear security concerns:
- Actions well outside the identity's normal scope of access
- Resource access patterns suggesting compromise
- Violation of critical security boundaries
MEDIUM Severity - Requires investigation:
- Significant expansion of identity's normal access patterns
- Unusual combination of valid permissions
- Behavior suggesting possible credential misuse
NONE Severity (Default) - Expected behavior:
- Actions within established identity patterns
- Minor variations in normal behavior
- Business-justified changes in access patterns
- New but authorized behavior extensions
Note: LOW severity should not be used - an action either
indicates a security concern (HIGH/MEDIUM) or it doesn't (NONE).
Analysis Process:
1. Compare event against identity's established profiles
2. Evaluate if deviations suggest potential security risks
3. Consider business context and authorization
4. Mark as anomalous ONLY if the deviation suggests security risk`
},
{
role: "user",
content: `Given these established behavioral profiles for an identity:
temporalProfile: "${identityProfile.temporalProfile}"
scopeProfile: "${identityProfile.scopeProfile}"
usageProfile: "${identityProfile.usageProfile}"
Analyze these events for anomalies:
${JSON.stringify(auditLogEvents, null, 2)}
Return response in this exact JSON format:
{ results: [
{
"auditLogId": "event-123",
"isAnomalous": false,
"reason": "",
"severity": "NONE"
},
{
"auditLogId": "event-124",
"isAnomalous": true,
"reason": "Accessing production secrets outside business hours",
"severity": "HIGH"
}
]}
`
}
],
response_format: zodResponseFormat(AnomalyResponseSchema, "anomaly_response_schema")
});
const parsedAnomalyResponse = anomalyResponse.choices[0].message.parsed;
const anomalyEvents = parsedAnomalyResponse?.results?.filter((val) => val.isAnomalous);
console.log("ANOMALY EVENTS:", anomalyEvents);
const anomalyEventMap = anomalyEvents?.reduce((accum, item) => {
return { ...accum, [item.auditLogId]: item };
}, {}) as {
[x: string]: {
reason: string;
severity: string;
};
};
const anomalousEventIds = anomalyEvents?.map((evt) => evt.auditLogId);
normalEvents = auditLogEvents.filter((evt) => !anomalousEventIds?.includes(evt.id));
await Promise.all(
(auditLogEvents ?? [])?.map(async (evt) => {
const anomalyDetails = anomalyEventMap[evt.id];
if (!anomalyDetails) {
return;
}
await automatedSecurityReportDAL.create({
status: "pending",
profileId: identityProfile.id,
remarks: anomalyDetails.reason,
severity: anomalyDetails.severity,
event: JSON.stringify(evt, null, 2)
});
})
);
}
console.log("Calibrating identity profile");
// profile identity
const profileResponse = await openAiClient.beta.chat.completions.parse({
model: "gpt-4o",
messages: [
{
role: "system",
content: `You are a security behavior analysis system that builds and maintains behavioral profiles.
Your task is to analyze security events and prior profile information to generate cumulative profiles.
Focus on three aspects:
1. Temporal patterns (WHEN/HOW OFTEN)
2. Scope patterns (WHAT/WHERE)
3. Usage patterns (HOW)
Build comprehensive profiles:
- Each profile should be 5-7 sentences to capture both established and emerging patterns
- Core patterns get 1-2 sentences
- Notable exceptions get 1-2 sentences
- Emerging behaviors get 1-2 sentences
- Key correlations get 1 sentence
Build profiles incrementally:
- ALWAYS retain existing profile information unless directly contradicted
- Add ANY new observed patterns, even from limited samples
- Mark patterns as "emerging" if based on few observations
- Use qualifiers like "occasionally" or "sometimes" for sparse patterns
- Never dismiss patterns as insignificant due to sample size
- Maintain history of both frequent and rare behaviors
When updating profiles:
- Keep all historical patterns unless explicitly contradicted
- Add new patterns with appropriate frequency qualifiers
- Note both common and occasional behaviors
- Use tentative language for new patterns ("appears to", "beginning to")
- Highlight any changes or new observations
Remember that dates being passed in are in ISO format`
},
{
role: "user",
content: `Current behavioral profiles:
${JSON.stringify(
{
temporalProfile: identityProfile.temporalProfile,
scopeProfile: identityProfile.scopeProfile,
usageProfile: identityProfile.usageProfile
},
null,
2
)}
New security events to analyze:
${JSON.stringify(normalEvents, null, 2)}
Generate updated profiles that preserve ALL existing patterns and add new observations.
Use appropriate qualifiers for frequency but include all behaviors.
Return exactly in this format:
temporalProfile: "..."
scopeProfile: "..."
usageProfile: "..."`
}
],
response_format: zodResponseFormat(ProfileIdentityResponseSchema, "profile_identity_response_schema")
});
const parsedResponse = profileResponse.choices[0].message.parsed;
await identityProfileDAL.updateById(identityProfile.id, {
temporalProfile: parsedResponse?.temporalProfile ?? "",
usageProfile: parsedResponse?.usageProfile ?? "",
scopeProfile: parsedResponse?.scopeProfile ?? ""
});
console.log("FINISH");
} catch (err) {
logger.error(err);
throw err;
}
})
);
})
);
};
const analyzeIdentityProjectPermission = async (userId: string, projectId: string) => {
const appCfg = getConfig();
const userProjectPermission = await permissionService.getUserProjectPermission({
userId,
projectId,
authMethod: AuthMethod.EMAIL,
actionProjectType: ActionProjectType.Any,
userOrgId: "1edc8c6e-8a39-499e-89a2-5d26149149cb"
});
const identityProfile = await identityProfileDAL.findOne({
userId
});
const openAiClient = new OpenAI({
apiKey: appCfg.AI_API_KEY
});
const caslRuleAnalysisResponse = await openAiClient.beta.chat.completions.parse({
model: "gpt-4o",
messages: [
{
role: "system",
content: `You are a security analysis system that evaluates CASL permission rules against behavioral profiles.
Your task is to identify CASL rules that appear unnecessary based on the identity's observed behavior.
Analysis process:
1. Review the identity's behavioral profiles (temporal, scope, usage)
2. Examine current CASL permission rules
3. Identify rules that grant permissions never or rarely used based on the profiles
4. Consider business context and security implications before marking rules as unnecessary
Classification criteria:
- UNNECESSARY: Rule grants permissions never observed in the behavior profile
- OVERPROVISIONED: Rule grants broader access than typically used
- QUESTIONABLE: Rule grants permissions used very rarely (<5% of activity)
Provide justification for each classification based on specific patterns in the profiles.
Consider both explicit patterns (directly mentioned) and implicit patterns (strongly implied).
Remember that security is the priority - when in doubt, mark a rule as QUESTIONABLE rather than UNNECESSARY.`
},
{
role: "user",
content: `Identity Behavioral Profiles:
${JSON.stringify(
{
temporalProfile: identityProfile.temporalProfile,
scopeProfile: identityProfile.scopeProfile,
usageProfile: identityProfile.usageProfile
},
null,
2
)}
Current CASL Permission Rules:
${JSON.stringify(userProjectPermission.permission.rules, null, 2)}
Analyze these rules against the behavioral profiles and identify which rules appear unnecessary.
Return in this exact format:
{
"results": [
{
"rule": "create secret",
"classification": "UNNECESSARY",
"justification": "No evidence in profile of needing this resource",
},
{
"rule": "delete secret",
"classification": "NECESSARY",
"justification": "Regular access pattern in usage profile",
}
]
}`
}
],
response_format: zodResponseFormat(CaslRuleAnalysisSchema, "casl_rule_analysis_schema")
});
return caslRuleAnalysisResponse.choices[0].message.parsed;
};
const startJob = async () => {
await queueService.stopRepeatableJob(
QueueName.AutomatedSecurity,
QueueJobs.ProfileIdentity,
{ pattern: "0 0 * * *", utc: true },
QueueName.AutomatedSecurity // just a job id
);
await queueService.queue(QueueName.AutomatedSecurity, QueueJobs.ProfileIdentity, undefined, {
delay: 5000,
jobId: QueueName.AutomatedSecurity,
repeat: { pattern: "0 0 * * *", utc: true }
});
};
queueService.start(QueueName.AutomatedSecurity, async (job) => {
if (job.name === QueueJobs.ProfileIdentity) {
await processSecurityJob();
}
});
queueService.listen(QueueName.AutomatedSecurity, "failed", (job, err) => {
logger.error(err, "Failed to process job", job?.data);
});
return {
processSecurityJob,
patchSecurityReportStatus,
analyzeIdentityProjectPermission,
startJob,
getReports
};
};

View File

@ -1,11 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityProfileDALFactory = ReturnType<typeof identityProfileDALFactory>;
export const identityProfileDALFactory = (db: TDbClient) => {
const identityProfileOrm = ormify(db, TableName.IdentityProfile);
return identityProfileOrm;
};

View File

@ -1,2 +0,0 @@
export * from "./mutations";
export * from "./queries";

View File

@ -1,20 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { automatedSecurityKeys } from "./queries";
export const usePatchSecurityReportStatus = (orgId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, status }: { id: string; status: string }) => {
await apiRequest.patch(`/api/v1/automated-security/reports/${id}/status`, {
status
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: automatedSecurityKeys.getReports(orgId) });
}
});
};

View File

@ -1,29 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
export const automatedSecurityKeys = {
getReports: (orgId: string) => [{ orgId }, "organization-security-reports"] as const
};
export const useGetAutomatedSecurityReports = (orgId: string) => {
return useQuery({
queryKey: automatedSecurityKeys.getReports(orgId),
queryFn: async () => {
const { data } = await apiRequest.get<
{
id: string;
profileId: string;
event: string;
remarks: string;
severity: string;
status: string;
userId: string;
name: string;
}[]
>("/api/v1/automated-security/reports");
return data;
}
});
};

View File

@ -74,6 +74,13 @@ export const OrganizationLayout = () => {
</MenuItem>
)}
</Link>
<Link to="/organization/dedicated-instances">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="moving-block">
Dedicated Instances
</MenuItem>
)}
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (

View File

@ -13,7 +13,8 @@ import {
faPlug,
faSignOut,
faUser,
faUsers
faUsers,
faServer
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
@ -332,6 +333,11 @@ export const MinimizedOrgSidebar = () => {
App Connections
</DropdownMenuItem>
</Link>
<Link to="/organization/dedicated-instances">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faServer} />}>
Dedicated Instances
</DropdownMenuItem>
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
@ -348,11 +354,6 @@ export const MinimizedOrgSidebar = () => {
Audit Logs
</DropdownMenuItem>
</Link>
<Link to="/organization/automated-security">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faCog} />}>
Automated Security
</DropdownMenuItem>
</Link>
<Link to="/organization/settings">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faCog} />}>
Organization Settings

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances')({
component: DedicatedInstancesPage,
});

View File

@ -1,103 +0,0 @@
import { Helmet } from "react-helmet";
import { createNotification } from "@app/components/notifications";
import {
Button,
PageHeader,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetAutomatedSecurityReports,
usePatchSecurityReportStatus
} from "@app/hooks/api/automated-security";
export const AutomatedSecurityPage = () => {
const { currentOrg } = useOrganization();
const { data: reportEntries } = useGetAutomatedSecurityReports(currentOrg.id);
const { mutateAsync: patchSecurityReportStatus } = usePatchSecurityReportStatus(currentOrg.id);
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>Infisical | Automated Security</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
title="Automated Security"
description="Your organization's reliable AI security guard"
/>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Identity ID</Th>
<Th>Name</Th>
<Th>Event</Th>
<Th>Remarks</Th>
<Th>Severity</Th>
<Th />
</Tr>
</THead>
<TBody>
{(reportEntries ?? []).map((entry) => (
<Tr>
<Td>{entry.userId}</Td>
<Td>{entry.name}</Td>
<Td>{JSON.stringify(entry.event, null, 2)}</Td>
<Td>{entry.remarks}</Td>
<Td>{entry.severity}</Td>
<Td>
<Button
className="mb-4 w-[5rem]"
onClick={async () => {
await patchSecurityReportStatus({
id: entry.id,
status: "resolved"
});
createNotification({
type: "success",
text: "Successfully resolved security report"
});
}}
>
Resolve
</Button>
<Button
colorSchema="secondary"
className="w-[5rem]"
onClick={async () => {
await patchSecurityReportStatus({
id: entry.id,
status: "ignored"
});
createNotification({
type: "success",
text: "Successfully ignored security report"
});
}}
>
Ignore
</Button>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</div>
</div>
</div>
);
};

View File

@ -1,23 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AutomatedSecurityPage } from "./AutomatedSecurityPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/automated-security"
)({
component: AutomatedSecurityPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Automated Security Page"
}
]
})
});

View File

@ -0,0 +1,371 @@
import { useState } from "react";
import {
Button,
Card,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Table,
TableContainer,
TBody,
Th,
THead,
Tr,
Td,
IconButton,
EmptyState,
Badge,
Tooltip,
Spinner
} from "@app/components/v2";
import {
faServer,
faBookOpen,
faArrowUpRightFromSquare,
faHome,
faTerminal,
faKey,
faLock,
faRotateRight,
faExclamationTriangle,
faChevronRight
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useParams } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { useOrganization } from "@app/context/OrganizationContext";
import { apiRequest } from "@app/config/request";
interface DedicatedInstanceDetails {
id: string;
orgId: string;
instanceName: string;
subdomain: string;
status: "RUNNING" | "UPGRADING" | "PROVISIONING" | "FAILED";
version: string;
versionUpgrades: "Automatic" | "Manual";
clusterId: string;
clusterTier: "Development" | "Production";
clusterSize: string;
highAvailability: boolean;
createdAt: string;
region: string;
provider: string;
publicAccess: boolean;
ipAllowlist: "Enabled" | "Disabled";
}
const fetchInstanceDetails = async (organizationId: string, instanceId: string) => {
const { data } = await apiRequest.get<DedicatedInstanceDetails>(
`/api/v1/organizations/${organizationId}/dedicated-instances/${instanceId}`
);
return data;
};
export const DedicatedInstanceDetailsPage = () => {
const { currentOrg } = useOrganization();
const { instanceId } = useParams({
from: "/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId"
});
const [isAPILockModalOpen, setIsAPILockModalOpen] = useState(false);
const [isRevokeTokensModalOpen, setIsRevokeTokensModalOpen] = useState(false);
const { data: instance, isLoading, error } = useQuery({
queryKey: ["dedicatedInstance", instanceId],
queryFn: () => fetchInstanceDetails(currentOrg?.id || "", instanceId || ""),
enabled: Boolean(currentOrg?.id && instanceId)
});
if (isLoading) {
return (
<div className="flex h-full items-center justify-center">
<Spinner size="lg" />
</div>
);
}
if (error || !instance) {
return (
<div className="flex h-full items-center justify-center">
<EmptyState
title="Error loading instance details"
icon={faServer}
>
<p className="text-sm text-bunker-400">
{error instanceof Error ? error.message : "Failed to load instance details"}
</p>
</EmptyState>
</div>
);
}
return (
<div className="flex h-full flex-col space-y-4 p-4">
<div className="mb-6">
<div className="flex items-center gap-2 text-sm text-mineshaft-300">
<Link to="/" className="flex items-center gap-1 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faHome} />
<span>Home</span>
</Link>
<FontAwesomeIcon icon={faChevronRight} className="text-xs" />
<Link to="/organization/dedicated-instances" className="hover:text-mineshaft-200">
Dedicated Instances
</Link>
<FontAwesomeIcon icon={faChevronRight} className="text-xs" />
<span className="text-mineshaft-200">{instance.instanceName}</span>
</div>
<div className="mt-6 flex items-center justify-between">
<div>
<h1 className="text-3xl font-semibold text-mineshaft-100">
{instance.instanceName} <span className="text-mineshaft-300">({instance.region})</span>
</h1>
</div>
<div>
<Button
variant="solid"
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faArrowUpRightFromSquare} />}
onClick={() => window.open(`https://${instance.subdomain}.infisical.com`, "_blank")}
>
Launch Web UI
</Button>
</div>
</div>
{instance.publicAccess && (
<div className="mt-4 flex items-center gap-2 rounded-lg bg-yellow-500/10 px-4 py-3 text-sm text-yellow-500">
<FontAwesomeIcon icon={faExclamationTriangle} />
<p>
This cluster's network configuration is set to public. Configure an IP Allowlist in the cluster's networking
settings to limit network access to the cluster's public endpoint.
</p>
</div>
)}
</div>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="grid grid-cols-2 gap-4">
<Card className="col-span-1 rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Cluster Details</h2>
<div className="space-y-4">
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Status</span>
<Badge variant={instance.status === "RUNNING" ? "success" : "primary"}>
{instance.status}
</Badge>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Vault Version</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.version}</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Version upgrades</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.versionUpgrades}</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Cluster Size</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.clusterSize}</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">High Availability</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.highAvailability ? "Yes" : "No"}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-mineshaft-300">Created</span>
<span className="text-sm font-medium text-mineshaft-100">
{new Date(instance.createdAt).toLocaleDateString()}
</span>
</div>
</div>
</div>
</Card>
<div className="col-span-1 space-y-4">
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Quick actions</h2>
<div className="grid grid-cols-2 gap-4">
<Card className="bg-mineshaft-700 rounded-lg">
<div className="p-4">
<h3 className="mb-2 text-sm font-medium text-mineshaft-100">How to access via</h3>
<div className="flex gap-4">
<Button
variant="outline"
colorSchema="secondary"
size="sm"
leftIcon={<FontAwesomeIcon icon={faTerminal} />}
>
Command-line (CLI)
</Button>
<Button
variant="outline"
colorSchema="secondary"
size="sm"
leftIcon={<FontAwesomeIcon icon={faKey} />}
>
API
</Button>
</div>
</div>
</Card>
<Card className="bg-mineshaft-700 rounded-lg">
<div className="p-4">
<h3 className="mb-2 text-sm font-medium text-mineshaft-100">New root token</h3>
<Tooltip content="Generate a root token">
<Button
variant="outline"
colorSchema="secondary"
size="sm"
leftIcon={<FontAwesomeIcon icon={faKey} />}
>
Generate token
</Button>
</Tooltip>
</div>
</Card>
</div>
</div>
</Card>
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Cluster URLs</h2>
<p className="mb-4 text-sm text-mineshaft-300">Copy the address into your CLI or browser to access the cluster.</p>
<div className="space-y-2">
<div className="flex items-center justify-between rounded-lg bg-mineshaft-700 p-3">
<div className="flex items-center gap-2">
<Badge variant="primary">Private</Badge>
<code className="text-sm text-mineshaft-300">https://{instance.subdomain}.infisical.com</code>
</div>
<Button variant="outline" colorSchema="secondary" size="sm">
Copy
</Button>
</div>
<div className="flex items-center justify-between rounded-lg bg-mineshaft-700 p-3">
<div className="flex items-center gap-2">
<Badge variant="primary">Public</Badge>
<code className="text-sm text-mineshaft-300">https://{instance.subdomain}.infisical.com</code>
</div>
<Button variant="outline" colorSchema="secondary" size="sm">
Copy
</Button>
</div>
</div>
</div>
</Card>
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">Cluster networking</h2>
<div className="space-y-4">
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Provider/region</span>
<span className="text-sm font-medium text-mineshaft-100">
{instance.provider} ({instance.region})
</span>
</div>
<div className="flex justify-between border-b border-mineshaft-700 pb-2">
<span className="text-sm text-mineshaft-300">Cluster accessibility</span>
<Badge variant={instance.publicAccess ? "danger" : "success"}>
{instance.publicAccess ? "Public" : "Private"}
</Badge>
</div>
<div className="flex justify-between">
<span className="text-sm text-mineshaft-300">IP allowlist</span>
<span className="text-sm font-medium text-mineshaft-100">{instance.ipAllowlist}</span>
</div>
</div>
</div>
</Card>
<Card className="rounded-lg border border-mineshaft-600 bg-mineshaft-800">
<div className="p-4">
<h2 className="mb-4 text-lg font-semibold text-mineshaft-100">In case of emergency</h2>
<div className="space-y-4">
<Button
variant="outline"
colorSchema="danger"
className="w-full"
leftIcon={<FontAwesomeIcon icon={faLock} />}
onClick={() => setIsAPILockModalOpen(true)}
>
API Lock
</Button>
<Button
variant="outline"
colorSchema="danger"
className="w-full"
leftIcon={<FontAwesomeIcon icon={faRotateRight} />}
onClick={() => setIsRevokeTokensModalOpen(true)}
>
Revoke all admin tokens
</Button>
</div>
</div>
</Card>
</div>
</div>
</div>
<Modal isOpen={isAPILockModalOpen} onOpenChange={setIsAPILockModalOpen}>
<ModalContent
title="API Lock"
subTitle="Are you sure you want to lock the API? This will prevent all API access until unlocked."
>
<div className="mt-8 flex justify-end space-x-4">
<Button
variant="plain"
colorSchema="secondary"
onClick={() => setIsAPILockModalOpen(false)}
>
Cancel
</Button>
<Button
variant="solid"
colorSchema="danger"
onClick={() => {
// Handle API lock
setIsAPILockModalOpen(false);
}}
>
Lock API
</Button>
</div>
</ModalContent>
</Modal>
<Modal isOpen={isRevokeTokensModalOpen} onOpenChange={setIsRevokeTokensModalOpen}>
<ModalContent
title="Revoke Admin Tokens"
subTitle="Are you sure you want to revoke all admin tokens? This action cannot be undone."
>
<div className="mt-8 flex justify-end space-x-4">
<Button
variant="plain"
colorSchema="secondary"
onClick={() => setIsRevokeTokensModalOpen(false)}
>
Cancel
</Button>
<Button
variant="solid"
colorSchema="danger"
onClick={() => {
// Handle token revocation
setIsRevokeTokensModalOpen(false);
}}
>
Revoke Tokens
</Button>
</div>
</ModalContent>
</Modal>
</div>
);
};

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstanceDetailsPage } from '../DedicatedInstanceDetailsPage';
export const Route = createFileRoute('/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage
});

View File

@ -0,0 +1,602 @@
import { useState } from "react";
import {
Button,
Card,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
DropdownMenu,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuContent,
Table,
TableContainer,
TBody,
Th,
THead,
Tr,
Td,
IconButton,
EmptyState,
FormHelperText,
Spinner
} from "@app/components/v2";
import {
faPlus,
faSearch,
faServer,
faEllipsisVertical,
faBookOpen,
faArrowUpRightFromSquare,
faArrowDown,
faArrowUp,
faMagnifyingGlass,
faHome,
faSpinner
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Link, useNavigate } from "@tanstack/react-router";
import { useQuery, useMutation, useQueryClient, useQueries } from "@tanstack/react-query";
import { useOrganization } from "@app/context/OrganizationContext";
import { apiRequest } from "@app/config/request";
interface DedicatedInstance {
id: string;
orgId: string;
instanceName: string;
subdomain: string;
status: "RUNNING" | "UPGRADING" | "PROVISIONING" | "FAILED";
stackStatus?: string;
stackStatusReason?: string;
events?: Array<{
timestamp?: Date;
logicalResourceId?: string;
resourceType?: string;
resourceStatus?: string;
resourceStatusReason?: string;
}>;
rdsInstanceType: string;
elasticCacheType: string;
elasticContainerMemory: number;
elasticContainerCpu: number;
region: string;
version: string;
backupRetentionDays: number;
lastBackupTime: string | null;
lastUpgradeTime: string | null;
publiclyAccessible: boolean;
vpcId: string | null;
subnetIds: string[] | null;
tags: Record<string, string> | null;
createdAt: string;
updatedAt: string;
}
const INSTANCE_SIZES = [
{ value: "small", label: "Small (2 vCPU, 8GB RAM)" },
{ value: "medium", label: "Medium (4 vCPU, 16GB RAM)" },
{ value: "large", label: "Large (8 vCPU, 32GB RAM)" }
];
type CloudProvider = "aws" | "gcp" | "azure";
interface CreateInstancePayload {
instanceName: string;
subdomain: string;
region: string;
provider: "aws";
publiclyAccessible: boolean;
clusterSize: "small" | "medium" | "large";
}
const INITIAL_INSTANCE_STATE: CreateInstancePayload = {
instanceName: "",
subdomain: "",
region: "",
provider: "aws",
publiclyAccessible: false,
clusterSize: "small"
};
const PROVIDERS: Array<{
value: CloudProvider;
label: string;
image: string;
description: string;
}> = [
{
value: "aws",
label: "Amazon Web Services",
image: "/images/integrations/Amazon Web Services.png",
description: "Deploy your instance on AWS infrastructure"
},
{
value: "gcp",
label: "Google Cloud Platform",
image: "/images/integrations/Google Cloud Platform.png",
description: "Deploy your instance on Google Cloud infrastructure"
},
{
value: "azure",
label: "Microsoft Azure",
image: "/images/integrations/Microsoft Azure.png",
description: "Deploy your instance on Azure infrastructure"
}
];
const REGIONS = {
aws: [
{ value: "us-east-1", label: "US East (N. Virginia)" },
{ value: "us-west-2", label: "US West (Oregon)" },
{ value: "eu-west-1", label: "EU West (Ireland)" },
{ value: "ap-southeast-1", label: "Asia Pacific (Singapore)" }
],
gcp: [
{ value: "us-east1", label: "US East (South Carolina)" },
{ value: "us-west1", label: "US West (Oregon)" },
{ value: "europe-west1", label: "Europe West (Belgium)" },
{ value: "asia-southeast1", label: "Asia Southeast (Singapore)" }
],
azure: [
{ value: "eastus", label: "East US (Virginia)" },
{ value: "westus", label: "West US (California)" },
{ value: "westeurope", label: "West Europe (Netherlands)" },
{ value: "southeastasia", label: "Southeast Asia (Singapore)" }
]
};
interface RouteParams {
organizationId: string;
}
const dedicatedInstanceKeys = {
getInstances: (organizationId: string) => ["dedicatedInstances", { organizationId }] as const,
getInstance: (organizationId: string, instanceId: string) => ["dedicatedInstance", { organizationId, instanceId }] as const
};
const getDeploymentStage = (events?: DedicatedInstance['events']) => {
if (!events?.length) return 'Deploying';
return 'Deploying';
};
const getDeploymentProgress = (instance: DedicatedInstance) => {
if (instance.status === 'RUNNING') return { stage: 'Complete', progress: 100 };
if (instance.status === 'FAILED') return { stage: 'Failed', progress: 0 };
const events = instance.events || [];
if (events.length === 0) return { stage: 'Deploying', progress: 0 };
// Count completed events vs total events
const completedEvents = events.filter(event =>
event.resourceStatus?.includes('COMPLETE') ||
event.resourceStatus?.includes('CREATE_COMPLETE')
).length;
const progress = Math.round((completedEvents / events.length) * 100);
return { stage: 'Deploying', progress };
};
const fetchInstances = async (organizationId: string) => {
const { data } = await apiRequest.get<{ instances: DedicatedInstance[] }>(
`/api/v1/organizations/${organizationId}/dedicated-instances`
);
return data;
};
const fetchInstanceDetails = async (organizationId: string, instanceId: string) => {
const { data } = await apiRequest.get<DedicatedInstance>(
`/api/v1/organizations/${organizationId}/dedicated-instances/${instanceId}`
);
return data;
};
const createInstance = async (organizationId: string, data: CreateInstancePayload) => {
const { data: response } = await apiRequest.post<DedicatedInstance>(
`/api/v1/organizations/${organizationId}/dedicated-instances`,
data
);
return response;
};
export const DedicatedInstancesPage = () => {
const { currentOrg } = useOrganization();
const navigate = useNavigate();
const organizationId = currentOrg?.id || "";
const queryClient = useQueryClient();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [newInstance, setNewInstance] = useState(INITIAL_INSTANCE_STATE);
type InstancesResponse = { instances: DedicatedInstance[] };
// Fetch all instances
const { data: instancesData, isLoading, error } = useQuery<InstancesResponse>({
queryKey: dedicatedInstanceKeys.getInstances(organizationId),
queryFn: () => fetchInstances(organizationId),
enabled: Boolean(organizationId),
});
// Fetch details for provisioning instances
const provisioningInstances = instancesData?.instances.filter(
instance => instance.status === "PROVISIONING"
) || [];
const instanceDetailsQueries = useQueries({
queries: provisioningInstances.map(instance => ({
queryKey: dedicatedInstanceKeys.getInstance(organizationId, instance.id),
queryFn: () => fetchInstanceDetails(organizationId, instance.id),
refetchInterval: 2000, // Poll every 2 seconds
}))
});
// Merge instance details with the main instances list
const instances = instancesData?.instances.map(instance => {
if (instance.status === "PROVISIONING") {
const detailsQuery = instanceDetailsQueries.find(
q => q.data?.id === instance.id
);
return detailsQuery?.data || instance;
}
return instance;
}) || [];
// Create instance mutation
const createInstanceMutation = useMutation({
mutationFn: (data: CreateInstancePayload) => createInstance(organizationId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: dedicatedInstanceKeys.getInstances(organizationId) });
setIsCreateModalOpen(false);
setNewInstance(INITIAL_INSTANCE_STATE);
}
});
const handleCreateInstance = () => {
createInstanceMutation.mutate(newInstance);
};
const handleModalOpenChange = (open: boolean) => {
setIsCreateModalOpen(open);
if (!open) {
setNewInstance(INITIAL_INSTANCE_STATE);
}
};
// Get available regions based on selected provider
const availableRegions = newInstance.provider ? REGIONS[newInstance.provider] : [];
// Filter instances based on search term
const filteredInstances = instances.filter((instance: DedicatedInstance) =>
instance.instanceName.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="flex h-full flex-col space-y-4 p-4">
<div className="mb-6">
<div className="flex items-center gap-2 text-sm text-mineshaft-300">
<Link to="/" className="flex items-center gap-1 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faHome} />
<span>Home</span>
</Link>
<span>/</span>
<span className="text-mineshaft-200">Dedicated Instances</span>
</div>
<div className="mt-4 flex items-center gap-3">
<h1 className="text-3xl font-semibold text-mineshaft-100">Dedicated Instances</h1>
<a
href="https://infisical.com/docs/dedicated-instances/overview"
target="_blank"
rel="noopener noreferrer"
className="mt-1.5 inline-flex items-center gap-1.5 rounded-md bg-yellow/20 px-2 py-1 text-sm text-yellow opacity-80 hover:opacity-100"
>
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="text-[10px]" />
</a>
</div>
<p className="mt-2 text-base text-mineshaft-300">
Create and manage dedicated Infisical instances across different regions
</p>
</div>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-end mb-4">
<Button
onClick={() => setIsCreateModalOpen(true)}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Create Instance
</Button>
</div>
<div>
<div className="flex gap-2">
<Input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search instances..."
className="flex-1"
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/4">Name</Th>
<Th className="w-1/4">Type</Th>
<Th className="w-1/4">Region</Th>
<Th className="w-1/4">Status</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading ? (
<tr>
<td colSpan={5}>
<div className="flex items-center justify-center py-8">
<Spinner size="md" className="text-mineshaft-300" />
</div>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={5}>
<EmptyState
title="Error loading instances"
icon={faServer}
>
<p className="text-sm text-bunker-400">
{error instanceof Error ? error.message : "An error occurred while loading instances"}
</p>
</EmptyState>
</td>
</tr>
) : filteredInstances.length === 0 ? (
<tr>
<td colSpan={5}>
<EmptyState
title={searchTerm ? "No Instances match search..." : "No Instances Found"}
icon={faServer}
>
<p className="text-sm text-bunker-400">
You don't have any dedicated instances yet. Create a new one to get started.
</p>
</EmptyState>
</td>
</tr>
) : (
filteredInstances.map((instance: DedicatedInstance) => (
<Tr
key={instance.id}
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700 cursor-pointer")}
onClick={() => {
navigate({
to: "/organization/dedicated-instances/$instanceId",
params: {
instanceId: instance.id
}
});
}}
>
<Td>
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-bunker-700/50">
<FontAwesomeIcon icon={faServer} className="text-bunker-300" />
</div>
<div>
<p className="text-sm font-medium text-bunker-100">{instance.instanceName}</p>
<p className="text-xs text-bunker-300">Created {new Date(instance.createdAt).toLocaleDateString()}</p>
</div>
</div>
</Td>
<Td>
<p className="text-sm font-medium text-bunker-100">
{`${instance.elasticContainerCpu / 1024} vCPU, ${instance.elasticContainerMemory / 1024}GB RAM`}
</p>
<p className="text-xs text-bunker-300">Instance Type</p>
</Td>
<Td>
<p className="text-sm font-medium text-bunker-100">
{REGIONS.aws.find(r => r.value === instance.region)?.label || instance.region}
</p>
<p className="text-xs text-bunker-300">Region</p>
</Td>
<Td>
{instance.status === "PROVISIONING" ? (
<div className="space-y-1">
<div className="flex items-center">
<span className="text-sm font-medium text-yellow-500">
{getDeploymentProgress(instance).stage}
</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-mineshaft-600">
<div
className="h-full rounded-full bg-yellow-500/50 transition-all duration-500 [animation:pulse_0.7s_cubic-bezier(0.4,0,0.6,1)_infinite]"
style={{ width: `${getDeploymentProgress(instance).progress}%` }}
/>
</div>
</div>
) : (
<div
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
instance.status === "RUNNING"
? "bg-emerald-100/10 text-emerald-500"
: instance.status === "FAILED"
? "bg-red-100/10 text-red-500"
: "bg-yellow-100/10 text-yellow-500"
}`}
>
{instance.status.toLowerCase()}
</div>
)}
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
onClick={(e) => e.stopPropagation()}
>
<FontAwesomeIcon icon={faEllipsisVertical} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(instance.id);
}}>
Copy Instance ID
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
// TODO: Start instance
}}>
Start Instance
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
// TODO: Stop instance
}}>
Stop Instance
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
// TODO: Delete instance
}} className="text-red-500">
Delete Instance
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))
)}
</TBody>
</Table>
</TableContainer>
</div>
</div>
<Modal isOpen={isCreateModalOpen} onOpenChange={handleModalOpenChange}>
<ModalContent title="Create Dedicated Instance" subTitle="Configure your dedicated Infisical instance">
<div className="mt-6 space-y-6">
<div>
<div className="mb-2 text-sm font-medium text-mineshaft-200">Cloud Provider</div>
<div className="grid grid-cols-3 gap-2">
{PROVIDERS.map(({ value, label, image, description }) => (
<button
key={value}
type="button"
onClick={() => {
if (value === "aws") {
setNewInstance({ ...newInstance, provider: value, region: "" });
}
}}
className={twMerge(
"group relative flex h-24 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600",
newInstance.provider === value && "border-primary/50 bg-primary/10"
)}
>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-mineshaft-800">
<img
src={image}
alt={`${label} logo`}
className="h-7 w-7 object-contain"
/>
</div>
<div className="mt-2 max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{label}
</div>
</button>
))}
</div>
<div className="mt-1 text-xs text-mineshaft-400">
{PROVIDERS.find(p => p.value === newInstance.provider)?.description || "Select a cloud provider for your instance"}
</div>
</div>
<FormControl label="Instance Name" helperText="Give your instance a unique name to identify it">
<Input
value={newInstance.instanceName}
onChange={(e) => setNewInstance({ ...newInstance, instanceName: e.target.value })}
placeholder="Enter instance name"
/>
</FormControl>
<FormControl label="Subdomain" helperText="Enter the subdomain for your instance">
<div className="flex items-center">
<Input
value={newInstance.subdomain}
onChange={(e) => setNewInstance({ ...newInstance, subdomain: e.target.value })}
placeholder="your-subdomain"
className="rounded-r-none"
/>
<div className="flex h-10 items-center rounded-r-lg border border-l-0 border-mineshaft-600 bg-mineshaft-800 px-3 text-sm text-mineshaft-300">
.infisical.com
</div>
</div>
</FormControl>
<FormControl label="Region" helperText="Select the geographical location where your instance will be deployed">
<Select
value={newInstance.region}
onValueChange={(value) => setNewInstance({ ...newInstance, region: value })}
placeholder="Select region"
isDisabled={!newInstance.provider}
>
{availableRegions.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Cluster Size" helperText="Select the size of your dedicated instance">
<Select
value={newInstance.clusterSize}
onValueChange={(value) => setNewInstance({ ...newInstance, clusterSize: value as "small" | "medium" | "large" })}
placeholder="Select size"
>
<SelectItem value="small">Small (1 vCPU, 2GB RAM)</SelectItem>
<SelectItem value="medium">Medium (2 vCPU, 4GB RAM)</SelectItem>
<SelectItem value="large">Large (4 vCPU, 8GB RAM)</SelectItem>
</Select>
</FormControl>
</div>
<div className="mt-8 flex justify-end space-x-4">
<Button
variant="plain"
colorSchema="secondary"
onClick={() => setIsCreateModalOpen(false)}
size="md"
disabled={createInstanceMutation.status === "pending"}
>
Cancel
</Button>
<Button
variant="solid"
colorSchema="primary"
onClick={handleCreateInstance}
disabled={!newInstance.instanceName || !newInstance.region || !newInstance.provider || !newInstance.subdomain || createInstanceMutation.status === "pending"}
size="md"
leftIcon={createInstanceMutation.status === "pending" ? <Spinner size="xs" className="text-black" /> : undefined}
>
{createInstanceMutation.status === "pending" ? "Creating..." : "Create Instance"}
</Button>
</div>
</ModalContent>
</Modal>
</div>
);
};

View File

@ -0,0 +1,2 @@
export { DedicatedInstancesPage } from './DedicatedInstancesPage';
export { DedicatedInstanceDetailsPage } from './DedicatedInstanceDetailsPage';

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
import { DedicatedInstancesPage } from './DedicatedInstancesPage'
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/',
)({
component: DedicatedInstancesPage,
})

View File

@ -44,7 +44,6 @@ import { Route as organizationSettingsPageRouteImport } from './pages/organizati
import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route'
import { Route as organizationSecretScanningPageRouteImport } from './pages/organization/SecretScanningPage/route'
import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route'
import { Route as organizationAutomatedSecurityPageRouteImport } from './pages/organization/AutomatedSecurityPage/route'
import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route'
import { Route as organizationAdminPageRouteImport } from './pages/organization/AdminPage/route'
import { Route as organizationAccessManagementPageRouteImport } from './pages/organization/AccessManagementPage/route'
@ -60,7 +59,9 @@ import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/org
import { Route as organizationKmsOverviewPageRouteImport } from './pages/organization/KmsOverviewPage/route'
import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route'
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
import { Route as organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteImport } from './pages/organization/DedicatedInstancesPage/DedicatedInstanceDetailsPage/route'
import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route'
import { Route as organizationDedicatedInstancesPageRouteImport } from './pages/organization/DedicatedInstancesPage/route'
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh'
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
@ -210,6 +211,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections',
@ -455,6 +460,16 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute =
getParentRoute: () => organizationLayoutRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport.update(
{
id: '/dedicated-instances',
path: '/dedicated-instances',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any,
)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport.update({
id: '/app-connections',
@ -509,14 +524,6 @@ const organizationBillingPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationAutomatedSecurityPageRouteRoute =
organizationAutomatedSecurityPageRouteImport.update({
id: '/automated-security',
path: '/automated-security',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationAuditLogsPageRouteRoute =
organizationAuditLogsPageRouteImport.update({
id: '/audit-logs',
@ -626,6 +633,16 @@ const organizationGroupDetailsByIDPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute =
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteImport.update(
{
id: '/$instanceId',
path: '/$instanceId',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute,
} as any,
)
const organizationCertManagerOverviewPageRouteRoute =
organizationCertManagerOverviewPageRouteImport.update({
id: '/cert-manager/overview',
@ -634,6 +651,14 @@ const organizationCertManagerOverviewPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationDedicatedInstancesPageRouteRoute =
organizationDedicatedInstancesPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute,
} as any)
const organizationAppConnectionsAppConnectionsPageRouteRoute =
organizationAppConnectionsAppConnectionsPageRouteImport.update({
id: '/',
@ -1846,13 +1871,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationAuditLogsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/automated-security': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/automated-security'
path: '/automated-security'
fullPath: '/organization/automated-security'
preLoaderRoute: typeof organizationAutomatedSecurityPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/billing': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/billing'
path: '/billing'
@ -1902,6 +1920,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances'
path: '/dedicated-instances'
fullPath: '/organization/dedicated-instances'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
path: '/secret-manager/$projectId'
@ -1923,6 +1948,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/'
path: '/'
fullPath: '/organization/dedicated-instances/'
preLoaderRoute: typeof organizationDedicatedInstancesPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
path: '/cert-manager/overview'
@ -1930,6 +1962,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationCertManagerOverviewPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId'
path: '/$instanceId'
fullPath: '/organization/dedicated-instances/$instanceId'
preLoaderRoute: typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
path: '/groups/$groupId'
@ -2916,16 +2955,34 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithCh
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren {
organizationDedicatedInstancesPageRouteRoute: typeof organizationDedicatedInstancesPageRouteRoute
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute: typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren =
{
organizationDedicatedInstancesPageRouteRoute:
organizationDedicatedInstancesPageRouteRoute,
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute:
organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute
organizationAutomatedSecurityPageRouteRoute: typeof organizationAutomatedSecurityPageRouteRoute
organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute
organizationSecretScanningPageRouteRoute: typeof organizationSecretScanningPageRouteRoute
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren
organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute
@ -2942,8 +2999,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
organizationAccessManagementPageRouteRoute,
organizationAdminPageRouteRoute: organizationAdminPageRouteRoute,
organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute,
organizationAutomatedSecurityPageRouteRoute:
organizationAutomatedSecurityPageRouteRoute,
organizationBillingPageRouteRoute: organizationBillingPageRouteRoute,
organizationSecretScanningPageRouteRoute:
organizationSecretScanningPageRouteRoute,
@ -2952,6 +3007,8 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren,
organizationCertManagerOverviewPageRouteRoute:
organizationCertManagerOverviewPageRouteRoute,
organizationGroupDetailsByIDPageRouteRoute:
@ -3620,7 +3677,6 @@ export interface FileRoutesByFullPath {
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/organization/admin': typeof organizationAdminPageRouteRoute
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/automated-security': typeof organizationAutomatedSecurityPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
'/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
@ -3628,10 +3684,13 @@ export interface FileRoutesByFullPath {
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/organization/dedicated-instances': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/dedicated-instances/': typeof organizationDedicatedInstancesPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/dedicated-instances/$instanceId': typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute
@ -3791,7 +3850,6 @@ export interface FileRoutesByTo {
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/organization/admin': typeof organizationAdminPageRouteRoute
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/automated-security': typeof organizationAutomatedSecurityPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
'/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
@ -3801,7 +3859,9 @@ export interface FileRoutesByTo {
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/dedicated-instances': typeof organizationDedicatedInstancesPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/dedicated-instances/$instanceId': typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute
@ -3970,7 +4030,6 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/admin': typeof organizationAdminPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/automated-security': typeof organizationAutomatedSecurityPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
@ -3978,10 +4037,13 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationDedicatedInstancesRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/': typeof organizationDedicatedInstancesPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId': typeof organizationDedicatedInstancesPageDedicatedInstanceDetailsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/kms/overview': typeof organizationKmsOverviewPageRouteRoute
@ -4151,7 +4213,6 @@ export interface FileRouteTypes {
| '/organization/access-management'
| '/organization/admin'
| '/organization/audit-logs'
| '/organization/automated-security'
| '/organization/billing'
| '/organization/secret-scanning'
| '/organization/secret-sharing'
@ -4159,10 +4220,13 @@ export interface FileRouteTypes {
| '/cert-manager/$projectId'
| '/kms/$projectId'
| '/organization/app-connections'
| '/organization/dedicated-instances'
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/app-connections/'
| '/organization/dedicated-instances/'
| '/organization/cert-manager/overview'
| '/organization/dedicated-instances/$instanceId'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
| '/organization/kms/overview'
@ -4321,7 +4385,6 @@ export interface FileRouteTypes {
| '/organization/access-management'
| '/organization/admin'
| '/organization/audit-logs'
| '/organization/automated-security'
| '/organization/billing'
| '/organization/secret-scanning'
| '/organization/secret-sharing'
@ -4331,7 +4394,9 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/app-connections'
| '/organization/dedicated-instances'
| '/organization/cert-manager/overview'
| '/organization/dedicated-instances/$instanceId'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
| '/organization/kms/overview'
@ -4498,7 +4563,6 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/organization/access-management'
| '/_authenticate/_inject-org-details/_org-layout/organization/admin'
| '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/organization/automated-security'
| '/_authenticate/_inject-org-details/_org-layout/organization/billing'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
@ -4506,10 +4570,13 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/'
| '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/'
| '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
| '/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId'
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
| '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId'
| '/_authenticate/_inject-org-details/_org-layout/organization/kms/overview'
@ -4868,12 +4935,12 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/access-management",
"/_authenticate/_inject-org-details/_org-layout/organization/admin",
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/organization/automated-security",
"/_authenticate/_inject-org-details/_org-layout/organization/billing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances",
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview",
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId",
@ -4907,10 +4974,6 @@ export const routeTree = rootRoute
"filePath": "organization/AuditLogsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/organization/automated-security": {
"filePath": "organization/AutomatedSecurityPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/organization/billing": {
"filePath": "organization/BillingPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
@ -4949,6 +5012,14 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
]
},
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
"children": [
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/",
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId"
]
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout",
@ -4967,10 +5038,18 @@ export const routeTree = rootRoute
"filePath": "organization/AppConnections/AppConnectionsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections"
},
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/": {
"filePath": "organization/DedicatedInstancesPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances"
},
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview": {
"filePath": "organization/CertManagerOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances/$instanceId": {
"filePath": "organization/DedicatedInstancesPage/DedicatedInstanceDetailsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/dedicated-instances"
},
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId": {
"filePath": "organization/GroupDetailsByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"

View File

@ -15,8 +15,11 @@ const organizationRoutes = route("/organization", [
route("/access-management", "organization/AccessManagementPage/route.tsx"),
route("/admin", "organization/AdminPage/route.tsx"),
route("/audit-logs", "organization/AuditLogsPage/route.tsx"),
route("/automated-security", "organization/AutomatedSecurityPage/route.tsx"),
route("/billing", "organization/BillingPage/route.tsx"),
route("/dedicated-instances", [
index("organization/DedicatedInstancesPage/route.tsx"),
route("/$instanceId", "organization/DedicatedInstancesPage/DedicatedInstanceDetailsPage/route.tsx")
]),
route("/secret-sharing", "organization/SecretSharingPage/route.tsx"),
route("/settings", "organization/SettingsPage/route.tsx"),
route("/secret-scanning", "organization/SecretScanningPage/route.tsx"),

View File

@ -0,0 +1,27 @@
import { Router, Route as RootRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage, DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
// ... existing routes ...
// Add dedicated instances routes
const dedicatedInstancesRoute = new RootRoute({
path: '/organization/:organizationId/dedicated-instances',
component: DedicatedInstancesPage
});
const dedicatedInstanceDetailsRoute = new RootRoute({
path: '/organization/:organizationId/dedicated-instances/:instanceId',
component: DedicatedInstanceDetailsPage
});
// Add to routeTree
routeTree.addChildren([dedicatedInstancesRoute, dedicatedInstanceDetailsRoute]);
// Create and export router
export const router = new Router({ routeTree });
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/organization/$organizationId/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage,
});

View File

@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage } from '@app/pages/organization/DedicatedInstancesPage';
export const Route = createFileRoute('/organization/$organizationId/dedicated-instances')({
component: DedicatedInstancesPage,
});

View File

@ -0,0 +1,12 @@
import { createFileRoute } from '@tanstack/react-router';
import { DedicatedInstancesPage, DedicatedInstanceDetailsPage } from '@app/pages/organization/DedicatedInstancesPage';
// List route
export const Route = createFileRoute('/organization/$organizationId/dedicated-instances')({
component: DedicatedInstancesPage
});
// Details route
export const InstanceRoute = createFileRoute('/organization/$organizationId/dedicated-instances/$instanceId')({
component: DedicatedInstanceDetailsPage
});