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 { seedData1 } from "@app/db/seed-data";
import { getDatabaseCredentials, getHsmConfig, initEnvConfig } from "@app/lib/config/env"; import { getDatabaseCredentials, getHsmConfig, initEnvConfig } from "@app/lib/config/env";
import { initLogger } from "@app/lib/logger"; 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 { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { mockSmtpServer } from "./mocks/smtp"; import { mockSmtpServer } from "./mocks/smtp";
@@ -83,7 +83,7 @@ export default {
await queue.initialize(); await queue.initialize();
const server = await main({ const { server, completeServerInitialization } = await main({
db, db,
smtp, smtp,
logger, logger,
@@ -96,6 +96,10 @@ export default {
envConfig: envCfg envConfig: envCfg
}); });
await completeServerInitialization();
markServerReady();
await bootstrapCheck({ db }); await bootstrapCheck({ db });
// @ts-expect-error type // @ts-expect-error type
+9 -1
View File
@@ -12,6 +12,7 @@ type TArgs = {
auditLogDb?: Knex; auditLogDb?: Knex;
applicationDb: Knex; applicationDb: Knex;
logger: Logger; logger: Logger;
onMigrationLockAcquired?: () => void;
}; };
const isProduction = process.env.NODE_ENV === "production"; const isProduction = process.env.NODE_ENV === "production";
@@ -30,7 +31,7 @@ const migrationStatusCheckErrorHandler = (err: Error) => {
throw err; throw err;
}; };
export const runMigrations = async ({ applicationDb, auditLogDb, logger }: TArgs) => { export const runMigrations = async ({ applicationDb, auditLogDb, logger, onMigrationLockAcquired }: TArgs) => {
try { try {
// akhilmhdh(Feb 10 2025): 2 years from now remove this // akhilmhdh(Feb 10 2025): 2 years from now remove this
if (isProduction) { if (isProduction) {
@@ -85,6 +86,13 @@ export const runMigrations = async ({ applicationDb, auditLogDb, logger }: TArgs
await applicationDb.transaction(async (tx) => { await applicationDb.transaction(async (tx) => {
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.BootUpMigration]); 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."); logger.info("Running application migrations.");
const didPreviousInstanceRunMigration = !(await applicationDb.migrate const didPreviousInstanceRunMigration = !(await applicationDb.migrate
@@ -2,7 +2,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas"; import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs"; import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -32,8 +31,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1)) if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); 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), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug), 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); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1)) if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); 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), projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.projectSlug),
path: z path: z
@@ -2,7 +2,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas"; import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs"; import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
@@ -32,8 +31,8 @@ export const registerKubernetesDynamicSecretLeaseRouter = async (server: Fastify
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
if (valMs > daysToMillisecond(1)) if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); 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), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug), 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 { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models"; import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs"; import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars"; import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
@@ -60,8 +59,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1)) if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}), }),
maxTTL: z maxTTL: z
.string() .string()
@@ -72,8 +71,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1)) if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}) })
.nullable(), .nullable(),
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash), 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); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1)) if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}), }),
maxTTL: z maxTTL: z
.string() .string()
@@ -142,8 +141,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val); const valMs = ms(val);
if (valMs < 60 * 1000) if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1)) if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}) })
.nullable(), .nullable(),
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(), 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 }); const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (existingDynamicSecret) 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 selectedProvider = dynamicSecretProviders[provider.type];
const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId }); const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId });
@@ -265,7 +265,7 @@ export const dynamicSecretServiceFactory = ({
if (newName) { if (newName) {
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id }); const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
if (existingDynamicSecret) 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 } = const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({ await kmsService.createCipherPairWithDataKey({
+1 -1
View File
@@ -84,7 +84,7 @@ export const isHsmActiveAndEnabled = async ({
rootKmsConfigEncryptionStrategy = (rootKmsConfig?.encryptionStrategy || null) as RootKeyEncryptionStrategy | null; rootKmsConfigEncryptionStrategy = (rootKmsConfig?.encryptionStrategy || null) as RootKeyEncryptionStrategy | null;
if ( if (
rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.HSM && (rootKmsConfigEncryptionStrategy === RootKeyEncryptionStrategy.HSM || isHsmConfigured) &&
licenseService && licenseService &&
!licenseService.onPremFeatures.hsm !licenseService.onPremFeatures.hsm
) { ) {
@@ -1517,7 +1517,7 @@ export const secretApprovalRequestServiceFactory = ({
})) }))
); );
if (secrets.length) 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( commits.push(
...createdSecrets.map((createdSecret) => ({ ...createdSecrets.map((createdSecret) => ({
@@ -42,168 +42,172 @@ export const secretRotationV2QueueServiceFactory = async ({
smtpService, smtpService,
notificationService notificationService
}: TSecretRotationV2QueueServiceFactoryDep) => { }: TSecretRotationV2QueueServiceFactoryDep) => {
const appCfg = getConfig(); const init = async () => {
const appCfg = getConfig();
if (appCfg.isRotationDevelopmentMode) { if (appCfg.isRotationDevelopmentMode) {
logger.warn("Secret Rotation V2 is in development mode."); logger.warn("Secret Rotation V2 is in development mode.");
} }
await queueService.startPg<QueueName.SecretRotationV2>( await queueService.startPg<QueueName.SecretRotationV2>(
QueueJobs.SecretRotationV2QueueRotations, QueueJobs.SecretRotationV2QueueRotations,
async () => { async () => {
try { try {
const rotateBy = getNextUtcRotationInterval(); const rotateBy = getNextUtcRotationInterval();
const currentTime = new Date(); const currentTime = new Date();
const secretRotations = await secretRotationV2DAL.findSecretRotationsToQueue(rotateBy); const secretRotations = await secretRotationV2DAL.findSecretRotationsToQueue(rotateBy);
logger.info(
`secretRotationV2Queue: Queue Rotations [currentTime=${currentTime.toISOString()}] [rotateBy=${rotateBy.toISOString()}] [count=${
secretRotations.length
}]`
);
for await (const rotation of secretRotations) {
logger.info( logger.info(
`secretRotationV2Queue: Queue Rotation [rotationId=${rotation.id}] [lastRotatedAt=${new Date( `secretRotationV2Queue: Queue Rotations [currentTime=${currentTime.toISOString()}] [rotateBy=${rotateBy.toISOString()}] [count=${
rotation.lastRotatedAt secretRotations.length
).toISOString()}] [rotateAt=${new Date(rotation.nextRotationAt!).toISOString()}]` }]`
); );
const data = { for await (const rotation of secretRotations) {
rotationId: rotation.id, logger.info(
queuedAt: currentTime `secretRotationV2Queue: Queue Rotation [rotationId=${rotation.id}] [lastRotatedAt=${new Date(
} as TSecretRotationRotateSecretsJobPayload; rotation.lastRotatedAt
).toISOString()}] [rotateAt=${new Date(rotation.nextRotationAt!).toISOString()}]`
if (appCfg.isTestMode) {
logger.warn("secretRotationV2Queue: Manually rotating secrets for test mode");
await rotateSecretsFns({
job: {
id: uuidv4(),
data,
retryCount: 0,
retryLimit: 0
},
secretRotationV2DAL,
secretRotationV2Service
});
} else {
await queueService.queuePg(
QueueJobs.SecretRotationV2RotateSecrets,
{
rotationId: rotation.id,
queuedAt: currentTime
},
getSecretRotationRotateSecretJobOptions(rotation)
); );
const data = {
rotationId: rotation.id,
queuedAt: currentTime
} as TSecretRotationRotateSecretsJobPayload;
if (appCfg.isTestMode) {
logger.warn("secretRotationV2Queue: Manually rotating secrets for test mode");
await rotateSecretsFns({
job: {
id: uuidv4(),
data,
retryCount: 0,
retryLimit: 0
},
secretRotationV2DAL,
secretRotationV2Service
});
} else {
await queueService.queuePg(
QueueJobs.SecretRotationV2RotateSecrets,
{
rotationId: rotation.id,
queuedAt: currentTime
},
getSecretRotationRotateSecretJobOptions(rotation)
);
}
} }
} catch (error) {
logger.error(error, "secretRotationV2Queue: Queue Rotations Error:");
throw error;
} }
} catch (error) { },
logger.error(error, "secretRotationV2Queue: Queue Rotations Error:"); {
throw error; batchSize: 1,
workerCount: 1,
pollingIntervalSeconds: appCfg.isRotationDevelopmentMode ? 0.5 : 30
} }
}, );
{
batchSize: 1,
workerCount: 1,
pollingIntervalSeconds: appCfg.isRotationDevelopmentMode ? 0.5 : 30
}
);
await queueService.startPg<QueueName.SecretRotationV2>( await queueService.startPg<QueueName.SecretRotationV2>(
QueueJobs.SecretRotationV2RotateSecrets, QueueJobs.SecretRotationV2RotateSecrets,
async ([job]) => { async ([job]) => {
await rotateSecretsFns({ await rotateSecretsFns({
job: { job: {
...job, ...job,
data: job.data as TSecretRotationRotateSecretsJobPayload data: job.data as TSecretRotationRotateSecretsJobPayload
}, },
secretRotationV2DAL, secretRotationV2DAL,
secretRotationV2Service secretRotationV2Service
});
},
{
batchSize: 1,
workerCount: 2,
pollingIntervalSeconds: 0.5
}
);
await queueService.startPg<QueueName.SecretRotationV2>(
QueueJobs.SecretRotationV2SendNotification,
async ([job]) => {
const { secretRotation } = job.data as TSecretRotationSendNotificationJobPayload;
try {
const {
name: rotationName,
type,
projectId,
lastRotationAttemptedAt,
folder,
environment,
id: rotationId
} = secretRotation;
logger.info(`secretRotationV2Queue: Sending Status Notification [rotationId=${rotationId}]`);
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
const project = await projectDAL.findById(projectId);
const projectAdmins = projectMembers.filter((member) =>
member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
);
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
const rotationPath = `/projects/secret-management/${projectId}/secrets/${environment.slug}`;
await notificationService.createUserNotifications(
projectAdmins.map((admin) => ({
userId: admin.userId,
orgId: project.orgId,
type: NotificationType.SECRET_ROTATION_FAILED,
title: "Secret Rotation Failed",
body: `Your **${rotationType}** rotation **${rotationName}** failed to rotate.`,
link: rotationPath
}))
);
await smtpService.sendMail({
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
template: SmtpTemplates.SecretRotationFailed,
subjectLine: `Secret Rotation Failed`,
substitutions: {
rotationName,
rotationType,
content: `Your ${rotationType} Rotation failed to rotate during it's scheduled rotation. The last rotation attempt occurred at ${new Date(
lastRotationAttemptedAt
).toISOString()}. Please check the rotation status in Infisical for more details.`,
secretPath: folder.path,
environment: environment.name,
projectName: project.name,
rotationUrl: encodeURI(`${appCfg.SITE_URL}${rotationPath}`)
}
}); });
} catch (error) { },
logger.error( {
error, batchSize: 1,
`secretRotationV2Queue: Failed to Send Status Notification [rotationId=${secretRotation.id}]` workerCount: 2,
); pollingIntervalSeconds: 0.5
throw error;
} }
}, );
{
batchSize: 1,
workerCount: 2,
pollingIntervalSeconds: 1
}
);
await queueService.schedulePg( await queueService.startPg<QueueName.SecretRotationV2>(
QueueJobs.SecretRotationV2QueueRotations, QueueJobs.SecretRotationV2SendNotification,
appCfg.isRotationDevelopmentMode ? "* * * * *" : "0 0 * * *", async ([job]) => {
undefined, const { secretRotation } = job.data as TSecretRotationSendNotificationJobPayload;
{ tz: "UTC" } try {
); const {
name: rotationName,
type,
projectId,
lastRotationAttemptedAt,
folder,
environment,
id: rotationId
} = secretRotation;
logger.info(`secretRotationV2Queue: Sending Status Notification [rotationId=${rotationId}]`);
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
const project = await projectDAL.findById(projectId);
const projectAdmins = projectMembers.filter((member) =>
member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
);
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
const rotationPath = `/projects/secret-management/${projectId}/secrets/${environment.slug}`;
await notificationService.createUserNotifications(
projectAdmins.map((admin) => ({
userId: admin.userId,
orgId: project.orgId,
type: NotificationType.SECRET_ROTATION_FAILED,
title: "Secret Rotation Failed",
body: `Your **${rotationType}** rotation **${rotationName}** failed to rotate.`,
link: rotationPath
}))
);
await smtpService.sendMail({
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
template: SmtpTemplates.SecretRotationFailed,
subjectLine: `Secret Rotation Failed`,
substitutions: {
rotationName,
rotationType,
content: `Your ${rotationType} Rotation failed to rotate during it's scheduled rotation. The last rotation attempt occurred at ${new Date(
lastRotationAttemptedAt
).toISOString()}. Please check the rotation status in Infisical for more details.`,
secretPath: folder.path,
environment: environment.name,
projectName: project.name,
rotationUrl: encodeURI(`${appCfg.SITE_URL}${rotationPath}`)
}
});
} catch (error) {
logger.error(
error,
`secretRotationV2Queue: Failed to Send Status Notification [rotationId=${secretRotation.id}]`
);
throw error;
}
},
{
batchSize: 1,
workerCount: 2,
pollingIntervalSeconds: 1
}
);
await queueService.schedulePg(
QueueJobs.SecretRotationV2QueueRotations,
appCfg.isRotationDevelopmentMode ? "* * * * *" : "0 0 * * *",
undefined,
{ tz: "UTC" }
);
};
return { init };
}; };
@@ -141,202 +141,6 @@ export const secretScanningV2QueueServiceFactory = async ({
} }
}; };
await queueService.startPg<QueueName.SecretScanningV2>(
QueueJobs.SecretScanningV2FullScan,
async ([job]) => {
const { scanId, resourceId, dataSourceId } = job.data as TQueueSecretScanningDataSourceFullScan;
const { retryCount, retryLimit } = job;
const logDetails = `[scanId=${scanId}] [resourceId=${resourceId}] [dataSourceId=${dataSourceId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`;
const tempFolder = await createTempFolder();
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
if (!dataSource) throw new Error(`Data source with ID "${dataSourceId}" not found`);
const resource = await secretScanningV2DAL.resources.findById(resourceId);
if (!resource) throw new Error(`Resource with ID "${resourceId}" not found`);
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
try {
try {
lock = await keyStore.acquireLock(
[KeyStorePrefixes.SecretScanningLock(dataSource.id, resource.externalId)],
60 * 1000 * 5
);
} catch (e) {
throw new Error("Failed to acquire scanning lock.");
}
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Scanning
}
);
let connection: TAppConnection | null = null;
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]({
kmsService,
appConnectionDAL
});
const findingsPath = join(tempFolder, "findings.json");
const scanPath = await factory.getFullScanPath({
dataSource: {
...dataSource,
connection
} as TSecretScanningDataSourceWithConnection,
resourceName: resource.name,
tempFolder
});
const config = await secretScanningV2DAL.configs.findOne({
projectId: dataSource.projectId
});
let configPath: string | undefined;
if (config && config.content) {
configPath = join(tempFolder, "infisical-scan.toml");
await writeTextToFile(configPath, config.content);
}
let findingsPayload: TFindingsPayload;
switch (resource.type) {
case SecretScanningResource.Repository:
case SecretScanningResource.Project:
findingsPayload = await scanGitRepositoryAndGetFindings(scanPath, findingsPath, configPath);
break;
default:
throw new Error("Unhandled resource type");
}
const allFindings = await secretScanningV2DAL.findings.transaction(async (tx) => {
let findings: TSecretScanningFindings[] = [];
if (findingsPayload.length) {
findings = await secretScanningV2DAL.findings.upsert(
findingsPayload.map((finding) => ({
...finding,
projectId: dataSource.projectId,
dataSourceName: dataSource.name,
dataSourceType: dataSource.type,
resourceName: resource.name,
resourceType: resource.type,
scanId
})),
["projectId", "fingerprint"],
tx,
["resourceName", "dataSourceName"]
);
}
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Completed,
statusMessage: null
}
);
return findings;
});
const newFindings = allFindings.filter((finding) => finding.scanId === scanId);
if (newFindings.length) {
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
status: SecretScanningScanStatus.Completed,
resourceName: resource.name,
isDiffScan: false,
dataSource,
numberOfSecrets: newFindings.length,
scanId
});
}
await auditLogService.createAuditLog({
projectId: dataSource.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
metadata: {
dataSourceId: dataSource.id,
dataSourceType: dataSource.type,
resourceId: resource.id,
resourceType: resource.type,
scanId,
scanStatus: SecretScanningScanStatus.Completed,
scanType: SecretScanningScanType.FullScan,
numberOfSecretsDetected: findingsPayload.length
}
}
});
logger.info(`secretScanningV2Queue: Full Scan Complete ${logDetails} findings=[${findingsPayload.length}]`);
} catch (error) {
if (retryCount === retryLimit) {
const errorMessage = parseScanErrorMessage(error);
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Failed,
statusMessage: errorMessage
}
);
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
status: SecretScanningScanStatus.Failed,
resourceName: resource.name,
dataSource,
errorMessage
});
await auditLogService.createAuditLog({
projectId: dataSource.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
metadata: {
dataSourceId: dataSource.id,
dataSourceType: dataSource.type,
resourceId: resource.id,
resourceType: resource.type,
scanId,
scanStatus: SecretScanningScanStatus.Failed,
scanType: SecretScanningScanType.FullScan
}
}
});
}
logger.error(error, `secretScanningV2Queue: Full Scan Failed ${logDetails}`);
throw error;
} finally {
await deleteTempFolder(tempFolder);
await lock?.release();
}
},
{
batchSize: 1,
workerCount: 2,
pollingIntervalSeconds: 1
}
);
const queueResourceDiffScan = async ({ const queueResourceDiffScan = async ({
payload, payload,
dataSourceId, dataSourceId,
@@ -391,148 +195,127 @@ export const secretScanningV2QueueServiceFactory = async ({
} }
}; };
await queueService.startPg<QueueName.SecretScanningV2>( const init = async () => {
QueueJobs.SecretScanningV2DiffScan, await queueService.startPg<QueueName.SecretScanningV2>(
async ([job]) => { QueueJobs.SecretScanningV2FullScan,
const { payload, dataSourceId, resourceId, scanId } = job.data as TQueueSecretScanningResourceDiffScan; async ([job]) => {
const { retryCount, retryLimit } = job; const { scanId, resourceId, dataSourceId } = job.data as TQueueSecretScanningDataSourceFullScan;
const { retryCount, retryLimit } = job;
const logDetails = `[dataSourceId=${dataSourceId}] [scanId=${scanId}] [resourceId=${resourceId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`; const logDetails = `[scanId=${scanId}] [resourceId=${resourceId}] [dataSourceId=${dataSourceId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`;
const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId); const tempFolder = await createTempFolder();
if (!dataSource) throw new Error(`Data source with ID "${dataSourceId}" not found`); const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
const resource = await secretScanningV2DAL.resources.findById(resourceId); if (!dataSource) throw new Error(`Data source with ID "${dataSourceId}" not found`);
if (!resource) throw new Error(`Resource with ID "${resourceId}" not found`); const resource = await secretScanningV2DAL.resources.findById(resourceId);
const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]({ if (!resource) throw new Error(`Resource with ID "${resourceId}" not found`);
kmsService,
appConnectionDAL
});
const tempFolder = await createTempFolder(); let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
try { try {
await secretScanningV2DAL.scans.update( try {
{ id: scanId }, lock = await keyStore.acquireLock(
{ [KeyStorePrefixes.SecretScanningLock(dataSource.id, resource.externalId)],
status: SecretScanningScanStatus.Scanning 60 * 1000 * 5
}
);
let connection: TAppConnection | null = null;
if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
const config = await secretScanningV2DAL.configs.findOne({
projectId: dataSource.projectId
});
let configPath: string | undefined;
if (config && config.content) {
configPath = join(tempFolder, "infisical-scan.toml");
await writeTextToFile(configPath, config.content);
}
const findingsPayload = await factory.getDiffScanFindingsPayload({
dataSource: {
...dataSource,
connection
} as TSecretScanningDataSourceWithConnection,
resourceName: resource.name,
payload,
configPath
});
const allFindings = await secretScanningV2DAL.findings.transaction(async (tx) => {
let findings: TSecretScanningFindings[] = [];
if (findingsPayload.length) {
findings = await secretScanningV2DAL.findings.upsert(
findingsPayload.map((finding) => ({
...finding,
projectId: dataSource.projectId,
dataSourceName: dataSource.name,
dataSourceType: dataSource.type,
resourceName: resource.name,
resourceType: resource.type,
scanId
})),
["projectId", "fingerprint"],
tx,
["resourceName", "dataSourceName"]
); );
} catch (e) {
throw new Error("Failed to acquire scanning lock.");
} }
await secretScanningV2DAL.scans.update( await secretScanningV2DAL.scans.update(
{ id: scanId }, { id: scanId },
{ {
status: SecretScanningScanStatus.Completed status: SecretScanningScanStatus.Scanning
} }
); );
return findings; let connection: TAppConnection | null = null;
}); if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
const newFindings = allFindings.filter((finding) => finding.scanId === scanId); const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]({
kmsService,
if (newFindings.length) { appConnectionDAL
const finding = newFindings[0] as TSecretScanningFinding;
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
status: SecretScanningScanStatus.Completed,
resourceName: resource.name,
isDiffScan: true,
dataSource,
numberOfSecrets: newFindings.length,
scanId,
authorName: finding?.details?.author,
authorEmail: finding?.details?.email
}); });
}
await auditLogService.createAuditLog({ const findingsPath = join(tempFolder, "findings.json");
projectId: dataSource.projectId,
actor: { const scanPath = await factory.getFullScanPath({
type: ActorType.PLATFORM, dataSource: {
metadata: {} ...dataSource,
}, connection
event: { } as TSecretScanningDataSourceWithConnection,
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN, resourceName: resource.name,
metadata: { tempFolder
dataSourceId: dataSource.id, });
dataSourceType: dataSource.type,
resourceId, const config = await secretScanningV2DAL.configs.findOne({
resourceType: resource.type, projectId: dataSource.projectId
scanId, });
scanStatus: SecretScanningScanStatus.Completed,
scanType: SecretScanningScanType.DiffScan, let configPath: string | undefined;
numberOfSecretsDetected: findingsPayload.length
} if (config && config.content) {
configPath = join(tempFolder, "infisical-scan.toml");
await writeTextToFile(configPath, config.content);
} }
});
logger.info(`secretScanningV2Queue: Diff Scan Complete ${logDetails}`); let findingsPayload: TFindingsPayload;
} catch (error) { switch (resource.type) {
if (retryCount === retryLimit) { case SecretScanningResource.Repository:
const errorMessage = parseScanErrorMessage(error); case SecretScanningResource.Project:
findingsPayload = await scanGitRepositoryAndGetFindings(scanPath, findingsPath, configPath);
break;
default:
throw new Error("Unhandled resource type");
}
await secretScanningV2DAL.scans.update( const allFindings = await secretScanningV2DAL.findings.transaction(async (tx) => {
{ id: scanId }, let findings: TSecretScanningFindings[] = [];
{ if (findingsPayload.length) {
status: SecretScanningScanStatus.Failed, findings = await secretScanningV2DAL.findings.upsert(
statusMessage: errorMessage findingsPayload.map((finding) => ({
...finding,
projectId: dataSource.projectId,
dataSourceName: dataSource.name,
dataSourceType: dataSource.type,
resourceName: resource.name,
resourceType: resource.type,
scanId
})),
["projectId", "fingerprint"],
tx,
["resourceName", "dataSourceName"]
);
} }
);
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, { await secretScanningV2DAL.scans.update(
status: SecretScanningScanStatus.Failed, { id: scanId },
resourceName: resource.name, {
dataSource, status: SecretScanningScanStatus.Completed,
errorMessage statusMessage: null
}
);
return findings;
}); });
const newFindings = allFindings.filter((finding) => finding.scanId === scanId);
if (newFindings.length) {
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
status: SecretScanningScanStatus.Completed,
resourceName: resource.name,
isDiffScan: false,
dataSource,
numberOfSecrets: newFindings.length,
scanId
});
}
await auditLogService.createAuditLog({ await auditLogService.createAuditLog({
projectId: dataSource.projectId, projectId: dataSource.projectId,
actor: { actor: {
@@ -547,128 +330,348 @@ export const secretScanningV2QueueServiceFactory = async ({
resourceId: resource.id, resourceId: resource.id,
resourceType: resource.type, resourceType: resource.type,
scanId, scanId,
scanStatus: SecretScanningScanStatus.Failed, scanStatus: SecretScanningScanStatus.Completed,
scanType: SecretScanningScanType.DiffScan scanType: SecretScanningScanType.FullScan,
numberOfSecretsDetected: findingsPayload.length
} }
} }
}); });
logger.info(`secretScanningV2Queue: Full Scan Complete ${logDetails} findings=[${findingsPayload.length}]`);
} catch (error) {
if (retryCount === retryLimit) {
const errorMessage = parseScanErrorMessage(error);
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Failed,
statusMessage: errorMessage
}
);
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
status: SecretScanningScanStatus.Failed,
resourceName: resource.name,
dataSource,
errorMessage
});
await auditLogService.createAuditLog({
projectId: dataSource.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
metadata: {
dataSourceId: dataSource.id,
dataSourceType: dataSource.type,
resourceId: resource.id,
resourceType: resource.type,
scanId,
scanStatus: SecretScanningScanStatus.Failed,
scanType: SecretScanningScanType.FullScan
}
}
});
}
logger.error(error, `secretScanningV2Queue: Full Scan Failed ${logDetails}`);
throw error;
} finally {
await deleteTempFolder(tempFolder);
await lock?.release();
} }
},
logger.error(error, `secretScanningV2Queue: Diff Scan Failed ${logDetails}`); {
throw error; batchSize: 1,
} finally { workerCount: 2,
await deleteTempFolder(tempFolder); pollingIntervalSeconds: 1
} }
}, );
{
batchSize: 1,
workerCount: 2,
pollingIntervalSeconds: 1
}
);
await queueService.startPg<QueueName.SecretScanningV2>( await queueService.startPg<QueueName.SecretScanningV2>(
QueueJobs.SecretScanningV2SendNotification, QueueJobs.SecretScanningV2DiffScan,
async ([job]) => { async ([job]) => {
const { dataSource, resourceName, ...payload } = job.data as TQueueSecretScanningSendNotification; const { payload, dataSourceId, resourceId, scanId } = job.data as TQueueSecretScanningResourceDiffScan;
const { retryCount, retryLimit } = job;
const appCfg = getConfig(); const logDetails = `[dataSourceId=${dataSourceId}] [scanId=${scanId}] [resourceId=${resourceId}] [jobId=${job.id}] retryCount=[${retryCount}/${retryLimit}]`;
if (!appCfg.isSmtpConfigured) return; const dataSource = await secretScanningV2DAL.dataSources.findById(dataSourceId);
try { if (!dataSource) throw new Error(`Data source with ID "${dataSourceId}" not found`);
const { projectId } = dataSource;
logger.info( const resource = await secretScanningV2DAL.resources.findById(resourceId);
`secretScanningV2Queue: Sending Status Notification [dataSourceId=${dataSource.id}] [resourceName=${resourceName}] [status=${payload.status}]`
);
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); if (!resource) throw new Error(`Resource with ID "${resourceId}" not found`);
const project = await projectDAL.findById(projectId);
const recipients = projectMembers.filter((member) => { const factory = SECRET_SCANNING_FACTORY_MAP[dataSource.type as SecretScanningDataSource]({
const isAdmin = member.roles.some((role) => role.role === ProjectMembershipRole.Admin); kmsService,
const isCompleted = payload.status === SecretScanningScanStatus.Completed; appConnectionDAL
// We assume that the committer is one of the project members
const isCommitter = isCompleted && payload.authorEmail === member.user.email;
return isAdmin || isCommitter;
}); });
const timestamp = new Date().toISOString(); const tempFolder = await createTempFolder();
const subjectLine = try {
payload.status === SecretScanningScanStatus.Completed await secretScanningV2DAL.scans.update(
? "Incident Alert: Secret(s) Leaked" { id: scanId },
: `Secret Scanning Failed`; {
status: SecretScanningScanStatus.Scanning
}
);
await notificationService.createUserNotifications( let connection: TAppConnection | null = null;
recipients.map((member) => ({ if (dataSource.connection) connection = await decryptAppConnection(dataSource.connection, kmsService);
userId: member.userId,
orgId: project.orgId,
type:
payload.status === SecretScanningScanStatus.Completed
? NotificationType.SECRET_SCANNING_SECRETS_DETECTED
: NotificationType.SECRET_SCANNING_SCAN_FAILED,
title: subjectLine,
body:
payload.status === SecretScanningScanStatus.Completed
? `Uncovered **${payload.numberOfSecrets}** secret(s) ${payload.isDiffScan ? " from a recent commit to" : " in"} **${resourceName}**.`
: `Encountered an error while attempting to scan the resource **${resourceName}**: ${payload.errorMessage}`,
link:
payload.status === SecretScanningScanStatus.Completed
? `/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
: `/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}`
}))
);
await smtpService.sendMail({ const config = await secretScanningV2DAL.configs.findOne({
recipients: recipients.map((member) => member.user.email!).filter(Boolean), projectId: dataSource.projectId
template: });
payload.status === SecretScanningScanStatus.Completed
? SmtpTemplates.SecretScanningV2SecretsDetected let configPath: string | undefined;
: SmtpTemplates.SecretScanningV2ScanFailed,
subjectLine, if (config && config.content) {
substitutions: configPath = join(tempFolder, "infisical-scan.toml");
payload.status === SecretScanningScanStatus.Completed await writeTextToFile(configPath, config.content);
? { }
authorName: payload.authorName,
authorEmail: payload.authorEmail, const findingsPayload = await factory.getDiffScanFindingsPayload({
resourceName, dataSource: {
numberOfSecrets: payload.numberOfSecrets, ...dataSource,
isDiffScan: payload.isDiffScan, connection
url: encodeURI( } as TSecretScanningDataSourceWithConnection,
`${appCfg.SITE_URL}/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}` resourceName: resource.name,
), payload,
timestamp configPath
} });
: {
const allFindings = await secretScanningV2DAL.findings.transaction(async (tx) => {
let findings: TSecretScanningFindings[] = [];
if (findingsPayload.length) {
findings = await secretScanningV2DAL.findings.upsert(
findingsPayload.map((finding) => ({
...finding,
projectId: dataSource.projectId,
dataSourceName: dataSource.name, dataSourceName: dataSource.name,
resourceName, dataSourceType: dataSource.type,
projectName: project.name, resourceName: resource.name,
timestamp, resourceType: resource.type,
errorMessage: payload.errorMessage, scanId
url: encodeURI( })),
`${appCfg.SITE_URL}/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}` ["projectId", "fingerprint"],
) tx,
["resourceName", "dataSourceName"]
);
}
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Completed
}
);
return findings;
});
const newFindings = allFindings.filter((finding) => finding.scanId === scanId);
if (newFindings.length) {
const finding = newFindings[0] as TSecretScanningFinding;
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
status: SecretScanningScanStatus.Completed,
resourceName: resource.name,
isDiffScan: true,
dataSource,
numberOfSecrets: newFindings.length,
scanId,
authorName: finding?.details?.author,
authorEmail: finding?.details?.email
});
}
await auditLogService.createAuditLog({
projectId: dataSource.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
metadata: {
dataSourceId: dataSource.id,
dataSourceType: dataSource.type,
resourceId,
resourceType: resource.type,
scanId,
scanStatus: SecretScanningScanStatus.Completed,
scanType: SecretScanningScanType.DiffScan,
numberOfSecretsDetected: findingsPayload.length
}
}
});
logger.info(`secretScanningV2Queue: Diff Scan Complete ${logDetails}`);
} catch (error) {
if (retryCount === retryLimit) {
const errorMessage = parseScanErrorMessage(error);
await secretScanningV2DAL.scans.update(
{ id: scanId },
{
status: SecretScanningScanStatus.Failed,
statusMessage: errorMessage
}
);
await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, {
status: SecretScanningScanStatus.Failed,
resourceName: resource.name,
dataSource,
errorMessage
});
await auditLogService.createAuditLog({
projectId: dataSource.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.SECRET_SCANNING_DATA_SOURCE_SCAN,
metadata: {
dataSourceId: dataSource.id,
dataSourceType: dataSource.type,
resourceId: resource.id,
resourceType: resource.type,
scanId,
scanStatus: SecretScanningScanStatus.Failed,
scanType: SecretScanningScanType.DiffScan
} }
}); }
} catch (error) { });
logger.error( }
error,
`secretScanningV2Queue: Failed to Send Status Notification [dataSourceId=${dataSource.id}] [resourceName=${resourceName}] [status=${payload.status}]` logger.error(error, `secretScanningV2Queue: Diff Scan Failed ${logDetails}`);
); throw error;
throw error; } finally {
await deleteTempFolder(tempFolder);
}
},
{
batchSize: 1,
workerCount: 2,
pollingIntervalSeconds: 1
} }
}, );
{
batchSize: 1, await queueService.startPg<QueueName.SecretScanningV2>(
workerCount: 2, QueueJobs.SecretScanningV2SendNotification,
pollingIntervalSeconds: 1 async ([job]) => {
} const { dataSource, resourceName, ...payload } = job.data as TQueueSecretScanningSendNotification;
);
const appCfg = getConfig();
if (!appCfg.isSmtpConfigured) return;
try {
const { projectId } = dataSource;
logger.info(
`secretScanningV2Queue: Sending Status Notification [dataSourceId=${dataSource.id}] [resourceName=${resourceName}] [status=${payload.status}]`
);
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
const project = await projectDAL.findById(projectId);
const recipients = projectMembers.filter((member) => {
const isAdmin = member.roles.some((role) => role.role === ProjectMembershipRole.Admin);
const isCompleted = payload.status === SecretScanningScanStatus.Completed;
// We assume that the committer is one of the project members
const isCommitter = isCompleted && payload.authorEmail === member.user.email;
return isAdmin || isCommitter;
});
const timestamp = new Date().toISOString();
const subjectLine =
payload.status === SecretScanningScanStatus.Completed
? "Incident Alert: Secret(s) Leaked"
: `Secret Scanning Failed`;
await notificationService.createUserNotifications(
recipients.map((member) => ({
userId: member.userId,
orgId: project.orgId,
type:
payload.status === SecretScanningScanStatus.Completed
? NotificationType.SECRET_SCANNING_SECRETS_DETECTED
: NotificationType.SECRET_SCANNING_SCAN_FAILED,
title: subjectLine,
body:
payload.status === SecretScanningScanStatus.Completed
? `Uncovered **${payload.numberOfSecrets}** secret(s) ${payload.isDiffScan ? " from a recent commit to" : " in"} **${resourceName}**.`
: `Encountered an error while attempting to scan the resource **${resourceName}**: ${payload.errorMessage}`,
link:
payload.status === SecretScanningScanStatus.Completed
? `/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
: `/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}`
}))
);
await smtpService.sendMail({
recipients: recipients.map((member) => member.user.email!).filter(Boolean),
template:
payload.status === SecretScanningScanStatus.Completed
? SmtpTemplates.SecretScanningV2SecretsDetected
: SmtpTemplates.SecretScanningV2ScanFailed,
subjectLine,
substitutions:
payload.status === SecretScanningScanStatus.Completed
? {
authorName: payload.authorName,
authorEmail: payload.authorEmail,
resourceName,
numberOfSecrets: payload.numberOfSecrets,
isDiffScan: payload.isDiffScan,
url: encodeURI(
`${appCfg.SITE_URL}/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
),
timestamp
}
: {
dataSourceName: dataSource.name,
resourceName,
projectName: project.name,
timestamp,
errorMessage: payload.errorMessage,
url: encodeURI(
`${appCfg.SITE_URL}/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}`
)
}
});
} catch (error) {
logger.error(
error,
`secretScanningV2Queue: Failed to Send Status Notification [dataSourceId=${dataSource.id}] [resourceName=${resourceName}] [status=${payload.status}]`
);
throw error;
}
},
{
batchSize: 1,
workerCount: 2,
pollingIntervalSeconds: 1
}
);
};
return { return {
queueDataSourceFullScan, queueDataSourceFullScan,
queueResourceDiffScan queueResourceDiffScan,
init
}; };
}; };
+40 -9
View File
@@ -16,7 +16,7 @@ import { buildRedisFromConfig } from "./lib/config/redis";
import { removeTemporaryBaseDirectory } from "./lib/files"; import { removeTemporaryBaseDirectory } from "./lib/files";
import { initLogger } from "./lib/logger"; import { initLogger } from "./lib/logger";
import { queueServiceFactory } from "./queue"; import { queueServiceFactory } from "./queue";
import { main } from "./server/app"; import { main, markRunningMigrations, markServerReady } from "./server/app";
import { bootstrapCheck } from "./server/boot-strap-check"; import { bootstrapCheck } from "./server/boot-strap-check";
import { kmsRootConfigDALFactory } from "./services/kms/kms-root-config-dal"; import { kmsRootConfigDALFactory } from "./services/kms/kms-root-config-dal";
import { smtpServiceFactory } from "./services/smtp/smtp-service"; import { smtpServiceFactory } from "./services/smtp/smtp-service";
@@ -59,8 +59,6 @@ const run = async () => {
}) })
: undefined; : undefined;
await runMigrations({ applicationDb: db, auditLogDb, logger });
const smtp = smtpServiceFactory(formatSmtpConfig()); const smtp = smtpServiceFactory(formatSmtpConfig());
const queue = queueServiceFactory(envConfig, { const queue = queueServiceFactory(envConfig, {
@@ -74,7 +72,7 @@ const run = async () => {
const keyStore = keyStoreFactory(envConfig, keyValueStoreDAL); const keyStore = keyStoreFactory(envConfig, keyValueStoreDAL);
const redis = buildRedisFromConfig(envConfig); const redis = buildRedisFromConfig(envConfig);
const server = await main({ const { server, completeServerInitialization } = await main({
db, db,
auditLogDb, auditLogDb,
superAdminDAL, superAdminDAL,
@@ -87,7 +85,6 @@ const run = async () => {
redis, redis,
envConfig envConfig
}); });
const bootstrap = await bootstrapCheck({ db });
// eslint-disable-next-line // eslint-disable-next-line
process.on("SIGINT", async () => { process.on("SIGINT", async () => {
@@ -121,12 +118,46 @@ const run = async () => {
await server.listen({ await server.listen({
port: envConfig.PORT, port: envConfig.PORT,
host: envConfig.HOST, host: envConfig.HOST
listenTextResolver: (address) => { });
void bootstrap();
return address; 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(); void run();
+89 -2
View File
@@ -1,5 +1,6 @@
/* eslint-disable import/extensions */ /* eslint-disable import/extensions */
import path from "node:path"; import path from "node:path";
import { monitorEventLoopDelay } from "perf_hooks";
import type { FastifyCookieOptions } from "@fastify/cookie"; import type { FastifyCookieOptions } from "@fastify/cookie";
import cookie 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 { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
import { TSmtpService } from "@app/services/smtp/smtp-service"; import { TSmtpService } from "@app/services/smtp/smtp-service";
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; 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 { globalRateLimiterCfg } from "./config/rateLimiter";
import { addErrorsToResponseSchemas } from "./plugins/add-errors-to-response-schemas"; 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 { fastifySwagger } from "./plugins/swagger";
import { registerRoutes } from "./routes"; 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 = { type TMain = {
auditLogDb?: Knex; auditLogDb?: Knex;
db: 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, smtp,
queue, queue,
db, db,
@@ -164,10 +240,21 @@ export const main = async ({
await server.ready(); await server.ready();
server.swagger(); server.swagger();
return server; return { server, completeServerInitialization };
} catch (err) { } catch (err) {
server.log.error(err); server.log.error(err);
await queue.shutdown(); await queue.shutdown();
process.exit(1); process.exit(1);
} }
}; };
export const markServerReady = () => {
serverState.isReady = true;
serverState.isRunningMigrations = false;
serverState.isWaitingForMigrations = false;
};
export const markRunningMigrations = () => {
serverState.isRunningMigrations = true;
serverState.isWaitingForMigrations = false;
};
+57 -51
View File
@@ -2208,7 +2208,7 @@ export const registerRoutes = async (
internalCaFns internalCaFns
}); });
await secretRotationV2QueueServiceFactory({ const secretRotationV2Queue = await secretRotationV2QueueServiceFactory({
secretRotationV2Service, secretRotationV2Service,
secretRotationV2DAL, secretRotationV2DAL,
queueService, queueService,
@@ -2305,8 +2305,6 @@ export const registerRoutes = async (
// If FIPS is enabled, we check to ensure that the users license includes FIPS mode. // If FIPS is enabled, we check to ensure that the users license includes FIPS mode.
crypto.verifyFipsLicense(licenseService); crypto.verifyFipsLicense(licenseService);
await superAdminService.initServerCfg();
// Start HSM service if it's configured/enabled. // Start HSM service if it's configured/enabled.
await hsmService.startService(); await hsmService.startService();
@@ -2331,21 +2329,60 @@ export const registerRoutes = async (
} }
} }
await telemetryQueue.startTelemetryCheck(); const completeServerInitialization = async () => {
await telemetryQueue.startAggregatedEventsJob(); await superAdminService.initServerCfg();
await dailyResourceCleanUp.init();
await healthAlert.init(); await telemetryQueue.startTelemetryCheck();
await pkiSyncCleanup.init(); await telemetryQueue.startAggregatedEventsJob();
await pamAccountRotation.init(); await dailyResourceCleanUp.init();
await dailyReminderQueueService.startDailyRemindersJob(); await healthAlert.init();
await dailyReminderQueueService.startSecretReminderMigrationJob(); await pkiSyncCleanup.init();
await dailyExpiringPkiItemAlert.startSendingAlerts(); await pamAccountRotation.init();
await pkiSubscriberQueue.startDailyAutoRenewalJob(); await dailyReminderQueueService.startDailyRemindersJob();
await certificateV3Queue.init(); await dailyReminderQueueService.startSecretReminderMigrationJob();
await kmsService.startService(hsmStatus); await dailyExpiringPkiItemAlert.startSendingAlerts();
await microsoftTeamsService.start(); await pkiSubscriberQueue.startDailyAutoRenewalJob();
await dynamicSecretQueueService.init(); await certificateV3Queue.init();
await eventBusService.init(); 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 // inject all services
server.decorate<FastifyZodProvider["services"]>("services", { server.decorate<FastifyZodProvider["services"]>("services", {
@@ -2475,38 +2512,6 @@ export const registerRoutes = async (
convertor: convertorService 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", { server.decorate<FastifyZodProvider["store"]>("store", {
user: userDAL, user: userDAL,
kmipClient: kmipClientDAL kmipClient: kmipClientDAL
@@ -2595,9 +2600,10 @@ export const registerRoutes = async (
await server.register(registerV4Routes, { prefix: "/api/v4" }); await server.register(registerV4Routes, { prefix: "/api/v4" });
server.addHook("onClose", async () => { server.addHook("onClose", async () => {
cronJobs.forEach((job) => job.stop());
await telemetryService.flushAll(); await telemetryService.flushAll();
await eventBusService.close(); await eventBusService.close();
sseService.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({ server.route({
method: "GET", method: "GET",
url: "/vault/secret-paths", url: "/vault/secret-paths",
@@ -5,5 +5,6 @@ export enum GitLabConnectionMethod {
export enum GitLabAccessTokenType { export enum GitLabAccessTokenType {
Project = "project", Project = "project",
Personal = "personal" Personal = "personal",
Group = "group"
} }
@@ -21,6 +21,8 @@ import {
THCVaultKubernetesAuthConfig, THCVaultKubernetesAuthConfig,
THCVaultKubernetesAuthRole, THCVaultKubernetesAuthRole,
THCVaultKubernetesAuthRoleWithConfig, THCVaultKubernetesAuthRoleWithConfig,
THCVaultKubernetesRole,
THCVaultKubernetesSecretsConfig,
THCVaultMount, THCVaultMount,
THCVaultMountResponse THCVaultMountResponse
} from "./hc-vault-connection-types"; } 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; config: THCVaultKubernetesAuthConfig;
mountPath: string; 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) { if (existingEnv) {
throw new BadRequestError({ throw new BadRequestError({
message: `Environment with slug '${slug}' already exist`, message: `Environment with slug '${slug}' already exists`,
name: "CreateEnvironment" name: "CreateEnvironment"
}); });
} }
@@ -312,7 +312,7 @@ export const importDataIntoInfisicalFn = async ({
); );
if (secretsByKeys.length) { if (secretsByKeys.length) {
throw new BadRequestError({ 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({ await fnSecretBulkInsert({
@@ -19,6 +19,7 @@ import {
convertVaultValueToString, convertVaultValueToString,
getHCVaultAuthMounts, getHCVaultAuthMounts,
getHCVaultKubernetesAuthRoles, getHCVaultKubernetesAuthRoles,
getHCVaultKubernetesRoles,
getHCVaultSecretsForPath, getHCVaultSecretsForPath,
HCVaultAuthType, HCVaultAuthType,
listHCVaultMounts, listHCVaultMounts,
@@ -762,6 +763,56 @@ export const externalMigrationServiceFactory = ({
return roles; 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 { return {
importEnvKeyData, importEnvKeyData,
importVaultData, importVaultData,
@@ -776,6 +827,7 @@ export const externalMigrationServiceFactory = ({
getVaultAuthMounts, getVaultAuthMounts,
getVaultSecretPaths, getVaultSecretPaths,
importVaultSecrets, importVaultSecrets,
getVaultKubernetesAuthRoles getVaultKubernetesAuthRoles,
getVaultKubernetesRoles
}; };
}; };
@@ -10,6 +10,7 @@ type TNotificationQueueServiceFactoryDep = {
export type TNotificationQueueServiceFactory = { export type TNotificationQueueServiceFactory = {
pushUserNotifications: (data: TCreateUserNotificationDTO[]) => Promise<void>; pushUserNotifications: (data: TCreateUserNotificationDTO[]) => Promise<void>;
init: () => Promise<void>;
}; };
export const notificationQueueServiceFactory = async ({ export const notificationQueueServiceFactory = async ({
@@ -20,20 +21,23 @@ export const notificationQueueServiceFactory = async ({
await queueService.queuePg(QueueJobs.UserNotification, { notifications: data }); await queueService.queuePg(QueueJobs.UserNotification, { notifications: data });
}; };
await queueService.startPg( const init = async () => {
QueueJobs.UserNotification, await queueService.startPg(
async ([job]) => { QueueJobs.UserNotification,
const { notifications } = job.data as { notifications: TCreateUserNotificationDTO[] }; async ([job]) => {
await userNotificationDAL.batchInsert(notifications); const { notifications } = job.data as { notifications: TCreateUserNotificationDTO[] };
}, await userNotificationDAL.batchInsert(notifications);
{ },
batchSize: 1, {
workerCount: 2, batchSize: 1,
pollingIntervalSeconds: 1 workerCount: 2,
} pollingIntervalSeconds: 1
); }
);
};
return { return {
pushUserNotifications pushUserNotifications,
init
}; };
}; };
+2 -2
View File
@@ -517,7 +517,7 @@ export const orgServiceFactory = ({
if (slug) { if (slug) {
const existingOrg = await orgDAL.findOne({ slug, rootOrgId: null }); const existingOrg = await orgDAL.findOne({ slug, rootOrgId: null });
if (existingOrg && existingOrg?.id !== orgId) 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) { if (googleSsoAuthEnforced) {
@@ -1149,7 +1149,7 @@ export const orgServiceFactory = ({
const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email }); const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email });
if (doesIncidentContactExist) { if (doesIncidentContactExist) {
throw new BadRequestError({ throw new BadRequestError({
message: "Incident contact already exist", message: "Incident contact already exists",
name: "Incident contact exist" name: "Incident contact exist"
}); });
} }
@@ -76,7 +76,7 @@ export const projectEnvServiceFactory = ({
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug); const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
if (existingEnv) if (existingEnv)
throw new BadRequestError({ throw new BadRequestError({
message: "Environment with slug already exist", message: "Environment with slug already exists",
name: "CreateEnvironment" name: "CreateEnvironment"
}); });
@@ -171,7 +171,7 @@ export const projectEnvServiceFactory = ({
const existingEnv = await projectEnvDAL.findOne({ slug, projectId }); const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
if (existingEnv && existingEnv.id !== id) { if (existingEnv && existingEnv.id !== id) {
throw new BadRequestError({ throw new BadRequestError({
message: "Environment with slug already exist", message: "Environment with slug already exists",
name: "UpdateEnvironment" name: "UpdateEnvironment"
}); });
} }
+55 -41
View File
@@ -10,7 +10,8 @@ import {
ProjectType, ProjectType,
ProjectVersion, ProjectVersion,
TableName, TableName,
TProjectEnvironments TProjectEnvironments,
TProjects
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; 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 { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; 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 { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types"; 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)}`);
{
name: workspaceName, let project: TProjects;
type, try {
description: workspaceDescription, project = await projectDAL.create(
orgId: organization.id, {
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), name: workspaceName,
kmsSecretManagerKeyId: kmsKeyId, type,
version: ProjectVersion.V3, description: workspaceDescription,
pitVersionLimit: 10 orgId: organization.id,
}, slug,
tx 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) { if (type === ProjectType.SSH) {
await bootstrapSshProject({ await bootstrapSshProject({
@@ -579,39 +596,36 @@ export const projectServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); 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)) { if (update.secretDetectionIgnoreValues && !hasRole(ProjectMembershipRole.Admin)) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Only admins can update secret detection ignore values" message: "Only admins can update secret detection ignore values"
}); });
} }
const updatedProject = await projectDAL.updateById(project.id, { try {
name: update.name, const updatedProject = await projectDAL.updateById(project.id, {
description: update.description, name: update.name,
autoCapitalization: update.autoCapitalization, description: update.description,
enforceCapitalization: update.autoCapitalization, autoCapitalization: update.autoCapitalization,
hasDeleteProtection: update.hasDeleteProtection, enforceCapitalization: update.autoCapitalization,
slug: update.slug, hasDeleteProtection: update.hasDeleteProtection,
secretSharing: update.secretSharing, slug: update.slug,
defaultProduct: update.defaultProduct, secretSharing: update.secretSharing,
showSnapshotsLegacy: update.showSnapshotsLegacy, defaultProduct: update.defaultProduct,
secretDetectionIgnoreValues: update.secretDetectionIgnoreValues, showSnapshotsLegacy: update.showSnapshotsLegacy,
pitVersionLimit: update.pitVersionLimit secretDetectionIgnoreValues: update.secretDetectionIgnoreValues,
}); pitVersionLimit: update.pitVersionLimit
});
return updatedProject; 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 ({ const toggleAutoCapitalization = async ({
@@ -35,7 +35,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
const existingTag = await secretTagDAL.findOne({ slug, projectId }); 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({ const newTag = await secretTagDAL.create({
projectId, projectId,
@@ -53,7 +53,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
if (slug) { if (slug) {
const existingTag = await secretTagDAL.findOne({ slug, projectId: tag.projectId }); 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({ const { permission } = await permissionService.getProjectPermission({
@@ -282,7 +282,7 @@ export const secretV2BridgeServiceFactory = ({
folderId folderId
}); });
if (inputSecret.type === SecretType.Shared && doesSecretExist) 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 user creating personal check its shared also exist
if (inputSecret.type === SecretType.Personal && !doesSecretExist) { if (inputSecret.type === SecretType.Personal && !doesSecretExist) {
@@ -527,7 +527,7 @@ export const secretV2BridgeServiceFactory = ({
type: SecretType.Shared, type: SecretType.Shared,
folderId 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( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
@@ -1674,7 +1674,7 @@ export const secretV2BridgeServiceFactory = ({
} }
}); });
if (secrets.length) 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); const project = await projectDAL.findById(projectId);
await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []); await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []);
+2 -2
View File
@@ -525,7 +525,7 @@ export const fnSecretBlindIndexCheck = async ({
); );
if (isNew) { if (isNew) {
if (secrets.length) throw new BadRequestError({ message: "Secret already exist" }); if (secrets.length) throw new BadRequestError({ message: "Secret already exists" });
} else { } else {
const secretKeysInDB = unique(secrets, (el) => el.secretBlindIndex as string).map( const secretKeysInDB = unique(secrets, (el) => el.secretBlindIndex as string).map(
(el) => blindIndex2KeyName[el.secretBlindIndex as string] (el) => blindIndex2KeyName[el.secretBlindIndex as string]
@@ -819,7 +819,7 @@ export const createManySecretsRawFnFactory = ({
); );
if (secretsStoredInDB.length) if (secretsStoredInDB.length)
throw new BadRequestError({ 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) => { const inputSecrets = secrets.map((secret) => {
@@ -2751,7 +2751,7 @@ export const secretServiceFactory = ({
const existingSecretTags = await secretDAL.getSecretTags(secret.id); const existingSecretTags = await secretDAL.getSecretTags(secret.id);
if (existingSecretTags.some((tag) => tagSlugs.includes(tag.slug))) { 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)]); 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. 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 ### Step 1: Set Up Your Vault Connection
<Steps> <Steps>
@@ -84,6 +90,19 @@ This migration approach lets you set up a connection to your Vault instance once
path "auth/+/role/*" { path "auth/+/role/*" {
capabilities = ["read"] 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> </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. must be manually provided in the form after importing the configuration.
</Note> </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 #### 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. 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

+82 -21
View File
@@ -187,31 +187,92 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
</Step> </Step>
</Steps> </Steps>
</Tab> </Tab>
</Tabs>
## Setup GitLab Access Token Connection in Infisical <Tab title="Group Access Token">
Group access tokens provide access to all projects within a GitLab group, offering group-level control.
<Steps> <Steps>
<Step title="Navigate to App Connections"> <Step title="Navigate to Group Settings">
Navigate to the **App Connections** page in the desired project. Go to your GitLab group and navigate to Settings > Access Tokens. Click **Add new token** to create a new group access token.
![App Connections Tab](/images/app-connections/general/add-connection.png) ![GitLab Group Access Tokens](/images/app-connections/gitlab/gitlab-group-access-token-list.png)
</Step> </Step>
<Step title="Add Connection"> <Step title="Configure Token">
Select the **GitLab Connection** option from the connection options modal. Fill in the token details:
![Select GitLab Connection](/images/app-connections/gitlab/select-gitlab-connection.png) - **Token name**: A descriptive name for the token
</Step> - **Expiration date**: Set an appropriate expiration date
<Step title="Configure Access Token"> - **Select role and scopes**: Depending on your use case, add the required role and one or more of the following scopes:
Select the **Access Token** method, paste your GitLab access token in the provided field, and select the appropriate token type.
![Configure Access Token](/images/app-connections/gitlab/create-gitlab-access-token-connection.png) <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
Click **Connect** to establish the connection. Your token will require the `api` scope.
</Step>
<Step title="Connection Created"> ![GitLab Create Group Token](/images/app-connections/gitlab/gitlab-group-access-token-form-secret-sync.png)
Your **GitLab Connection** is now available for use.
![GitLab Access Token Connection](/images/app-connections/gitlab/gitlab-access-token-connection.png) Click **Create group access token** to create the token.
</Step>
</Steps> <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>
<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">
Navigate to the **App Connections** page in the desired project.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Add Connection">
Select the **GitLab Connection** option from the connection options modal.
![Select GitLab Connection](/images/app-connections/gitlab/select-gitlab-connection.png)
</Step>
<Step title="Configure Access Token">
Select the **Access Token** method, paste your GitLab access token in the provided field, and select the appropriate token type.
![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>
</Tab> </Tab>
</Tabs> </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"}]' 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 ### Operational Security
#### User Access Management #### User Access Management
@@ -207,14 +234,17 @@ docker run --memory=1g --cpus=0.5 infisical/infisical:latest
#### Health Monitoring #### Health Monitoring
**Configure health checks**. Set up Docker health checks: **Configure health checks**. Set up Docker health checks using the appropriate endpoint:
```dockerfile ```dockerfile
# In Dockerfile or docker-compose.yml # 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 \ 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 #### Network Security
**Host firewall configuration**. Configure host-level firewall for Docker deployments: **Host firewall configuration**. Configure host-level firewall for Docker deployments:
@@ -433,26 +463,32 @@ stringData:
#### Health Monitoring #### 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 ```yaml
# Health check configuration # Health check configuration
containers: containers:
- name: infisical - name: infisical
# Use /api/ready for readiness (traffic routing)
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /api/status path: /api/ready
port: 8080 port: 8080
initialDelaySeconds: 10 initialDelaySeconds: 10
periodSeconds: 5 periodSeconds: 5
failureThreshold: 3
# Use /api/health for liveness (container restart)
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /api/status path: /api/health
port: 8080 port: 8080
initialDelaySeconds: 30 initialDelaySeconds: 30
periodSeconds: 10 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 #### 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. **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) => { export const DocumentDecorator: Decorator = (Story) => {
useEffect(() => { useEffect(() => {
const root = document.getElementsByTagName("html")[0]; const root = document.documentElement;
root.setAttribute("class", "overflow-visible"); root.setAttribute("class", "overflow-visible");
}, []); }, []);
@@ -1,9 +1,11 @@
import { components, OptionProps } from "react-select"; import { components, OptionProps } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; 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 { 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"; import { TAvailableAppConnection } from "@app/hooks/api/appConnections";
export const AppConnectionOption = ({ export const AppConnectionOption = ({
@@ -13,6 +15,8 @@ export const AppConnectionOption = ({
}: OptionProps<TAvailableAppConnection>) => { }: OptionProps<TAvailableAppConnection>) => {
const isCreateOption = props.data.id === "_create"; const isCreateOption = props.data.id === "_create";
const { isSubOrganization } = useOrganization();
return ( return (
<components.Option isSelected={isSelected} {...props}> <components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between"> <div className="flex flex-row items-center justify-between">
@@ -23,15 +27,22 @@ export const AppConnectionOption = ({
</div> </div>
) : ( ) : (
<> <>
<p className="truncate">{children}</p> <p className="mr-auto truncate">{children}</p>
{!props.data.projectId && ( {!props.data.projectId && (
<Tooltip content="This connection belongs to your organization."> <Tooltip
<div className="mr-auto ml-2"> content={`This connection belongs to your ${isSubOrganization ? "sub-" : ""}organization.`}
<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" /> {isSubOrganization ? (
<Badge variant="sub-org">
<SubOrgIcon />
Sub-Organization
</Badge>
) : (
<Badge variant="org">
<OrgIcon />
Organization Organization
</Badge> </Badge>
</div> )}
</Tooltip> </Tooltip>
)} )}
{isSelected && ( {isSelected && (
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react"; import { ReactNode, useEffect, useMemo, useState } from "react";
import { import { faXmark } from "@fortawesome/free-solid-svg-icons";
faCheck,
faDownload,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns"; 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 { Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge"; import { Badge, TBadgeProps } from "@app/components/v3";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs"; import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = { type Props = {
pkiSync: TPkiSync; pkiSync: TPkiSync;
className?: string;
mini?: boolean; mini?: boolean;
}; };
export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) => { export const PkiSyncImportStatusBadge = ({ pkiSync, mini }: Props) => {
const { importStatus, lastImportMessage, lastImportedAt, destination } = pkiSync; const { importStatus, lastImportMessage, lastImportedAt, destination } = pkiSync;
const [hide, setHide] = useState(importStatus === PkiSyncStatus.Succeeded); const [hide, setHide] = useState(importStatus === PkiSyncStatus.Succeeded);
const destinationName = PKI_SYNC_MAP[destination].name; const destinationName = PKI_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) =>
if (!importStatus || hide) return null; if (!importStatus || hide) return null;
let variant: BadgeProps["variant"]; let variant: TBadgeProps["variant"];
let label: string; let label: string;
let icon: IconDefinition; let Icon: LucideIcon;
let tooltipContent: ReactNode; let tooltipContent: ReactNode;
switch (importStatus) { switch (importStatus) {
case PkiSyncStatus.Pending: case PkiSyncStatus.Pending:
case PkiSyncStatus.Running: case PkiSyncStatus.Running:
variant = "primary"; variant = "warning";
label = "Importing Certificates..."; label = "Importing Certificates...";
tooltipContent = `Importing certificates from ${destinationName}. This may take a moment.`; tooltipContent = `Importing certificates from ${destinationName}. This may take a moment.`;
icon = faDownload; Icon = DownloadIcon;
break; break;
case PkiSyncStatus.Failed: case PkiSyncStatus.Failed:
variant = "danger"; variant = "danger";
label = "Failed to Import Certificates"; label = "Failed to Import Certificates";
icon = faTriangleExclamation; Icon = TriangleAlertIcon;
tooltipContent = ( tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal"> <div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && ( {failureMessage && (
@@ -93,20 +86,15 @@ export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) =>
tooltipContent = "Successfully imported certificates."; tooltipContent = "Successfully imported certificates.";
variant = "success"; variant = "success";
label = "Certificates Imported"; label = "Certificates Imported";
icon = faCheck; Icon = CheckIcon;
} }
return ( return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}> <Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div> <Badge isSquare={mini} variant={variant}>
<Badge <Icon />
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)} {!mini && label}
variant={variant} </Badge>
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
</Tooltip> </Tooltip>
); );
}; };
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; import { DocumentationLinkBadge } from "@app/components/v3";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync } from "@app/hooks/api/pkiSyncs"; import { PkiSync } from "@app/hooks/api/pkiSyncs";
@@ -20,23 +18,11 @@ export const PkiSyncModalHeader = ({ destination, isConfigured }: Props) => {
className="h-12 w-12 rounded-md bg-bunker-500 p-2" className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/> />
<div> <div>
<div className="flex items-center text-mineshaft-300"> <div className="flex items-center gap-x-2 text-mineshaft-300">
{destinationDetails.name} Certificate Sync {destinationDetails.name} Certificate Sync
<a <DocumentationLinkBadge
target="_blank" href={`https://infisical.com/docs/documentation/platform/pki/certificate-syncs/${destination}`}
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]"
/>
</div>
</a>
</div> </div>
<p className="text-sm leading-4 text-mineshaft-400"> <p className="text-sm leading-4 text-mineshaft-400">
{isConfigured {isConfigured
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react"; import { ReactNode, useEffect, useMemo, useState } from "react";
import { import { faXmark } from "@fortawesome/free-solid-svg-icons";
faCheck,
faEraser,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns"; 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 { Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge"; import { Badge, TBadgeProps } from "@app/components/v3";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs"; import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = { type Props = {
pkiSync: TPkiSync; pkiSync: TPkiSync;
className?: string;
mini?: boolean; mini?: boolean;
}; };
export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) => { export const PkiSyncRemoveStatusBadge = ({ pkiSync, mini }: Props) => {
const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = pkiSync; const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = pkiSync;
const [hide, setHide] = useState(removeStatus === PkiSyncStatus.Succeeded); const [hide, setHide] = useState(removeStatus === PkiSyncStatus.Succeeded);
const destinationName = PKI_SYNC_MAP[destination].name; const destinationName = PKI_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) =>
if (!removeStatus || hide) return null; if (!removeStatus || hide) return null;
let variant: BadgeProps["variant"]; let variant: TBadgeProps["variant"];
let label: string; let label: string;
let icon: IconDefinition; let Icon: LucideIcon;
let tooltipContent: ReactNode; let tooltipContent: ReactNode;
switch (removeStatus) { switch (removeStatus) {
case PkiSyncStatus.Pending: case PkiSyncStatus.Pending:
case PkiSyncStatus.Running: case PkiSyncStatus.Running:
variant = "primary"; variant = "warning";
label = "Removing Certificates..."; label = "Removing Certificates...";
tooltipContent = `Removing certificates from ${destinationName}. This may take a moment.`; tooltipContent = `Removing certificates from ${destinationName}. This may take a moment.`;
icon = faEraser; Icon = EraserIcon;
break; break;
case PkiSyncStatus.Failed: case PkiSyncStatus.Failed:
variant = "danger"; variant = "danger";
label = "Failed to Remove Certificates"; label = "Failed to Remove Certificates";
icon = faTriangleExclamation; Icon = AlertTriangleIcon;
tooltipContent = ( tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal"> <div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && ( {failureMessage && (
@@ -93,20 +86,15 @@ export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) =>
tooltipContent = "Successfully removed certificates."; tooltipContent = "Successfully removed certificates.";
variant = "success"; variant = "success";
label = "Certificates Removed"; label = "Certificates Removed";
icon = faCheck; Icon = CheckIcon;
} }
return ( return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}> <Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div> <Badge isSquare={mini} variant={variant}>
<Badge <Icon />
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)} {!mini && label}
variant={variant} </Badge>
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
</Tooltip> </Tooltip>
); );
}; };
@@ -69,7 +69,10 @@ export const PkiSyncSelect = ({ onSelect }: Props) => {
type="button" type="button"
onClick={() => onClick={() =>
enterprise && !subscription.enterpriseCertificateSyncs 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) : 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" 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 <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("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." text="You can use every Certificate Sync if you switch to Infisical's Enterprise plan."
/> />
</div> </div>
@@ -1,55 +1,51 @@
import { import {
faCheck, AlertTriangleIcon,
faExclamationTriangle, CheckIcon,
faHourglass, HourglassIcon,
faRotate, LucideIcon,
IconDefinition RefreshCwIcon
} from "@fortawesome/free-solid-svg-icons"; } from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge"; import { Badge, TBadgeProps } from "@app/components/v3";
import { PkiSyncStatus } from "@app/hooks/api/pkiSyncs"; import { PkiSyncStatus } from "@app/hooks/api/pkiSyncs";
type Props = { type Props = {
status: PkiSyncStatus; status: PkiSyncStatus;
} & Omit<BadgeProps, "children" | "variant">; } & Omit<TBadgeProps, "children" | "variant">;
export const PkiSyncStatusBadge = ({ status }: Props) => { export const PkiSyncStatusBadge = ({ status }: Props) => {
let variant: BadgeProps["variant"]; let variant: TBadgeProps["variant"];
let text: string; let text: string;
let icon: IconDefinition; let Icon: LucideIcon;
switch (status) { switch (status) {
case PkiSyncStatus.Failed: case PkiSyncStatus.Failed:
variant = "danger"; variant = "danger";
text = "Failed to Sync"; text = "Failed to Sync";
icon = faExclamationTriangle; Icon = AlertTriangleIcon;
break; break;
case PkiSyncStatus.Succeeded: case PkiSyncStatus.Succeeded:
variant = "success"; variant = "success";
text = "Synced"; text = "Synced";
icon = faCheck; Icon = CheckIcon;
break; break;
case PkiSyncStatus.Pending: case PkiSyncStatus.Pending:
variant = "primary"; variant = "info";
text = "Queued"; text = "Queued";
icon = faHourglass; Icon = HourglassIcon;
break; break;
case PkiSyncStatus.Running: case PkiSyncStatus.Running:
default: default:
variant = "primary"; variant = "info";
text = "Syncing"; text = "Syncing";
icon = faRotate; Icon = RefreshCwIcon;
break; break;
} }
return ( return (
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}> <Badge variant={variant}>
<FontAwesomeIcon <Icon className={status === PkiSyncStatus.Running ? "animate-spin" : ""} />
icon={icon} {text}
className={status === PkiSyncStatus.Running ? "animate-spin" : ""}
/>
<span>{text}</span>
</Badge> </Badge>
); );
}; };
@@ -2,7 +2,6 @@ import { faPlug, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
Badge,
EmptyState, EmptyState,
IconButton, IconButton,
Table, Table,
@@ -13,6 +12,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { TPkiSync } from "@app/hooks/api/pkiSyncs"; import { TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = { type Props = {
@@ -29,10 +29,10 @@ const getSyncStatusBadge = (status?: string) => {
case "FAILED": case "FAILED":
return <Badge variant="danger">Failed</Badge>; return <Badge variant="danger">Failed</Badge>;
case "RUNNING": case "RUNNING":
return <Badge variant="primary">Running</Badge>; return <Badge variant="warning">Running</Badge>;
case "PENDING": case "PENDING":
default: 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>{pkiSync.name}</Td>
<Td> <Td>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Badge variant="primary">{pkiSync.destination}</Badge> <Badge variant="warning">{pkiSync.destination}</Badge>
</div> </div>
</Td> </Td>
<Td>{pkiSync.appConnectionName || "Unknown"}</Td> <Td>{pkiSync.appConnectionName || "Unknown"}</Td>
@@ -1,6 +1,7 @@
import { useFormContext } from "react-hook-form"; 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 { useProject } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { useListWorkspacePkiSubscribers } from "@app/hooks/api"; import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
@@ -1,6 +1,4 @@
import { useEffect, useState } from "react"; 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 { useNavigate, useRouterState } from "@tanstack/react-router";
import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms"; 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 { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader";
import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select"; import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select";
import { Modal, ModalContent } from "@app/components/v2"; import { Modal, ModalContent } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import { ProjectEnv } from "@app/hooks/api/projects/types"; import { ProjectEnv } from "@app/hooks/api/projects/types";
import { SecretRotation, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; import { SecretRotation, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
@@ -115,23 +114,9 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
selectedRotation ? ( selectedRotation ? (
<SecretRotationV2ModalHeader isConfigured={false} type={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 Add Secret Rotation
<a <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/secret-rotation/overview" />
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>
</div> </div>
) )
} }
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; import { DocumentationLinkBadge } from "@app/components/v3";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2"; import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
@@ -20,23 +18,11 @@ export const SecretRotationV2ModalHeader = ({ type, isConfigured }: Props) => {
className="h-12 w-12 rounded-md bg-bunker-500 p-2" className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/> />
<div> <div>
<div className="flex items-center text-mineshaft-300"> <div className="flex items-center gap-x-2 text-mineshaft-300">
{destinationDetails.name} Rotation {destinationDetails.name} Rotation
<a <DocumentationLinkBadge
target="_blank"
href={`https://infisical.com/docs/documentation/platform/secret-rotation/${type}`} 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> </div>
<p className="text-sm leading-4 text-mineshaft-400"> <p className="text-sm leading-4 text-mineshaft-400">
{isConfigured {isConfigured
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, formatDistanceToNow } from "date-fns"; 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 { 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"; import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
type Props = { type Props = {
secretRotation: TSecretRotationV2; secretRotation: TSecretRotationV2;
className?: string;
}; };
export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props) => { export const SecretRotationV2StatusBadge = ({ secretRotation }: Props) => {
const { isAutoRotationEnabled, rotationStatus, nextRotationAt, lastRotationMessage } = const { isAutoRotationEnabled, rotationStatus, nextRotationAt, lastRotationMessage } =
secretRotation; secretRotation;
@@ -50,28 +49,18 @@ export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props
</div> </div>
} }
> >
<div> <Badge variant="danger">
<Badge <XIcon />
variant="danger" Rotation Failed
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)} </Badge>
>
<FontAwesomeIcon icon={faXmark} />
Rotation Failed
</Badge>
</div>
</Tooltip> </Tooltip>
); );
} }
if (!isAutoRotationEnabled) { if (!isAutoRotationEnabled) {
return ( return (
<Badge <Badge variant="neutral">
className={twMerge( <BanIcon />
"flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300",
className
)}
>
<FontAwesomeIcon icon={faBan} />
Auto-Rotation Disabled Auto-Rotation Disabled
</Badge> </Badge>
); );
@@ -92,20 +81,12 @@ export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props
</> </>
} }
> >
<div> <Badge variant={daysToRotation >= 7 ? "info" : "warning"} className="capitalize">
<Badge <RefreshCwIcon />
variant={daysToRotation >= 7 ? "success" : "primary"} {daysToRotation < 0
className={twMerge( ? "Rotating"
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize", : `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`}
className </Badge>
)}
>
<FontAwesomeIcon icon={faRotate} />
{daysToRotation < 0
? "Rotating"
: `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`}
</Badge>
</div>
</Tooltip> </Tooltip>
); );
}; };
@@ -1,8 +1,10 @@
import { ReactNode } from "react"; 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 { 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 = { type Props = {
items: { name: string; input: ReactNode }[]; items: { name: string; input: ReactNode }[];
@@ -32,9 +34,10 @@ export const SecretsMappingTable = ({ items }: Props) => {
<tr key={name}> <tr key={name}>
<td className="whitespace-nowrap"> <td className="whitespace-nowrap">
<div className="mb-4 flex h-full items-start justify-center"> <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"> {/* TODO(scott): probably shouldn't be a badge */}
<FontAwesomeIcon icon={faKey} /> <Badge variant="neutral" className="h-[36px] w-full justify-center text-xs">
<span>{name}</span> <KeyIcon />
{name}
</Badge> </Badge>
</div> </div>
</td> </td>
@@ -1,10 +1,9 @@
import { useEffect, useState } from "react"; 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 { useNavigate, useRouterState } from "@tanstack/react-router";
import { TSecretScanningDataSourceForm } from "@app/components/secret-scanning/forms/schemas"; import { TSecretScanningDataSourceForm } from "@app/components/secret-scanning/forms/schemas";
import { Modal, ModalContent } from "@app/components/v2"; import { Modal, ModalContent } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import { import {
SecretScanningDataSource, SecretScanningDataSource,
TSecretScanningDataSource TSecretScanningDataSource
@@ -108,23 +107,9 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
selectedDataSource ? ( selectedDataSource ? (
<SecretScanningDataSourceModalHeader isConfigured={false} type={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 Add Data Source
<a <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/secret-scanning/overview" />
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>
</div> </div>
) )
} }
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; import { DocumentationLinkBadge } from "@app/components/v3";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2"; import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
@@ -20,23 +18,11 @@ export const SecretScanningDataSourceModalHeader = ({ type, isConfigured }: Prop
className="h-12 rounded-md bg-bunker-500 p-2" className="h-12 rounded-md bg-bunker-500 p-2"
/> />
<div> <div>
<div className="flex items-center text-mineshaft-300"> <div className="flex items-center gap-x-2 text-mineshaft-300">
{dataSourceDetails.name} Data Source {dataSourceDetails.name} Data Source
<a <DocumentationLinkBadge
target="_blank"
href={`https://infisical.com/docs/documentation/platform/secret-scanning/${type}`} 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> </div>
<p className="text-sm leading-4 text-mineshaft-400"> <p className="text-sm leading-4 text-mineshaft-400">
{isConfigured ? "Edit" : "Connect a"} {dataSourceDetails.name} Data Source {isConfigured ? "Edit" : "Connect a"} {dataSourceDetails.name} Data Source
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { formatDistance } from "date-fns"; 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"; import { SecretScanningScanStatus } from "@app/hooks/api/secretScanningV2";
type Props = { type Props = {
status: SecretScanningScanStatus; status: SecretScanningScanStatus;
statusMessage?: string | null; statusMessage?: string | null;
className?: string;
scannedAt?: string | null; scannedAt?: string | null;
}; };
export const SecretScanningScanStatusBadge = ({ export const SecretScanningScanStatusBadge = ({
status, status,
statusMessage, statusMessage,
className,
scannedAt scannedAt
}: Props) => { }: Props) => {
if (status === SecretScanningScanStatus.Failed) { if (status === SecretScanningScanStatus.Failed) {
@@ -53,11 +54,8 @@ export const SecretScanningScanStatusBadge = ({
} }
> >
<div> <div>
<Badge <Badge variant="danger">
variant="danger" <XIcon />
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
>
<FontAwesomeIcon icon={faXmark} />
Scan Error Scan Error
</Badge> </Badge>
</div> </div>
@@ -67,26 +65,17 @@ export const SecretScanningScanStatusBadge = ({
if (status === SecretScanningScanStatus.Queued || status === SecretScanningScanStatus.Scanning) { if (status === SecretScanningScanStatus.Queued || status === SecretScanningScanStatus.Scanning) {
return ( return (
<Badge <Badge variant="info">
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)} <RotateCwIcon className="animate-spin" />
variant="primary" Scanning
>
<FontAwesomeIcon icon={faArrowRotateForward} className="animate-spin" />
<span>Scanning</span>
</Badge> </Badge>
); );
} }
return ( return (
<Badge <Badge variant="success">
variant="success" <CheckIcon />
className={twMerge( Complete
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
className
)}
>
<FontAwesomeIcon icon={faCheck} />
<span>Complete</span>
</Badge> </Badge>
); );
}; };
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react"; import { ReactNode, useEffect, useMemo, useState } from "react";
import { import { faXmark } from "@fortawesome/free-solid-svg-icons";
faCheck,
faDownload,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns"; 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 { Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge"; import { Badge, TBadgeProps } from "@app/components/v3";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = { type Props = {
secretSync: TSecretSync; secretSync: TSecretSync;
className?: string;
mini?: boolean; mini?: boolean;
}; };
export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Props) => { export const SecretSyncImportStatusBadge = ({ secretSync, mini }: Props) => {
const { importStatus, lastImportMessage, lastImportedAt, destination } = secretSync; const { importStatus, lastImportMessage, lastImportedAt, destination } = secretSync;
const [hide, setHide] = useState(importStatus === SecretSyncStatus.Succeeded); const [hide, setHide] = useState(importStatus === SecretSyncStatus.Succeeded);
const destinationName = SECRET_SYNC_MAP[destination].name; const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Pro
if (!importStatus || hide) return null; if (!importStatus || hide) return null;
let variant: BadgeProps["variant"]; let variant: TBadgeProps["variant"];
let label: string; let label: string;
let icon: IconDefinition; let Icon: LucideIcon;
let tooltipContent: ReactNode; let tooltipContent: ReactNode;
switch (importStatus) { switch (importStatus) {
case SecretSyncStatus.Pending: case SecretSyncStatus.Pending:
case SecretSyncStatus.Running: case SecretSyncStatus.Running:
variant = "primary"; variant = "warning";
label = "Importing Secrets..."; label = "Importing Secrets...";
tooltipContent = `Importing secrets from ${destinationName}. This may take a moment.`; tooltipContent = `Importing secrets from ${destinationName}. This may take a moment.`;
icon = faDownload; Icon = DownloadIcon;
break; break;
case SecretSyncStatus.Failed: case SecretSyncStatus.Failed:
variant = "danger"; variant = "danger";
label = "Failed to Import Secrets"; label = "Failed to Import Secrets";
icon = faTriangleExclamation; Icon = AlertTriangleIcon;
tooltipContent = ( tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal"> <div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && ( {failureMessage && (
@@ -93,20 +86,15 @@ export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Pro
tooltipContent = "Successfully imported secrets."; tooltipContent = "Successfully imported secrets.";
variant = "success"; variant = "success";
label = "Secrets Imported"; label = "Secrets Imported";
icon = faCheck; Icon = CheckIcon;
} }
return ( return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}> <Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div> <Badge isSquare={mini} variant={variant}>
<Badge <Icon />
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)} {!mini && label}
variant={variant} </Badge>
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
</Tooltip> </Tooltip>
); );
}; };
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; import { DocumentationLinkBadge } from "@app/components/v3";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
@@ -20,23 +18,11 @@ export const SecretSyncModalHeader = ({ destination, isConfigured }: Props) => {
className="h-12 w-12 rounded-md bg-bunker-500 object-contain p-2" className="h-12 w-12 rounded-md bg-bunker-500 object-contain p-2"
/> />
<div> <div>
<div className="flex items-center text-mineshaft-300"> <div className="flex items-center gap-x-2 text-mineshaft-300">
{destinationDetails.name} Sync {destinationDetails.name} Sync
<a <DocumentationLinkBadge
target="_blank"
href={`https://infisical.com/docs/integrations/secret-syncs/${destination}`} 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> </div>
<p className="text-sm leading-4 text-mineshaft-400"> <p className="text-sm leading-4 text-mineshaft-400">
{isConfigured {isConfigured
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react"; import { ReactNode, useEffect, useMemo, useState } from "react";
import { import { faXmark } from "@fortawesome/free-solid-svg-icons";
faCheck,
faEraser,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns"; 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 { Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge"; import { Badge, TBadgeProps } from "@app/components/v3";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = { type Props = {
secretSync: TSecretSync; secretSync: TSecretSync;
className?: string;
mini?: boolean; mini?: boolean;
}; };
export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Props) => { export const SecretSyncRemoveStatusBadge = ({ secretSync, mini }: Props) => {
const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = secretSync; const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = secretSync;
const [hide, setHide] = useState(removeStatus === SecretSyncStatus.Succeeded); const [hide, setHide] = useState(removeStatus === SecretSyncStatus.Succeeded);
const destinationName = SECRET_SYNC_MAP[destination].name; const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Pro
if (!removeStatus || hide) return null; if (!removeStatus || hide) return null;
let variant: BadgeProps["variant"]; let variant: TBadgeProps["variant"];
let label: string; let label: string;
let icon: IconDefinition; let Icon: LucideIcon;
let tooltipContent: ReactNode; let tooltipContent: ReactNode;
switch (removeStatus) { switch (removeStatus) {
case SecretSyncStatus.Pending: case SecretSyncStatus.Pending:
case SecretSyncStatus.Running: case SecretSyncStatus.Running:
variant = "primary"; variant = "warning";
label = "Removing Secrets..."; label = "Removing Secrets...";
tooltipContent = `Removing secrets from ${destinationName}. This may take a moment.`; tooltipContent = `Removing secrets from ${destinationName}. This may take a moment.`;
icon = faEraser; Icon = EraserIcon;
break; break;
case SecretSyncStatus.Failed: case SecretSyncStatus.Failed:
variant = "danger"; variant = "danger";
label = "Failed to Remove Secrets"; label = "Failed to Remove Secrets";
icon = faTriangleExclamation; Icon = AlertTriangleIcon;
tooltipContent = ( tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal"> <div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && ( {failureMessage && (
@@ -93,20 +86,15 @@ export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Pro
tooltipContent = "Successfully removed secrets."; tooltipContent = "Successfully removed secrets.";
variant = "success"; variant = "success";
label = "Secrets Removed"; label = "Secrets Removed";
icon = faCheck; Icon = CheckIcon;
} }
return ( return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}> <Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div> <Badge isSquare={mini} variant={variant}>
<Badge <Icon />
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)} {!mini && label}
variant={variant} </Badge>
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
</Tooltip> </Tooltip>
); );
}; };
@@ -67,7 +67,9 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
type="button" type="button"
onClick={() => onClick={() =>
enterprise && !subscription.enterpriseSecretSyncs enterprise && !subscription.enterpriseSecretSyncs
? handlePopUpOpen("upgradePlan") ? handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
})
: onSelect(destination) : 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" 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 <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use every Secret Sync if you switch to Infisical's Enterprise plan." text="You can use every Secret Sync if you switch to Infisical's Enterprise plan."
/> />
@@ -1,55 +1,51 @@
import { import {
faCheck, AlertTriangleIcon,
faExclamationTriangle, CheckIcon,
faHourglass, HourglassIcon,
faRotate, LucideIcon,
IconDefinition RefreshCwIcon
} from "@fortawesome/free-solid-svg-icons"; } from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge"; import { Badge, TBadgeProps } from "@app/components/v3";
import { SecretSyncStatus } from "@app/hooks/api/secretSyncs"; import { SecretSyncStatus } from "@app/hooks/api/secretSyncs";
type Props = { type Props = {
status: SecretSyncStatus; status: SecretSyncStatus;
} & Omit<BadgeProps, "children" | "variant">; } & Omit<TBadgeProps, "children" | "variant">;
export const SecretSyncStatusBadge = ({ status }: Props) => { export const SecretSyncStatusBadge = ({ status }: Props) => {
let variant: BadgeProps["variant"]; let variant: TBadgeProps["variant"];
let text: string; let text: string;
let icon: IconDefinition; let Icon: LucideIcon;
switch (status) { switch (status) {
case SecretSyncStatus.Failed: case SecretSyncStatus.Failed:
variant = "danger"; variant = "danger";
text = "Failed to Sync"; text = "Failed to Sync";
icon = faExclamationTriangle; Icon = AlertTriangleIcon;
break; break;
case SecretSyncStatus.Succeeded: case SecretSyncStatus.Succeeded:
variant = "success"; variant = "success";
text = "Synced"; text = "Synced";
icon = faCheck; Icon = CheckIcon;
break; break;
case SecretSyncStatus.Pending: case SecretSyncStatus.Pending:
variant = "primary"; variant = "info";
text = "Queued"; text = "Queued";
icon = faHourglass; Icon = HourglassIcon;
break; break;
case SecretSyncStatus.Running: case SecretSyncStatus.Running:
default: default:
variant = "primary"; variant = "info";
text = "Syncing"; text = "Syncing";
icon = faRotate; Icon = RefreshCwIcon;
break; break;
} }
return ( return (
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}> <Badge variant={variant}>
<FontAwesomeIcon <Icon className={[SecretSyncStatus.Running].includes(status) ? "animate-spin" : ""} />
icon={icon} {text}
className={[SecretSyncStatus.Running].includes(status) ? "animate-spin" : ""}
/>
<span>{text}</span>
</Badge> </Badge>
); );
}; };
@@ -5,14 +5,8 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
Badge, import { Badge } from "@app/components/v3";
FilterableSelect,
FormControl,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { GCP_SYNC_SCOPES } from "@app/helpers/secretSyncs"; import { GCP_SYNC_SCOPES } from "@app/helpers/secretSyncs";
import { import {
useGcpConnectionListProjectLocations, useGcpConnectionListProjectLocations,
@@ -26,10 +20,7 @@ import { TSecretSyncForm } from "../schemas";
const formatOptionLabel = ({ displayName, locationId }: TGcpLocation) => ( const formatOptionLabel = ({ displayName, locationId }: TGcpLocation) => (
<div className="flex w-full flex-row items-center gap-1"> <div className="flex w-full flex-row items-center gap-1">
<span>{displayName}</span>{" "} <span>{displayName}</span> <Badge variant="info">{locationId}</Badge>
<Badge className="h-5 leading-5" variant="success">
{locationId}
</Badge>
</div> </div>
); );
@@ -2,7 +2,8 @@ import { components, OptionProps, SingleValue } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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"; import { AWS_REGIONS } from "@app/helpers/appConnections";
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => { 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}> <components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between"> <div className="flex flex-row items-center justify-between">
<p className="truncate">{children}</p> <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} {props.data.slug}
</Badge> </Badge>
{isSelected && ( {isSelected && (
@@ -1,10 +1,10 @@
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { faEye } from "@fortawesome/free-solid-svg-icons"; import { EyeIcon } from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { GenericFieldLabel } from "@app/components/secret-syncs"; import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; 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 { AWS_REGIONS } from "@app/helpers/appConnections";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
@@ -41,11 +41,9 @@ export const AwsParameterStoreSyncOptionsReviewFields = () => {
} }
> >
<div className="w-min"> <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"> <Badge variant="neutral">
<FontAwesomeIcon icon={faEye} /> <EyeIcon />
<span> {tags.length} Tag{tags.length > 1 ? "s" : ""}
{tags.length} Tag{tags.length > 1 ? "s" : ""}
</span>
</Badge> </Badge>
</div> </div>
</Tooltip> </Tooltip>
@@ -1,10 +1,10 @@
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { faEye } from "@fortawesome/free-solid-svg-icons"; import { EyeIcon } from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { GenericFieldLabel } from "@app/components/secret-syncs"; import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; 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 { AWS_REGIONS } from "@app/helpers/appConnections";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync"; import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
@@ -26,7 +26,7 @@ export const AwsSecretsManagerSyncReviewFields = () => {
<> <>
<GenericFieldLabel label="Region"> <GenericFieldLabel label="Region">
{awsRegion?.name} {awsRegion?.name}
<Badge className="ml-1" variant="success"> <Badge className="ml-1" variant="info">
{awsRegion?.slug}{" "} {awsRegion?.slug}{" "}
</Badge> </Badge>
</GenericFieldLabel> </GenericFieldLabel>
@@ -73,11 +73,9 @@ export const AwsSecretsManagerSyncOptionsReviewFields = () => {
} }
> >
<div className="w-min"> <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"> <Badge variant="neutral">
<FontAwesomeIcon icon={faEye} /> <EyeIcon />
<span> {tags.length} Tag{tags.length > 1 ? "s" : ""}
{tags.length} Tag{tags.length > 1 ? "s" : ""}
</span>
</Badge> </Badge>
</div> </div>
</Tooltip> </Tooltip>
@@ -2,7 +2,7 @@ import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/secret-syncs"; import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; 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 { SecretSync } from "@app/hooks/api/secretSyncs";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync"; 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 { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; 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 { useOrganization, useProject } from "@app/context";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync, useDuplicateDestinationCheck } from "@app/hooks/api/secretSyncs"; import { SecretSync, useDuplicateDestinationCheck } from "@app/hooks/api/secretSyncs";
@@ -257,7 +257,7 @@ export const SecretSyncReviewFields = () => {
</div> </div>
<div className="flex flex-wrap gap-x-8 gap-y-2"> <div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Auto-Sync"> <GenericFieldLabel label="Auto-Sync">
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}> <Badge variant={isAutoSyncEnabled ? "success" : "neutral"}>
{isAutoSyncEnabled ? "Enabled" : "Disabled"} {isAutoSyncEnabled ? "Enabled" : "Disabled"}
</Badge> </Badge>
</GenericFieldLabel> </GenericFieldLabel>
@@ -268,7 +268,7 @@ export const SecretSyncReviewFields = () => {
{AdditionalSyncOptionsFieldsComponent} {AdditionalSyncOptionsFieldsComponent}
{disableSecretDeletion && ( {disableSecretDeletion && (
<GenericFieldLabel label="Secret Deletion"> <GenericFieldLabel label="Secret Deletion">
<Badge variant="primary">Disabled</Badge> <Badge variant="warning">Disabled</Badge>
</GenericFieldLabel> </GenericFieldLabel>
)} )}
</div> </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 { createElement } from "react";
import { faCube, faCubes, faGlobe, faServer } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ReactNode } from "@tanstack/react-router"; import { ReactNode } from "@tanstack/react-router";
import { LucideIcon } from "lucide-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Badge } from "@app/components/v2"; import {
import { BadgeProps } from "@app/components/v2/Badge/Badge"; Badge,
InstanceIcon,
OrgIcon,
ProjectIcon,
SubOrgIcon,
TBadgeProps
} from "@app/components/v3";
import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectType } from "@app/hooks/api/projects/types";
type Props = { type Props = {
@@ -16,19 +21,19 @@ type Props = {
scope: "org" | "namespace" | "instance" | ProjectType | null; scope: "org" | "namespace" | "instance" | ProjectType | null;
}; };
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: IconDefinition }> = { const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: LucideIcon }> = {
org: { label: "Organization", icon: faGlobe }, org: { label: "Organization", icon: OrgIcon },
[ProjectType.SecretManager]: { label: "Project", icon: faCube }, [ProjectType.SecretManager]: { label: "Project", icon: ProjectIcon },
[ProjectType.CertificateManager]: { label: "Project", icon: faCube }, [ProjectType.CertificateManager]: { label: "Project", icon: ProjectIcon },
[ProjectType.SSH]: { label: "Project", icon: faCube }, [ProjectType.SSH]: { label: "Project", icon: ProjectIcon },
[ProjectType.KMS]: { label: "Project", icon: faCube }, [ProjectType.KMS]: { label: "Project", icon: ProjectIcon },
[ProjectType.PAM]: { label: "Project", icon: faCube }, [ProjectType.PAM]: { label: "Project", icon: ProjectIcon },
[ProjectType.SecretScanning]: { label: "Project", icon: faCube }, [ProjectType.SecretScanning]: { label: "Project", icon: ProjectIcon },
namespace: { label: "Sub-Organization", icon: faCubes }, namespace: { label: "Sub-Organization", icon: SubOrgIcon },
instance: { label: "Server", icon: faServer } 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", org: "org",
[ProjectType.SecretManager]: "project", [ProjectType.SecretManager]: "project",
[ProjectType.CertificateManager]: "project", [ProjectType.CertificateManager]: "project",
@@ -36,8 +41,8 @@ const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, BadgeProps["variant"]>
[ProjectType.KMS]: "project", [ProjectType.KMS]: "project",
[ProjectType.PAM]: "project", [ProjectType.PAM]: "project",
[ProjectType.SecretScanning]: "project", [ProjectType.SecretScanning]: "project",
namespace: "namespace", namespace: "sub-org",
instance: "instance" instance: "neutral"
}; };
export const PageHeader = ({ title, description, children, className, scope }: Props) => ( 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> <h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
{scope && ( {scope && (
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5"> <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} {SCOPE_NAME[scope].label}
</Badge> </Badge>
)} )}
@@ -3,6 +3,7 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faCaretDown, faCaretUp, faCheck } from "@fortawesome/free-solid-svg-icons"; import { faCaretDown, faCaretUp, faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as SelectPrimitive from "@radix-ui/react-select"; import * as SelectPrimitive from "@radix-ui/react-select";
import { LucideIcon } from "lucide-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Spinner } from "../Spinner"; import { Spinner } from "../Spinner";
@@ -19,6 +20,7 @@ type Props = {
icon?: IconProp; icon?: IconProp;
isMulti?: boolean; isMulti?: boolean;
iconClassName?: string; iconClassName?: string;
LucideIcon?: LucideIcon;
dropdownContainerStyle?: React.CSSProperties; dropdownContainerStyle?: React.CSSProperties;
side?: SelectPrimitive.SelectContentProps["side"]; side?: SelectPrimitive.SelectContentProps["side"];
}; };
@@ -39,6 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
iconClassName, iconClassName,
dropdownContainerStyle, dropdownContainerStyle,
side, side,
LucideIcon: Lucide,
...props ...props
}, },
ref 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"> <div className="flex items-center space-x-2 overflow-hidden text-ellipsis whitespace-nowrap">
{props.icon && <FontAwesomeIcon icon={props.icon} className={iconClassName} />} {props.icon && <FontAwesomeIcon icon={props.icon} className={iconClassName} />}
{Lucide && <Lucide className={twMerge("size-3.5", iconClassName)} />}
<div className="flex-1 truncate"> <div className="flex-1 truncate">
<SelectPrimitive.Value placeholder={placeholder} /> <SelectPrimitive.Value placeholder={placeholder} />
</div> </div>
-1
View File
@@ -2,7 +2,6 @@
export * from "./AccessRestrictedBanner"; export * from "./AccessRestrictedBanner";
export * from "./Accordion"; export * from "./Accordion";
export * from "./Alert"; export * from "./Alert";
export * from "./Badge";
export * from "./Breadcrumb"; export * from "./Breadcrumb";
export * from "./Button"; export * from "./Button";
export * from "./Card"; export * from "./Card";
@@ -1,9 +1,9 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { import {
AlertTriangleIcon,
AsteriskIcon,
BanIcon, BanIcon,
BoxesIcon,
BoxIcon,
CheckIcon, CheckIcon,
ChevronsUpDownIcon, ChevronsUpDownIcon,
CircleXIcon, CircleXIcon,
@@ -11,9 +11,11 @@ import {
GlobeIcon, GlobeIcon,
InfoIcon, InfoIcon,
RadarIcon, RadarIcon,
TriangleAlertIcon TriangleAlertIcon,
UserIcon
} from "lucide-react"; } from "lucide-react";
import { OrgIcon, ProjectIcon, SubOrgIcon } from "../../platform";
import { Badge } from "./Badge"; import { Badge } from "./Badge";
/** /**
@@ -69,7 +71,29 @@ export const Neutral: Story = {
parameters: { parameters: {
docs: { docs: {
description: { 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: { parameters: {
docs: { docs: {
description: { description: {
story: story: "Use this variant when indicating informational states."
"Use this variant when indicating informational states or linking to external references."
} }
} }
} }
@@ -162,7 +185,7 @@ export const Organization: Story = {
variant: "org", variant: "org",
children: ( children: (
<> <>
<GlobeIcon /> <OrgIcon />
Organization Organization
</> </>
) )
@@ -182,7 +205,7 @@ export const SubOrganization: Story = {
variant: "sub-org", variant: "sub-org",
children: ( children: (
<> <>
<BoxesIcon /> <SubOrgIcon />
Sub-Organization Sub-Organization
</> </>
) )
@@ -202,7 +225,7 @@ export const Project: Story = {
variant: "project", variant: "project",
children: ( children: (
<> <>
<BoxIcon /> <ProjectIcon />
Project Project
</> </>
) )
@@ -283,10 +306,11 @@ export const AsButton: Story = {
export const IsTruncatable: Story = { export const IsTruncatable: Story = {
name: "Example: isTruncatable", name: "Example: isTruncatable",
args: { args: {
variant: "org",
isTruncatable: true, isTruncatable: true,
children: ( children: (
<> <>
<GlobeIcon /> <OrgIcon />
<span>Infisical Infrastructure</span> <span>Infisical Infrastructure</span>
</> </>
) )
@@ -295,7 +319,7 @@ export const IsTruncatable: Story = {
docs: { docs: {
description: { description: {
story: 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> </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( const badgeVariants = cva(
[ [
"select-none items-center rounded-sm px-1.5 py-0.5 text-xs", "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", "gap-x-1 [a&,button&]:cursor-pointer inline-flex font-normal",
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&>svg]:size-3", "[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&_svg:not([class*='size-'])]:size-3",
"transition duration-200 ease-in-out" "transition duration-200 ease-in-out"
], ],
{ {
variants: { variants: {
isTruncatable: { isTruncatable: {
true: "[&>span,&>p]:truncate min-w-0", 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: { variant: {
neutral: "bg-neutral/30 text-neutral [a&,button&]:hover:bg-neutral/40", ghost: "text-mineshaft-200 gap-x-2",
success: "bg-success/30 text-success [a&,button&]:hover:bg-success/40", neutral: "bg-neutral/25 text-neutral [a&,button&]:hover:bg-neutral/35",
info: "bg-info/30 text-info [a&,button&]:hover:bg-info/40", success: "bg-success/25 text-success [a&,button&]:hover:bg-success/35",
warning: "bg-warning/30 text-warning [a&,button&]:hover:bg-warning/40", info: "bg-info/25 text-info [a&,button&]:hover:bg-info/35",
danger: "bg-danger/30 text-danger [a&,button&]:hover:bg-danger/40", warning: "bg-warning/25 text-warning [a&,button&]:hover:bg-warning/35",
project: "bg-project/30 text-project [a&,button&]:hover:bg-project/40", danger: "bg-danger/25 text-danger [a&,button&]:hover:bg-danger/35",
org: "bg-org/30 text-org [a&,button&]:hover:bg-org/40", project: "bg-project/25 text-project [a&,button&]:hover:bg-project/35",
"sub-org": "bg-sub-org/30 text-sub-org [a&,button&]:hover:bg-sub-org/40" 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: { defaultVariants: {
variant: "success" variant: "neutral"
} }
} }
); );
@@ -37,16 +44,28 @@ const badgeVariants = cva(
type TBadgeProps = VariantProps<typeof badgeVariants> & type TBadgeProps = VariantProps<typeof badgeVariants> &
React.ComponentProps<"span"> & { React.ComponentProps<"span"> & {
asChild?: boolean; asChild?: boolean;
variant: NonNullable<VariantProps<typeof badgeVariants>["variant"]>; // TODO: REMOVE
}; };
const Badge = forwardRef<HTMLSpanElement, TBadgeProps>( 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"; const Comp = asChild ? Slot : "span";
return ( return (
<Comp <Comp
ref={ref} ref={ref}
data-slot="badge" data-slot="badge"
className={cn(badgeVariants({ variant, isTruncatable }), className)} className={cn(badgeVariants({ variant, isTruncatable, isFullWidth, isSquare }), className)}
{...props} {...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 { LucideIcon, UserCheckIcon, UserPenIcon } from "lucide-react";
import { faArrowRightToBracket, faEdit } from "@fortawesome/free-solid-svg-icons";
import { TBadgeProps } from "@app/components/v3";
import { PolicyType } from "@app/hooks/api/policies/enums"; import { PolicyType } from "@app/hooks/api/policies/enums";
export const policyDetails: Record< export const policyDetails: Record<
PolicyType, PolicyType,
{ name: string; className: string; icon: IconDefinition } { name: string; variant: TBadgeProps["variant"]; Icon: LucideIcon }
> = { > = {
[PolicyType.AccessPolicy]: { [PolicyType.AccessPolicy]: {
className: "bg-green/20 text-green", variant: "ghost",
name: "Access Policy", name: "Access Policy",
icon: faArrowRightToBracket Icon: UserCheckIcon
}, },
[PolicyType.ChangePolicy]: { [PolicyType.ChangePolicy]: {
className: "bg-yellow/20 text-yellow", variant: "ghost",
name: "Change Policy", name: "Change Policy",
icon: faEdit Icon: UserPenIcon
} }
}; };
+24 -12
View File
@@ -1,10 +1,6 @@
import { import { AlertTriangleIcon, BanIcon, CheckIcon, LucideIcon, SearchSlashIcon } from "lucide-react";
faBan,
faCheck,
faMagnifyingGlassMinus,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { TBadgeProps } from "@app/components/v3";
import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { import {
SecretScanningDataSource, SecretScanningDataSource,
@@ -74,12 +70,28 @@ export const RESOURCE_DESCRIPTION_HELPER: Record<
} }
}; };
export const SECRET_SCANNING_FINDING_STATUS_ICON_MAP = { export const SECRET_SCANNING_FINDING_STATUS_MAP: Record<
[SecretScanningFindingStatus.Resolved]: { icon: faCheck, className: "text-green" }, SecretScanningFindingStatus,
[SecretScanningFindingStatus.Unresolved]: { icon: faWarning, className: "text-yellow" }, { Icon: LucideIcon; variant: TBadgeProps["variant"]; className: string }
[SecretScanningFindingStatus.Ignore]: { icon: faBan, className: "text-mineshaft-400" }, > = {
[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]: { [SecretScanningFindingStatus.FalsePositive]: {
icon: faMagnifyingGlassMinus, Icon: SearchSlashIcon,
className: "text-mineshaft-400" variant: "neutral",
className: "text-neutral"
} }
}; };
@@ -10,5 +10,6 @@ export type TGitLabGroup = {
export enum GitLabAccessTokenType { export enum GitLabAccessTokenType {
Personal = "personal", 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: case CaStatus.DISABLED:
return "danger"; return "danger";
default: default:
return "primary"; return "warning";
} }
}; };
+34 -1
View File
@@ -5,7 +5,8 @@ import { apiRequest } from "@app/config/request";
import { import {
ExternalMigrationProviders, ExternalMigrationProviders,
TVaultExternalMigrationConfig, TVaultExternalMigrationConfig,
VaultKubernetesAuthRole VaultKubernetesAuthRole,
VaultKubernetesRole
} from "./types"; } from "./types";
export const externalMigrationQueryKeys = { export const externalMigrationQueryKeys = {
@@ -31,6 +32,11 @@ export const externalMigrationQueryKeys = {
"vault-kubernetes-auth-roles", "vault-kubernetes-auth-roles",
namespace, namespace,
mountPath mountPath
],
vaultKubernetesRoles: (namespace?: string, mountPath?: string) => [
"vault-kubernetes-roles",
namespace,
mountPath
] ]
}; };
@@ -172,3 +178,30 @@ export const useGetVaultKubernetesAuthRoles = (
enabled: enabled && !!namespace && !!mountPath 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; 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: case PkiSubscriberStatus.DISABLED:
return "danger"; return "danger";
default: default:
return "primary"; return "warning";
} }
}; };
@@ -7,15 +7,12 @@ import {
faCaretDown, faCaretDown,
faCheck, faCheck,
faChevronRight, faChevronRight,
faCubes,
faEnvelope, faEnvelope,
faExclamationTriangle, faExclamationTriangle,
faGlobe,
faInfinity, faInfinity,
faInfo, faInfo,
faInfoCircle, faInfoCircle,
faPlus, faPlus,
faServer,
faSignOut, faSignOut,
faToolbox, faToolbox,
faUser, faUser,
@@ -30,7 +27,6 @@ import { Mfa } from "@app/components/auth/Mfa";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { import {
Badge,
BreadcrumbContainer, BreadcrumbContainer,
Button, Button,
DropdownMenu, DropdownMenu,
@@ -46,6 +42,7 @@ import {
TBreadcrumbFormat, TBreadcrumbFormat,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
import { envConfig } from "@app/config/env"; import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context"; import { useOrganization, useSubscription, useUser } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform"; import { isInfisicalCloud } from "@app/helpers/platform";
@@ -271,9 +268,7 @@ export const Navbar = () => {
to="/admin" to="/admin"
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary" className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
> >
<div> <InstanceIcon className="size-3.5 text-xs text-bunker-300" />
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
</div>
<div className="whitespace-nowrap">Server Console</div> <div className="whitespace-nowrap">Server Console</div>
</Link> </Link>
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p> <p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
@@ -288,24 +283,31 @@ export const Navbar = () => {
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}> <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"> <div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
<Badge <Badge
onClick={async () => { asChild
navigate({
to: "/organization/projects",
search: (search) => ({ ...search, subOrganization: undefined })
});
if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
}
}}
variant="org" variant="org"
isTruncatable
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
className={twMerge( className={twMerge(
"max-w-full min-w-0 cursor-pointer text-sm", "gap-x-1.5 text-sm",
(!isOrgScope || isSubOrganization) && (!isOrgScope || isSubOrganization) &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline" "bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-org"
)} )}
> >
<FontAwesomeIcon icon={faGlobe} /> <button
<p className="truncate">{currentOrg?.name}</p> type="button"
onClick={async () => {
navigate({
to: "/organization/projects",
search: (search) => ({ ...search, subOrganization: undefined })
});
if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
}
}}
>
<OrgIcon className="size-[12px]" />
<span>{currentOrg?.name}</span>
</button>
</Badge> </Badge>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!"> <div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
{getPlan(subscription)} {getPlan(subscription)}
@@ -443,19 +445,22 @@ export const Navbar = () => {
<> <>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p> <p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<Link className="overflow-hidden" to="/organization/projects"> <Badge
<Badge asChild
variant="namespace" isTruncatable
className={twMerge( variant="sub-org"
"flex max-w-full min-w-0 cursor-pointer text-sm", // TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
!isOrgScope && className={twMerge(
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline" "gap-x-1.5 text-sm",
)} !isOrgScope &&
> "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">
</Link> <SubOrgIcon className="size-[12px]" />
<span>{currentOrg.subOrganization.name}</span>
</Link>
</Badge>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div> <div>
<IconButton <IconButton
@@ -549,10 +554,10 @@ export const Navbar = () => {
)} )}
{user.superAdmin && !location.pathname.startsWith("/admin") && ( {user.superAdmin && !location.pathname.startsWith("/admin") && (
<Link <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" to="/admin"
> >
<FontAwesomeIcon icon={faServer} className="mr-2" /> <InstanceIcon className="mr-2 inline-block size-3.5" />
Server Console Server Console
</Link> </Link>
)} )}
+6 -2
View File
@@ -18,7 +18,10 @@ export const PamLayout = () => {
useEffect(() => { useEffect(() => {
if (subscription && !subscription.pam) { if (subscription && !subscription.pam) {
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan", {
description: "You can use PAM if you switch to Infisical's Enterprise plan.",
isEnterpriseFeature: true
});
} }
}, [subscription]); }, [subscription]);
@@ -111,7 +114,8 @@ export const PamLayout = () => {
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("upgradePlan", 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 { import {
faCaretDown, faCaretDown,
faCheck, faCheck,
faCube,
faMagnifyingGlass, faMagnifyingGlass,
faPlus, faPlus,
faStar as faSolidStar faStar as faSolidStar
@@ -16,7 +15,6 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { NewProjectModal } from "@app/components/projects"; import { NewProjectModal } from "@app/components/projects";
import { import {
Badge,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
@@ -25,6 +23,7 @@ import {
Input, Input,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge, ProjectIcon } from "@app/components/v3";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
@@ -120,11 +119,9 @@ export const ProjectSelect = () => {
<p className="inline-block truncate text-mineshaft-200 group-hover:underline"> <p className="inline-block truncate text-mineshaft-200 group-hover:underline">
{currentWorkspace?.name} {currentWorkspace?.name}
</p> </p>
<Badge variant="project" className="cursor-pointer"> <Badge variant="project">
<FontAwesomeIcon icon={faCube} /> <ProjectIcon />
<span> {currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
</span>
</Badge> </Badge>
</Link> </Link>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -4,7 +4,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet, useLocation } from "@tanstack/react-router"; import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion"; 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 { useProject, useProjectPermission } from "@app/context";
import { import {
useGetAccessRequestsCount, useGetAccessRequestsCount,
@@ -88,7 +89,7 @@ export const SecretManagerLayout = () => {
{Boolean( {Boolean(
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
) && ( ) && (
<Badge variant="primary" className="ml-1.5"> <Badge variant="warning" isSquare className="ml-1.5">
{pendingRequestsCount} {pendingRequestsCount}
</Badge> </Badge>
)} )}
@@ -1,7 +1,8 @@
import { Link, Outlet, useLocation } from "@tanstack/react-router"; import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion"; 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 { import {
ProjectPermissionSub, ProjectPermissionSub,
useProject, useProject,
@@ -66,7 +67,7 @@ export const SecretScanningLayout = () => {
<Tab value={isActive ? "selected" : ""}> <Tab value={isActive ? "selected" : ""}>
Findings Findings
{Boolean(unresolvedFindings) && ( {Boolean(unresolvedFindings) && (
<Badge variant="primary" className="ml-2 h-min"> <Badge isSquare variant="warning" className="ml-2">
{unresolvedFindings} {unresolvedFindings}
</Badge> </Badge>
)} )}
@@ -7,16 +7,15 @@ import {
faTrash, faTrash,
faUsers, faUsers,
faUserXmark, faUserXmark,
faWarning,
faXmark faXmark
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AlertTriangleIcon } from "lucide-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Badge,
Button, Button,
Checkbox, Checkbox,
DeleteActionModal, DeleteActionModal,
@@ -38,6 +37,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useSubscription, useUser } from "@app/context"; import { useSubscription, useUser } from "@app/context";
import { import {
getUserTablePreference, getUserTablePreference,
@@ -451,7 +451,7 @@ export const ServerAdminsTable = () => {
const email = user.email ?? user.username; const email = user.email ?? user.username;
return ( return (
<li key={user.id}> <li key={user.id}>
<div className="flex items-center"> <div className="flex items-center gap-x-1">
<p> <p>
{user.firstName || user.lastName ? ( {user.firstName || user.lastName ? (
<> <>
@@ -464,15 +464,10 @@ export const ServerAdminsTable = () => {
</p> </p>
{userId === user.id && ( {userId === user.id && (
<Tooltip content="Are you sure you want to remove yourself from this instance?"> <Tooltip content="Are you sure you want to remove yourself from this instance?">
<div className="inline-block"> <Badge variant="danger">
<Badge <AlertTriangleIcon />
variant="primary" Deleting Yourself
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap" </Badge>
>
<FontAwesomeIcon icon={faWarning} />
<span>Deleting Yourself</span>
</Badge>
</div>
</Tooltip> </Tooltip>
)} )}
</div> </div>
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { faRotate } from "@fortawesome/free-solid-svg-icons"; import { RefreshCwIcon } from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; 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 { useUser } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useInvalidateCache } from "@app/hooks/api"; import { useInvalidateCache } from "@app/hooks/api";
@@ -62,11 +62,8 @@ export const CachingPageForm = () => {
<div className="mb-2 flex items-center gap-3"> <div className="mb-2 flex items-center gap-3">
<span className="text-xl font-medium text-mineshaft-100">Secrets Cache</span> <span className="text-xl font-medium text-mineshaft-100">Secrets Cache</span>
{isInvalidating && ( {isInvalidating && (
<Badge <Badge variant="danger">
variant="danger" <RefreshCwIcon className="animate-spin" />
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
Invalidating Cache Invalidating Cache
</Badge> </Badge>
)} )}
@@ -1,13 +1,13 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { Controller, useForm } from "react-hook-form"; 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 { zodResolver } from "@hookform/resolvers/zod";
import { InfoIcon } from "lucide-react";
import { z } from "zod"; import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; 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 { useServerConfig, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import {
@@ -54,6 +54,7 @@ export const EncryptionPageForm = () => {
if (!subscription.hsm) { if (!subscription.hsm) {
handlePopUpOpen("upgradePlan", { handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true,
description: "Hardware Security Module's (HSM's), are only available on Enterprise plans." description: "Hardware Security Module's (HSM's), are only available on Enterprise plans."
}); });
return; return;
@@ -132,12 +133,10 @@ export const EncryptionPageForm = () => {
{config.fipsEnabled && ( {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."> <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 variant="info">
<Badge className="flex items-center gap-2" variant="primary"> FIPS Mode: Enabled
FIPS Mode: Enabled <InfoIcon />
<FontAwesomeIcon icon={faInfoCircle} /> </Badge>
</Badge>
</div>
</Tooltip> </Tooltip>
)} )}
</div> </div>
@@ -146,6 +145,7 @@ export const EncryptionPageForm = () => {
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description} 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 { useCallback, useEffect, useMemo, useState } from "react";
import { Control, Controller, useForm, useWatch } from "react-hook-form"; import { Control, Controller, useForm, useWatch } from "react-hook-form";
import { import {
faArrowUpRightFromSquare,
faBookOpen,
faChevronRight, faChevronRight,
faExclamationTriangle, faExclamationTriangle,
faMagnifyingGlass faMagnifyingGlass
@@ -14,6 +12,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2"; import { Button, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText"; import { HighlightText } from "@app/components/v2/HighlightText";
import { DocumentationLinkBadge } from "@app/components/v3";
import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api"; import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api";
type TForm = Record<string, string>; type TForm = Record<string, string>;
@@ -213,22 +212,9 @@ export const EnvironmentPageForm = () => {
> >
<div className="flex w-full flex-row items-center justify-between"> <div className="flex w-full flex-row items-center justify-between">
<div> <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> <p className="text-xl font-medium text-mineshaft-100">Overrides</p>
<a <DocumentationLinkBadge href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides" />
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>
</div> </div>
<p className="text-sm text-bunker-300"> <p className="text-sm text-bunker-300">
Override specific environment variables. After saving, it may take up to 5 minutes for Override specific environment variables. After saving, it may take up to 5 minutes for
@@ -7,10 +7,10 @@ import {
faXmark faXmark
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ServerCogIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Badge,
DeleteActionModal, DeleteActionModal,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -29,6 +29,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { import {
getUserTablePreference, getUserTablePreference,
PreferenceKey, PreferenceKey,
@@ -110,7 +111,8 @@ const IdentityPanelTable = ({
<Td> <Td>
{name} {name}
{isInstanceAdmin && ( {isInstanceAdmin && (
<Badge variant="primary" className="ml-2"> <Badge variant="info" className="ml-2">
<ServerCogIcon />
Server Admin Server Admin
</Badge> </Badge>
)} )}
@@ -3,7 +3,6 @@ import {
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faBuilding, faBuilding,
faCircleQuestion,
faEllipsisV, faEllipsisV,
faEnvelope, faEnvelope,
faEye, faEye,
@@ -19,11 +18,11 @@ import {
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { CircleQuestionMarkIcon } from "lucide-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Badge,
Button, Button,
DeleteActionModal, DeleteActionModal,
DropdownMenu, DropdownMenu,
@@ -46,6 +45,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useUser } from "@app/context"; import { useUser } from "@app/context";
import { OrgMembershipRole } from "@app/helpers/roles"; import { OrgMembershipRole } from "@app/helpers/roles";
import { import {
@@ -295,15 +295,17 @@ const ViewMembersModalContent = ({
)} )}
</div> </div>
</Td> </Td>
<Td className="max-w-0"> <Td>
<Badge className="flex w-fit max-w-full items-center gap-x-1 bg-mineshaft-400/50 whitespace-nowrap text-bunker-200"> <div className="flex max-w-32">
<p className="truncate capitalize">{member.role.replace("-", " ")}</p> <Tooltip
{Boolean(member.roleId) && ( content={member.roleId ? "This member has a custom role assigned." : ""}
<Tooltip content="This member has a custom role assigned."> >
<FontAwesomeIcon icon={faCircleQuestion} className="w-3" /> <Badge isTruncatable variant="neutral">
</Tooltip> <span className="capitalize">{member.role.replace("-", " ")}</span>
)} {Boolean(member.roleId) && <CircleQuestionMarkIcon />}
</Badge> </Badge>
</Tooltip>
</div>
</Td> </Td>
<Td> <Td>
<div className="flex justify-end"> <div className="flex justify-end">
@@ -9,16 +9,15 @@ import {
faUsers, faUsers,
faUserShield, faUserShield,
faUserXmark, faUserXmark,
faWarning,
faXmark faXmark
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AlertTriangleIcon, UserCogIcon } from "lucide-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Badge,
Button, Button,
Checkbox, Checkbox,
DeleteActionModal, DeleteActionModal,
@@ -41,6 +40,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useSubscription, useUser } from "@app/context"; import { useSubscription, useUser } from "@app/context";
import { import {
getUserTablePreference, getUserTablePreference,
@@ -218,7 +218,8 @@ const UserPanelTable = ({
{name ?? <span className="text-mineshaft-400">Not Set</span>} {name ?? <span className="text-mineshaft-400">Not Set</span>}
</p> </p>
{superAdmin && ( {superAdmin && (
<Badge variant="primary" className="ml-2 whitespace-nowrap"> <Badge variant="info" className="ml-2">
<UserCogIcon />
Server Admin Server Admin
</Badge> </Badge>
)} )}
@@ -548,7 +549,7 @@ export const UserIdentitiesTable = () => {
const email = user.email ?? user.username; const email = user.email ?? user.username;
return ( return (
<li key={user.id}> <li key={user.id}>
<div className="flex items-center"> <div className="flex items-center gap-x-1">
<p> <p>
{user.firstName || user.lastName ? ( {user.firstName || user.lastName ? (
<> <>
@@ -561,15 +562,10 @@ export const UserIdentitiesTable = () => {
</p> </p>
{userId === user.id && ( {userId === user.id && (
<Tooltip content="Are you sure you want to remove yourself from this instance?"> <Tooltip content="Are you sure you want to remove yourself from this instance?">
<div className="inline-block"> <Badge variant="danger">
<Badge <AlertTriangleIcon />
variant="primary" Deleting Yourself
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap" </Badge>
>
<FontAwesomeIcon icon={faWarning} />
<span>Deleting Yourself</span>
</Badge>
</div>
</Tooltip> </Tooltip>
)} )}
</div> </div>
@@ -7,7 +7,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Badge,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
@@ -22,6 +21,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useGetCaCerts } from "@app/hooks/api"; import { useGetCaCerts } from "@app/hooks/api";
@@ -60,7 +60,7 @@ export const CaCertificatesTable = ({ caId }: Props) => {
<div className="flex items-center"> <div className="flex items-center">
CA Certificate {caCert.version} CA Certificate {caCert.version}
{isLastItem && ( {isLastItem && (
<Badge variant="success" className="ml-4"> <Badge variant="info" className="ml-4">
Current Current
</Badge> </Badge>
)} )}
@@ -6,7 +6,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Badge,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
@@ -22,6 +21,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api"; import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
import { import {
@@ -10,7 +10,6 @@ import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Badge,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
@@ -26,6 +25,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api"; import { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants"; import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
@@ -104,6 +104,7 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("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." text="Managing template enrollment options for EST is only available on Infisical's Enterprise plan."
/> />
</div> </div>
@@ -36,6 +36,7 @@ type Props = {
data?: { data?: {
id?: string; id?: string;
name?: string; name?: string;
isEnterpriseFeature?: boolean;
} }
) => void; ) => void;
}; };
@@ -90,7 +91,9 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
if (!subscription?.pkiEst) { if (!subscription?.pkiEst) {
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
});
return; return;
} }
@@ -5,18 +5,17 @@ import {
faEllipsis, faEllipsis,
faEye, faEye,
faFileExport, faFileExport,
faQuestionCircle,
faRedo, faRedo,
faTrash faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
import { CircleQuestionMarkIcon } from "lucide-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Badge,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
@@ -33,6 +32,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { import {
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionSub, ProjectPermissionSub,
@@ -58,7 +58,7 @@ const isExpiringWithinOneDay = (notAfter: string): boolean => {
const getAutoRenewalInfo = (certificate: TCertificate) => { const getAutoRenewalInfo = (certificate: TCertificate) => {
if (certificate.renewedByCertificateId) { if (certificate.renewedByCertificateId) {
return { text: "Renewed", variant: "instance" as const }; return { text: "Renewed", variant: "neutral" as const };
} }
const isRevoked = certificate.status === CertStatus.REVOKED; const isRevoked = certificate.status === CertStatus.REVOKED;
@@ -69,7 +69,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
if (isRevoked) { if (isRevoked) {
return { return {
text: "Not Available", text: "Not Available",
variant: "instance" as const, variant: "neutral" as const,
tooltip: "Renewal is not available for revoked certificates" tooltip: "Renewal is not available for revoked certificates"
}; };
} }
@@ -77,7 +77,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
if (isExpired) { if (isExpired) {
return { return {
text: "Not Available", text: "Not Available",
variant: "instance" as const, variant: "neutral" as const,
tooltip: "Renewal is not available for expired certificates" tooltip: "Renewal is not available for expired certificates"
}; };
} }
@@ -85,7 +85,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
if (hasNoProfile) { if (hasNoProfile) {
return { return {
text: "Not Available", text: "Not Available",
variant: "instance" as const, variant: "neutral" as const,
tooltip: "Renewal requires a certificate profile" tooltip: "Renewal requires a certificate profile"
}; };
} }
@@ -93,7 +93,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
if (certificate.hasPrivateKey === false) { if (certificate.hasPrivateKey === false) {
return { return {
text: "Not Available", text: "Not Available",
variant: "instance" as const, variant: "neutral" as const,
tooltip: "Renewal is not available for certificates with externally generated private keys" tooltip: "Renewal is not available for certificates with externally generated private keys"
}; };
} }
@@ -101,7 +101,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
if (isExpiringWithinDay) { if (isExpiringWithinDay) {
return { return {
text: "Not Available", text: "Not Available",
variant: "instance" as const, variant: "neutral" as const,
tooltip: "Auto-renewal is not available for certificates expiring within 24 hours" tooltip: "Auto-renewal is not available for certificates expiring within 24 hours"
}; };
} }
@@ -115,7 +115,7 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
} }
if (!certificate.renewBeforeDays) { 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); const notAfterDate = new Date(certificate.notAfter);
@@ -133,11 +133,11 @@ const getAutoRenewalInfo = (certificate: TCertificate) => {
); );
if (daysUntilRenewal === 0) { if (daysUntilRenewal === 0) {
return { text: "Renews today", variant: "primary" as const }; return { text: "Renews today", variant: "warning" as const };
} }
if (daysUntilRenewal <= 7) { 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 }; return { text: `Renews in ${daysUntilRenewal}d`, variant: "success" as const };
@@ -279,16 +279,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
{autoRenewalInfo && {autoRenewalInfo &&
(autoRenewalInfo.tooltip ? ( (autoRenewalInfo.tooltip ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant={autoRenewalInfo.variant}> <Tooltip content={autoRenewalInfo.tooltip}>
{autoRenewalInfo.text} <Badge variant={autoRenewalInfo.variant}>
<Tooltip content={autoRenewalInfo.tooltip}> {autoRenewalInfo.text}
<FontAwesomeIcon <CircleQuestionMarkIcon />
icon={faQuestionCircle} </Badge>
className="ml-1 cursor-help text-red-400 hover:text-red-300" </Tooltip>
size="sm"
/>
</Tooltip>
</Badge>
</div> </div>
) : ( ) : (
<Badge variant={autoRenewalInfo.variant}>{autoRenewalInfo.text}</Badge> <Badge variant={autoRenewalInfo.variant}>{autoRenewalInfo.text}</Badge>
@@ -5,13 +5,13 @@ export const getCertValidUntilBadgeDetails = (notAfter: string) => {
const notAfterDate = new Date(notAfter).getTime(); const notAfterDate = new Date(notAfter).getTime();
const diffInMs = notAfterDate - currentDate; const diffInMs = notAfterDate - currentDate;
let variant: "success" | "primary" | "danger" = "success"; let variant: "success" | "warning" | "danger" = "success";
let label = "Healthy"; let label = "Healthy";
if (diffInMs > ms("60d")) { if (diffInMs > ms("60d")) {
variant = "success"; variant = "success";
} else if (diffInMs > ms("30d")) { } else if (diffInMs > ms("30d")) {
variant = "primary"; variant = "warning";
} else { } else {
variant = "danger"; variant = "danger";
} }

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