Merge branch 'main' into ENG-4006

This commit is contained in:
x032205
2025-10-28 17:58:08 -04:00
257 changed files with 3374 additions and 2826 deletions
+6 -2
View File
@@ -8,7 +8,7 @@ import path from "path";
import { seedData1 } from "@app/db/seed-data";
import { getDatabaseCredentials, getHsmConfig, initEnvConfig } from "@app/lib/config/env";
import { initLogger } from "@app/lib/logger";
import { main } from "@app/server/app";
import { main, markServerReady } from "@app/server/app";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { mockSmtpServer } from "./mocks/smtp";
@@ -83,7 +83,7 @@ export default {
await queue.initialize();
const server = await main({
const { server, completeServerInitialization } = await main({
db,
smtp,
logger,
@@ -96,6 +96,10 @@ export default {
envConfig: envCfg
});
await completeServerInitialization();
markServerReady();
await bootstrapCheck({ db });
// @ts-expect-error type
+9 -1
View File
@@ -12,6 +12,7 @@ type TArgs = {
auditLogDb?: Knex;
applicationDb: Knex;
logger: Logger;
onMigrationLockAcquired?: () => void;
};
const isProduction = process.env.NODE_ENV === "production";
@@ -30,7 +31,7 @@ const migrationStatusCheckErrorHandler = (err: Error) => {
throw err;
};
export const runMigrations = async ({ applicationDb, auditLogDb, logger }: TArgs) => {
export const runMigrations = async ({ applicationDb, auditLogDb, logger, onMigrationLockAcquired }: TArgs) => {
try {
// akhilmhdh(Feb 10 2025): 2 years from now remove this
if (isProduction) {
@@ -85,6 +86,13 @@ export const runMigrations = async ({ applicationDb, auditLogDb, logger }: TArgs
await applicationDb.transaction(async (tx) => {
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.BootUpMigration]);
// Signal that this container is running migrations so that it can be marked as healthy/alive
// This is to prevent the container from being killed by the orchestrator
if (onMigrationLockAcquired) {
onMigrationLockAcquired();
}
logger.info("Running application migrations.");
const didPreviousInstanceRunMigration = !(await applicationDb.migrate
@@ -2,7 +2,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -32,8 +31,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
@@ -127,8 +126,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.projectSlug),
path: z
@@ -2,7 +2,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
@@ -32,8 +31,8 @@ export const registerKubernetesDynamicSecretLeaseRouter = async (server: Fastify
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
@@ -3,7 +3,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
@@ -60,8 +59,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
maxTTL: z
.string()
@@ -72,8 +71,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
})
.nullable(),
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
@@ -130,8 +129,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
maxTTL: z
.string()
@@ -142,8 +141,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
})
.nullable(),
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
@@ -112,7 +112,7 @@ export const dynamicSecretServiceFactory = ({
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (existingDynamicSecret)
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
throw new BadRequestError({ message: "Provided dynamic secret already exists under the folder" });
const selectedProvider = dynamicSecretProviders[provider.type];
const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId });
@@ -265,7 +265,7 @@ export const dynamicSecretServiceFactory = ({
if (newName) {
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
if (existingDynamicSecret)
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
throw new BadRequestError({ message: "Provided dynamic secret already exists under the folder" });
}
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({
+1 -1
View File
@@ -84,7 +84,7 @@ export const isHsmActiveAndEnabled = async ({
rootKmsConfigEncryptionStrategy = (rootKmsConfig?.encryptionStrategy || null) as RootKeyEncryptionStrategy | null;
if (
rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.HSM &&
(rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.HSM || isHsmConfigured) &&
licenseService &&
!licenseService.onPremFeatures.hsm
) {
@@ -1517,7 +1517,7 @@ export const secretApprovalRequestServiceFactory = ({
}))
);
if (secrets.length)
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
throw new BadRequestError({ message: `Secret already exists: ${secrets.map((el) => el.key).join(",")}` });
commits.push(
...createdSecrets.map((createdSecret) => ({
@@ -42,6 +42,7 @@ export const secretRotationV2QueueServiceFactory = async ({
smtpService,
notificationService
}: TSecretRotationV2QueueServiceFactoryDep) => {
const init = async () => {
const appCfg = getConfig();
if (appCfg.isRotationDevelopmentMode) {
@@ -206,4 +207,7 @@ export const secretRotationV2QueueServiceFactory = async ({
undefined,
{ tz: "UTC" }
);
};
return { init };
};
@@ -141,6 +141,61 @@ export const secretScanningV2QueueServiceFactory = async ({
}
};
const queueResourceDiffScan = async ({
payload,
dataSourceId,
dataSourceType
}: Pick<TQueueSecretScanningResourceDiffScan, "payload" | "dataSourceId" | "dataSourceType">) => {
const factory = SECRET_SCANNING_FACTORY_MAP[dataSourceType as SecretScanningDataSource]({
kmsService,
appConnectionDAL
});
const resourcePayload = factory.getDiffScanResourcePayload(payload);
try {
const { resourceId, scanId } = await secretScanningV2DAL.resources.transaction(async (tx) => {
const [resource] = await secretScanningV2DAL.resources.upsert(
[
{
...resourcePayload,
dataSourceId
}
],
["externalId", "dataSourceId"],
tx
);
const scan = await secretScanningV2DAL.scans.create(
{
resourceId: resource.id,
type: SecretScanningScanType.DiffScan
},
tx
);
return {
resourceId: resource.id,
scanId: scan.id
};
});
await queueService.queuePg(QueueJobs.SecretScanningV2DiffScan, {
payload,
dataSourceId,
dataSourceType,
scanId,
resourceId
});
} catch (error) {
logger.error(
error,
`secretScanningV2Queue: Failed to queue diff scan [dataSourceId=${dataSourceId}] [resourceExternalId=${resourcePayload.externalId}]`
);
}
};
const init = async () => {
await queueService.startPg<QueueName.SecretScanningV2>(
QueueJobs.SecretScanningV2FullScan,
async ([job]) => {
@@ -337,60 +392,6 @@ export const secretScanningV2QueueServiceFactory = async ({
}
);
const queueResourceDiffScan = async ({
payload,
dataSourceId,
dataSourceType
}: Pick<TQueueSecretScanningResourceDiffScan, "payload" | "dataSourceId" | "dataSourceType">) => {
const factory = SECRET_SCANNING_FACTORY_MAP[dataSourceType as SecretScanningDataSource]({
kmsService,
appConnectionDAL
});
const resourcePayload = factory.getDiffScanResourcePayload(payload);
try {
const { resourceId, scanId } = await secretScanningV2DAL.resources.transaction(async (tx) => {
const [resource] = await secretScanningV2DAL.resources.upsert(
[
{
...resourcePayload,
dataSourceId
}
],
["externalId", "dataSourceId"],
tx
);
const scan = await secretScanningV2DAL.scans.create(
{
resourceId: resource.id,
type: SecretScanningScanType.DiffScan
},
tx
);
return {
resourceId: resource.id,
scanId: scan.id
};
});
await queueService.queuePg(QueueJobs.SecretScanningV2DiffScan, {
payload,
dataSourceId,
dataSourceType,
scanId,
resourceId
});
} catch (error) {
logger.error(
error,
`secretScanningV2Queue: Failed to queue diff scan [dataSourceId=${dataSourceId}] [resourceExternalId=${resourcePayload.externalId}]`
);
}
};
await queueService.startPg<QueueName.SecretScanningV2>(
QueueJobs.SecretScanningV2DiffScan,
async ([job]) => {
@@ -666,9 +667,11 @@ export const secretScanningV2QueueServiceFactory = async ({
pollingIntervalSeconds: 1
}
);
};
return {
queueDataSourceFullScan,
queueResourceDiffScan
queueResourceDiffScan,
init
};
};
+40 -9
View File
@@ -16,7 +16,7 @@ import { buildRedisFromConfig } from "./lib/config/redis";
import { removeTemporaryBaseDirectory } from "./lib/files";
import { initLogger } from "./lib/logger";
import { queueServiceFactory } from "./queue";
import { main } from "./server/app";
import { main, markRunningMigrations, markServerReady } from "./server/app";
import { bootstrapCheck } from "./server/boot-strap-check";
import { kmsRootConfigDALFactory } from "./services/kms/kms-root-config-dal";
import { smtpServiceFactory } from "./services/smtp/smtp-service";
@@ -59,8 +59,6 @@ const run = async () => {
})
: undefined;
await runMigrations({ applicationDb: db, auditLogDb, logger });
const smtp = smtpServiceFactory(formatSmtpConfig());
const queue = queueServiceFactory(envConfig, {
@@ -74,7 +72,7 @@ const run = async () => {
const keyStore = keyStoreFactory(envConfig, keyValueStoreDAL);
const redis = buildRedisFromConfig(envConfig);
const server = await main({
const { server, completeServerInitialization } = await main({
db,
auditLogDb,
superAdminDAL,
@@ -87,7 +85,6 @@ const run = async () => {
redis,
envConfig
});
const bootstrap = await bootstrapCheck({ db });
// eslint-disable-next-line
process.on("SIGINT", async () => {
@@ -121,12 +118,46 @@ const run = async () => {
await server.listen({
port: envConfig.PORT,
host: envConfig.HOST,
listenTextResolver: (address) => {
void bootstrap();
return address;
host: envConfig.HOST
});
logger.info(`Server listening on ${envConfig.HOST}:${envConfig.PORT}`);
logger.info("Running migrations...");
// Run migrations while server is up
// All containers start as NOT HEALTHY (waiting for migrations)
// Container that acquires lock: becomes HEALTHY (running migrations) + NOT READY (no traffic)
// Other containers waiting: stay NOT HEALTHY (waiting) + NOT READY (no traffic)
await runMigrations({
applicationDb: db,
auditLogDb,
logger,
onMigrationLockAcquired: () => {
// Called after successfully acquiring the lock
// This container is now the migration runner
markRunningMigrations();
logger.info("Migration lock acquired! This container is running migrations.");
}
});
logger.info("Migrations complete. Completing server initialization...");
try {
await completeServerInitialization();
} catch (error) {
logger.error(error, "Failed to complete server initialization");
await server.close();
await queue.shutdown();
process.exit(1);
}
logger.info("Server initialization complete. Marking server as READY...");
markServerReady();
logger.info("Server is ready to accept traffic");
const bootstrap = await bootstrapCheck({ db });
void bootstrap();
};
void run();
+89 -2
View File
@@ -1,5 +1,6 @@
/* eslint-disable import/extensions */
import path from "node:path";
import { monitorEventLoopDelay } from "perf_hooks";
import type { FastifyCookieOptions } from "@fastify/cookie";
import cookie from "@fastify/cookie";
@@ -24,6 +25,7 @@ import { TQueueServiceFactory } from "@app/queue";
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
import { TSmtpService } from "@app/services/smtp/smtp-service";
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { globalRateLimiterCfg } from "./config/rateLimiter";
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas";
@@ -36,6 +38,15 @@ import { registerServeUI } from "./plugins/serve-ui";
import { fastifySwagger } from "./plugins/swagger";
import { registerRoutes } from "./routes";
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
const serverState = {
isReady: false,
isRunningMigrations: false,
isWaitingForMigrations: true // Start as true - containers are unhealthy until they acquire migration lock or complete
};
type TMain = {
auditLogDb?: Knex;
db: Knex;
@@ -145,7 +156,72 @@ export const main = async ({
})
});
await server.register(registerRoutes, {
// Health check - returns 200 only if doing useful work (running migrations or ready)
// Returns 503 if waiting for another container to finish migrations
server.get("/api/health", async (_, reply) => {
if (serverState.isWaitingForMigrations) {
return reply.code(503).send({
status: "waiting",
message: "Waiting for migrations to complete in another container"
});
}
return { status: "ok", message: "Server is alive" };
});
// Global preHandler to block requests during migrations
server.addHook("preHandler", async (request, reply) => {
if (request.url === "/api/health" || request.url === "/api/ready") {
return;
}
if (!serverState.isReady) {
return reply.code(503).send({
status: "unavailable",
message: "Server is starting up, migrations in progress. Please try again in a moment."
});
}
});
// Readiness check - returns 503 until migrations are complete
server.get("/api/ready", async (request, reply) => {
const cfg = getConfig();
const meanLagMs = histogram.mean / 1e6;
const maxLagMs = histogram.max / 1e6;
const p99LagMs = histogram.percentile(99) / 1e6;
request.log.info(
`Event loop stats - Mean: ${meanLagMs.toFixed(2)}ms, Max: ${maxLagMs.toFixed(2)}ms, p99: ${p99LagMs.toFixed(2)}ms`
);
request.log.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`);
if (!serverState.isReady) {
return reply.code(503).send({
date: new Date(),
message: "Server is starting up, migrations in progress",
emailConfigured: cfg.isSmtpConfigured,
redisConfigured: cfg.isRedisConfigured,
secretScanningConfigured: cfg.isSecretScanningConfigured,
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug,
auditLogStorageDisabled: Boolean(cfg.DISABLE_AUDIT_LOG_STORAGE)
});
}
const serverCfg = await getServerCfg();
return {
date: new Date(),
message: "Ok",
emailConfigured: cfg.isSmtpConfigured,
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
redisConfigured: cfg.isRedisConfigured,
secretScanningConfigured: cfg.isSecretScanningConfigured,
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug,
auditLogStorageDisabled: Boolean(cfg.DISABLE_AUDIT_LOG_STORAGE)
};
});
const completeServerInitialization = await registerRoutes(server, {
smtp,
queue,
db,
@@ -164,10 +240,21 @@ export const main = async ({
await server.ready();
server.swagger();
return server;
return { server, completeServerInitialization };
} catch (err) {
server.log.error(err);
await queue.shutdown();
process.exit(1);
}
};
export const markServerReady = () => {
serverState.isReady = true;
serverState.isRunningMigrations = false;
serverState.isWaitingForMigrations = false;
};
export const markRunningMigrations = () => {
serverState.isRunningMigrations = true;
serverState.isWaitingForMigrations = false;
};
+42 -36
View File
@@ -2208,7 +2208,7 @@ export const registerRoutes = async (
internalCaFns
});
await secretRotationV2QueueServiceFactory({
const secretRotationV2Queue = await secretRotationV2QueueServiceFactory({
secretRotationV2Service,
secretRotationV2DAL,
queueService,
@@ -2305,8 +2305,6 @@ export const registerRoutes = async (
// If FIPS is enabled, we check to ensure that the users license includes FIPS mode.
crypto.verifyFipsLicense(licenseService);
await superAdminService.initServerCfg();
// Start HSM service if it's configured/enabled.
await hsmService.startService();
@@ -2331,6 +2329,9 @@ export const registerRoutes = async (
}
}
const completeServerInitialization = async () => {
await superAdminService.initServerCfg();
await telemetryQueue.startTelemetryCheck();
await telemetryQueue.startAggregatedEventsJob();
await dailyResourceCleanUp.init();
@@ -2345,8 +2346,44 @@ export const registerRoutes = async (
await kmsService.startService(hsmStatus);
await microsoftTeamsService.start();
await dynamicSecretQueueService.init();
await secretScanningV2Queue.init();
await secretRotationV2Queue.init();
await notificationQueue.init();
await eventBusService.init();
const cronJobs: CronJob[] = [];
if (appCfg.isProductionMode) {
const rateLimitSyncJob = await rateLimitService.initializeBackgroundSync();
if (rateLimitSyncJob) {
cronJobs.push(rateLimitSyncJob);
}
const licenseSyncJob = await licenseService.initializeBackgroundSync();
if (licenseSyncJob) {
cronJobs.push(licenseSyncJob);
}
const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync();
if (microsoftTeamsSyncJob) {
cronJobs.push(microsoftTeamsSyncJob);
}
const adminIntegrationsSyncJob = await superAdminService.initializeAdminIntegrationConfigSync();
if (adminIntegrationsSyncJob) {
cronJobs.push(adminIntegrationsSyncJob);
}
}
const configSyncJob = await superAdminService.initializeEnvConfigSync();
if (configSyncJob) {
cronJobs.push(configSyncJob);
}
const oauthConfigSyncJob = await initializeOauthConfigSync();
if (oauthConfigSyncJob) {
cronJobs.push(oauthConfigSyncJob);
}
};
// inject all services
server.decorate<FastifyZodProvider["services"]>("services", {
login: loginService,
@@ -2475,38 +2512,6 @@ export const registerRoutes = async (
convertor: convertorService
});
const cronJobs: CronJob[] = [];
if (appCfg.isProductionMode) {
const rateLimitSyncJob = await rateLimitService.initializeBackgroundSync();
if (rateLimitSyncJob) {
cronJobs.push(rateLimitSyncJob);
}
const licenseSyncJob = await licenseService.initializeBackgroundSync();
if (licenseSyncJob) {
cronJobs.push(licenseSyncJob);
}
const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync();
if (microsoftTeamsSyncJob) {
cronJobs.push(microsoftTeamsSyncJob);
}
const adminIntegrationsSyncJob = await superAdminService.initializeAdminIntegrationConfigSync();
if (adminIntegrationsSyncJob) {
cronJobs.push(adminIntegrationsSyncJob);
}
}
const configSyncJob = await superAdminService.initializeEnvConfigSync();
if (configSyncJob) {
cronJobs.push(configSyncJob);
}
const oauthConfigSyncJob = await initializeOauthConfigSync();
if (oauthConfigSyncJob) {
cronJobs.push(oauthConfigSyncJob);
}
server.decorate<FastifyZodProvider["store"]>("store", {
user: userDAL,
kmipClient: kmipClientDAL
@@ -2595,9 +2600,10 @@ export const registerRoutes = async (
await server.register(registerV4Routes, { prefix: "/api/v4" });
server.addHook("onClose", async () => {
cronJobs.forEach((job) => job.stop());
await telemetryService.flushAll();
await eventBusService.close();
sseService.close();
});
return completeServerInitialization;
};
@@ -393,6 +393,56 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
}
});
server.route({
method: "GET",
url: "/vault/kubernetes-roles",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string(),
mountPath: z.string()
}),
response: {
200: z.object({
roles: z.array(
z.object({
name: z.string(),
mountPath: z.string(),
allowed_kubernetes_namespaces: z.array(z.string()).nullish(),
allowed_kubernetes_namespace_selector: z.string().nullish(),
token_max_ttl: z.number().nullish(),
token_default_ttl: z.number().nullish(),
token_default_audiences: z.array(z.string()).nullish(),
service_account_name: z.string().nullish(),
kubernetes_role_name: z.string().nullish(),
kubernetes_role_type: z.string().nullish(),
generated_role_rules: z.string().nullish(),
name_template: z.string().nullish(),
extra_annotations: z.record(z.string()).nullish(),
extra_labels: z.record(z.string()).nullish(),
config: z.object({
kubernetes_host: z.string(),
kubernetes_ca_cert: z.string().nullish()
})
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const roles = await server.services.migration.getVaultKubernetesRoles({
actor: req.permission,
namespace: req.query.namespace,
mountPath: req.query.mountPath
});
return { roles };
}
});
server.route({
method: "GET",
url: "/vault/secret-paths",
@@ -5,5 +5,6 @@ export enum GitLabConnectionMethod {
export enum GitLabAccessTokenType {
Project = "project",
Personal = "personal"
Personal = "personal",
Group = "group"
}
@@ -21,6 +21,8 @@ import {
THCVaultKubernetesAuthConfig,
THCVaultKubernetesAuthRole,
THCVaultKubernetesAuthRoleWithConfig,
THCVaultKubernetesRole,
THCVaultKubernetesSecretsConfig,
THCVaultMount,
THCVaultMountResponse
} from "./hc-vault-connection-types";
@@ -816,3 +818,122 @@ export const getHCVaultKubernetesAuthRoles = async (
});
}
};
export const getHCVaultKubernetesRoles = async (
namespace: string,
mountPath: string,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
): Promise<THCVaultKubernetesRole[]> => {
// Remove trailing slash from mount path
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
try {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
// 1. Get the Kubernetes secrets engine configuration for this mount
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesSecretsConfig }>(
connection,
gatewayService,
{
url: `${instanceUrl}/v1/${cleanMountPath}/config`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
}
);
const kubernetesConfig = configResponse.data;
// 2. List all roles in this mount
let roleNames: string[] = [];
try {
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
connection,
gatewayService,
{
url: `${instanceUrl}/v1/${cleanMountPath}/roles?list=true`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
}
);
roleNames = roleListResponse.data.keys || [];
} catch (error) {
// Vault returns 404 when no roles are configured yet
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 404) {
return [];
}
}
throw error;
}
if (!roleNames || roleNames.length === 0) {
return [];
}
// 3. Fetch details for each role with concurrency control
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
const roleDetailsPromises = roleNames.map((roleName) =>
limiter(async () => {
const { data: roleResponse } = await requestWithHCVaultGateway<{
data: {
allowed_kubernetes_namespaces?: string[];
allowed_kubernetes_namespace_selector?: string;
token_max_ttl?: number;
token_default_ttl?: number;
token_default_audiences?: string[];
service_account_name?: string;
kubernetes_role_name?: string;
kubernetes_role_type?: string;
generated_role_rules?: string;
name_template?: string;
extra_annotations?: Record<string, string>;
extra_labels?: Record<string, string>;
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/${cleanMountPath}/roles/${roleName}`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
// 4. Merge the role with the config
return {
...roleResponse.data,
name: roleName,
config: kubernetesConfig,
mountPath: cleanMountPath
} as THCVaultKubernetesRole;
})
);
const roles = await Promise.all(roleDetailsPromises);
return roles;
} catch (error: unknown) {
logger.error(error, "Unable to list HC Vault Kubernetes secrets engine roles");
if (error instanceof AxiosError) {
const errorMessage =
(error.response?.data as { errors?: string[] })?.errors?.[0] || error.message || "Unknown error";
throw new BadRequestError({
message: `Failed to list Kubernetes secrets engine roles: ${errorMessage}`
});
}
throw new BadRequestError({
message: "Unable to list Kubernetes secrets engine roles from HashiCorp Vault"
});
}
};
@@ -95,3 +95,26 @@ export type THCVaultKubernetesAuthRoleWithConfig = THCVaultKubernetesAuthRole &
config: THCVaultKubernetesAuthConfig;
mountPath: string;
};
export type THCVaultKubernetesSecretsConfig = {
kubernetes_host: string;
kubernetes_ca_cert?: string;
};
export type THCVaultKubernetesRole = {
name: string;
allowed_kubernetes_namespaces?: string[];
allowed_kubernetes_namespace_selector?: string;
token_max_ttl?: number;
token_default_ttl?: number;
token_default_audiences?: string[];
service_account_name?: string;
kubernetes_role_name?: string;
kubernetes_role_type?: string;
generated_role_rules?: string;
name_template?: string;
extra_annotations?: Record<string, string>;
extra_labels?: Record<string, string>;
config: THCVaultKubernetesSecretsConfig;
mountPath: string;
};
@@ -82,7 +82,7 @@ export const importDataIntoInfisicalFn = async ({
if (existingEnv) {
throw new BadRequestError({
message: `Environment with slug '${slug}' already exist`,
message: `Environment with slug '${slug}' already exists`,
name: "CreateEnvironment"
});
}
@@ -312,7 +312,7 @@ export const importDataIntoInfisicalFn = async ({
);
if (secretsByKeys.length) {
throw new BadRequestError({
message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}`
message: `Secret already exists: ${secretsByKeys.map((el) => el.key).join(",")}`
});
}
await fnSecretBulkInsert({
@@ -19,6 +19,7 @@ import {
convertVaultValueToString,
getHCVaultAuthMounts,
getHCVaultKubernetesAuthRoles,
getHCVaultKubernetesRoles,
getHCVaultSecretsForPath,
HCVaultAuthType,
listHCVaultMounts,
@@ -762,6 +763,56 @@ export const externalMigrationServiceFactory = ({
return roles;
};
const getVaultKubernetesRoles = async ({
actor,
namespace,
mountPath
}: {
actor: OrgServiceActor;
namespace: string;
mountPath: string;
}) => {
const { hasRole } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: actor.type,
actorId: actor.id,
orgId: actor.orgId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can get Kubernetes roles" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
return getHCVaultKubernetesRoles(namespace, mountPath, connection, gatewayService);
};
return {
importEnvKeyData,
importVaultData,
@@ -776,6 +827,7 @@ export const externalMigrationServiceFactory = ({
getVaultAuthMounts,
getVaultSecretPaths,
importVaultSecrets,
getVaultKubernetesAuthRoles
getVaultKubernetesAuthRoles,
getVaultKubernetesRoles
};
};
@@ -10,6 +10,7 @@ type TNotificationQueueServiceFactoryDep = {
export type TNotificationQueueServiceFactory = {
pushUserNotifications: (data: TCreateUserNotificationDTO[]) => Promise<void>;
init: () => Promise<void>;
};
export const notificationQueueServiceFactory = async ({
@@ -20,6 +21,7 @@ export const notificationQueueServiceFactory = async ({
await queueService.queuePg(QueueJobs.UserNotification, { notifications: data });
};
const init = async () => {
await queueService.startPg(
QueueJobs.UserNotification,
async ([job]) => {
@@ -32,8 +34,10 @@ export const notificationQueueServiceFactory = async ({
pollingIntervalSeconds: 1
}
);
};
return {
pushUserNotifications
pushUserNotifications,
init
};
};
+2 -2
View File
@@ -517,7 +517,7 @@ export const orgServiceFactory = ({
if (slug) {
const existingOrg = await orgDAL.findOne({ slug, rootOrgId: null });
if (existingOrg && existingOrg?.id !== orgId)
throw new BadRequestError({ message: `Organization with slug ${slug} already exist` });
throw new BadRequestError({ message: `Organization with slug ${slug} already exists` });
}
if (googleSsoAuthEnforced) {
@@ -1149,7 +1149,7 @@ export const orgServiceFactory = ({
const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email });
if (doesIncidentContactExist) {
throw new BadRequestError({
message: "Incident contact already exist",
message: "Incident contact already exists",
name: "Incident contact exist"
});
}
@@ -76,7 +76,7 @@ export const projectEnvServiceFactory = ({
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
if (existingEnv)
throw new BadRequestError({
message: "Environment with slug already exist",
message: "Environment with slug already exists",
name: "CreateEnvironment"
});
@@ -171,7 +171,7 @@ export const projectEnvServiceFactory = ({
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
if (existingEnv && existingEnv.id !== id) {
throw new BadRequestError({
message: "Environment with slug already exist",
message: "Environment with slug already exists",
name: "UpdateEnvironment"
});
}
+30 -16
View File
@@ -10,7 +10,8 @@ import {
ProjectType,
ProjectVersion,
TableName,
TProjectEnvironments
TProjectEnvironments,
TProjects
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
@@ -40,7 +41,8 @@ import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-hos
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types";
@@ -303,19 +305,34 @@ export const projectServiceFactory = ({
});
}
const project = await projectDAL.create(
const slug = projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`);
let project: TProjects;
try {
project = await projectDAL.create(
{
name: workspaceName,
type,
description: workspaceDescription,
orgId: organization.id,
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
slug,
kmsSecretManagerKeyId: kmsKeyId,
version: ProjectVersion.V3,
pitVersionLimit: 10
},
tx
);
} catch (err) {
if (
err instanceof DatabaseError &&
(err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation
) {
throw new BadRequestError({
message: `A project with the slug "${slug}" already exists in your organization. Please choose a different name or slug.`
});
}
throw err;
}
if (type === ProjectType.SSH) {
await bootstrapSshProject({
@@ -579,24 +596,13 @@ export const projectServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
if (update.slug) {
const existingProject = await projectDAL.findOne({
slug: update.slug,
orgId: actorOrgId
});
if (existingProject && existingProject.id !== project.id) {
throw new BadRequestError({
message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
});
}
}
if (update.secretDetectionIgnoreValues && !hasRole(ProjectMembershipRole.Admin)) {
throw new ForbiddenRequestError({
message: "Only admins can update secret detection ignore values"
});
}
try {
const updatedProject = await projectDAL.updateById(project.id, {
name: update.name,
description: update.description,
@@ -612,6 +618,14 @@ export const projectServiceFactory = ({
});
return updatedProject;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `Failed to update project. A project with the slug "${update.slug}" already exists in your organization. Please choose a different slug.`
});
}
throw err;
}
};
const toggleAutoCapitalization = async ({
@@ -35,7 +35,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
const existingTag = await secretTagDAL.findOne({ slug, projectId });
if (existingTag) throw new BadRequestError({ message: "Tag already exist" });
if (existingTag) throw new BadRequestError({ message: "Tag already exists" });
const newTag = await secretTagDAL.create({
projectId,
@@ -53,7 +53,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
if (slug) {
const existingTag = await secretTagDAL.findOne({ slug, projectId: tag.projectId });
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" });
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exists" });
}
const { permission } = await permissionService.getProjectPermission({
@@ -282,7 +282,7 @@ export const secretV2BridgeServiceFactory = ({
folderId
});
if (inputSecret.type === SecretType.Shared && doesSecretExist)
throw new BadRequestError({ message: "Secret already exist" });
throw new BadRequestError({ message: "Secret already exists" });
// if user creating personal check its shared also exist
if (inputSecret.type === SecretType.Personal && !doesSecretExist) {
@@ -527,7 +527,7 @@ export const secretV2BridgeServiceFactory = ({
type: SecretType.Shared,
folderId
});
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" });
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exists" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
@@ -1674,7 +1674,7 @@ export const secretV2BridgeServiceFactory = ({
}
});
if (secrets.length)
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
throw new BadRequestError({ message: `Secret already exists: ${secrets.map((el) => el.key).join(",")}` });
const project = await projectDAL.findById(projectId);
await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []);
+2 -2
View File
@@ -525,7 +525,7 @@ export const fnSecretBlindIndexCheck = async ({
);
if (isNew) {
if (secrets.length) throw new BadRequestError({ message: "Secret already exist" });
if (secrets.length) throw new BadRequestError({ message: "Secret already exists" });
} else {
const secretKeysInDB = unique(secrets, (el) => el.secretBlindIndex as string).map(
(el) => blindIndex2KeyName[el.secretBlindIndex as string]
@@ -819,7 +819,7 @@ export const createManySecretsRawFnFactory = ({
);
if (secretsStoredInDB.length)
throw new BadRequestError({
message: `Secret already exist: ${secretsStoredInDB.map((el) => el.key).join(",")}`
message: `Secret already exists: ${secretsStoredInDB.map((el) => el.key).join(",")}`
});
const inputSecrets = secrets.map((secret) => {
@@ -2751,7 +2751,7 @@ export const secretServiceFactory = ({
const existingSecretTags = await secretDAL.getSecretTags(secret.id);
if (existingSecretTags.some((tag) => tagSlugs.includes(tag.slug))) {
throw new BadRequestError({ message: "One or more tags already exist on the secret" });
throw new BadRequestError({ message: "One or more tags already exists on the secret" });
}
const combinedTags = new Set([...existingSecretTags.map((tag) => tag.id), ...tags.map((el) => el.id)]);
@@ -26,6 +26,12 @@ Infisical provides two approaches for migrating from HashiCorp Vault.
This migration approach lets you set up a connection to your Vault instance once, then import specific resources as needed throughout Infisical.
<Warning>
**Organization Admin Access Required:** All in-platform migration features
(importing secrets, Kubernetes configurations, and policies from Vault) are
only accessible to organization admins.
</Warning>
### Step 1: Set Up Your Vault Connection
<Steps>
@@ -84,6 +90,19 @@ This migration approach lets you set up a connection to your Vault instance once
path "auth/+/role/*" {
capabilities = ["read"]
}
# Kubernetes secrets engine - for reading secrets engine configuration and roles
path "+/config" {
capabilities = ["read"]
}
path "+/roles" {
capabilities = ["list"]
}
path "+/roles/*" {
capabilities = ["read"]
}
```
</Accordion>
@@ -160,6 +179,34 @@ The authentication settings (service accounts, TTL, policies, etc.) will be auto
must be manually provided in the form after importing the configuration.
</Note>
#### Import Kubernetes Dynamic Secret Configurations
When creating a Kubernetes dynamic secret, you can import the configuration from a Vault Kubernetes secrets engine role:
1. Navigate to your project and select an environment
2. Click **"+ Add Secret"** dropdown and choose **"Dynamic Secret"**
3. Select **Kubernetes** as the provider
4. Click **"Load from Vault"** at the top of the form
![Load Kubernetes Dynamic Secret from Vault](/images/platform/external-migrations/vault-in-platform/import-vault-kubernetes-role-modal.png)
5. Select your Vault namespace, Kubernetes secrets engine mount, and role
6. Click **"Load Configuration"**
The form will be automatically populated with the role's configuration including:
- Cluster URL and CA certificate
- Credential type (Static or Dynamic)
- Service account name or Kubernetes role settings
- Allowed namespaces
- Token TTL values
- Token audiences
<Note>
Sensitive values like cluster tokens cannot be retrieved from Vault and must
be manually provided in the form after loading the configuration.
</Note>
#### Import and Translate Access Control Policies
When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions.
Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

+63 -2
View File
@@ -187,9 +187,68 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
</Step>
</Steps>
</Tab>
<Tab title="Group Access Token">
Group access tokens provide access to all projects within a GitLab group, offering group-level control.
<Steps>
<Step title="Navigate to Group Settings">
Go to your GitLab group and navigate to Settings > Access Tokens. Click **Add new token** to create a new group access token.
![GitLab Group Access Tokens](/images/app-connections/gitlab/gitlab-group-access-token-list.png)
</Step>
<Step title="Configure Token">
Fill in the token details:
- **Token name**: A descriptive name for the token
- **Expiration date**: Set an appropriate expiration date
- **Select role and scopes**: Depending on your use case, add the required role and one or more of the following scopes:
<Tabs>
<Tab title="Secret Sync">
For Secret Syncs, the required role depends on your sync destination:
- **Project variables**: Requires **Maintainer** role or higher
- **Group variables**: Requires **Owner** role
Your token will require the `api` scope.
![GitLab Create Group Token](/images/app-connections/gitlab/gitlab-group-access-token-form-secret-sync.png)
Click **Create group access token** to create the token.
<Note>
Use the **Owner** role if you need to sync to group-level variables. The **Maintainer** role is sufficient only for project-level variables.
</Note>
</Tab>
<Tab title="Secret Scanning">
To set up Secret Scanning, the required permissions depend on the data source level:
- **Project-level data source:** Requires **Maintainer** role or higher
- **Group-level data source:** Requires **Owner** role
Your token will require the `api` scope.
![GitLab Create Group Token](/images/app-connections/gitlab/gitlab-group-access-token-form-secret-sync.png)
Click **Create group access token** to create the token.
</Tab>
</Tabs>
## Setup GitLab Access Token Connection in Infisical
<Info>
Group Access Token connections require manual token rotation when your GitLab access token expires or is regenerated. Monitor your connection status and update the token as needed.
</Info>
</Step>
<Step title="Copy Token">
Copy the generated token immediately as it won't be shown again.
![GitLab Group Token Created](/images/app-connections/gitlab/gitlab-group-access-token-created.png)
<Warning>
Keep your access token secure and do not share it. Anyone with access to this token can access all projects within your GitLab group.
</Warning>
</Step>
</Steps>
</Tab>
</Tabs>
## Setup GitLab Access Token Connection in Infisical
<Steps>
<Step title="Navigate to App Connections">
@@ -206,12 +265,14 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
![Configure Access Token](/images/app-connections/gitlab/create-gitlab-access-token-connection.png)
Click **Connect** to establish the connection.
</Step>
<Step title="Connection Created">
Your **GitLab Connection** is now available for use.
![GitLab Access Token Connection](/images/app-connections/gitlab/gitlab-access-token-connection.png)
</Step>
</Steps>
</Steps>
</Tab>
</Tabs>
@@ -137,6 +137,33 @@ Configure database read replicas for high availability PostgreSQL setups:
DB_READ_REPLICAS='[{"DB_CONNECTION_URI":"postgresql://user:pass@replica:5432/db?sslmode=require"}]'
```
### Health Check Endpoints
Infisical provides two health check endpoints for proper container orchestration and load balancer integration:
#### `/api/health` - Container Health Check
Determines whether the application container should be kept alive or terminated.
- Returns `200` if the application is running and operational
- Returns `200` even during startup tasks
- Returns `503` only if the application has crashed or is unable to start
**Use for**: Docker health checks, Kubernetes liveness probes, ECS task health checks.
#### `/api/ready` - Traffic Readiness Check
Determines whether the application instance is ready to receive production traffic.
- Returns `200` when the application is fully ready to serve requests
- Returns `503` during startup tasks (e.g., database migrations, initialization)
**Use for**: Load balancer health checks, Kubernetes readiness probes, ALB target health checks.
#### Why Two Endpoints?
Using both endpoints together enables zero-downtime deployments: containers stay alive during startup tasks (`/api/health` returns `200`) while load balancers avoid sending traffic to instances that aren't ready (`/api/ready` returns `503`). This ensures existing instances continue serving traffic until new instances complete their initialization.
### Operational Security
#### User Access Management
@@ -207,14 +234,17 @@ docker run --memory=1g --cpus=0.5 infisical/infisical:latest
#### Health Monitoring
**Configure health checks**. Set up Docker health checks:
**Configure health checks**. Set up Docker health checks using the appropriate endpoint:
```dockerfile
# In Dockerfile or docker-compose.yml
# Use /api/health for container health (keeps container alive during startup)
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/api/status || exit 1
CMD curl -f http://localhost:8080/api/health || exit 1
```
**Note**: Use `/api/health` for container health checks and `/api/ready` for load balancer readiness checks. See [Health Check Endpoints](#health-check-endpoints) for detailed information.
#### Network Security
**Host firewall configuration**. Configure host-level firewall for Docker deployments:
@@ -433,26 +463,32 @@ stringData:
#### Health Monitoring
**Set up health checks**. Configure readiness and liveness probes:
**Set up health checks**. Configure readiness and liveness probes using the appropriate endpoints:
```yaml
# Health check configuration
containers:
- name: infisical
# Use /api/ready for readiness (traffic routing)
readinessProbe:
httpGet:
path: /api/status
path: /api/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
# Use /api/health for liveness (container restart)
livenessProbe:
httpGet:
path: /api/status
path: /api/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
```
**Important**: The `readinessProbe` uses `/api/ready` to ensure traffic is only sent to pods that are fully initialized. The `livenessProbe` uses `/api/health` to keep the container alive during startup. See [Health Check Endpoints](#health-check-endpoints) for detailed information.
#### Infrastructure Considerations
**Use managed databases (if possible)**. For production deployments, consider using managed PostgreSQL and Redis services instead of in-cluster instances when feasible, as they typically provide better security, backup, and maintenance capabilities.
@@ -3,7 +3,7 @@ import type { Decorator } from "@storybook/react-vite";
export const DocumentDecorator: Decorator = (Story) => {
useEffect(() => {
const root = document.getElementsByTagName("html")[0];
const root = document.documentElement;
root.setAttribute("class", "overflow-visible");
}, []);
@@ -1,9 +1,11 @@
import { components, OptionProps } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { faBuilding, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, Tooltip } from "@app/components/v2";
import { Tooltip } from "@app/components/v2";
import { Badge, OrgIcon, SubOrgIcon } from "@app/components/v3";
import { useOrganization } from "@app/context";
import { TAvailableAppConnection } from "@app/hooks/api/appConnections";
export const AppConnectionOption = ({
@@ -13,6 +15,8 @@ export const AppConnectionOption = ({
}: OptionProps<TAvailableAppConnection>) => {
const isCreateOption = props.data.id === "_create";
const { isSubOrganization } = useOrganization();
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
@@ -23,15 +27,22 @@ export const AppConnectionOption = ({
</div>
) : (
<>
<p className="truncate">{children}</p>
<p className="mr-auto truncate">{children}</p>
{!props.data.projectId && (
<Tooltip content="This connection belongs to your organization.">
<div className="mr-auto ml-2">
<Badge className="flex h-5 w-min items-center gap-1 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300 hover:text-bunker-300">
<FontAwesomeIcon icon={faBuilding} size="sm" />
<Tooltip
content={`This connection belongs to your ${isSubOrganization ? "sub-" : ""}organization.`}
>
{isSubOrganization ? (
<Badge variant="sub-org">
<SubOrgIcon />
Sub-Organization
</Badge>
) : (
<Badge variant="org">
<OrgIcon />
Organization
</Badge>
</div>
)}
</Tooltip>
)}
{isSelected && (
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faDownload,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { CheckIcon, DownloadIcon, LucideIcon, TriangleAlertIcon } from "lucide-react";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { Tooltip } from "@app/components/v2";
import { Badge, TBadgeProps } from "@app/components/v3";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync: TPkiSync;
className?: string;
mini?: boolean;
};
export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) => {
export const PkiSyncImportStatusBadge = ({ pkiSync, mini }: Props) => {
const { importStatus, lastImportMessage, lastImportedAt, destination } = pkiSync;
const [hide, setHide] = useState(importStatus === PkiSyncStatus.Succeeded);
const destinationName = PKI_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) =>
if (!importStatus || hide) return null;
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let Icon: LucideIcon;
let tooltipContent: ReactNode;
switch (importStatus) {
case PkiSyncStatus.Pending:
case PkiSyncStatus.Running:
variant = "primary";
variant = "warning";
label = "Importing Certificates...";
tooltipContent = `Importing certificates from ${destinationName}. This may take a moment.`;
icon = faDownload;
Icon = DownloadIcon;
break;
case PkiSyncStatus.Failed:
variant = "danger";
label = "Failed to Import Certificates";
icon = faTriangleExclamation;
Icon = TriangleAlertIcon;
tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && (
@@ -93,20 +86,15 @@ export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) =>
tooltipContent = "Successfully imported certificates.";
variant = "success";
label = "Certificates Imported";
icon = faCheck;
Icon = CheckIcon;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
<Badge isSquare={mini} variant={variant}>
<Icon />
{!mini && label}
</Badge>
</div>
</Tooltip>
);
};
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentationLinkBadge } from "@app/components/v3";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
@@ -20,24 +18,12 @@ export const PkiSyncModalHeader = ({ destination, isConfigured }: Props) => {
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
{destinationDetails.name} Certificate Sync
<a
target="_blank"
href={`https://infisical.com/docs/integrations/pki-syncs/${destination}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
<DocumentationLinkBadge
href={`https://infisical.com/docs/documentation/platform/pki/certificate-syncs/${destination}`}
/>
</div>
</a>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured
? `Edit ${destinationDetails.name} Certificate Sync`
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faEraser,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { AlertTriangleIcon, CheckIcon, EraserIcon, LucideIcon } from "lucide-react";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { Tooltip } from "@app/components/v2";
import { Badge, TBadgeProps } from "@app/components/v3";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync: TPkiSync;
className?: string;
mini?: boolean;
};
export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) => {
export const PkiSyncRemoveStatusBadge = ({ pkiSync, mini }: Props) => {
const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = pkiSync;
const [hide, setHide] = useState(removeStatus === PkiSyncStatus.Succeeded);
const destinationName = PKI_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) =>
if (!removeStatus || hide) return null;
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let Icon: LucideIcon;
let tooltipContent: ReactNode;
switch (removeStatus) {
case PkiSyncStatus.Pending:
case PkiSyncStatus.Running:
variant = "primary";
variant = "warning";
label = "Removing Certificates...";
tooltipContent = `Removing certificates from ${destinationName}. This may take a moment.`;
icon = faEraser;
Icon = EraserIcon;
break;
case PkiSyncStatus.Failed:
variant = "danger";
label = "Failed to Remove Certificates";
icon = faTriangleExclamation;
Icon = AlertTriangleIcon;
tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && (
@@ -93,20 +86,15 @@ export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) =>
tooltipContent = "Successfully removed certificates.";
variant = "success";
label = "Certificates Removed";
icon = faCheck;
Icon = CheckIcon;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
<Badge isSquare={mini} variant={variant}>
<Icon />
{!mini && label}
</Badge>
</div>
</Tooltip>
);
};
@@ -69,7 +69,10 @@ export const PkiSyncSelect = ({ onSelect }: Props) => {
type="button"
onClick={() =>
enterprise && !subscription.enterpriseCertificateSyncs
? handlePopUpOpen("upgradePlan")
? handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true,
text: "You can use every Certificate Sync if you switch to Infisical's Enterprise plan."
})
: onSelect(destination)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
@@ -148,6 +151,7 @@ export const PkiSyncSelect = ({ onSelect }: Props) => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text="You can use every Certificate Sync if you switch to Infisical's Enterprise plan."
/>
</div>
@@ -1,55 +1,51 @@
import {
faCheck,
faExclamationTriangle,
faHourglass,
faRotate,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
AlertTriangleIcon,
CheckIcon,
HourglassIcon,
LucideIcon,
RefreshCwIcon
} from "lucide-react";
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge";
import { Badge, TBadgeProps } from "@app/components/v3";
import { PkiSyncStatus } from "@app/hooks/api/pkiSyncs";
type Props = {
status: PkiSyncStatus;
} & Omit<BadgeProps, "children" | "variant">;
} & Omit<TBadgeProps, "children" | "variant">;
export const PkiSyncStatusBadge = ({ status }: Props) => {
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let text: string;
let icon: IconDefinition;
let Icon: LucideIcon;
switch (status) {
case PkiSyncStatus.Failed:
variant = "danger";
text = "Failed to Sync";
icon = faExclamationTriangle;
Icon = AlertTriangleIcon;
break;
case PkiSyncStatus.Succeeded:
variant = "success";
text = "Synced";
icon = faCheck;
Icon = CheckIcon;
break;
case PkiSyncStatus.Pending:
variant = "primary";
variant = "info";
text = "Queued";
icon = faHourglass;
Icon = HourglassIcon;
break;
case PkiSyncStatus.Running:
default:
variant = "primary";
variant = "info";
text = "Syncing";
icon = faRotate;
Icon = RefreshCwIcon;
break;
}
return (
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}>
<FontAwesomeIcon
icon={icon}
className={status === PkiSyncStatus.Running ? "animate-spin" : ""}
/>
<span>{text}</span>
<Badge variant={variant}>
<Icon className={status === PkiSyncStatus.Running ? "animate-spin" : ""} />
{text}
</Badge>
);
};
@@ -2,7 +2,6 @@ import { faPlug, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Badge,
EmptyState,
IconButton,
Table,
@@ -13,6 +12,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
@@ -29,10 +29,10 @@ const getSyncStatusBadge = (status?: string) => {
case "FAILED":
return <Badge variant="danger">Failed</Badge>;
case "RUNNING":
return <Badge variant="primary">Running</Badge>;
return <Badge variant="warning">Running</Badge>;
case "PENDING":
default:
return <Badge variant="primary">Pending</Badge>;
return <Badge variant="warning">Pending</Badge>;
}
};
@@ -71,7 +71,7 @@ export const PkiSyncTable = ({ pkiSyncs, onEdit, onDelete, onTrigger }: Props) =
<Td>{pkiSync.name}</Td>
<Td>
<div className="flex items-center space-x-2">
<Badge variant="primary">{pkiSync.destination}</Badge>
<Badge variant="warning">{pkiSync.destination}</Badge>
</div>
</Td>
<Td>{pkiSync.appConnectionName || "Unknown"}</Td>
@@ -1,6 +1,7 @@
import { useFormContext } from "react-hook-form";
import { Badge, GenericFieldLabel } from "@app/components/v2";
import { GenericFieldLabel } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useProject } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
@@ -1,6 +1,4 @@
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms";
@@ -8,6 +6,7 @@ import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms
import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader";
import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select";
import { Modal, ModalContent } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import { ProjectEnv } from "@app/hooks/api/projects/types";
import { SecretRotation, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
@@ -115,23 +114,9 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
selectedRotation ? (
<SecretRotationV2ModalHeader isConfigured={false} type={selectedRotation} />
) : (
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
Add Secret Rotation
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-rotation/overview"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/secret-rotation/overview" />
</div>
)
}
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentationLinkBadge } from "@app/components/v3";
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
@@ -20,24 +18,12 @@ export const SecretRotationV2ModalHeader = ({ type, isConfigured }: Props) => {
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
{destinationDetails.name} Rotation
<a
target="_blank"
<DocumentationLinkBadge
href={`https://infisical.com/docs/documentation/platform/secret-rotation/${type}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured
? `Edit ${destinationDetails.name} Rotation`
@@ -1,18 +1,17 @@
import { faBan, faRotate, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, formatDistanceToNow } from "date-fns";
import { twMerge } from "tailwind-merge";
import { BanIcon, RefreshCwIcon, XIcon } from "lucide-react";
import { Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge/Badge";
import { Badge } from "@app/components/v3";
import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
type Props = {
secretRotation: TSecretRotationV2;
className?: string;
};
export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props) => {
export const SecretRotationV2StatusBadge = ({ secretRotation }: Props) => {
const { isAutoRotationEnabled, rotationStatus, nextRotationAt, lastRotationMessage } =
secretRotation;
@@ -50,28 +49,18 @@ export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props
</div>
}
>
<div>
<Badge
variant="danger"
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
>
<FontAwesomeIcon icon={faXmark} />
<Badge variant="danger">
<XIcon />
Rotation Failed
</Badge>
</div>
</Tooltip>
);
}
if (!isAutoRotationEnabled) {
return (
<Badge
className={twMerge(
"flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300",
className
)}
>
<FontAwesomeIcon icon={faBan} />
<Badge variant="neutral">
<BanIcon />
Auto-Rotation Disabled
</Badge>
);
@@ -92,20 +81,12 @@ export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props
</>
}
>
<div>
<Badge
variant={daysToRotation >= 7 ? "success" : "primary"}
className={twMerge(
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
className
)}
>
<FontAwesomeIcon icon={faRotate} />
<Badge variant={daysToRotation >= 7 ? "info" : "warning"} className="capitalize">
<RefreshCwIcon />
{daysToRotation < 0
? "Rotating"
: `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`}
</Badge>
</div>
</Tooltip>
);
};
@@ -1,8 +1,10 @@
import { ReactNode } from "react";
import { faArrowRight, faKey } from "@fortawesome/free-solid-svg-icons";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { KeyIcon } from "lucide-react";
import { Badge, FormLabel } from "@app/components/v2";
import { FormLabel } from "@app/components/v2";
import { Badge } from "@app/components/v3";
type Props = {
items: { name: string; input: ReactNode }[];
@@ -32,9 +34,10 @@ export const SecretsMappingTable = ({ items }: Props) => {
<tr key={name}>
<td className="whitespace-nowrap">
<div className="mb-4 flex h-full items-start justify-center">
<Badge className="pointer-events-none flex h-[36px] w-full items-center justify-center gap-1.5 border border-mineshaft-600 bg-mineshaft-600 whitespace-nowrap text-bunker-200">
<FontAwesomeIcon icon={faKey} />
<span>{name}</span>
{/* TODO(scott): probably shouldn't be a badge */}
<Badge variant="neutral" className="h-[36px] w-full justify-center text-xs">
<KeyIcon />
{name}
</Badge>
</div>
</td>
@@ -1,10 +1,9 @@
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { TSecretScanningDataSourceForm } from "@app/components/secret-scanning/forms/schemas";
import { Modal, ModalContent } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import {
SecretScanningDataSource,
TSecretScanningDataSource
@@ -108,23 +107,9 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
selectedDataSource ? (
<SecretScanningDataSourceModalHeader isConfigured={false} type={selectedDataSource} />
) : (
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
Add Data Source
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/secret-scanning/overview" />
</div>
)
}
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentationLinkBadge } from "@app/components/v3";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
@@ -20,24 +18,12 @@ export const SecretScanningDataSourceModalHeader = ({ type, isConfigured }: Prop
className="h-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
{dataSourceDetails.name} Data Source
<a
target="_blank"
<DocumentationLinkBadge
href={`https://infisical.com/docs/documentation/platform/secret-scanning/${type}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured ? "Edit" : "Connect a"} {dataSourceDetails.name} Data Source
</p>
@@ -1,22 +1,23 @@
import { faArrowRotateForward, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { formatDistance } from "date-fns";
import { twMerge } from "tailwind-merge";
import { CheckIcon, RotateCwIcon, XIcon } from "lucide-react";
import { Badge, Tooltip } from "@app/components/v2";
import { Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { SecretScanningScanStatus } from "@app/hooks/api/secretScanningV2";
type Props = {
status: SecretScanningScanStatus;
statusMessage?: string | null;
className?: string;
scannedAt?: string | null;
};
export const SecretScanningScanStatusBadge = ({
status,
statusMessage,
className,
scannedAt
}: Props) => {
if (status === SecretScanningScanStatus.Failed) {
@@ -53,11 +54,8 @@ export const SecretScanningScanStatusBadge = ({
}
>
<div>
<Badge
variant="danger"
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
>
<FontAwesomeIcon icon={faXmark} />
<Badge variant="danger">
<XIcon />
Scan Error
</Badge>
</div>
@@ -67,26 +65,17 @@ export const SecretScanningScanStatusBadge = ({
if (status === SecretScanningScanStatus.Queued || status === SecretScanningScanStatus.Scanning) {
return (
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant="primary"
>
<FontAwesomeIcon icon={faArrowRotateForward} className="animate-spin" />
<span>Scanning</span>
<Badge variant="info">
<RotateCwIcon className="animate-spin" />
Scanning
</Badge>
);
}
return (
<Badge
variant="success"
className={twMerge(
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
className
)}
>
<FontAwesomeIcon icon={faCheck} />
<span>Complete</span>
<Badge variant="success">
<CheckIcon />
Complete
</Badge>
);
};
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faDownload,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { AlertTriangleIcon, CheckIcon, DownloadIcon, LucideIcon } from "lucide-react";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { Tooltip } from "@app/components/v2";
import { Badge, TBadgeProps } from "@app/components/v3";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = {
secretSync: TSecretSync;
className?: string;
mini?: boolean;
};
export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Props) => {
export const SecretSyncImportStatusBadge = ({ secretSync, mini }: Props) => {
const { importStatus, lastImportMessage, lastImportedAt, destination } = secretSync;
const [hide, setHide] = useState(importStatus === SecretSyncStatus.Succeeded);
const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Pro
if (!importStatus || hide) return null;
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let Icon: LucideIcon;
let tooltipContent: ReactNode;
switch (importStatus) {
case SecretSyncStatus.Pending:
case SecretSyncStatus.Running:
variant = "primary";
variant = "warning";
label = "Importing Secrets...";
tooltipContent = `Importing secrets from ${destinationName}. This may take a moment.`;
icon = faDownload;
Icon = DownloadIcon;
break;
case SecretSyncStatus.Failed:
variant = "danger";
label = "Failed to Import Secrets";
icon = faTriangleExclamation;
Icon = AlertTriangleIcon;
tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && (
@@ -93,20 +86,15 @@ export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Pro
tooltipContent = "Successfully imported secrets.";
variant = "success";
label = "Secrets Imported";
icon = faCheck;
Icon = CheckIcon;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
<Badge isSquare={mini} variant={variant}>
<Icon />
{!mini && label}
</Badge>
</div>
</Tooltip>
);
};
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentationLinkBadge } from "@app/components/v3";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync } from "@app/hooks/api/secretSyncs";
@@ -20,24 +18,12 @@ export const SecretSyncModalHeader = ({ destination, isConfigured }: Props) => {
className="h-12 w-12 rounded-md bg-bunker-500 object-contain p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
{destinationDetails.name} Sync
<a
target="_blank"
<DocumentationLinkBadge
href={`https://infisical.com/docs/integrations/secret-syncs/${destination}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured
? `Edit ${destinationDetails.name} Sync`
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faEraser,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { AlertTriangleIcon, CheckIcon, EraserIcon, LucideIcon } from "lucide-react";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { Tooltip } from "@app/components/v2";
import { Badge, TBadgeProps } from "@app/components/v3";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = {
secretSync: TSecretSync;
className?: string;
mini?: boolean;
};
export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Props) => {
export const SecretSyncRemoveStatusBadge = ({ secretSync, mini }: Props) => {
const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = secretSync;
const [hide, setHide] = useState(removeStatus === SecretSyncStatus.Succeeded);
const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Pro
if (!removeStatus || hide) return null;
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let Icon: LucideIcon;
let tooltipContent: ReactNode;
switch (removeStatus) {
case SecretSyncStatus.Pending:
case SecretSyncStatus.Running:
variant = "primary";
variant = "warning";
label = "Removing Secrets...";
tooltipContent = `Removing secrets from ${destinationName}. This may take a moment.`;
icon = faEraser;
Icon = EraserIcon;
break;
case SecretSyncStatus.Failed:
variant = "danger";
label = "Failed to Remove Secrets";
icon = faTriangleExclamation;
Icon = AlertTriangleIcon;
tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && (
@@ -93,20 +86,15 @@ export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Pro
tooltipContent = "Successfully removed secrets.";
variant = "success";
label = "Secrets Removed";
icon = faCheck;
Icon = CheckIcon;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
<Badge isSquare={mini} variant={variant}>
<Icon />
{!mini && label}
</Badge>
</div>
</Tooltip>
);
};
@@ -67,7 +67,9 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
type="button"
onClick={() =>
enterprise && !subscription.enterpriseSecretSyncs
? handlePopUpOpen("upgradePlan")
? handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
})
: onSelect(destination)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
@@ -145,6 +147,7 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
)}
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use every Secret Sync if you switch to Infisical's Enterprise plan."
/>
@@ -1,55 +1,51 @@
import {
faCheck,
faExclamationTriangle,
faHourglass,
faRotate,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
AlertTriangleIcon,
CheckIcon,
HourglassIcon,
LucideIcon,
RefreshCwIcon
} from "lucide-react";
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge";
import { Badge, TBadgeProps } from "@app/components/v3";
import { SecretSyncStatus } from "@app/hooks/api/secretSyncs";
type Props = {
status: SecretSyncStatus;
} & Omit<BadgeProps, "children" | "variant">;
} & Omit<TBadgeProps, "children" | "variant">;
export const SecretSyncStatusBadge = ({ status }: Props) => {
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let text: string;
let icon: IconDefinition;
let Icon: LucideIcon;
switch (status) {
case SecretSyncStatus.Failed:
variant = "danger";
text = "Failed to Sync";
icon = faExclamationTriangle;
Icon = AlertTriangleIcon;
break;
case SecretSyncStatus.Succeeded:
variant = "success";
text = "Synced";
icon = faCheck;
Icon = CheckIcon;
break;
case SecretSyncStatus.Pending:
variant = "primary";
variant = "info";
text = "Queued";
icon = faHourglass;
Icon = HourglassIcon;
break;
case SecretSyncStatus.Running:
default:
variant = "primary";
variant = "info";
text = "Syncing";
icon = faRotate;
Icon = RefreshCwIcon;
break;
}
return (
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}>
<FontAwesomeIcon
icon={icon}
className={[SecretSyncStatus.Running].includes(status) ? "animate-spin" : ""}
/>
<span>{text}</span>
<Badge variant={variant}>
<Icon className={[SecretSyncStatus.Running].includes(status) ? "animate-spin" : ""} />
{text}
</Badge>
);
};
@@ -5,14 +5,8 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import {
Badge,
FilterableSelect,
FormControl,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { GCP_SYNC_SCOPES } from "@app/helpers/secretSyncs";
import {
useGcpConnectionListProjectLocations,
@@ -26,10 +20,7 @@ import { TSecretSyncForm } from "../schemas";
const formatOptionLabel = ({ displayName, locationId }: TGcpLocation) => (
<div className="flex w-full flex-row items-center gap-1">
<span>{displayName}</span>{" "}
<Badge className="h-5 leading-5" variant="success">
{locationId}
</Badge>
<span>{displayName}</span> <Badge variant="info">{locationId}</Badge>
</div>
);
@@ -2,7 +2,8 @@ import { components, OptionProps, SingleValue } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, FilterableSelect } from "@app/components/v2";
import { FilterableSelect } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { AWS_REGIONS } from "@app/helpers/appConnections";
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => {
@@ -10,7 +11,7 @@ const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGI
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
<p className="truncate">{children}</p>
<Badge variant="success" className="mr-auto ml-1 cursor-pointer">
<Badge variant="neutral" className="mr-auto ml-1">
{props.data.slug}
</Badge>
{isSelected && (
@@ -1,10 +1,10 @@
import { useFormContext } from "react-hook-form";
import { faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EyeIcon } from "lucide-react";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import { SecretSync } from "@app/hooks/api/secretSyncs";
@@ -41,11 +41,9 @@ export const AwsParameterStoreSyncOptionsReviewFields = () => {
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>
<Badge variant="neutral">
<EyeIcon />
{tags.length} Tag{tags.length > 1 ? "s" : ""}
</span>
</Badge>
</div>
</Tooltip>
@@ -1,10 +1,10 @@
import { useFormContext } from "react-hook-form";
import { faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EyeIcon } from "lucide-react";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
@@ -26,7 +26,7 @@ export const AwsSecretsManagerSyncReviewFields = () => {
<>
<GenericFieldLabel label="Region">
{awsRegion?.name}
<Badge className="ml-1" variant="success">
<Badge className="ml-1" variant="info">
{awsRegion?.slug}{" "}
</Badge>
</GenericFieldLabel>
@@ -73,11 +73,9 @@ export const AwsSecretsManagerSyncOptionsReviewFields = () => {
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>
<Badge variant="neutral">
<EyeIcon />
{tags.length} Tag{tags.length > 1 ? "s" : ""}
</span>
</Badge>
</div>
</Tooltip>
@@ -2,7 +2,7 @@ import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useOrganization, useProject } from "@app/context";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync, useDuplicateDestinationCheck } from "@app/hooks/api/secretSyncs";
@@ -257,7 +257,7 @@ export const SecretSyncReviewFields = () => {
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Auto-Sync">
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
<Badge variant={isAutoSyncEnabled ? "success" : "neutral"}>
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
@@ -268,7 +268,7 @@ export const SecretSyncReviewFields = () => {
{AdditionalSyncOptionsFieldsComponent}
{disableSecretDeletion && (
<GenericFieldLabel label="Secret Deletion">
<Badge variant="primary">Disabled</Badge>
<Badge variant="warning">Disabled</Badge>
</GenericFieldLabel>
)}
</div>
@@ -1,47 +0,0 @@
import { forwardRef } from "react";
import { cva, VariantProps } from "cva";
import { twMerge } from "tailwind-merge";
interface IProps {
children: React.ReactNode;
className?: string;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}
const badgeVariants = cva(
[
"inline-block cursor-default rounded-md bg-yellow/20 px-1.5 py-0.5 text-xs text-yellow opacity-80 hover:opacity-100"
],
{
variants: {
variant: {
primary: "bg-yellow/20 text-yellow",
danger: "bg-red/20 text-red",
success: "bg-green/20 text-green",
org: "bg-org-v1/20 text-org-v1 [&_svg]:text-org-v1 flex items-center opacity-100 hover:bg-org-v1/10 [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
namespace:
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1.5 w-min whitespace-nowrap",
project:
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 w-min gap-x-1.5 whitespace-nowrap",
instance:
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1.5 w-min whitespace-nowrap"
}
}
}
);
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
({ children, className, variant, ...props }, ref) => {
return (
<div
className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}
{...props}
ref={ref}
>
{children}
</div>
);
}
);
@@ -1 +0,0 @@
export { Badge } from "./Badge";
@@ -1,11 +1,16 @@
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { faCube, faCubes, faGlobe, faServer } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createElement } from "react";
import { ReactNode } from "@tanstack/react-router";
import { LucideIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { Badge } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import {
Badge,
InstanceIcon,
OrgIcon,
ProjectIcon,
SubOrgIcon,
TBadgeProps
} from "@app/components/v3";
import { ProjectType } from "@app/hooks/api/projects/types";
type Props = {
@@ -16,19 +21,19 @@ type Props = {
scope: "org" | "namespace" | "instance" | ProjectType | null;
};
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: IconDefinition }> = {
org: { label: "Organization", icon: faGlobe },
[ProjectType.SecretManager]: { label: "Project", icon: faCube },
[ProjectType.CertificateManager]: { label: "Project", icon: faCube },
[ProjectType.SSH]: { label: "Project", icon: faCube },
[ProjectType.KMS]: { label: "Project", icon: faCube },
[ProjectType.PAM]: { label: "Project", icon: faCube },
[ProjectType.SecretScanning]: { label: "Project", icon: faCube },
namespace: { label: "Sub-Organization", icon: faCubes },
instance: { label: "Server", icon: faServer }
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: LucideIcon }> = {
org: { label: "Organization", icon: OrgIcon },
[ProjectType.SecretManager]: { label: "Project", icon: ProjectIcon },
[ProjectType.CertificateManager]: { label: "Project", icon: ProjectIcon },
[ProjectType.SSH]: { label: "Project", icon: ProjectIcon },
[ProjectType.KMS]: { label: "Project", icon: ProjectIcon },
[ProjectType.PAM]: { label: "Project", icon: ProjectIcon },
[ProjectType.SecretScanning]: { label: "Project", icon: ProjectIcon },
namespace: { label: "Sub-Organization", icon: SubOrgIcon },
instance: { label: "Server", icon: InstanceIcon }
};
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, BadgeProps["variant"]> = {
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, TBadgeProps["variant"]> = {
org: "org",
[ProjectType.SecretManager]: "project",
[ProjectType.CertificateManager]: "project",
@@ -36,8 +41,8 @@ const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, BadgeProps["variant"]>
[ProjectType.KMS]: "project",
[ProjectType.PAM]: "project",
[ProjectType.SecretScanning]: "project",
namespace: "namespace",
instance: "instance"
namespace: "sub-org",
instance: "neutral"
};
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
@@ -47,7 +52,7 @@ export const PageHeader = ({ title, description, children, className, scope }: P
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
{scope && (
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5">
<FontAwesomeIcon icon={SCOPE_NAME[scope].icon} />
{createElement(SCOPE_NAME[scope].icon)}
{SCOPE_NAME[scope].label}
</Badge>
)}
@@ -3,6 +3,7 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faCaretDown, faCaretUp, faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as SelectPrimitive from "@radix-ui/react-select";
import { LucideIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { Spinner } from "../Spinner";
@@ -19,6 +20,7 @@ type Props = {
icon?: IconProp;
isMulti?: boolean;
iconClassName?: string;
LucideIcon?: LucideIcon;
dropdownContainerStyle?: React.CSSProperties;
side?: SelectPrimitive.SelectContentProps["side"];
};
@@ -39,6 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
iconClassName,
dropdownContainerStyle,
side,
LucideIcon: Lucide,
...props
},
ref
@@ -65,6 +68,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
>
<div className="flex items-center space-x-2 overflow-hidden text-ellipsis whitespace-nowrap">
{props.icon && <FontAwesomeIcon icon={props.icon} className={iconClassName} />}
{Lucide && <Lucide className={twMerge("size-3.5", iconClassName)} />}
<div className="flex-1 truncate">
<SelectPrimitive.Value placeholder={placeholder} />
</div>
-1
View File
@@ -2,7 +2,6 @@
export * from "./AccessRestrictedBanner";
export * from "./Accordion";
export * from "./Alert";
export * from "./Badge";
export * from "./Breadcrumb";
export * from "./Button";
export * from "./Card";
@@ -1,9 +1,9 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Link } from "@tanstack/react-router";
import {
AlertTriangleIcon,
AsteriskIcon,
BanIcon,
BoxesIcon,
BoxIcon,
CheckIcon,
ChevronsUpDownIcon,
CircleXIcon,
@@ -11,9 +11,11 @@ import {
GlobeIcon,
InfoIcon,
RadarIcon,
TriangleAlertIcon
TriangleAlertIcon,
UserIcon
} from "lucide-react";
import { OrgIcon, ProjectIcon, SubOrgIcon } from "../../platform";
import { Badge } from "./Badge";
/**
@@ -69,7 +71,29 @@ export const Neutral: Story = {
parameters: {
docs: {
description: {
story: "Use this variant when indicating neutral or disabled states."
story:
"Use this variant when indicating neutral or disabled states or when linking to external documents."
}
}
}
};
export const Ghost: Story = {
name: "Variant: Ghost",
args: {
variant: "ghost",
children: (
<>
<UserIcon />
User
</>
)
},
parameters: {
docs: {
description: {
story:
"Use this variant when indicating a configuration or property value. Avoid using this variant as an interactive element as it is not intuitive to interact with."
}
}
}
@@ -109,8 +133,7 @@ export const Info: Story = {
parameters: {
docs: {
description: {
story:
"Use this variant when indicating informational states or linking to external references."
story: "Use this variant when indicating informational states."
}
}
}
@@ -162,7 +185,7 @@ export const Organization: Story = {
variant: "org",
children: (
<>
<GlobeIcon />
<OrgIcon />
Organization
</>
)
@@ -182,7 +205,7 @@ export const SubOrganization: Story = {
variant: "sub-org",
children: (
<>
<BoxesIcon />
<SubOrgIcon />
Sub-Organization
</>
)
@@ -202,7 +225,7 @@ export const Project: Story = {
variant: "project",
children: (
<>
<BoxIcon />
<ProjectIcon />
Project
</>
)
@@ -283,10 +306,11 @@ export const AsButton: Story = {
export const IsTruncatable: Story = {
name: "Example: isTruncatable",
args: {
variant: "org",
isTruncatable: true,
children: (
<>
<GlobeIcon />
<OrgIcon />
<span>Infisical Infrastructure</span>
</>
)
@@ -295,7 +319,7 @@ export const IsTruncatable: Story = {
docs: {
description: {
story:
"Use the `isTruncatable` prop with a `span` tag wrapping the text content to support truncation."
"Use the `isTruncatable` prop with a `span` tag wrapping the text content to support truncation. Parent `div` should have a fixed width and `flex` class."
}
}
},
@@ -305,3 +329,48 @@ export const IsTruncatable: Story = {
</div>
)
};
export const IsSquare: Story = {
name: "Example: isSquare",
args: {
variant: "danger",
isSquare: true,
children: <AlertTriangleIcon />
},
parameters: {
docs: {
description: {
story:
"Use the `isSquare` prop when displaying a squared badge with 1-2 character text or only an icon."
}
}
}
};
export const IsFullWidth: Story = {
name: "Example: isFullWidth",
args: {
variant: "neutral",
isFullWidth: true,
children: (
<>
<AsteriskIcon />
Secret Value
</>
)
},
parameters: {
docs: {
description: {
story:
"Use the `isFullWidth` prop to expand the badges width to fill it's parent container."
}
}
},
decorators: (Story) => (
<div className="w-32">
<Story />
</div>
)
};
@@ -6,30 +6,37 @@ import { cn } from "@app/components/v3/utils";
const badgeVariants = cva(
[
"select-none items-center rounded-sm px-1.5 py-0.5 text-xs",
"gap-x-1 [a&,button&]:cursor-pointer inline-flex",
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&>svg]:size-3",
"select-none items-center align-middle rounded-sm h-4.5 px-1.5 text-xs",
"gap-x-1 [a&,button&]:cursor-pointer inline-flex font-normal",
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&_svg:not([class*='size-'])]:size-3",
"transition duration-200 ease-in-out"
],
{
variants: {
isTruncatable: {
true: "[&>span,&>p]:truncate min-w-0",
false: "w-fit shrink-0 whitespace-nowrap overflow-hidden"
false: "w-fit shrink-0 min-w-fit whitespace-nowrap overflow-hidden"
},
isFullWidth: {
true: "w-full justify-center"
},
isSquare: {
true: "w-4.5 justify-center px-0.5"
},
variant: {
neutral: "bg-neutral/30 text-neutral [a&,button&]:hover:bg-neutral/40",
success: "bg-success/30 text-success [a&,button&]:hover:bg-success/40",
info: "bg-info/30 text-info [a&,button&]:hover:bg-info/40",
warning: "bg-warning/30 text-warning [a&,button&]:hover:bg-warning/40",
danger: "bg-danger/30 text-danger [a&,button&]:hover:bg-danger/40",
project: "bg-project/30 text-project [a&,button&]:hover:bg-project/40",
org: "bg-org/30 text-org [a&,button&]:hover:bg-org/40",
"sub-org": "bg-sub-org/30 text-sub-org [a&,button&]:hover:bg-sub-org/40"
ghost: "text-mineshaft-200 gap-x-2",
neutral: "bg-neutral/25 text-neutral [a&,button&]:hover:bg-neutral/35",
success: "bg-success/25 text-success [a&,button&]:hover:bg-success/35",
info: "bg-info/25 text-info [a&,button&]:hover:bg-info/35",
warning: "bg-warning/25 text-warning [a&,button&]:hover:bg-warning/35",
danger: "bg-danger/25 text-danger [a&,button&]:hover:bg-danger/35",
project: "bg-project/25 text-project [a&,button&]:hover:bg-project/35",
org: "bg-org/25 text-org [a&,button&]:hover:bg-org/35",
"sub-org": "bg-sub-org/25 text-sub-org [a&,button&]:hover:bg-sub-org/35"
}
},
defaultVariants: {
variant: "success"
variant: "neutral"
}
}
);
@@ -37,16 +44,28 @@ const badgeVariants = cva(
type TBadgeProps = VariantProps<typeof badgeVariants> &
React.ComponentProps<"span"> & {
asChild?: boolean;
variant: NonNullable<VariantProps<typeof badgeVariants>["variant"]>; // TODO: REMOVE
};
const Badge = forwardRef<HTMLSpanElement, TBadgeProps>(
({ className, variant, asChild = false, isTruncatable = false, ...props }, ref): JSX.Element => {
(
{
className,
variant,
asChild = false,
isTruncatable = false,
isFullWidth = false,
isSquare = false,
...props
},
ref
): JSX.Element => {
const Comp = asChild ? Slot : "span";
return (
<Comp
ref={ref}
data-slot="badge"
className={cn(badgeVariants({ variant, isTruncatable }), className)}
className={cn(badgeVariants({ variant, isTruncatable, isFullWidth, isSquare }), className)}
{...props}
/>
);
+2
View File
@@ -0,0 +1,2 @@
export * from "./generic";
export * from "./platform";
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { DocumentationLinkBadge } from "./DocumentationLinkBadge";
/**
* The documentation link badge is a re-usable component to link to Infisical documentation references.
*/
const meta = {
title: "Platform/Documentation Link Badge",
component: DocumentationLinkBadge,
parameters: {
layout: "centered"
},
tags: ["autodocs"],
argTypes: {},
args: { href: "https://infisical.com/docs/documentation/getting-started/introduction" }
} satisfies Meta<typeof DocumentationLinkBadge>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
name: "Example: Default"
};
@@ -0,0 +1,18 @@
import { BookOpenIcon } from "lucide-react";
import { Badge } from "@app/components/v3";
type TDocumentationLinkBadgeProps = {
href: string;
};
export function DocumentationLinkBadge({ href }: TDocumentationLinkBadgeProps) {
return (
<Badge variant="neutral" asChild>
<a href={href} target="_blank" rel="noopener noreferrer">
<BookOpenIcon />
Documentation
</a>
</Badge>
);
}
@@ -0,0 +1 @@
export * from "./DocumentationLinkBadge";
@@ -0,0 +1,8 @@
import { BoxesIcon, BoxIcon, Building2Icon, ServerIcon } from "lucide-react";
const InstanceIcon = ServerIcon;
const OrgIcon = Building2Icon;
const SubOrgIcon = BoxesIcon;
const ProjectIcon = BoxIcon;
export { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon };
@@ -0,0 +1,2 @@
export * from "./DocumentationLinkBadge";
export * from "./ScopeIcons";
+7 -7
View File
@@ -1,20 +1,20 @@
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { faArrowRightToBracket, faEdit } from "@fortawesome/free-solid-svg-icons";
import { LucideIcon, UserCheckIcon, UserPenIcon } from "lucide-react";
import { TBadgeProps } from "@app/components/v3";
import { PolicyType } from "@app/hooks/api/policies/enums";
export const policyDetails: Record<
PolicyType,
{ name: string; className: string; icon: IconDefinition }
{ name: string; variant: TBadgeProps["variant"]; Icon: LucideIcon }
> = {
[PolicyType.AccessPolicy]: {
className: "bg-green/20 text-green",
variant: "ghost",
name: "Access Policy",
icon: faArrowRightToBracket
Icon: UserCheckIcon
},
[PolicyType.ChangePolicy]: {
className: "bg-yellow/20 text-yellow",
variant: "ghost",
name: "Change Policy",
icon: faEdit
Icon: UserPenIcon
}
};
+24 -12
View File
@@ -1,10 +1,6 @@
import {
faBan,
faCheck,
faMagnifyingGlassMinus,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { AlertTriangleIcon, BanIcon, CheckIcon, LucideIcon, SearchSlashIcon } from "lucide-react";
import { TBadgeProps } from "@app/components/v3";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
SecretScanningDataSource,
@@ -74,12 +70,28 @@ export const RESOURCE_DESCRIPTION_HELPER: Record<
}
};
export const SECRET_SCANNING_FINDING_STATUS_ICON_MAP = {
[SecretScanningFindingStatus.Resolved]: { icon: faCheck, className: "text-green" },
[SecretScanningFindingStatus.Unresolved]: { icon: faWarning, className: "text-yellow" },
[SecretScanningFindingStatus.Ignore]: { icon: faBan, className: "text-mineshaft-400" },
export const SECRET_SCANNING_FINDING_STATUS_MAP: Record<
SecretScanningFindingStatus,
{ Icon: LucideIcon; variant: TBadgeProps["variant"]; className: string }
> = {
[SecretScanningFindingStatus.Resolved]: {
Icon: CheckIcon,
variant: "success",
className: "text-success"
},
[SecretScanningFindingStatus.Unresolved]: {
Icon: AlertTriangleIcon,
variant: "warning",
className: "text-warning"
},
[SecretScanningFindingStatus.Ignore]: {
Icon: BanIcon,
variant: "neutral",
className: "text-neutral"
},
[SecretScanningFindingStatus.FalsePositive]: {
icon: faMagnifyingGlassMinus,
className: "text-mineshaft-400"
Icon: SearchSlashIcon,
variant: "neutral",
className: "text-neutral"
}
};
@@ -10,5 +10,6 @@ export type TGitLabGroup = {
export enum GitLabAccessTokenType {
Personal = "personal",
Project = "project"
Project = "project",
Group = "group"
}
+1 -1
View File
@@ -53,6 +53,6 @@ export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCert
case CaStatus.DISABLED:
return "danger";
default:
return "primary";
return "warning";
}
};
+34 -1
View File
@@ -5,7 +5,8 @@ import { apiRequest } from "@app/config/request";
import {
ExternalMigrationProviders,
TVaultExternalMigrationConfig,
VaultKubernetesAuthRole
VaultKubernetesAuthRole,
VaultKubernetesRole
} from "./types";
export const externalMigrationQueryKeys = {
@@ -31,6 +32,11 @@ export const externalMigrationQueryKeys = {
"vault-kubernetes-auth-roles",
namespace,
mountPath
],
vaultKubernetesRoles: (namespace?: string, mountPath?: string) => [
"vault-kubernetes-roles",
namespace,
mountPath
]
};
@@ -172,3 +178,30 @@ export const useGetVaultKubernetesAuthRoles = (
enabled: enabled && !!namespace && !!mountPath
});
};
export const useGetVaultKubernetesRoles = (
enabled = true,
namespace?: string,
mountPath?: string
) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultKubernetesRoles(namespace, mountPath),
queryFn: async () => {
if (!namespace || !mountPath) {
throw new Error("Both namespace and mountPath are required");
}
const { data } = await apiRequest.get<{
roles: VaultKubernetesRole[];
}>("/api/v3/external-migration/vault/kubernetes-roles", {
params: {
namespace,
mountPath
}
});
return data.roles;
},
enabled: enabled && !!namespace && !!mountPath
});
};
+21
View File
@@ -49,3 +49,24 @@ export type VaultKubernetesAuthRole = {
disable_local_ca_jwt?: boolean;
};
};
export type VaultKubernetesRole = {
name: string;
mountPath: string;
allowed_kubernetes_namespaces?: string[];
allowed_kubernetes_namespace_selector?: string;
token_max_ttl?: number;
token_default_ttl?: number;
token_default_audiences?: string[];
service_account_name?: string;
kubernetes_role_name?: string;
kubernetes_role_type?: string;
generated_role_rules?: string;
name_template?: string;
extra_annotations?: Record<string, string>;
extra_labels?: Record<string, string>;
config: {
kubernetes_host: string;
kubernetes_ca_cert?: string;
};
};
@@ -15,6 +15,6 @@ export const getPkiSubscriberStatusBadgeVariant = (status: PkiSubscriberStatus)
case PkiSubscriberStatus.DISABLED:
return "danger";
default:
return "primary";
return "warning";
}
};
@@ -7,15 +7,12 @@ import {
faCaretDown,
faCheck,
faChevronRight,
faCubes,
faEnvelope,
faExclamationTriangle,
faGlobe,
faInfinity,
faInfo,
faInfoCircle,
faPlus,
faServer,
faSignOut,
faToolbox,
faUser,
@@ -30,7 +27,6 @@ import { Mfa } from "@app/components/auth/Mfa";
import { createNotification } from "@app/components/notifications";
import SecurityClient from "@app/components/utilities/SecurityClient";
import {
Badge,
BreadcrumbContainer,
Button,
DropdownMenu,
@@ -46,6 +42,7 @@ import {
TBreadcrumbFormat,
Tooltip
} from "@app/components/v2";
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
@@ -271,9 +268,7 @@ export const Navbar = () => {
to="/admin"
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
>
<div>
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
</div>
<InstanceIcon className="size-3.5 text-xs text-bunker-300" />
<div className="whitespace-nowrap">Server Console</div>
</Link>
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
@@ -288,6 +283,18 @@ export const Navbar = () => {
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
<Badge
asChild
variant="org"
isTruncatable
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
className={twMerge(
"gap-x-1.5 text-sm",
(!isOrgScope || isSubOrganization) &&
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-org"
)}
>
<button
type="button"
onClick={async () => {
navigate({
to: "/organization/projects",
@@ -297,15 +304,10 @@ export const Navbar = () => {
await router.invalidate({ sync: true }).catch(() => null);
}
}}
variant="org"
className={twMerge(
"max-w-full min-w-0 cursor-pointer text-sm",
(!isOrgScope || isSubOrganization) &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
)}
>
<FontAwesomeIcon icon={faGlobe} />
<p className="truncate">{currentOrg?.name}</p>
<OrgIcon className="size-[12px]" />
<span>{currentOrg?.name}</span>
</button>
</Badge>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
{getPlan(subscription)}
@@ -443,19 +445,22 @@ export const Navbar = () => {
<>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
<DropdownMenu modal={false}>
<Link className="overflow-hidden" to="/organization/projects">
<Badge
variant="namespace"
asChild
isTruncatable
variant="sub-org"
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
className={twMerge(
"flex max-w-full min-w-0 cursor-pointer text-sm",
"gap-x-1.5 text-sm",
!isOrgScope &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org"
)}
>
<FontAwesomeIcon icon={faCubes} />
<p className="truncate">{currentOrg.subOrganization.name}</p>
</Badge>
<Link to="/organization/projects">
<SubOrgIcon className="size-[12px]" />
<span>{currentOrg.subOrganization.name}</span>
</Link>
</Badge>
<DropdownMenuTrigger asChild>
<div>
<IconButton
@@ -549,10 +554,10 @@ export const Navbar = () => {
)}
{user.superAdmin && !location.pathname.startsWith("/admin") && (
<Link
className="mr-2 rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
className="mr-2 flex items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
to="/admin"
>
<FontAwesomeIcon icon={faServer} className="mr-2" />
<InstanceIcon className="mr-2 inline-block size-3.5" />
Server Console
</Link>
)}
+6 -2
View File
@@ -18,7 +18,10 @@ export const PamLayout = () => {
useEffect(() => {
if (subscription && !subscription.pam) {
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
description: "You can use PAM if you switch to Infisical's Enterprise plan.",
isEnterpriseFeature: true
});
}
}, [subscription]);
@@ -111,7 +114,8 @@ export const PamLayout = () => {
onOpenChange={(isOpen) => {
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use PAM if you switch to a paid Infisical plan."
text={popUp.upgradePlan.data?.description}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</>
);
@@ -3,7 +3,6 @@ import { faStar } from "@fortawesome/free-regular-svg-icons";
import {
faCaretDown,
faCheck,
faCube,
faMagnifyingGlass,
faPlus,
faStar as faSolidStar
@@ -16,7 +15,6 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { NewProjectModal } from "@app/components/projects";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -25,6 +23,7 @@ import {
Input,
Tooltip
} from "@app/components/v2";
import { Badge, ProjectIcon } from "@app/components/v3";
import {
OrgPermissionActions,
OrgPermissionSubjects,
@@ -120,11 +119,9 @@ export const ProjectSelect = () => {
<p className="inline-block truncate text-mineshaft-200 group-hover:underline">
{currentWorkspace?.name}
</p>
<Badge variant="project" className="cursor-pointer">
<FontAwesomeIcon icon={faCube} />
<span>
<Badge variant="project">
<ProjectIcon />
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
</span>
</Badge>
</Link>
<DropdownMenuTrigger asChild>
@@ -4,7 +4,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useProject, useProjectPermission } from "@app/context";
import {
useGetAccessRequestsCount,
@@ -88,7 +89,7 @@ export const SecretManagerLayout = () => {
{Boolean(
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
<Badge variant="warning" isSquare className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
@@ -1,7 +1,8 @@
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import {
ProjectPermissionSub,
useProject,
@@ -66,7 +67,7 @@ export const SecretScanningLayout = () => {
<Tab value={isActive ? "selected" : ""}>
Findings
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="ml-2 h-min">
<Badge isSquare variant="warning" className="ml-2">
{unresolvedFindings}
</Badge>
)}
@@ -7,16 +7,15 @@ import {
faTrash,
faUsers,
faUserXmark,
faWarning,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AlertTriangleIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
Checkbox,
DeleteActionModal,
@@ -38,6 +37,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useSubscription, useUser } from "@app/context";
import {
getUserTablePreference,
@@ -451,7 +451,7 @@ export const ServerAdminsTable = () => {
const email = user.email ?? user.username;
return (
<li key={user.id}>
<div className="flex items-center">
<div className="flex items-center gap-x-1">
<p>
{user.firstName || user.lastName ? (
<>
@@ -464,15 +464,10 @@ export const ServerAdminsTable = () => {
</p>
{userId === user.id && (
<Tooltip content="Are you sure you want to remove yourself from this instance?">
<div className="inline-block">
<Badge
variant="primary"
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faWarning} />
<span>Deleting Yourself</span>
<Badge variant="danger">
<AlertTriangleIcon />
Deleting Yourself
</Badge>
</div>
</Tooltip>
)}
</div>
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { faRotate } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { RefreshCwIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications";
import { Badge, Button, DeleteActionModal } from "@app/components/v2";
import { Button, DeleteActionModal } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useUser } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useInvalidateCache } from "@app/hooks/api";
@@ -62,11 +62,8 @@ export const CachingPageForm = () => {
<div className="mb-2 flex items-center gap-3">
<span className="text-xl font-medium text-mineshaft-100">Secrets Cache</span>
{isInvalidating && (
<Badge
variant="danger"
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
<Badge variant="danger">
<RefreshCwIcon className="animate-spin" />
Invalidating Cache
</Badge>
)}
@@ -1,13 +1,13 @@
import { useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { InfoIcon } from "lucide-react";
import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { Badge, Button, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { Button, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useServerConfig, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
@@ -54,6 +54,7 @@ export const EncryptionPageForm = () => {
if (!subscription.hsm) {
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true,
description: "Hardware Security Module's (HSM's), are only available on Enterprise plans."
});
return;
@@ -132,12 +133,10 @@ export const EncryptionPageForm = () => {
{config.fipsEnabled && (
<Tooltip content="FIPS mode of operation is enabled for your instance. All cryptographic operations within the FIPS boundaries are validated to be FIPS compliant.">
<div>
<Badge className="flex items-center gap-2" variant="primary">
<Badge variant="info">
FIPS Mode: Enabled
<FontAwesomeIcon icon={faInfoCircle} />
<InfoIcon />
</Badge>
</div>
</Tooltip>
)}
</div>
@@ -146,6 +145,7 @@ export const EncryptionPageForm = () => {
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
isEnterpriseFeature={popUp.upgradePlan?.data?.isEnterpriseFeature}
/>
</>
);
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Control, Controller, useForm, useWatch } from "react-hook-form";
import {
faArrowUpRightFromSquare,
faBookOpen,
faChevronRight,
faExclamationTriangle,
faMagnifyingGlass
@@ -14,6 +12,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText";
import { DocumentationLinkBadge } from "@app/components/v3";
import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api";
type TForm = Record<string, string>;
@@ -213,22 +212,9 @@ export const EnvironmentPageForm = () => {
>
<div className="flex w-full flex-row items-center justify-between">
<div>
<div className="flex items-start gap-1">
<div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Overrides</p>
<a
href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides"
target="_blank"
rel="noopener noreferrer"
>
<div className="mt-[0.32rem] ml-1 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
<DocumentationLinkBadge href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides" />
</div>
<p className="text-sm text-bunker-300">
Override specific environment variables. After saving, it may take up to 5 minutes for
@@ -7,10 +7,10 @@ import {
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ServerCogIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications";
import {
Badge,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
@@ -29,6 +29,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import {
getUserTablePreference,
PreferenceKey,
@@ -110,7 +111,8 @@ const IdentityPanelTable = ({
<Td>
{name}
{isInstanceAdmin && (
<Badge variant="primary" className="ml-2">
<Badge variant="info" className="ml-2">
<ServerCogIcon />
Server Admin
</Badge>
)}
@@ -3,7 +3,6 @@ import {
faArrowDown,
faArrowUp,
faBuilding,
faCircleQuestion,
faEllipsisV,
faEnvelope,
faEye,
@@ -19,11 +18,11 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { CircleQuestionMarkIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
@@ -46,6 +45,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useUser } from "@app/context";
import { OrgMembershipRole } from "@app/helpers/roles";
import {
@@ -295,15 +295,17 @@ const ViewMembersModalContent = ({
)}
</div>
</Td>
<Td className="max-w-0">
<Badge className="flex w-fit max-w-full items-center gap-x-1 bg-mineshaft-400/50 whitespace-nowrap text-bunker-200">
<p className="truncate capitalize">{member.role.replace("-", " ")}</p>
{Boolean(member.roleId) && (
<Tooltip content="This member has a custom role assigned.">
<FontAwesomeIcon icon={faCircleQuestion} className="w-3" />
</Tooltip>
)}
<Td>
<div className="flex max-w-32">
<Tooltip
content={member.roleId ? "This member has a custom role assigned." : ""}
>
<Badge isTruncatable variant="neutral">
<span className="capitalize">{member.role.replace("-", " ")}</span>
{Boolean(member.roleId) && <CircleQuestionMarkIcon />}
</Badge>
</Tooltip>
</div>
</Td>
<Td>
<div className="flex justify-end">
@@ -9,16 +9,15 @@ import {
faUsers,
faUserShield,
faUserXmark,
faWarning,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AlertTriangleIcon, UserCogIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
Checkbox,
DeleteActionModal,
@@ -41,6 +40,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useSubscription, useUser } from "@app/context";
import {
getUserTablePreference,
@@ -218,7 +218,8 @@ const UserPanelTable = ({
{name ?? <span className="text-mineshaft-400">Not Set</span>}
</p>
{superAdmin && (
<Badge variant="primary" className="ml-2 whitespace-nowrap">
<Badge variant="info" className="ml-2">
<UserCogIcon />
Server Admin
</Badge>
)}
@@ -548,7 +549,7 @@ export const UserIdentitiesTable = () => {
const email = user.email ?? user.username;
return (
<li key={user.id}>
<div className="flex items-center">
<div className="flex items-center gap-x-1">
<p>
{user.firstName || user.lastName ? (
<>
@@ -561,15 +562,10 @@ export const UserIdentitiesTable = () => {
</p>
{userId === user.id && (
<Tooltip content="Are you sure you want to remove yourself from this instance?">
<div className="inline-block">
<Badge
variant="primary"
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faWarning} />
<span>Deleting Yourself</span>
<Badge variant="danger">
<AlertTriangleIcon />
Deleting Yourself
</Badge>
</div>
</Tooltip>
)}
</div>
@@ -7,7 +7,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -22,6 +21,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useGetCaCerts } from "@app/hooks/api";
@@ -60,7 +60,7 @@ export const CaCertificatesTable = ({ caId }: Props) => {
<div className="flex items-center">
CA Certificate {caCert.version}
{isLastItem && (
<Badge variant="success" className="ml-4">
<Badge variant="info" className="ml-4">
Current
</Badge>
)}
@@ -6,7 +6,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -22,6 +21,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
import {
@@ -10,7 +10,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -26,6 +25,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
@@ -104,6 +104,7 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text="Managing template enrollment options for EST is only available on Infisical's Enterprise plan."
/>
</div>
@@ -36,6 +36,7 @@ type Props = {
data?: {
id?: string;
name?: string;
isEnterpriseFeature?: boolean;
}
) => void;
};
@@ -90,7 +91,9 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
<DropdownMenuItem
onClick={() => {
if (!subscription?.pkiEst) {
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
});
return;
}
@@ -5,18 +5,17 @@ import {
faEllipsis,
faEye,
faFileExport,
faQuestionCircle,
faRedo,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { CircleQuestionMarkIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -33,6 +32,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
@@ -58,7 +58,7 @@ const isExpiringWithinOneDay = (notAfter: string): boolean => {
const getAutoRenewalInfo = (certificate: TCertificate) => {
if (certificate.renewedByCertificateId) {
return { text: "Renewed", variant: "instance" as const };
return { text: "Renewed", variant: "neutral" as const };
}
const isRevoked = certificate.status === CertStatus.REVOKED;
@@ -69,7 +69,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
if (isRevoked) {
return {
text: "Not Available",
variant: "instance" as const,
variant: "neutral" as const,
tooltip: "Renewal is not available for revoked certificates"
};
}
@@ -77,7 +77,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
if (isExpired) {
return {
text: "Not Available",
variant: "instance" as const,
variant: "neutral" as const,
tooltip: "Renewal is not available for expired certificates"
};
}
@@ -85,7 +85,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
if (hasNoProfile) {
return {
text: "Not Available",
variant: "instance" as const,
variant: "neutral" as const,
tooltip: "Renewal requires a certificate profile"
};
}
@@ -93,7 +93,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
if (certificate.hasPrivateKey === false) {
return {
text: "Not Available",
variant: "instance" as const,
variant: "neutral" as const,
tooltip: "Renewal is not available for certificates with externally generated private keys"
};
}
@@ -101,7 +101,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
if (isExpiringWithinDay) {
return {
text: "Not Available",
variant: "instance" as const,
variant: "neutral" as const,
tooltip: "Auto-renewal is not available for certificates expiring within 24 hours"
};
}
@@ -115,7 +115,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
}
if (!certificate.renewBeforeDays) {
return { text: "Auto-Renewal Disabled", variant: "primary" as const };
return { text: "Auto-Renewal Disabled", variant: "warning" as const };
}
const notAfterDate = new Date(certificate.notAfter);
@@ -133,11 +133,11 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
);
if (daysUntilRenewal === 0) {
return { text: "Renews today", variant: "primary" as const };
return { text: "Renews today", variant: "warning" as const };
}
if (daysUntilRenewal <= 7) {
return { text: `Renews in ${daysUntilRenewal}d`, variant: "primary" as const };
return { text: `Renews in ${daysUntilRenewal}d`, variant: "warning" as const };
}
return { text: `Renews in ${daysUntilRenewal}d`, variant: "success" as const };
@@ -279,16 +279,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
{autoRenewalInfo &&
(autoRenewalInfo.tooltip ? (
<div className="flex items-center gap-2">
<Tooltip content={autoRenewalInfo.tooltip}>
<Badge variant={autoRenewalInfo.variant}>
{autoRenewalInfo.text}
<Tooltip content={autoRenewalInfo.tooltip}>
<FontAwesomeIcon
icon={faQuestionCircle}
className="ml-1 cursor-help text-red-400 hover:text-red-300"
size="sm"
/>
</Tooltip>
<CircleQuestionMarkIcon />
</Badge>
</Tooltip>
</div>
) : (
<Badge variant={autoRenewalInfo.variant}>{autoRenewalInfo.text}</Badge>
@@ -5,13 +5,13 @@ export const getCertValidUntilBadgeDetails = (notAfter: string) => {
const notAfterDate = new Date(notAfter).getTime();
const diffInMs = notAfterDate - currentDate;
let variant: "success" | "primary" | "danger" = "success";
let variant: "success" | "warning" | "danger" = "success";
let label = "Healthy";
if (diffInMs > ms("60d")) {
variant = "success";
} else if (diffInMs > ms("30d")) {
variant = "primary";
variant = "warning";
} else {
variant = "danger";
}

Some files were not shown because too many files have changed in this diff Show More