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

|
||||||
|
|
||||||
|
5. Select your Vault namespace, Kubernetes secrets engine mount, and role
|
||||||
|
6. Click **"Load Configuration"**
|
||||||
|
|
||||||
|
The form will be automatically populated with the role's configuration including:
|
||||||
|
|
||||||
|
- Cluster URL and CA certificate
|
||||||
|
- Credential type (Static or Dynamic)
|
||||||
|
- Service account name or Kubernetes role settings
|
||||||
|
- Allowed namespaces
|
||||||
|
- Token TTL values
|
||||||
|
- Token audiences
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Sensitive values like cluster tokens cannot be retrieved from Vault and must
|
||||||
|
be manually provided in the form after loading the configuration.
|
||||||
|
</Note>
|
||||||
|
|
||||||
#### Import and Translate Access Control Policies
|
#### Import and Translate Access Control Policies
|
||||||
|
|
||||||
When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions.
|
When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 424 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
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>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Setup GitLab Access Token Connection in Infisical
|
<Tab title="Group Access Token">
|
||||||
|
Group access tokens provide access to all projects within a GitLab group, offering group-level control.
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Navigate to App Connections">
|
<Step title="Navigate to Group Settings">
|
||||||
Navigate to the **App Connections** page in the desired project.
|
Go to your GitLab group and navigate to Settings > Access Tokens. Click **Add new token** to create a new group access token.
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Add Connection">
|
<Step title="Configure Token">
|
||||||
Select the **GitLab Connection** option from the connection options modal.
|
Fill in the token details:
|
||||||

|
- **Token name**: A descriptive name for the token
|
||||||
</Step>
|
- **Expiration date**: Set an appropriate expiration date
|
||||||
<Step title="Configure Access Token">
|
- **Select role and scopes**: Depending on your use case, add the required role and one or more of the following scopes:
|
||||||
Select the **Access Token** method, paste your GitLab access token in the provided field, and select the appropriate token type.
|
|
||||||
|
|
||||||

|
<Tabs>
|
||||||
|
<Tab title="Secret Sync">
|
||||||
|
For Secret Syncs, the required role depends on your sync destination:
|
||||||
|
- **Project variables**: Requires **Maintainer** role or higher
|
||||||
|
- **Group variables**: Requires **Owner** role
|
||||||
|
|
||||||
Click **Connect** to establish the connection.
|
Your token will require the `api` scope.
|
||||||
</Step>
|
|
||||||
<Step title="Connection Created">
|

|
||||||
Your **GitLab Connection** is now available for use.
|
|
||||||

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

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