Merge pull request #2242 from akhilmhdh/fix/tag-filter-secret-api

Tag based filtering for secret endpoint
This commit is contained in:
Akhil Mohan
2024-08-09 20:33:43 +05:30
committed by GitHub
31 changed files with 193 additions and 158 deletions
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
if (hasNameField) {
await knex.schema.alterTable(TableName.SecretTag, (t) => {
t.dropColumn("name");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
if (!hasNameField) {
await knex.schema.alterTable(TableName.SecretTag, (t) => {
t.string("name");
});
}
}
-1
View File
@@ -9,7 +9,6 @@ import { TImmutableDBKeys } from "./models";
export const SecretTagsSchema = z.object({ export const SecretTagsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string(),
slug: z.string(), slug: z.string(),
color: z.string().nullable().optional(), color: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
@@ -88,8 +88,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
) )
.select( .select(
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"),
@@ -124,9 +123,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "tagJnId", key: "tagJnId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
id, id,
name, name: slug,
slug, slug,
color color
}) })
@@ -269,8 +268,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
) )
.select( .select(
db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"), db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"),
@@ -292,9 +290,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "tagJnId", key: "tagJnId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
id, id,
name, name: slug,
slug, slug,
color color
}) })
@@ -100,8 +100,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
return sqlNestRelationships({ return sqlNestRelationships({
data, data,
@@ -132,9 +131,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id, id,
name, name: slug,
slug, slug,
color, color,
vId vId
@@ -195,8 +194,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
return sqlNestRelationships({ return sqlNestRelationships({
data, data,
@@ -227,9 +225,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id, id,
name, name: slug,
slug, slug,
color, color,
vId vId
@@ -353,8 +351,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
const formated = sqlNestRelationships({ const formated = sqlNestRelationships({
@@ -377,9 +374,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id, id,
name, name: slug,
slug, slug,
color, color,
vId vId
@@ -508,8 +505,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"), db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
); );
const formated = sqlNestRelationships({ const formated = sqlNestRelationships({
@@ -532,9 +528,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
{ {
key: "tagVersionId", key: "tagVersionId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({ mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
id, id,
name, name: slug,
slug, slug,
color, color,
vId vId
+2 -1
View File
@@ -596,7 +596,8 @@ export const RAW_SECRETS = {
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.", "The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
environment: "The slug of the environment to list secrets from.", environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path to list secrets from.", secretPath: "The secret path to list secrets from.",
includeImports: "Weather to include imported secrets or not." includeImports: "Weather to include imported secrets or not.",
tagSlugs: "The comma separated tag slugs to filter secrets"
}, },
CREATE: { CREATE: {
secretName: "The name of the secret to create.", secretName: "The name of the secret to create.",
@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas"; import { SecretTagsSchema } from "@app/db/schemas";
@@ -49,7 +50,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspaceTag: SecretTagsSchema // akhilmhdh: for terraform backward compatiability
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
}) })
} }
}, },
@@ -79,7 +81,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspaceTag: SecretTagsSchema // akhilmhdh: for terraform backward compatiability
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
}) })
} }
}, },
@@ -108,8 +111,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId) projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().describe(SECRET_TAGS.CREATE.name), slug: z
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug), .string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.CREATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim().describe(SECRET_TAGS.CREATE.color) color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}), }),
response: { response: {
@@ -144,8 +153,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId) tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim().describe(SECRET_TAGS.UPDATE.name), slug: z
slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug), .string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.UPDATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color) color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
}), }),
response: { response: {
+25 -17
View File
@@ -59,9 +59,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}).array() })
.extend({ name: z.string() })
.array()
}) })
) )
}) })
@@ -116,16 +117,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
z.object({ tags: SecretTagsSchema.pick({
tags: SecretTagsSchema.pick({ id: true,
id: true, slug: true,
slug: true, color: true
name: true,
color: true
}).array()
}) })
) .extend({ name: z.string() })
.array()
})
}) })
} }
}, },
@@ -180,7 +180,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.enum(["true", "false"]) .enum(["true", "false"])
.default("false") .default("false")
.transform((value) => value === "true") .transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.includeImports) .describe(RAW_SECRETS.LIST.includeImports),
tagSlugs: z
.string()
.describe(RAW_SECRETS.LIST.tagSlugs)
.optional()
// split by comma and trim the strings
.transform((el) => (el ? el.split(",").map((i) => i.trim()) : []))
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -190,9 +196,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}) })
.extend({ name: z.string() })
.array() .array()
.optional() .optional()
}) })
@@ -251,7 +257,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId: workspaceId, projectId: workspaceId,
path: secretPath, path: secretPath,
includeImports: req.query.include_imports, includeImports: req.query.include_imports,
recursive: req.query.recursive recursive: req.query.recursive,
tagSlugs: req.query.tagSlugs
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -325,9 +332,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}) })
.extend({ name: z.string() })
.array() .array()
.optional() .optional()
}) })
@@ -731,9 +738,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tags: SecretTagsSchema.pick({ tags: SecretTagsSchema.pick({
id: true, id: true,
slug: true, slug: true,
name: true,
color: true color: true
}).array() })
.extend({ name: z.string() })
.array()
}) })
.array(), .array(),
imports: z imports: z
@@ -22,16 +22,7 @@ type TSecretTagServiceFactoryDep = {
export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>; export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>;
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => { export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
const createTag = async ({ const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
name,
slug,
actor,
color,
actorId,
actorOrgId,
actorAuthMethod,
projectId
}: TCreateTagDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -46,7 +37,6 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
const newTag = await secretTagDAL.create({ const newTag = await secretTagDAL.create({
projectId, projectId,
name,
slug, slug,
color, color,
createdBy: actorId, createdBy: actorId,
@@ -55,7 +45,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
return newTag; return newTag;
}; };
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, name, color, slug }: TUpdateTagDTO) => { const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, color, slug }: TUpdateTagDTO) => {
const tag = await secretTagDAL.findById(id); const tag = await secretTagDAL.findById(id);
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" }); if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
@@ -73,7 +63,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug }); const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug });
return updatedTag; return updatedTag;
}; };
@@ -107,7 +97,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag; return { ...tag, name: tag.slug };
}; };
const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => { const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => {
@@ -123,7 +113,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag; return { ...tag, name: tag.slug };
}; };
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => { const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
@@ -1,14 +1,12 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
export type TCreateTagDTO = { export type TCreateTagDTO = {
name: string;
color: string; color: string;
slug: string; slug: string;
} & TProjectPermission; } & TProjectPermission;
export type TUpdateTagDTO = { export type TUpdateTagDTO = {
id: string; id: string;
name?: string;
slug?: string; slug?: string;
color?: string; color?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -136,7 +136,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
@@ -147,11 +146,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -169,14 +168,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.where({ [`${TableName.SecretV2}Id` as const]: secretId }) .where({ [`${TableName.SecretV2}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
return tags.map((el) => ({ return tags.map((el) => ({
id: el.tagId, id: el.tagId,
color: el.tagColor, color: el.tagColor,
slug: el.tagSlug, slug: el.tagSlug,
name: el.tagName name: el.tagSlug
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "get secret tags" }); throw new DatabaseError({ error, name: "get secret tags" });
@@ -210,7 +208,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
@@ -221,11 +218,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -350,8 +347,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.SecretV2)) .select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
const docs = sqlNestRelationships({ const docs = sqlNestRelationships({
data: rawDocs, data: rawDocs,
key: "id", key: "id",
@@ -360,11 +356,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -429,6 +429,7 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
includeImports, includeImports,
recursive, recursive,
tagSlugs = [],
expandSecretReferences: shouldExpandSecretReferences expandSecretReferences: shouldExpandSecretReferences
}: TGetSecretsDTO) => { }: TGetSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
@@ -496,6 +497,9 @@ export const secretV2BridgeServiceFactory = ({
: "" : ""
}) })
); );
const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets;
const expandSecretReferences = expandSecretReferencesFactory({ const expandSecretReferences = expandSecretReferencesFactory({
projectId, projectId,
folderDAL, folderDAL,
@@ -504,7 +508,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
if (shouldExpandSecretReferences) { if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath); const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
for (const secretPathKey in secretsGroupByPath) { for (const secretPathKey in secretsGroupByPath) {
if (Object.hasOwn(secretsGroupByPath, secretPathKey)) { if (Object.hasOwn(secretsGroupByPath, secretPathKey)) {
const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce( const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce(
@@ -530,7 +534,7 @@ export const secretV2BridgeServiceFactory = ({
if (!includeImports) { if (!includeImports) {
return { return {
secrets: decryptedSecrets secrets: filteredSecrets
}; };
} }
@@ -558,7 +562,7 @@ export const secretV2BridgeServiceFactory = ({
}); });
return { return {
secrets: decryptedSecrets, secrets: filteredSecrets,
imports: importedSecrets imports: importedSecrets
}; };
}; };
@@ -20,6 +20,7 @@ export type TGetSecretsDTO = {
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[];
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretDTO = { export type TGetASecretDTO = {
+9 -13
View File
@@ -123,7 +123,6 @@ export const secretDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
data: secs, data: secs,
@@ -133,11 +132,11 @@ export const secretDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -155,14 +154,13 @@ export const secretDALFactory = (db: TDbClient) => {
.where({ [`${TableName.Secret}Id` as const]: secretId }) .where({ [`${TableName.Secret}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
return tags.map((el) => ({ return tags.map((el) => ({
id: el.tagId, id: el.tagId,
color: el.tagColor, color: el.tagColor,
slug: el.tagSlug, slug: el.tagSlug,
name: el.tagName name: el.tagSlug
})); }));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "get secret tags" }); throw new DatabaseError({ error, name: "get secret tags" });
@@ -188,7 +186,6 @@ export const secretDALFactory = (db: TDbClient) => {
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
.orderBy("id", "asc"); .orderBy("id", "asc");
const data = sqlNestRelationships({ const data = sqlNestRelationships({
data: secs, data: secs,
@@ -198,11 +195,11 @@ export const secretDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
@@ -318,8 +315,7 @@ export const secretDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.Secret)) .select(selectAllTableCols(TableName.Secret))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
const docs = sqlNestRelationships({ const docs = sqlNestRelationships({
data: rawDocs, data: rawDocs,
key: "id", key: "id",
@@ -328,11 +324,11 @@ export const secretDALFactory = (db: TDbClient) => {
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id, id,
color, color,
slug, slug,
name name: slug
}) })
} }
] ]
+1 -2
View File
@@ -370,7 +370,6 @@ export const decryptSecretRaw = (
id: string; id: string;
slug: string; slug: string;
color?: string | null; color?: string | null;
name: string;
}[]; }[];
}, },
key: string key: string
@@ -412,7 +411,7 @@ export const decryptSecretRaw = (
_id: secret.id, _id: secret.id,
id: secret.id, id: secret.id,
user: secret.userId, user: secret.userId,
tags: secret.tags, tags: secret.tags?.map((el) => ({ ...el, name: el.slug })),
skipMultilineEncoding: secret.skipMultilineEncoding, skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.secretReminderRepeatDays, secretReminderRepeatDays: secret.secretReminderRepeatDays,
secretReminderNote: secret.secretReminderNote, secretReminderNote: secret.secretReminderNote,
+10 -5
View File
@@ -964,7 +964,8 @@ export const secretServiceFactory = ({
environment, environment,
includeImports, includeImports,
expandSecretReferences, expandSecretReferences,
recursive recursive,
tagSlugs = []
}: TGetSecretsRawDTO) => { }: TGetSecretsRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
@@ -978,7 +979,8 @@ export const secretServiceFactory = ({
path, path,
recursive, recursive,
actorAuthMethod, actorAuthMethod,
includeImports includeImports,
tagSlugs
}); });
return { secrets, imports }; return { secrets, imports };
} }
@@ -998,6 +1000,9 @@ export const secretServiceFactory = ({
}); });
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets;
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => { const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
const decryptedImportSecrets = importedSecrets.map((sec) => const decryptedImportSecrets = importedSecrets.map((sec) =>
decryptSecretRaw( decryptSecretRaw(
@@ -1106,14 +1111,14 @@ export const secretServiceFactory = ({
}; };
// expand secrets // expand secrets
await batchSecretsExpand(decryptedSecrets); await batchSecretsExpand(filteredSecrets);
// expand imports by batch // expand imports by batch
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets))); await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
} }
return { return {
secrets: decryptedSecrets, secrets: filteredSecrets,
imports: processedImports imports: processedImports
}; };
}; };
@@ -2081,7 +2086,7 @@ export const secretServiceFactory = ({
return { return {
...updatedSecret[0], ...updatedSecret[0],
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color })) tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.slug, color: t.color }))
}; };
}; };
@@ -149,6 +149,7 @@ export type TGetSecretsRawDTO = {
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[];
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretRawDTO = { export type TGetASecretRawDTO = {
+4
View File
@@ -404,6 +404,10 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("secretPath", request.SecretPath) SetQueryParam("secretPath", request.SecretPath)
if request.TagSlugs != "" {
req.SetQueryParam("tagSlugs", request.TagSlugs)
}
if request.IncludeImport { if request.IncludeImport {
req.SetQueryParam("include_imports", "true") req.SetQueryParam("include_imports", "true")
} }
+1
View File
@@ -574,6 +574,7 @@ type GetRawSecretsV3Request struct {
SecretPath string `json:"secretPath"` SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"` IncludeImport bool `json:"include_imports"`
Recursive bool `json:"recursive"` Recursive bool `json:"recursive"`
TagSlugs string `json:"tagSlugs,omitempty"`
} }
type GetRawSecretsV3Response struct { type GetRawSecretsV3Response struct {
+1 -1
View File
@@ -312,7 +312,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) { func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) {
return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) { return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) {
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false) res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
+7 -6
View File
@@ -21,7 +21,7 @@ import (
"github.com/zalando/go-keyring" "github.com/zalando/go-keyring"
) )
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string) ([]models.SingleEnvironmentVariable, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again") return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@@ -54,6 +54,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
SecretPath: secretPath, SecretPath: secretPath,
IncludeImport: includeImports, IncludeImport: includeImports,
Recursive: recursive, Recursive: recursive,
TagSlugs: tagSlugs,
}) })
if err != nil { if err != nil {
@@ -77,7 +78,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
} }
func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) { func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool, tagSlugs string) (models.PlaintextSecretResult, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(accessToken). httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
@@ -87,7 +88,7 @@ func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentNa
Environment: environmentName, Environment: environmentName,
IncludeImport: includeImports, IncludeImport: includeImports,
Recursive: recursive, Recursive: recursive,
// TagSlugs: tagSlugs, TagSlugs: tagSlugs,
} }
if secretsPath != "" { if secretsPath != "" {
@@ -282,7 +283,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
} }
res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, infisicalDotJson.WorkspaceId, res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, infisicalDotJson.WorkspaceId,
params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err)
if err == nil { if err == nil {
@@ -311,7 +312,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
} else { } else {
if params.InfisicalToken != "" { if params.InfisicalToken != "" {
log.Debug().Msg("Trying to fetch secrets using service token") log.Debug().Msg("Trying to fetch secrets using service token")
secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
} else if params.UniversalAuthAccessToken != "" { } else if params.UniversalAuthAccessToken != "" {
if params.WorkspaceId == "" { if params.WorkspaceId == "" {
@@ -319,7 +320,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
} }
log.Debug().Msg("Trying to fetch secrets using universal auth") log.Debug().Msg("Trying to fetch secrets using universal auth")
res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
errorToReturn = err errorToReturn = err
secretsToReturn = res.Secrets secretsToReturn = res.Secrets
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
import { faCheck } from "@fortawesome/free-solid-svg-icons"; import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -87,7 +88,13 @@ type Props = {
}; };
const createTagSchema = z.object({ const createTagSchema = z.object({
name: z.string().trim(), slug: z
.string()
.trim()
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim() color: z.string().trim()
}); });
@@ -110,7 +117,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(createTagSchema) resolver: zodResolver(createTagSchema)
}); });
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
@@ -123,13 +130,12 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
if (!isOpen) reset(); if (!isOpen) reset();
}, [isOpen]); }, [isOpen]);
const onFormSubmit = async ({ name, color }: FormData) => { const onFormSubmit = async ({ slug, color }: FormData) => {
try { try {
await createWsTag({ await createWsTag({
workspaceID: workspaceId, workspaceID: workspaceId,
tagName: name,
tagColor: color, tagColor: color,
tagSlug: name.replace(" ", "_") tagSlug: slug
}); });
onToggle(false); onToggle(false);
reset(); reset();
@@ -155,11 +161,11 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Controller
control={control} control={control}
name="name" name="slug"
defaultValue="" defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Tag Name" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Tag Slug" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="Type your tag name" /> <Input {...field} placeholder="Type your tag slug" />
</FormControl> </FormControl>
)} )}
/> />
+1 -2
View File
@@ -28,11 +28,10 @@ export const useCreateWsTag = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<WsTag, {}, CreateTagDTO>({ return useMutation<WsTag, {}, CreateTagDTO>({
mutationFn: async ({ workspaceID, tagName, tagColor, tagSlug }) => { mutationFn: async ({ workspaceID, tagColor, tagSlug }) => {
const { data } = await apiRequest.post<{ workspaceTag: WsTag }>( const { data } = await apiRequest.post<{ workspaceTag: WsTag }>(
`/api/v1/workspace/${workspaceID}/tags`, `/api/v1/workspace/${workspaceID}/tags`,
{ {
name: tagName,
color: tagColor || "", color: tagColor || "",
slug: tagSlug slug: tagSlug
} }
-2
View File
@@ -2,7 +2,6 @@ export type UserWsTags = WsTag[];
export type WsTag = { export type WsTag = {
id: string; id: string;
name: string;
slug: string; slug: string;
color?: string; color?: string;
projectId: string; projectId: string;
@@ -16,7 +15,6 @@ export type WorkspaceTag = { id: string; name: string; slug: string };
export type CreateTagDTO = { export type CreateTagDTO = {
workspaceID: string; workspaceID: string;
tagSlug: string; tagSlug: string;
tagName: string;
tagColor: string; tagColor: string;
}; };
@@ -97,7 +97,7 @@ export const SecretApprovalRequestChangeItem = ({
</Td> </Td>
<Td>{secretVersion?.secretComment}</Td> <Td>{secretVersion?.secretComment}</Td>
<Td> <Td>
{secretVersion?.tags?.map(({ name, id: tagId, color }) => ( {secretVersion?.tags?.map(({ slug, id: tagId, color }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-2"
key={`${secretVersion.id}-${tagId}`} key={`${secretVersion.id}-${tagId}`}
@@ -106,7 +106,7 @@ export const SecretApprovalRequestChangeItem = ({
className="h-3 w-3 rounded-full" className="h-3 w-3 rounded-full"
style={{ backgroundColor: color || "#bec2c8" }} style={{ backgroundColor: color || "#bec2c8" }}
/> />
<div className="text-sm">{name}</div> <div className="text-sm">{slug}</div>
</Tag> </Tag>
))} ))}
</Td> </Td>
@@ -119,7 +119,7 @@ export const SecretApprovalRequestChangeItem = ({
</Td> </Td>
<Td>{newVersion?.secretComment}</Td> <Td>{newVersion?.secretComment}</Td>
<Td> <Td>
{newVersion?.tags?.map(({ name, id: tagId, color }) => ( {newVersion?.tags?.map(({ slug, id: tagId, color }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-2"
key={`${newVersion.id}-${tagId}`} key={`${newVersion.id}-${tagId}`}
@@ -128,7 +128,7 @@ export const SecretApprovalRequestChangeItem = ({
className="h-3 w-3 rounded-full" className="h-3 w-3 rounded-full"
style={{ backgroundColor: color || "#bec2c8" }} style={{ backgroundColor: color || "#bec2c8" }}
/> />
<div className="text-sm">{name}</div> <div className="text-sm">{slug}</div>
</Tag> </Tag>
))} ))}
</Td> </Td>
@@ -157,7 +157,7 @@ export const SecretApprovalRequestChangeItem = ({
</Td> </Td>
<Td> <Td>
{(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map( {(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map(
({ name, id: tagId, color }) => ( ({ slug, id: tagId, color }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-2"
key={`${ key={`${
@@ -168,7 +168,7 @@ export const SecretApprovalRequestChangeItem = ({
className="h-3 w-3 rounded-full" className="h-3 w-3 rounded-full"
style={{ backgroundColor: color || "#bec2c8" }} style={{ backgroundColor: color || "#bec2c8" }}
/> />
<div className="text-sm">{name}</div> <div className="text-sm">{slug}</div>
</Tag> </Tag>
) )
)} )}
@@ -327,7 +327,7 @@ export const ActionBar = ({
</DropdownSubMenuTrigger> </DropdownSubMenuTrigger>
<DropdownSubMenuContent className="rounded-l-none"> <DropdownSubMenuContent className="rounded-l-none">
<DropdownMenuLabel>Apply tags to filter secrets</DropdownMenuLabel> <DropdownMenuLabel>Apply tags to filter secrets</DropdownMenuLabel>
{tags.map(({ id, name, color }) => ( {tags.map(({ id, slug, color }) => (
<DropdownMenuItem <DropdownMenuItem
onClick={(evt) => { onClick={(evt) => {
evt.preventDefault(); evt.preventDefault();
@@ -342,7 +342,7 @@ export const ActionBar = ({
className="mr-2 h-2 w-2 rounded-full" className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }} style={{ background: color || "#bec2c8" }}
/> />
{name} {slug}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
@@ -254,7 +254,7 @@ export const SecretDetailSidebar = ({
)} )}
<FormControl label="Tags" className=""> <FormControl label="Tags" className="">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2"> <div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{fields.map(({ tagColor, id: formId, name, id }) => ( {fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-2"
key={formId} key={formId}
@@ -271,7 +271,7 @@ export const SecretDetailSidebar = ({
className="h-3 w-3 rounded-full" className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }} style={{ backgroundColor: tagColor || "#bec2c8" }}
/> />
<div className="text-sm">{name}</div> <div className="text-sm">{slug}</div>
</Tag> </Tag>
))} ))}
<DropdownMenu> <DropdownMenu>
@@ -296,7 +296,7 @@ export const SecretDetailSidebar = ({
<DropdownMenuContent align="end" className="z-[100]"> <DropdownMenuContent align="end" className="z-[100]">
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel> <DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => { {tags.map((tag) => {
const { id: tagId, name, color } = tag; const { id: tagId, slug, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId]; const isSelected = selectedTagsGroupById?.[tagId];
return ( return (
@@ -311,7 +311,7 @@ export const SecretDetailSidebar = ({
className="mr-2 h-2 w-2 rounded-full" className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }} style={{ background: color || "#bec2c8" }}
/> />
{name} {slug}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
); );
@@ -336,7 +336,7 @@ export const SecretItem = memo(
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel> <DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => { {tags.map((tag) => {
const { id: tagId, name, color } = tag; const { id: tagId, slug, color } = tag;
const isTagSelected = selectedTagsGroupById?.[tagId]; const isTagSelected = selectedTagsGroupById?.[tagId];
return ( return (
@@ -358,7 +358,7 @@ export const SecretItem = memo(
className="mr-2 h-2 w-2 rounded-full" className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }} style={{ background: color || "#bec2c8" }}
/> />
{name} {slug}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
); );
@@ -49,7 +49,6 @@ export const formSchema = z.object({
tags: z tags: z
.object({ .object({
id: z.string(), id: z.string(),
name: z.string(),
slug: z.string(), slug: z.string(),
tagColor: z.string().optional() tagColor: z.string().optional()
}) })
@@ -151,7 +151,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<Td className="border-r border-mineshaft-600">Tags</Td> <Td className="border-r border-mineshaft-600">Tags</Td>
{isModified && ( {isModified && (
<Td className="border-r border-mineshaft-600"> <Td className="border-r border-mineshaft-600">
{preSecret?.tags?.map(({ name, id: tagId, color }) => ( {preSecret?.tags?.map(({ slug, id: tagId, color }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-2"
key={`${preSecret.id}-${tagId}`} key={`${preSecret.id}-${tagId}`}
@@ -160,13 +160,13 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
className="h-3 w-3 rounded-full" className="h-3 w-3 rounded-full"
style={{ backgroundColor: color || "#bec2c8" }} style={{ backgroundColor: color || "#bec2c8" }}
/> />
<div className="text-sm">{name}</div> <div className="text-sm">{slug}</div>
</Tag> </Tag>
))} ))}
</Td> </Td>
)} )}
<Td> <Td>
{postSecret?.tags?.map(({ name, id: tagId, color }) => ( {postSecret?.tags?.map(({ slug, id: tagId, color }) => (
<Tag <Tag
className="flex w-min items-center space-x-2" className="flex w-min items-center space-x-2"
key={`${postSecret.id}-${tagId}`} key={`${postSecret.id}-${tagId}`}
@@ -175,7 +175,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
className="h-3 w-3 rounded-full" className="h-3 w-3 rounded-full"
style={{ backgroundColor: color || "#bec2c8" }} style={{ backgroundColor: color || "#bec2c8" }}
/> />
<div className="text-sm">{name}</div> <div className="text-sm">{slug}</div>
</Tag> </Tag>
))} ))}
</Td> </Td>
@@ -1,6 +1,7 @@
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { zodResolver } from "@hookform/resolvers/zod";
import * as yup from "yup"; import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2"; import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2";
@@ -8,11 +9,13 @@ import { useWorkspace } from "@app/context";
import { useCreateWsTag } from "@app/hooks/api"; import { useCreateWsTag } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({ const schema = z.object({
name: yup.string().required().label("Tag Name") slug: z.string().refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
})
}); });
export type FormData = yup.InferType<typeof schema>; export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["CreateSecretTag", "deleteTagConfirmation"]>; popUp: UsePopUpState<["CreateSecretTag", "deleteTagConfirmation"]>;
@@ -26,7 +29,6 @@ type Props = {
}; };
export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const createWsTag = useCreateWsTag(); const createWsTag = useCreateWsTag();
const { const {
@@ -35,17 +37,16 @@ export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }
handleSubmit, handleSubmit,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: yupResolver(schema) resolver: zodResolver(schema)
}); });
const onFormSubmit = async ({ name }: FormData) => { const onFormSubmit = async ({ slug }: FormData) => {
try { try {
if (!currentWorkspace?.id) return; if (!currentWorkspace?.id) return;
await createWsTag.mutateAsync({ await createWsTag.mutateAsync({
workspaceID: currentWorkspace?.id, workspaceID: currentWorkspace?.id,
tagName: name, tagSlug: slug,
tagSlug: name.replace(/\s+/g, " ").replace(" ", "_"),
tagColor: "" tagColor: ""
}); });
@@ -80,11 +81,11 @@ export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Controller
control={control} control={control}
name="name" name="slug"
defaultValue="" defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Tag Name" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Tag Slug" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="Type your tag name" /> <Input {...field} placeholder="Type your tag slug" />
</FormControl> </FormControl>
)} )}
/> />
@@ -40,7 +40,6 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
<Th>Tag</Th>
<Th>Slug</Th> <Th>Slug</Th>
<Th aria-label="button" /> <Th aria-label="button" />
</Tr> </Tr>
@@ -49,9 +48,8 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
{isLoading && <TableSkeleton columns={3} innerKey="secret-tags" />} {isLoading && <TableSkeleton columns={3} innerKey="secret-tags" />}
{!isLoading && {!isLoading &&
data && data &&
data.map(({ id, name, slug }) => ( data.map(({ id, slug }) => (
<Tr key={name}> <Tr key={id}>
<Td>{name}</Td>
<Td>{slug}</Td> <Td>{slug}</Td>
<Td className="flex items-center justify-end"> <Td className="flex items-center justify-end">
<ProjectPermissionCan <ProjectPermissionCan
@@ -62,7 +60,7 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
<IconButton <IconButton
onClick={() => onClick={() =>
handlePopUpOpen("deleteTagConfirmation", { handlePopUpOpen("deleteTagConfirmation", {
name, name: slug,
id id
}) })
} }