mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-29 13:26:20 +00:00
Compare commits
1 Commits
sheen/hack
...
dedicated-
Author | SHA1 | Date | |
---|---|---|---|
65b6f61b53 |
2292
backend/package-lock.json
generated
2292
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@ -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
|
||||
|
19
backend/src/@types/knex.d.ts
vendored
19
backend/src/@types/knex.d.ts
vendored
@ -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
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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'))`);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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>
|
||||
>;
|
34
backend/src/db/schemas/dedicated-instances.ts
Normal file
34
backend/src/db/schemas/dedicated-instances.ts
Normal 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>>;
|
@ -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>>;
|
@ -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";
|
||||
|
@ -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";
|
||||
|
1
backend/src/ee/migrations/dedicated-instance.ts
Normal file
1
backend/src/ee/migrations/dedicated-instance.ts
Normal file
@ -0,0 +1 @@
|
||||
|
141
backend/src/ee/routes/v1/dedicated-instance-router.ts
Normal file
141
backend/src/ee/routes/v1/dedicated-instance-router.ts
Normal 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
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
@ -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);
|
||||
|
@ -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
|
||||
};
|
||||
};
|
@ -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
|
||||
};
|
||||
};
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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[] = [];
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
@ -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" });
|
||||
};
|
||||
|
@ -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
|
||||
};
|
||||
};
|
@ -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
|
||||
};
|
||||
};
|
@ -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;
|
||||
};
|
@ -1,2 +0,0 @@
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
@ -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) });
|
||||
}
|
||||
});
|
||||
};
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
@ -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")) && (
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
});
|
@ -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,
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export { DedicatedInstancesPage } from './DedicatedInstancesPage';
|
||||
export { DedicatedInstanceDetailsPage } from './DedicatedInstanceDetailsPage';
|
@ -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,
|
||||
})
|
@ -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"
|
||||
|
@ -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"),
|
||||
|
27
frontend/src/routes/index.tsx
Normal file
27
frontend/src/routes/index.tsx
Normal 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
@ -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,
|
||||
});
|
12
frontend/src/routes/organization/dedicated-instances.tsx
Normal file
12
frontend/src/routes/organization/dedicated-instances.tsx
Normal 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
|
||||
});
|
Reference in New Issue
Block a user