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

|
||||
|
||||
5. Select your Vault namespace, Kubernetes secrets engine mount, and role
|
||||
6. Click **"Load Configuration"**
|
||||
|
||||
The form will be automatically populated with the role's configuration including:
|
||||
|
||||
- Cluster URL and CA certificate
|
||||
- Credential type (Static or Dynamic)
|
||||
- Service account name or Kubernetes role settings
|
||||
- Allowed namespaces
|
||||
- Token TTL values
|
||||
- Token audiences
|
||||
|
||||
<Note>
|
||||
Sensitive values like cluster tokens cannot be retrieved from Vault and must
|
||||
be manually provided in the form after loading the configuration.
|
||||
</Note>
|
||||
|
||||
#### Import and Translate Access Control Policies
|
||||
|
||||
When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 424 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 408 KiB |
@@ -187,31 +187,92 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
|
||||
</Step>
|
||||
</Steps>
|
||||
</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>
|
||||
<Step title="Navigate to App Connections">
|
||||
Navigate to the **App Connections** page in the desired project.
|
||||

|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **GitLab Connection** option from the connection options modal.
|
||||

|
||||
</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.
|
||||
<Steps>
|
||||
<Step title="Navigate to Group Settings">
|
||||
Go to your GitLab group and navigate to Settings > Access Tokens. Click **Add new token** to create a new group access token.
|
||||

|
||||
</Step>
|
||||
<Step title="Configure Token">
|
||||
Fill in the token details:
|
||||
- **Token name**: A descriptive name for the token
|
||||
- **Expiration date**: Set an appropriate expiration date
|
||||
- **Select role and scopes**: Depending on your use case, add the required role and one or more of the following scopes:
|
||||
|
||||

|
||||
<Tabs>
|
||||
<Tab title="Secret Sync">
|
||||
For Secret Syncs, the required role depends on your sync destination:
|
||||
- **Project variables**: Requires **Maintainer** role or higher
|
||||
- **Group variables**: Requires **Owner** role
|
||||
|
||||
Click **Connect** to establish the connection.
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **GitLab Connection** is now available for use.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
Your token will require the `api` scope.
|
||||
|
||||

|
||||
|
||||
Click **Create group access token** to create the token.
|
||||
|
||||
<Note>
|
||||
Use the **Owner** role if you need to sync to group-level variables. The **Maintainer** role is sufficient only for project-level variables.
|
||||
</Note>
|
||||
</Tab>
|
||||
<Tab title="Secret Scanning">
|
||||
To set up Secret Scanning, the required permissions depend on the data source level:
|
||||
- **Project-level data source:** Requires **Maintainer** role or higher
|
||||
- **Group-level data source:** Requires **Owner** role
|
||||
|
||||
Your token will require the `api` scope.
|
||||
|
||||

|
||||
|
||||
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.
|
||||

|
||||
<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.
|
||||

|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **GitLab Connection** option from the connection options modal.
|
||||

|
||||
</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.
|
||||
|
||||

|
||||
|
||||
Click **Connect** to establish the connection.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Connection Created">
|
||||
Your **GitLab Connection** is now available for use.
|
||||

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