mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 22:28:47 +00:00
Merge origin/main to feat/pki-sync-v2
This commit is contained in:
@@ -2,7 +2,7 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
||||||
|
|
||||||
import { AccessScope, TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId");
|
const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId");
|
||||||
@@ -18,8 +18,6 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
await dropConstraintIfExists(TableName.Organization, "organizations_slug_unique", knex);
|
await dropConstraintIfExists(TableName.Organization, "organizations_slug_unique", knex);
|
||||||
t.unique(["rootOrgId", "parentOrgId", "slug"]);
|
t.unique(["rootOrgId", "parentOrgId", "slug"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// had to switch to raw for null not distinct
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId");
|
const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId");
|
||||||
@@ -28,24 +26,6 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.uuid("orgId");
|
t.uuid("orgId");
|
||||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex.raw(
|
|
||||||
`
|
|
||||||
UPDATE ?? AS identity
|
|
||||||
SET "orgId" = membership."scopeOrgId"
|
|
||||||
FROM ?? AS membership
|
|
||||||
WHERE
|
|
||||||
membership."actorIdentityId" = identity."id"
|
|
||||||
AND membership."scope" = ?
|
|
||||||
`,
|
|
||||||
[TableName.Identity, TableName.Membership, AccessScope.Organization]
|
|
||||||
);
|
|
||||||
|
|
||||||
await knex.raw(`DELETE FROM ?? WHERE "orgId" IS NULL`, [TableName.Identity]);
|
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
|
||||||
t.uuid("orgId").notNullable().alter();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { chunkArray } from "@app/lib/fn";
|
||||||
|
|
||||||
|
import { AccessScope, TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.transaction(async (tx) => {
|
||||||
|
const hasIdentityOrgCol = await tx.schema.hasColumn(TableName.Identity, "orgId");
|
||||||
|
if (hasIdentityOrgCol) {
|
||||||
|
const identityMemberships = await tx(TableName.Membership)
|
||||||
|
.where({
|
||||||
|
scope: AccessScope.Organization
|
||||||
|
})
|
||||||
|
.whereNotNull("actorIdentityId")
|
||||||
|
.select("actorIdentityId", "scopeOrgId");
|
||||||
|
|
||||||
|
const identityToOrgMapping: Record<string, string> = {};
|
||||||
|
identityMemberships.forEach((el) => {
|
||||||
|
if (el.actorIdentityId) {
|
||||||
|
identityToOrgMapping[el.actorIdentityId] = el.scopeOrgId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchMemberships = chunkArray(identityMemberships, 500);
|
||||||
|
for await (const membership of batchMemberships) {
|
||||||
|
const identityIds = membership.map((el) => el.actorIdentityId).filter(Boolean) as string[];
|
||||||
|
if (identityIds.length) {
|
||||||
|
const identities = await tx(TableName.Identity).whereIn("id", identityIds).select("*");
|
||||||
|
await tx(TableName.Identity)
|
||||||
|
.insert(
|
||||||
|
identities.map((el) => ({
|
||||||
|
...el,
|
||||||
|
orgId: identityToOrgMapping[el.id]
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.onConflict("id")
|
||||||
|
.merge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {}
|
||||||
|
|
||||||
|
const config = { transaction: false };
|
||||||
|
export { config };
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasOrgBlockDuplicateColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.Organization,
|
||||||
|
"blockDuplicateSecretSyncDestinations"
|
||||||
|
);
|
||||||
|
if (!hasOrgBlockDuplicateColumn) {
|
||||||
|
await knex.schema.table(TableName.Organization, (table) => {
|
||||||
|
table.boolean("blockDuplicateSecretSyncDestinations").notNullable().defaultTo(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasOrgBlockDuplicateColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.Organization,
|
||||||
|
"blockDuplicateSecretSyncDestinations"
|
||||||
|
);
|
||||||
|
if (hasOrgBlockDuplicateColumn) {
|
||||||
|
await knex.schema.table(TableName.Organization, (table) => {
|
||||||
|
table.dropColumn("blockDuplicateSecretSyncDestinations");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.transaction(async (tx) => {
|
||||||
|
await tx.schema.alterTable(TableName.IdentityAccessToken, (table) => {
|
||||||
|
table.dropForeign("identityId");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.transaction(async (tx) => {
|
||||||
|
await tx.schema.alterTable(TableName.IdentityAccessToken, (table) => {
|
||||||
|
table.foreign("identityId").references("id").inTable(TableName.Identity);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = { transaction: false };
|
||||||
|
export { config };
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
const MIGRATION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const result = await knex.raw("SHOW statement_timeout");
|
||||||
|
const originalTimeout = result.rows[0].statement_timeout;
|
||||||
|
|
||||||
|
await knex.transaction(async (tx) => {
|
||||||
|
try {
|
||||||
|
await tx.raw(`SET statement_timeout = ${MIGRATION_TIMEOUT}`);
|
||||||
|
const hasIdentityOrgCol = await tx.schema.hasColumn(TableName.Identity, "orgId");
|
||||||
|
if (hasIdentityOrgCol) {
|
||||||
|
await tx(TableName.Identity).whereNull("orgId").delete();
|
||||||
|
await tx.schema.alterTable(TableName.Identity, (t) => {
|
||||||
|
t.uuid("orgId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await tx.raw(`SET statement_timeout = '${originalTimeout}'`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {}
|
||||||
|
|
||||||
|
const config = { transaction: false };
|
||||||
|
export { config };
|
||||||
@@ -40,7 +40,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
googleSsoAuthEnforced: z.boolean().default(false),
|
googleSsoAuthEnforced: z.boolean().default(false),
|
||||||
googleSsoAuthLastUsed: z.date().nullable().optional(),
|
googleSsoAuthLastUsed: z.date().nullable().optional(),
|
||||||
parentOrgId: z.string().uuid().nullable().optional(),
|
parentOrgId: z.string().uuid().nullable().optional(),
|
||||||
rootOrgId: z.string().uuid().nullable().optional()
|
rootOrgId: z.string().uuid().nullable().optional(),
|
||||||
|
blockDuplicateSecretSyncDestinations: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -182,7 +182,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
|||||||
algorithm: z.string(),
|
algorithm: z.string(),
|
||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
kmipMetadata: z.record(z.any()).nullish()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -384,7 +385,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
|||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
algorithm: z.string(),
|
algorithm: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
kmipMetadata: z.record(z.any()).nullish()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -341,7 +341,8 @@ export const kmipOperationServiceFactory = ({
|
|||||||
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
|
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
|
||||||
isActive: !key.isDisabled,
|
isActive: !key.isDisabled,
|
||||||
createdAt: key.createdAt,
|
createdAt: key.createdAt,
|
||||||
updatedAt: key.updatedAt
|
updatedAt: key.updatedAt,
|
||||||
|
kmipMetadata: key.kmipMetadata as Record<string, unknown>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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) => ({
|
||||||
|
|||||||
@@ -1960,6 +1960,8 @@ export const registerRoutes = async (
|
|||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
|
projectDAL,
|
||||||
|
orgDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretSyncQueue,
|
secretSyncQueue,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
|
|||||||
@@ -323,7 +323,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
.min(1, "Max Shared Secret view count cannot be lower than 1")
|
.min(1, "Max Shared Secret view count cannot be lower than 1")
|
||||||
.max(1000, "Max Shared Secret view count cannot exceed 1000")
|
.max(1000, "Max Shared Secret view count cannot exceed 1000")
|
||||||
.nullable()
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
blockDuplicateSecretSyncDestinations: z
|
||||||
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
|
.describe("Block duplicate secret sync destinations across the organization")
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -112,7 +112,8 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
|||||||
...KmsKeysSchema.parse(entry),
|
...KmsKeysSchema.parse(entry),
|
||||||
isActive: !entry.isDisabled,
|
isActive: !entry.isDisabled,
|
||||||
algorithm: entry.internalKmsEncryptionAlgorithm,
|
algorithm: entry.internalKmsEncryptionAlgorithm,
|
||||||
version: entry.internalKmsVersion
|
version: entry.internalKmsVersion,
|
||||||
|
kmipMetadata: entry.kmipMetadata as Record<string, unknown>
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find project cmeks" });
|
throw new DatabaseError({ error, name: "Find project cmeks" });
|
||||||
|
|||||||
@@ -27,5 +27,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
|||||||
scannerProductEnabled: true,
|
scannerProductEnabled: true,
|
||||||
shareSecretsProductEnabled: true,
|
shareSecretsProductEnabled: true,
|
||||||
maxSharedSecretLifetime: true,
|
maxSharedSecretLifetime: true,
|
||||||
maxSharedSecretViewLimit: true
|
maxSharedSecretViewLimit: true,
|
||||||
|
blockDuplicateSecretSyncDestinations: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -405,7 +405,8 @@ export const orgServiceFactory = ({
|
|||||||
scannerProductEnabled,
|
scannerProductEnabled,
|
||||||
shareSecretsProductEnabled,
|
shareSecretsProductEnabled,
|
||||||
maxSharedSecretLifetime,
|
maxSharedSecretLifetime,
|
||||||
maxSharedSecretViewLimit
|
maxSharedSecretViewLimit,
|
||||||
|
blockDuplicateSecretSyncDestinations
|
||||||
}
|
}
|
||||||
}: TUpdateOrgDTO) => {
|
}: TUpdateOrgDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -516,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) {
|
||||||
@@ -589,7 +590,8 @@ export const orgServiceFactory = ({
|
|||||||
scannerProductEnabled,
|
scannerProductEnabled,
|
||||||
shareSecretsProductEnabled,
|
shareSecretsProductEnabled,
|
||||||
maxSharedSecretLifetime,
|
maxSharedSecretLifetime,
|
||||||
maxSharedSecretViewLimit
|
maxSharedSecretViewLimit,
|
||||||
|
blockDuplicateSecretSyncDestinations
|
||||||
});
|
});
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
return org;
|
return org;
|
||||||
@@ -1147,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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export type TUpdateOrgDTO = {
|
|||||||
shareSecretsProductEnabled: boolean;
|
shareSecretsProductEnabled: boolean;
|
||||||
maxSharedSecretLifetime: number;
|
maxSharedSecretLifetime: number;
|
||||||
maxSharedSecretViewLimit: number | null;
|
maxSharedSecretViewLimit: number | null;
|
||||||
|
blockDuplicateSecretSyncDestinations: boolean;
|
||||||
}>;
|
}>;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -310,19 +312,34 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await projectDAL.create(
|
const slug = projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`);
|
||||||
|
|
||||||
|
let project: TProjects;
|
||||||
|
try {
|
||||||
|
project = await projectDAL.create(
|
||||||
{
|
{
|
||||||
name: workspaceName,
|
name: workspaceName,
|
||||||
type,
|
type,
|
||||||
description: workspaceDescription,
|
description: workspaceDescription,
|
||||||
orgId: organization.id,
|
orgId: organization.id,
|
||||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
slug,
|
||||||
kmsSecretManagerKeyId: kmsKeyId,
|
kmsSecretManagerKeyId: kmsKeyId,
|
||||||
version: ProjectVersion.V3,
|
version: ProjectVersion.V3,
|
||||||
pitVersionLimit: 10
|
pitVersionLimit: 10
|
||||||
},
|
},
|
||||||
tx
|
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({
|
||||||
@@ -586,24 +603,13 @@ 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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const updatedProject = await projectDAL.updateById(project.id, {
|
const updatedProject = await projectDAL.updateById(project.id, {
|
||||||
name: update.name,
|
name: update.name,
|
||||||
description: update.description,
|
description: update.description,
|
||||||
@@ -619,6 +625,14 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
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 ({
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { deepEqualSkipFields } from "@app/lib/fn/object";
|
import { deepEqualSkipFields } from "@app/lib/fn/object";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
@@ -50,6 +52,8 @@ type TSecretSyncServiceFactoryDep = {
|
|||||||
secretImportDAL: TSecretImportDALFactory;
|
secretImportDAL: TSecretImportDALFactory;
|
||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "findById">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "getItem">;
|
keyStore: Pick<TKeyStoreFactory, "getItem">;
|
||||||
@@ -68,6 +72,8 @@ export const secretSyncServiceFactory = ({
|
|||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
|
projectDAL,
|
||||||
|
orgDAL,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
secretSyncQueue,
|
secretSyncQueue,
|
||||||
keyStore,
|
keyStore,
|
||||||
@@ -225,6 +231,61 @@ export const secretSyncServiceFactory = ({
|
|||||||
return secretSync as TSecretSync;
|
return secretSync as TSecretSync;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkDuplicateDestination = async (
|
||||||
|
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination];
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSecretSyncActions.Read,
|
||||||
|
ProjectPermissionSub.SecretSyncs
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
|
||||||
|
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId);
|
||||||
|
|
||||||
|
const duplicates = existingSyncs.filter((sync) => {
|
||||||
|
if (sync.id === excludeSyncId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
|
||||||
|
if (baseFieldsMatch) {
|
||||||
|
return DESTINATION_DUPLICATE_CHECK_MAP[destination](
|
||||||
|
sync.destinationConfig as Record<string, unknown>,
|
||||||
|
destinationConfig
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasDuplicate = duplicates.length > 0;
|
||||||
|
return {
|
||||||
|
hasDuplicate,
|
||||||
|
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const createSecretSync = async (
|
const createSecretSync = async (
|
||||||
{ projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO,
|
{ projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
@@ -271,6 +332,30 @@ export const secretSyncServiceFactory = ({
|
|||||||
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({ message: "Project not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const organization = await orgDAL.findById(project.orgId);
|
||||||
|
if (organization?.blockDuplicateSecretSyncDestinations) {
|
||||||
|
const duplicateCheck = await checkDuplicateDestination(
|
||||||
|
{
|
||||||
|
destination: params.destination,
|
||||||
|
destinationConfig: params.destinationConfig,
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
actor
|
||||||
|
);
|
||||||
|
if (duplicateCheck.hasDuplicate) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A secret sync with this destination already exists${
|
||||||
|
duplicateCheck.duplicateProjectId ? ` in project ${duplicateCheck.duplicateProjectId}` : ""
|
||||||
|
}.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
|
const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
|
||||||
|
|
||||||
// validates permission to connect and app is valid for sync destination
|
// validates permission to connect and app is valid for sync destination
|
||||||
@@ -369,6 +454,33 @@ export const secretSyncServiceFactory = ({
|
|||||||
|
|
||||||
let { folderId } = secretSync;
|
let { folderId } = secretSync;
|
||||||
|
|
||||||
|
if (params.destinationConfig) {
|
||||||
|
const project = await projectDAL.findById(secretSync.projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundError({ message: "Project not found" });
|
||||||
|
}
|
||||||
|
const organization = await orgDAL.findById(project.orgId);
|
||||||
|
|
||||||
|
if (organization?.blockDuplicateSecretSyncDestinations) {
|
||||||
|
const duplicateCheck = await checkDuplicateDestination(
|
||||||
|
{
|
||||||
|
destination,
|
||||||
|
destinationConfig: params.destinationConfig,
|
||||||
|
projectId: secretSync.projectId,
|
||||||
|
excludeSyncId: secretSync.id
|
||||||
|
},
|
||||||
|
actor
|
||||||
|
);
|
||||||
|
if (duplicateCheck.hasDuplicate) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A secret sync with this destination already exists${
|
||||||
|
duplicateCheck.duplicateProjectId ? ` in project ${duplicateCheck.duplicateProjectId}` : ""
|
||||||
|
}.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (params.connectionId) {
|
if (params.connectionId) {
|
||||||
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
|
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
|
||||||
|
|
||||||
@@ -703,61 +815,6 @@ export const secretSyncServiceFactory = ({
|
|||||||
return updatedSecretSync as TSecretSync;
|
return updatedSecretSync as TSecretSync;
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkDuplicateDestination = async (
|
|
||||||
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
|
|
||||||
actor: OrgServiceActor
|
|
||||||
) => {
|
|
||||||
const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination];
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
|
||||||
actor: actor.type,
|
|
||||||
actorId: actor.id,
|
|
||||||
actorAuthMethod: actor.authMethod,
|
|
||||||
actorOrgId: actor.orgId,
|
|
||||||
actionProjectType: ActionProjectType.SecretManager,
|
|
||||||
projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionSecretSyncActions.Read,
|
|
||||||
ProjectPermissionSub.SecretSyncs
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
|
|
||||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId);
|
|
||||||
|
|
||||||
const duplicates = existingSyncs.filter((sync) => {
|
|
||||||
if (sync.id === excludeSyncId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
|
|
||||||
if (baseFieldsMatch) {
|
|
||||||
return DESTINATION_DUPLICATE_CHECK_MAP[destination](
|
|
||||||
sync.destinationConfig as Record<string, unknown>,
|
|
||||||
destinationConfig
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasDuplicate = duplicates.length > 0;
|
|
||||||
return {
|
|
||||||
hasDuplicate,
|
|
||||||
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
listSecretSyncOptions,
|
listSecretSyncOptions,
|
||||||
listSecretSyncsByProjectId,
|
listSecretSyncsByProjectId,
|
||||||
|
|||||||
@@ -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)]);
|
||||||
|
|||||||
@@ -8,3 +8,9 @@ secrets via HTTPS requests. This can be useful for automating tasks, such as
|
|||||||
rotating credentials, or for integrating secret management into a larger system.
|
rotating credentials, or for integrating secret management into a larger system.
|
||||||
|
|
||||||
With the Public API, you can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
|
With the Public API, you can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
|
||||||
|
|
||||||
|
## API Versioning
|
||||||
|
|
||||||
|
The API is versioned on a per-resource basis. A resource's version is only incremented for breaking changes, so different endpoints may have different version numbers (e.g., `/api/v4/secrets` vs. `/api/v1/secret-syncs`).
|
||||||
|
|
||||||
|
As a best practice, always use the latest available version for each endpoint to ensure access to the most recent features and improvements.
|
||||||
|
|||||||
@@ -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,6 +187,65 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
|
|||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab title="Group Access Token">
|
||||||
|
Group access tokens provide access to all projects within a GitLab group, offering group-level control.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to Group Settings">
|
||||||
|
Go to your GitLab group and navigate to Settings > Access Tokens. Click **Add new token** to create a new group access token.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Configure Token">
|
||||||
|
Fill in the token details:
|
||||||
|
- **Token name**: A descriptive name for the token
|
||||||
|
- **Expiration date**: Set an appropriate expiration date
|
||||||
|
- **Select role and scopes**: Depending on your use case, add the required role and one or more of the following scopes:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Secret Sync">
|
||||||
|
For Secret Syncs, the required role depends on your sync destination:
|
||||||
|
- **Project variables**: Requires **Maintainer** role or higher
|
||||||
|
- **Group variables**: Requires **Owner** role
|
||||||
|
|
||||||
|
Your token will require the `api` scope.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
Click **Create group access token** to create the token.
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Group Access Token connections require manual token rotation when your GitLab access token expires or is regenerated. Monitor your connection status and update the token as needed.
|
||||||
|
</Info>
|
||||||
|
</Step>
|
||||||
|
<Step title="Copy Token">
|
||||||
|
Copy the generated token immediately as it won't be shown again.
|
||||||
|

|
||||||
|
<Warning>
|
||||||
|
Keep your access token secure and do not share it. Anyone with access to this token can access all projects within your GitLab group.
|
||||||
|
</Warning>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
## Setup GitLab Access Token Connection in Infisical
|
## Setup GitLab Access Token Connection in Infisical
|
||||||
@@ -206,7 +265,9 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
|
|||||||

|

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

|

|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import { Badge } from "@app/components/v3";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import {
|
import {
|
||||||
CertStatus,
|
CertStatus,
|
||||||
|
|||||||
@@ -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}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={icon} />
|
|
||||||
{!mini && <span>{label}</span>}
|
|
||||||
</Badge>
|
</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,24 +18,12 @@ 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>
|
</div>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm leading-4 text-mineshaft-400">
|
<p className="text-sm leading-4 text-mineshaft-400">
|
||||||
{isConfigured
|
{isConfigured
|
||||||
? `Edit ${destinationDetails.name} Certificate Sync`
|
? `Edit ${destinationDetails.name} Certificate Sync`
|
||||||
|
|||||||
@@ -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}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={icon} />
|
|
||||||
{!mini && <span>{label}</span>}
|
|
||||||
</Badge>
|
</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 { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||||
|
|||||||
@@ -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,24 +18,12 @@ 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>
|
</div>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm leading-4 text-mineshaft-400">
|
<p className="text-sm leading-4 text-mineshaft-400">
|
||||||
{isConfigured
|
{isConfigured
|
||||||
? `Edit ${destinationDetails.name} Rotation`
|
? `Edit ${destinationDetails.name} Rotation`
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import { faBan, faRotate, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { 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"
|
|
||||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
Rotation Failed
|
Rotation Failed
|
||||||
</Badge>
|
</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"}
|
|
||||||
className={twMerge(
|
|
||||||
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faRotate} />
|
|
||||||
{daysToRotation < 0
|
{daysToRotation < 0
|
||||||
? "Rotating"
|
? "Rotating"
|
||||||
: `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`}
|
: `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`}
|
||||||
</Badge>
|
</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,24 +18,12 @@ 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>
|
</div>
|
||||||
</a>
|
|
||||||
</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
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { faArrowRotateForward, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { 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}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={icon} />
|
|
||||||
{!mini && <span>{label}</span>}
|
|
||||||
</Badge>
|
</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,24 +18,12 @@ 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>
|
</div>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm leading-4 text-mineshaft-400">
|
<p className="text-sm leading-4 text-mineshaft-400">
|
||||||
{isConfigured
|
{isConfigured
|
||||||
? `Edit ${destinationDetails.name} Sync`
|
? `Edit ${destinationDetails.name} Sync`
|
||||||
|
|||||||
@@ -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}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={icon} />
|
|
||||||
{!mini && <span>{label}</span>}
|
|
||||||
</Badge>
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import { twMerge } from "tailwind-merge";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Switch } from "@app/components/v2";
|
import { Button, FormControl, Switch } from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useOrganization, useProject } from "@app/context";
|
||||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||||
import {
|
import {
|
||||||
SecretSync,
|
SecretSync,
|
||||||
SecretSyncInitialSyncBehavior,
|
SecretSyncInitialSyncBehavior,
|
||||||
TSecretSync,
|
TSecretSync,
|
||||||
useCreateSecretSync,
|
useCreateSecretSync,
|
||||||
|
useDuplicateDestinationCheck,
|
||||||
useSecretSyncOption
|
useSecretSyncOption
|
||||||
} from "@app/hooks/api/secretSyncs";
|
} from "@app/hooks/api/secretSyncs";
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ export const CreateSecretSyncForm = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const createSecretSync = useCreateSecretSync();
|
const createSecretSync = useCreateSecretSync();
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
const { name: destinationName } = SECRET_SYNC_MAP[destination];
|
||||||
|
|
||||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||||
@@ -106,11 +108,20 @@ export const CreateSecretSyncForm = ({
|
|||||||
setSelectedTabIndex((prev) => prev - 1);
|
setSelectedTabIndex((prev) => prev - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { handleSubmit, trigger, control } = formMethods;
|
const { handleSubmit, trigger, control, watch } = formMethods;
|
||||||
|
|
||||||
|
const { hasDuplicate } = useDuplicateDestinationCheck({
|
||||||
|
destination,
|
||||||
|
projectId: currentProject?.id || "",
|
||||||
|
enabled: true,
|
||||||
|
destinationConfig: watch("destinationConfig")
|
||||||
|
});
|
||||||
|
|
||||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||||
|
|
||||||
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
|
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
|
||||||
|
const isCreateButtonDisabled =
|
||||||
|
isFinalStep && hasDuplicate && currentOrg?.blockDuplicateSecretSyncDestinations;
|
||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
if (isFinalStep) {
|
if (isFinalStep) {
|
||||||
@@ -245,7 +256,7 @@ export const CreateSecretSyncForm = ({
|
|||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||||
<Button onClick={handleNext} colorSchema="secondary">
|
<Button onClick={handleNext} colorSchema="secondary" isDisabled={isCreateButtonDisabled}>
|
||||||
{isFinalStep ? "Create Sync" : "Next"}
|
{isFinalStep ? "Create Sync" : "Next"}
|
||||||
</Button>
|
</Button>
|
||||||
{selectedTabIndex > 0 && (
|
{selectedTabIndex > 0 && (
|
||||||
|
|||||||
+12
-3
@@ -6,6 +6,7 @@ type Props = {
|
|||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
duplicateProjectId?: string;
|
duplicateProjectId?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DuplicateDestinationConfirmationModal = ({
|
export const DuplicateDestinationConfirmationModal = ({
|
||||||
@@ -13,7 +14,8 @@ export const DuplicateDestinationConfirmationModal = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
isLoading,
|
isLoading,
|
||||||
duplicateProjectId
|
duplicateProjectId,
|
||||||
|
isDisabled
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
@@ -21,7 +23,12 @@ export const DuplicateDestinationConfirmationModal = ({
|
|||||||
<div className="mb-4 text-sm">
|
<div className="mb-4 text-sm">
|
||||||
<p>
|
<p>
|
||||||
Another secret sync in your organization is already configured with the same
|
Another secret sync in your organization is already configured with the same
|
||||||
destination. Proceeding may cause conflicts or overwrite existing data.
|
destination.{" "}
|
||||||
|
<span className={isDisabled ? "text-red-400" : ""}>
|
||||||
|
{isDisabled
|
||||||
|
? "Your organization does not allow duplicate destination configurations."
|
||||||
|
: "Proceeding may cause conflicts or overwrite existing data."}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
{duplicateProjectId && (
|
{duplicateProjectId && (
|
||||||
<p className="mt-2 text-xs text-mineshaft-400">
|
<p className="mt-2 text-xs text-mineshaft-400">
|
||||||
@@ -31,9 +38,10 @@ export const DuplicateDestinationConfirmationModal = ({
|
|||||||
</code>
|
</code>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="mt-2">Are you sure you want to continue?</p>
|
{!isDisabled && <p className="mt-2">Are you sure you want to continue?</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isDisabled && (
|
||||||
<div className="flex items-center gap-4 pt-4">
|
<div className="flex items-center gap-4 pt-4">
|
||||||
<ModalClose asChild>
|
<ModalClose asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -51,6 +59,7 @@ export const DuplicateDestinationConfirmationModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</ModalClose>
|
</ModalClose>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { SecretSyncEditFields } from "@app/components/secret-syncs/types";
|
import { SecretSyncEditFields } from "@app/components/secret-syncs/types";
|
||||||
import { Button, ModalClose } from "@app/components/v2";
|
import { Button, ModalClose } from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||||
import {
|
import {
|
||||||
TSecretSync,
|
TSecretSync,
|
||||||
@@ -30,6 +31,7 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
|
|||||||
const { name: destinationName } = SECRET_SYNC_MAP[secretSync.destination];
|
const { name: destinationName } = SECRET_SYNC_MAP[secretSync.destination];
|
||||||
const [showDuplicateConfirmation, setShowDuplicateConfirmation] = useState(false);
|
const [showDuplicateConfirmation, setShowDuplicateConfirmation] = useState(false);
|
||||||
const [pendingFormData, setPendingFormData] = useState<TSecretSyncForm | null>(null);
|
const [pendingFormData, setPendingFormData] = useState<TSecretSyncForm | null>(null);
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
const formMethods = useForm<TSecretSyncForm>({
|
const formMethods = useForm<TSecretSyncForm>({
|
||||||
resolver: zodResolver(UpdateSecretSyncFormSchema),
|
resolver: zodResolver(UpdateSecretSyncFormSchema),
|
||||||
@@ -209,6 +211,7 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
|
|||||||
onConfirm={handleConfirmDuplicate}
|
onConfirm={handleConfirmDuplicate}
|
||||||
isLoading={updateSecretSync.isPending}
|
isLoading={updateSecretSync.isPending}
|
||||||
duplicateProjectId={storedDuplicateProjectId}
|
duplicateProjectId={storedDuplicateProjectId}
|
||||||
|
isDisabled={currentOrg?.blockDuplicateSecretSyncDestinations}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+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 && (
|
||||||
|
|||||||
+5
-7
@@ -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>
|
||||||
|
|||||||
+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";
|
||||||
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";
|
||||||
|
|
||||||
|
|||||||
+44
-11
@@ -5,8 +5,8 @@ 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 { 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";
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@ import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields";
|
|||||||
export const SecretSyncReviewFields = () => {
|
export const SecretSyncReviewFields = () => {
|
||||||
const { watch } = useFormContext<TSecretSyncForm>();
|
const { watch } = useFormContext<TSecretSyncForm>();
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
let DestinationFieldsComponent: ReactNode;
|
let DestinationFieldsComponent: ReactNode;
|
||||||
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
||||||
@@ -193,18 +194,50 @@ export const SecretSyncReviewFields = () => {
|
|||||||
{isChecking && <span className="text-xs text-mineshaft-400">Checking...</span>}
|
{isChecking && <span className="text-xs text-mineshaft-400">Checking...</span>}
|
||||||
</div>
|
</div>
|
||||||
{hasDuplicate && (
|
{hasDuplicate && (
|
||||||
<div className="mb-2 flex items-start rounded-md border border-yellow-600 bg-yellow-900/20 px-3 py-2">
|
<div
|
||||||
<div className="flex text-sm text-yellow-100">
|
className={`mb-2 flex items-start rounded-md border px-3 py-2 ${
|
||||||
<FontAwesomeIcon icon={faWarning} className="mt-1 mr-2 text-yellow-600" />
|
currentOrg?.blockDuplicateSecretSyncDestinations
|
||||||
|
? "border-red-600 bg-red-900/20"
|
||||||
|
: "border-yellow-600 bg-yellow-900/20"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex text-sm ${
|
||||||
|
currentOrg?.blockDuplicateSecretSyncDestinations
|
||||||
|
? "text-red-100"
|
||||||
|
: "text-yellow-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faWarning}
|
||||||
|
className={`mt-1 mr-2 ${
|
||||||
|
currentOrg?.blockDuplicateSecretSyncDestinations
|
||||||
|
? "text-red-600"
|
||||||
|
: "text-yellow-600"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Another secret sync in your organization is already configured with the same
|
{currentOrg?.blockDuplicateSecretSyncDestinations
|
||||||
destination. This may lead to conflicts or unexpected behavior.
|
? "Another secret sync in your organization is already configured with the same destination. Your organization does not allow duplicate destination configurations."
|
||||||
|
: "Another secret sync in your organization is already configured with the same destination. This may lead to conflicts or unexpected behavior."}
|
||||||
</p>
|
</p>
|
||||||
{duplicateProjectId && (
|
{duplicateProjectId && (
|
||||||
<p className="mt-1 text-xs text-yellow-200">
|
<p
|
||||||
|
className={`mt-1 text-xs ${
|
||||||
|
currentOrg?.blockDuplicateSecretSyncDestinations
|
||||||
|
? "text-red-200"
|
||||||
|
: "text-yellow-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
Duplicate found in project ID:{" "}
|
Duplicate found in project ID:{" "}
|
||||||
<code className="rounded-sm bg-yellow-800/50 px-1 py-0.5">
|
<code
|
||||||
|
className={`rounded-sm px-1 py-0.5 ${
|
||||||
|
currentOrg?.blockDuplicateSecretSyncDestinations
|
||||||
|
? "bg-red-800/50"
|
||||||
|
: "bg-yellow-800/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{duplicateProjectId}
|
{duplicateProjectId}
|
||||||
</code>
|
</code>
|
||||||
</p>
|
</p>
|
||||||
@@ -224,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>
|
||||||
@@ -235,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";
|
||||||
@@ -301,10 +301,6 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
"/projects/cert-management/$projectId/subscribers",
|
"/projects/cert-management/$projectId/subscribers",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
|
||||||
),
|
),
|
||||||
CertificatesPage: setRoute(
|
|
||||||
"/projects/cert-management/$projectId/certificates",
|
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates"
|
|
||||||
),
|
|
||||||
CertificateAuthoritiesPage: setRoute(
|
CertificateAuthoritiesPage: setRoute(
|
||||||
"/projects/cert-management/$projectId/certificate-authorities",
|
"/projects/cert-management/$projectId/certificate-authorities",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -125,7 +125,8 @@ export const useUpdateOrg = () => {
|
|||||||
scannerProductEnabled,
|
scannerProductEnabled,
|
||||||
shareSecretsProductEnabled,
|
shareSecretsProductEnabled,
|
||||||
maxSharedSecretLifetime,
|
maxSharedSecretLifetime,
|
||||||
maxSharedSecretViewLimit
|
maxSharedSecretViewLimit,
|
||||||
|
blockDuplicateSecretSyncDestinations
|
||||||
}) => {
|
}) => {
|
||||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||||
name,
|
name,
|
||||||
@@ -146,7 +147,8 @@ export const useUpdateOrg = () => {
|
|||||||
scannerProductEnabled,
|
scannerProductEnabled,
|
||||||
shareSecretsProductEnabled,
|
shareSecretsProductEnabled,
|
||||||
maxSharedSecretLifetime,
|
maxSharedSecretLifetime,
|
||||||
maxSharedSecretViewLimit
|
maxSharedSecretViewLimit,
|
||||||
|
blockDuplicateSecretSyncDestinations
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export type Organization = {
|
|||||||
shareSecretsProductEnabled: boolean;
|
shareSecretsProductEnabled: boolean;
|
||||||
maxSharedSecretLifetime: number;
|
maxSharedSecretLifetime: number;
|
||||||
maxSharedSecretViewLimit: number | null;
|
maxSharedSecretViewLimit: number | null;
|
||||||
|
blockDuplicateSecretSyncDestinations: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateOrgDTO = {
|
export type UpdateOrgDTO = {
|
||||||
@@ -52,6 +53,7 @@ export type UpdateOrgDTO = {
|
|||||||
shareSecretsProductEnabled?: boolean;
|
shareSecretsProductEnabled?: boolean;
|
||||||
maxSharedSecretViewLimit?: number | null;
|
maxSharedSecretViewLimit?: number | null;
|
||||||
maxSharedSecretLifetime?: number;
|
maxSharedSecretLifetime?: number;
|
||||||
|
blockDuplicateSecretSyncDestinations?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BillingDetails = {
|
export type BillingDetails = {
|
||||||
|
|||||||
@@ -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,6 +283,18 @@ 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
|
||||||
|
asChild
|
||||||
|
variant="org"
|
||||||
|
isTruncatable
|
||||||
|
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
|
||||||
|
className={twMerge(
|
||||||
|
"gap-x-1.5 text-sm",
|
||||||
|
(!isOrgScope || isSubOrganization) &&
|
||||||
|
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-org"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/organization/projects",
|
to: "/organization/projects",
|
||||||
@@ -297,15 +304,10 @@ export const Navbar = () => {
|
|||||||
await router.invalidate({ sync: true }).catch(() => null);
|
await router.invalidate({ sync: true }).catch(() => null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="org"
|
|
||||||
className={twMerge(
|
|
||||||
"max-w-full min-w-0 cursor-pointer text-sm",
|
|
||||||
(!isOrgScope || isSubOrganization) &&
|
|
||||||
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faGlobe} />
|
<OrgIcon className="size-[12px]" />
|
||||||
<p className="truncate">{currentOrg?.name}</p>
|
<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
|
||||||
variant="namespace"
|
asChild
|
||||||
|
isTruncatable
|
||||||
|
variant="sub-org"
|
||||||
|
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex max-w-full min-w-0 cursor-pointer text-sm",
|
"gap-x-1.5 text-sm",
|
||||||
!isOrgScope &&
|
!isOrgScope &&
|
||||||
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
|
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCubes} />
|
<Link to="/organization/projects">
|
||||||
<p className="truncate">{currentOrg.subOrganization.name}</p>
|
<SubOrgIcon className="size-[12px]" />
|
||||||
</Badge>
|
<span>{currentOrg.subOrganization.name}</span>
|
||||||
</Link>
|
</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"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faWarning} />
|
|
||||||
<span>Deleting Yourself</span>
|
|
||||||
</Badge>
|
</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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user