feat: integrated secret metadata to ui

This commit is contained in:
Sheen Capadngan
2024-12-19 22:02:20 +08:00
parent 8c98565715
commit ef688efc8d
13 changed files with 148 additions and 24 deletions
@@ -14,6 +14,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns"; import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue"; import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue";
@@ -154,8 +155,10 @@ export const secretReplicationServiceFactory = ({
}; };
const $getReplicatedSecretsV2 = ( const $getReplicatedSecretsV2 = (
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[], localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[],
importedSecrets: { secrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[] }[] importedSecrets: {
secrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[];
}[]
) => { ) => {
const deDupe = new Set<string>(); const deDupe = new Set<string>();
const secrets = [...localSecrets]; const secrets = [...localSecrets];
@@ -272,6 +275,7 @@ export const secretReplicationServiceFactory = ({
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined : undefined
})); }));
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets); const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key); const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);
@@ -426,6 +430,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue, encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment, encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding, skipMultilineEncoding: doc.skipMultilineEncoding,
secretMetadata: doc.secretMetadata,
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
}; };
}) })
@@ -454,6 +459,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue as Buffer, encryptedValue: doc.encryptedValue as Buffer,
encryptedComment: doc.encryptedComment, encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding, skipMultilineEncoding: doc.skipMultilineEncoding,
secretMetadata: doc.secretMetadata,
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
} }
}; };
@@ -222,7 +222,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string(), secretPath: z.string(),
environment: z.string(), environment: z.string(),
folderId: z.string().optional(), folderId: z.string().optional(),
secrets: secretRawSchema.omit({ createdAt: true, updatedAt: true }).array() secrets: secretRawSchema
.omit({ createdAt: true, updatedAt: true })
.extend({
secretMetadata: ResourceMetadataSchema.optional()
})
.array()
}) })
.array() .array()
.optional() .optional()
@@ -3,7 +3,7 @@ import z from "zod";
export const ResourceMetadataSchema = z export const ResourceMetadataSchema = z
.object({ .object({
key: z.string().trim().min(1), key: z.string().trim().min(1),
value: z.string().trim().min(1) value: z.string().trim().default("")
}) })
.array(); .array();
@@ -1,6 +1,7 @@
import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas"; import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas";
import { groupBy, unique } from "@app/lib/fn"; import { groupBy, unique } from "@app/lib/fn";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
@@ -39,6 +40,7 @@ type TSecretImportSecretsV2 = {
// But for somereason ts consider ? and undefined explicit as different just ts things // But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string; secretValue: string;
secretComment: string; secretComment: string;
secretMetadata?: ResourceMetadataDTO;
})[]; })[];
}; };
@@ -78,6 +78,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id` `${TableName.SecretTag}.id`
) )
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
)
.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"))
@@ -103,6 +109,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
slug, slug,
name: slug name: slug
}) })
},
{
key: "metadataId",
label: "secretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
} }
] ]
}); });
@@ -144,7 +144,7 @@ export const secretV2BridgeServiceFactory = ({
}, },
{ {
operator: "eq", operator: "eq",
field: "key", field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey value: el.secretKey
} }
] ]
@@ -1127,7 +1127,7 @@ export const secretV2BridgeServiceFactory = ({
value: [ value: [
{ {
operator: "eq", operator: "eq",
field: "key", field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey value: el.secretKey
}, },
{ {
@@ -1199,6 +1199,7 @@ export const secretV2BridgeServiceFactory = ({
key: el.secretKey, key: el.secretKey,
tagIds: el.tagIds, tagIds: el.tagIds,
references, references,
secretMetadata: el.secretMetadata,
type: SecretType.Shared type: SecretType.Shared
}; };
}), }),
@@ -1271,7 +1272,7 @@ export const secretV2BridgeServiceFactory = ({
value: [ value: [
{ {
operator: "eq", operator: "eq",
field: "key", field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey value: el.secretKey
}, },
{ {
@@ -1336,7 +1337,7 @@ export const secretV2BridgeServiceFactory = ({
value: [ value: [
{ {
operator: "eq", operator: "eq",
field: "key", field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey value: el.secretKey
}, },
{ {
@@ -1482,7 +1483,7 @@ export const secretV2BridgeServiceFactory = ({
value: [ value: [
{ {
operator: "eq", operator: "eq",
field: "key", field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey value: el.secretKey
}, },
{ {
@@ -1854,6 +1855,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: doc.skipMultilineEncoding, skipMultilineEncoding: doc.skipMultilineEncoding,
reminderNote: doc.reminderNote, reminderNote: doc.reminderNote,
reminderRepeatDays: doc.reminderRepeatDays, reminderRepeatDays: doc.reminderRepeatDays,
secretMetadata: doc.secretMetadata,
references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : [] references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : []
}; };
}) })
@@ -1881,6 +1883,7 @@ export const secretV2BridgeServiceFactory = ({
encryptedComment: doc.encryptedComment, encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding, skipMultilineEncoding: doc.skipMultilineEncoding,
reminderNote: doc.reminderNote, reminderNote: doc.reminderNote,
secretMetadata: doc.secretMetadata,
reminderRepeatDays: doc.reminderRepeatDays, reminderRepeatDays: doc.reminderRepeatDays,
...(doc.encryptedValue ...(doc.encryptedValue
? { ? {
@@ -98,6 +98,7 @@ export type TCreateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
secretComment?: string; secretComment?: string;
skipMultilineEncoding?: boolean; skipMultilineEncoding?: boolean;
tagIds?: string[]; tagIds?: string[];
secretMetadata?: ResourceMetadataDTO;
metadata?: { metadata?: {
source?: string; source?: string;
}; };
+4 -2
View File
@@ -84,7 +84,8 @@ export const useUpdateSecretV3 = ({
secretReminderRepeatDays, secretReminderRepeatDays,
secretReminderNote, secretReminderNote,
newSecretName, newSecretName,
skipMultilineEncoding skipMultilineEncoding,
secretMetadata
}) => { }) => {
const { data } = await apiRequest.patch(`/api/v3/secrets/raw/${secretKey}`, { const { data } = await apiRequest.patch(`/api/v3/secrets/raw/${secretKey}`, {
workspaceId, workspaceId,
@@ -97,7 +98,8 @@ export const useUpdateSecretV3 = ({
newSecretName, newSecretName,
secretComment, secretComment,
tagIds, tagIds,
secretValue secretValue,
secretMetadata
}); });
return data; return data;
}, },
+2 -1
View File
@@ -67,7 +67,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
updatedAt: el.updatedAt, updatedAt: el.updatedAt,
version: el.version, version: el.version,
skipMultilineEncoding: el.skipMultilineEncoding, skipMultilineEncoding: el.skipMultilineEncoding,
path: el.secretPath path: el.secretPath,
secretMetadata: el.secretMetadata
}; };
if (el.type === SecretType.Personal) { if (el.type === SecretType.Personal) {
+3
View File
@@ -48,6 +48,7 @@ export type SecretV3RawSanitized = {
overrideAction?: string; overrideAction?: string;
folderId?: string; folderId?: string;
skipMultilineEncoding?: boolean; skipMultilineEncoding?: boolean;
secretMetadata?: { key: string; value: string }[];
}; };
export type SecretV3Raw = { export type SecretV3Raw = {
@@ -63,6 +64,7 @@ export type SecretV3Raw = {
secretComment?: string; secretComment?: string;
secretReminderNote?: string; secretReminderNote?: string;
secretReminderRepeatDays?: number; secretReminderRepeatDays?: number;
secretMetadata?: { key: string; value: string }[];
skipMultilineEncoding?: boolean; skipMultilineEncoding?: boolean;
metadata?: Record<string, string>; metadata?: Record<string, string>;
tags?: WsTag[]; tags?: WsTag[];
@@ -148,6 +150,7 @@ export type TUpdateSecretsV3DTO = {
secretReminderRepeatDays?: number | null; secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null; secretReminderNote?: string | null;
tagIds?: string[]; tagIds?: string[];
secretMetadata?: { key: string; value: string }[];
}; };
export type TDeleteSecretsV3DTO = { export type TDeleteSecretsV3DTO = {
@@ -8,7 +8,8 @@ import {
faClock, faClock,
faPlus, faPlus,
faShare, faShare,
faTag faTag,
faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -26,6 +27,7 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
FormControl, FormControl,
FormLabel,
IconButton, IconButton,
Input, Input,
Switch, Switch,
@@ -87,11 +89,16 @@ export const SecretDetailSidebar = ({
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const { fields, append, remove } = useFieldArray({ const tagFields = useFieldArray({
control, control,
name: "tags" name: "tags"
}); });
const metadataFormFields = useFieldArray({
control,
name: "secretMetadata"
});
const secretKey = secret?.key || ""; const secretKey = secret?.key || "";
const selectedTags = watch("tags", []) || []; const selectedTags = watch("tags", []) || [];
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>( const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
@@ -153,10 +160,10 @@ export const SecretDetailSidebar = ({
if (selectedTagsGroupById?.[tag.id]) { if (selectedTagsGroupById?.[tag.id]) {
const tagPos = selectedTags.findIndex(({ id }) => id === tag.id); const tagPos = selectedTags.findIndex(({ id }) => id === tag.id);
if (tagPos !== -1) { if (tagPos !== -1) {
remove(tagPos); tagFields.remove(tagPos);
} }
} else { } else {
append(tag); tagFields.append(tag);
} }
}; };
@@ -277,9 +284,73 @@ export const SecretDetailSidebar = ({
)} )}
/> />
)} )}
<FormControl label="Metadata">
<div className="flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`secretMetadata.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<div className="flex-grow">
{i === 0 && (
<FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)}
<Controller
control={control}
name={`secretMetadata.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 max-h-8"
variant="outline_bg"
onClick={() => metadataFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div className="mt-2">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
Add Key
</Button>
</div>
</div>
</FormControl>
<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, slug, id }) => ( {tagFields.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}
@@ -78,7 +78,8 @@ export const SecretListView = ({
tags, tags,
skipMultilineEncoding, skipMultilineEncoding,
newKey, newKey,
secretId secretId,
secretMetadata
}: Partial<{ }: Partial<{
value: string; value: string;
comment: string; comment: string;
@@ -88,6 +89,7 @@ export const SecretListView = ({
skipMultilineEncoding: boolean; skipMultilineEncoding: boolean;
newKey: string; newKey: string;
secretId: string; secretId: string;
secretMetadata?: { key: string; value: string }[];
}> = {} }> = {}
) => { ) => {
if (operation === "delete") { if (operation === "delete") {
@@ -115,7 +117,8 @@ export const SecretListView = ({
secretReminderRepeatDays: reminderRepeatDays, secretReminderRepeatDays: reminderRepeatDays,
secretReminderNote: reminderNote, secretReminderNote: reminderNote,
skipMultilineEncoding, skipMultilineEncoding,
newSecretName: newKey newSecretName: newKey,
secretMetadata
}); });
return; return;
} }
@@ -138,7 +141,10 @@ export const SecretListView = ({
const handleSaveSecret = useCallback( const handleSaveSecret = useCallback(
async ( async (
orgSecret: SecretV3RawSanitized, orgSecret: SecretV3RawSanitized,
modSecret: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] }, modSecret: Omit<SecretV3RawSanitized, "tags"> & {
tags?: { id: string }[];
secretMetadata?: { key: string; value: string }[];
},
cb?: () => void cb?: () => void
) => { ) => {
const { key: oldKey } = orgSecret; const { key: oldKey } = orgSecret;
@@ -151,7 +157,8 @@ export const SecretListView = ({
tags, tags,
comment, comment,
reminderRepeatDays, reminderRepeatDays,
reminderNote reminderNote,
secretMetadata
} = modSecret; } = modSecret;
const hasKeyChanged = oldKey !== key && key; const hasKeyChanged = oldKey !== key && key;
@@ -166,7 +173,8 @@ export const SecretListView = ({
"comment", "comment",
"skipMultilineEncoding", "skipMultilineEncoding",
"reminderRepeatDays", "reminderRepeatDays",
"reminderNote" "reminderNote",
"secretMetadata"
] as const ] as const
).every((el) => orgSecret[el] === modSecret[el]) && isSameTags; ).every((el) => orgSecret[el] === modSecret[el]) && isSameTags;
@@ -199,7 +207,8 @@ export const SecretListView = ({
reminderNote, reminderNote,
secretId: orgSecret.id, secretId: orgSecret.id,
newKey: hasKeyChanged ? key : undefined, newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding skipMultilineEncoding: modSecret.skipMultilineEncoding,
secretMetadata
}); });
if (cb) cb(); if (cb) cb();
} }
@@ -47,7 +47,13 @@ export const formSchema = z.object({
.nullable() .nullable()
.optional(), .optional(),
reminderNote: z.string().trim().nullable().optional(), reminderNote: z.string().trim().nullable().optional(),
secretMetadata: z
.object({
key: z.string().trim().min(1),
value: z.string().trim().default("")
})
.array()
.optional(),
tags: z tags: z
.object({ .object({
id: z.string(), id: z.string(),