Reminders

This commit is contained in:
Daniel Hougaard
2023-11-28 00:54:55 +04:00
parent f289b99cf1
commit 33fc968055
12 changed files with 310 additions and 26 deletions
+6
View File
@@ -717,6 +717,8 @@ export const updateSecretHelper = async ({
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath, secretPath,
secretReminderCron,
secretReminderNote,
tags, tags,
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
@@ -781,6 +783,10 @@ export const updateSecretHelper = async ({
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretCommentCiphertext, secretCommentCiphertext,
secretReminderCron,
secretReminderNote,
skipMultilineEncoding, skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex, secretBlindIndex: newSecretNameBlindIndex,
secretKeyIV, secretKeyIV,
@@ -58,6 +58,10 @@ export interface UpdateSecretParams {
secretCommentCiphertext?: string; secretCommentCiphertext?: string;
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretReminderCron?: string | null;
secretReminderNote?: string | null ;
skipMultilineEncoding?: boolean; skipMultilineEncoding?: boolean;
tags?: string[]; tags?: string[];
} }
+19
View File
@@ -27,6 +27,12 @@ export interface ISecret {
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
// ? QUESTION: This works great for workspace-level reminders.
// ? If we want to do it on a user-basis, we should ideally have a seperate model for reminders.
secretReminderCron?: string | null;
secretReminderNote?: string | null;
skipMultilineEncoding?: boolean; skipMultilineEncoding?: boolean;
algorithm: "aes-256-gcm"; algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64"; keyEncoding: "utf8" | "base64";
@@ -118,10 +124,23 @@ const secretSchema = new Schema<ISecret>(
type: String, type: String,
required: false required: false
}, },
secretReminderCron: {
type: String,
required: false,
default: null
},
secretReminderNote: {
type: String,
required: false,
default: null
},
skipMultilineEncoding: { skipMultilineEncoding: {
type: Boolean, type: Boolean,
required: false required: false
}, },
algorithm: { algorithm: {
// the encryption algorithm used // the encryption algorithm used
type: String, type: String,
@@ -0,0 +1,60 @@
import Queue, { Job } from "bull";
import { Secret, Workspace } from "../../models";
import { Types } from "mongoose";
type TSendSecretReminders = {
workspaceId: string
secretId: string
cron: string
note: string | undefined | null
}
type TDeleteSecretReminder = {
secretId: string
cron: string
}
export const sendSecretReminders = new Queue("send-secret-reminders", process.env.REDIS_URL as string);
sendSecretReminders.process(async (job: Job) => {
const { workspaceId, secretId }: TSendSecretReminders = job.data
const secret = await Secret.findById(new Types.ObjectId(secretId));
const workspace = await Workspace.findById(new Types.ObjectId(workspaceId));
if(!workspace || !secret) {
throw new Error("Workspace or secret not found")
}
// Send email stuff here
})
export const createSecretReminderCron = (jobDetails: TSendSecretReminders) => {
return sendSecretReminders.add(jobDetails, {
repeat: {
cron: jobDetails.cron
},
jobId: `reminder-${jobDetails.secretId}`,
})
}
export const deleteSecretReminderCron = (jobDetails: TDeleteSecretReminder) => {
return sendSecretReminders.removeRepeatable({
cron: jobDetails.cron,
"jobId": `reminder-${jobDetails.secretId}`,
})
}
export const updateSecretReminderCron = async (jobDetails: TSendSecretReminders) => {
// We need to delete the potentially existing cron job first, or the new one won't be created.
await deleteSecretReminderCron(jobDetails)
await createSecretReminderCron(jobDetails)
}
+12 -1
View File
@@ -10,7 +10,7 @@ import { AuthData } from "../interfaces/middleware";
import { ActorType } from "../ee/models"; import { ActorType } from "../ee/models";
import { z } from "zod"; import { z } from "zod";
import { SECRET_PERSONAL, SECRET_SHARED } from "../variables"; import { SECRET_PERSONAL, SECRET_SHARED } from "../variables";
import { isValidCron } from "cron-validator";
/** /**
* Validate authenticated clients for secrets with id [secretId] based * Validate authenticated clients for secrets with id [secretId] based
* on any known permissions. * on any known permissions.
@@ -260,6 +260,7 @@ export const CreateSecretRawV3 = z.object({
.string() .string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())), .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
secretComment: z.string().trim().optional().default(""), secretComment: z.string().trim().optional().default(""),
skipMultilineEncoding: z.boolean().optional(), skipMultilineEncoding: z.boolean().optional(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]) type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
}), }),
@@ -275,6 +276,7 @@ export const UpdateSecretByNameRawV3 = z.object({
body: z.object({ body: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
secretValue: z secretValue: z
.string() .string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())), .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
@@ -360,6 +362,15 @@ export const UpdateSecretByNameV3 = z.object({
secretCommentCiphertext: z.string().trim().optional(), secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(), secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(), secretCommentTag: z.string().trim().optional(),
secretReminderCron: z
.string()
.trim()
.optional()
.nullable()
.refine((val) => val === null || (val && isValidCron(val))),
secretReminderNote: z.string().trim().nullable().optional(),
tags: z.string().array().optional(), tags: z.string().array().optional(),
skipMultilineEncoding: z.boolean().optional(), skipMultilineEncoding: z.boolean().optional(),
// to update secret name // to update secret name
@@ -139,6 +139,8 @@ export const useUpdateSecretV3 = ({
latestFileKey, latestFileKey,
tags, tags,
secretComment, secretComment,
secretReminderCron,
secretReminderNote,
newSecretName, newSecretName,
skipMultilineEncoding skipMultilineEncoding
}) => { }) => {
@@ -157,6 +159,8 @@ export const useUpdateSecretV3 = ({
workspaceId, workspaceId,
environment, environment,
type, type,
secretReminderNote,
secretReminderCron,
secretPath, secretPath,
secretId, secretId,
...encryptSecret(randomBytes, newSecretName ?? secretName, secretValue, secretComment), ...encryptSecret(randomBytes, newSecretName ?? secretName, secretValue, secretComment),
@@ -69,6 +69,8 @@ export const decryptSecrets = (
value: secretValue, value: secretValue,
tags: encSecret.tags, tags: encSecret.tags,
comment: secretComment, comment: secretComment,
reminderCron: encSecret.secretReminderCron,
reminderNote: encSecret.secretReminderNote,
createdAt: encSecret.createdAt, createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt, updatedAt: encSecret.updatedAt,
version: encSecret.version, version: encSecret.version,
+6
View File
@@ -20,6 +20,8 @@ export type EncryptedSecret = {
secretCommentCiphertext: string; secretCommentCiphertext: string;
secretCommentIV: string; secretCommentIV: string;
secretCommentTag: string; secretCommentTag: string;
secretReminderCron?: string | null;
secretReminderNote?: string | null;
tags: WsTag[]; tags: WsTag[];
}; };
@@ -29,6 +31,8 @@ export type DecryptedSecret = {
key: string; key: string;
value: string; value: string;
comment: string; comment: string;
reminderCron?: string | null;
reminderNote?: string | null;
tags: WsTag[]; tags: WsTag[];
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -112,6 +116,8 @@ export type TUpdateSecretsV3DTO = {
secretId?: string; secretId?: string;
secretValue: string; secretValue: string;
secretComment?: string; secretComment?: string;
secretReminderCron?: string | null;
secretReminderNote?: string | null;
tags?: string[]; tags?: string[];
}; };
@@ -0,0 +1,114 @@
import { useForm } from "react-hook-form";
import { faClock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { isValidCron } from "cron-validator";
import cronstrue from "cronstrue";
import { z } from "zod";
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2";
interface ReminderFormProps {
isOpen: boolean;
onClose: (data?: {cron: string, note?: string}) => void;
}
const ReminderFormSchema = z.object({
note: z.string().optional(),
cron: z.string().refine(isValidCron, {message: "Invalid cron expression"})
});
type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
export const CreateReminderForm = ({isOpen, onClose}: ReminderFormProps) => {
const {
register,
watch,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<TReminderFormSchema>({ resolver: zodResolver(ReminderFormSchema) });
const cronWatch = watch("cron");
const handleFormSubmit = async (data: TReminderFormSchema) => {
return onClose(data);
}
return (
<Modal
isOpen={isOpen}
onOpenChange={(state) => !state && onClose()}
>
<ModalContent
title="Create secret reminder"
// ? QUESTION: Should this specifically say its for secret rotation?
// ? Or should we be call it something more generic?
subTitle={
<div>
Set up a reminder for when this secret should be rotated.
<div>
Format is in{" "}
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a target='_blank' href="https://crontab.guru/every-month">
<span className="text-primary-400 hover:text-primary-500 cursor-pointer">
cron format.
</span>
</a>
</div>
</div>
}
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-4">
<div>
<FormControl className="mb-0" label="How often" isError={Boolean(errors?.cron)} errorText={errors?.cron?.message}>
<Input
{...register("cron")}
placeholder="0 0 1 * *"
/>
</FormControl>
{!!cronWatch && isValidCron(cronWatch) && (
<div className="text-xs opacity-60 mt-2 ml-1">{cronstrue.toString(cronWatch)}</div>
)}
</div>
<FormControl label="Note" className="mb-0">
<TextArea
placeholder="Remember to rotate the AWS secret every month."
className="border border-mineshaft-600 text-sm"
rows={8}
reSize='none'
cols={30}
{...register("note")}
/>
</FormControl>
</div>
<div className="mt-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="mr-4"
leftIcon={<FontAwesomeIcon icon={faClock}/>}
type="submit"
>
Create reminder
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => onClose()}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
@@ -1,9 +1,11 @@
/* eslint-disable simple-import-sort/imports */
import { memo, useEffect } from "react"; import { memo, useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { import {
faCheck, faCheck,
faClock,
faClose, faClose,
faCodeBranch, faCodeBranch,
faComment, faComment,
@@ -17,7 +19,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
@@ -49,6 +50,8 @@ import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { WsTag } from "@app/hooks/api/types"; import { WsTag } from "@app/hooks/api/types";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
import { CreateReminderForm } from "./CreateReminderForm";
type Props = { type Props = {
secret: DecryptedSecret; secret: DecryptedSecret;
@@ -68,6 +71,8 @@ type Props = {
secretPath: string; secretPath: string;
}; };
export const SecretItem = memo( export const SecretItem = memo(
({ ({
secret, secret,
@@ -111,6 +116,7 @@ export const SecretItem = memo(
const overrideAction = watch("overrideAction"); const overrideAction = watch("overrideAction");
const hasComment = Boolean(watch("comment")); const hasComment = Boolean(watch("comment"));
const hasReminder = Boolean(watch("reminderCron"));
const selectedTags = watch("tags", []); const selectedTags = watch("tags", []);
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>( const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
@@ -123,6 +129,7 @@ export const SecretItem = memo(
}); });
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
if (isSecValueCopied) { if (isSecValueCopied) {
@@ -181,17 +188,30 @@ export const SecretItem = memo(
}; };
return ( return (
<>
<CreateReminderForm
isOpen={createReminderFormOpen}
onClose={data => {
if(data) {
setValue("reminderCron", data.cron, {shouldDirty: true});
setValue("reminderNote", data.note, {shouldDirty: true});
}
setCreateReminderFormOpen.off();
}}
/>
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
<div <div
className={twMerge( className={twMerge(
"shadow-none border-b border-mineshaft-600 bg-mineshaft-800 hover:bg-mineshaft-700", "border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700",
isDirty && "border-primary-400/50" isDirty && "border-primary-400/50"
)} )}
> >
<div className="flex group"> <div className="group flex">
<div <div
className={twMerge( className={twMerge(
"flex items-center justify-center w-11 px-4 py-3 h-11", "flex h-11 w-11 items-center justify-center px-4 py-3",
isDirty && "text-primary" isDirty && "text-primary"
)} )}
> >
@@ -199,14 +219,14 @@ export const SecretItem = memo(
id={`checkbox-${secret._id}`} id={`checkbox-${secret._id}`}
isChecked={isSelected} isChecked={isSelected}
onCheckedChange={() => onToggleSecretSelect(secret._id)} onCheckedChange={() => onToggleSecretSelect(secret._id)}
className={twMerge("group-hover:flex hidden ml-3", isSelected && "flex")} className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
/> />
<FontAwesomeIcon <FontAwesomeIcon
icon={faKey} icon={faKey}
className={twMerge("group-hover:hidden block ml-3", isSelected && "hidden")} className={twMerge("ml-3 block group-hover:hidden", isSelected && "hidden")}
/> />
</div> </div>
<div className="w-80 h-11 flex items-center px-4 py-2 flex-shrink-0"> <div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
<Controller <Controller
name="key" name="key"
control={control} control={control}
@@ -218,13 +238,13 @@ export const SecretItem = memo(
variant="plain" variant="plain"
isDisabled={isOverriden} isDisabled={isOverriden}
{...field} {...field}
className="w-full focus:text-bunker-100 focus:ring-transparent px-0" className="w-full px-0 focus:text-bunker-100 focus:ring-transparent"
/> />
)} )}
/> />
</div> </div>
<div <div
className="flex-grow flex items-center border-x border-mineshaft-600 pl-4 pr-2 py-1" className="flex flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
tabIndex={0} tabIndex={0}
role="button" role="button"
> >
@@ -259,13 +279,13 @@ export const SecretItem = memo(
)} )}
/> />
)} )}
<div key="actions" className="h-8 flex self-start flex-shrink-0 transition-all"> <div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all">
<Tooltip content="Copy secret"> <Tooltip content="Copy secret">
<IconButton <IconButton
ariaLabel="copy-value" ariaLabel="copy-value"
variant="plain" variant="plain"
size="sm" size="sm"
className="w-0 group-hover:w-5 group-hover:mr-2 overflow-hidden p-0" className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5"
onClick={copyTokenToClipboard} onClick={copyTokenToClipboard}
> >
<FontAwesomeIcon icon={isSecValueCopied ? faCheck : faCopy} /> <FontAwesomeIcon icon={isSecValueCopied ? faCheck : faCopy} />
@@ -283,7 +303,7 @@ export const SecretItem = memo(
variant="plain" variant="plain"
size="sm" size="sm"
className={twMerge( className={twMerge(
"w-0 group-hover:w-5 group-hover:mr-2 overflow-hidden p-0 data-[state=open]:w-5", "w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-5",
hasTagsApplied && "w-5 text-primary" hasTagsApplied && "w-5 text-primary"
)} )}
isDisabled={!isAllowed} isDisabled={!isAllowed}
@@ -310,7 +330,7 @@ export const SecretItem = memo(
> >
<div className="flex items-center"> <div className="flex items-center">
<div <div
className="w-2 h-2 rounded-full mr-2" className="mr-2 h-2 w-2 rounded-full"
style={{ background: tagColor || "#bec2c8" }} style={{ background: tagColor || "#bec2c8" }}
/> />
{name} {name}
@@ -346,7 +366,7 @@ export const SecretItem = memo(
size="sm" size="sm"
onClick={handleOverrideClick} onClick={handleOverrideClick}
className={twMerge( className={twMerge(
"w-0 group-hover:w-5 group-hover:mr-2 overflow-hidden p-0", "w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5",
isOverriden && "w-5 text-primary" isOverriden && "w-5 text-primary"
)} )}
> >
@@ -354,6 +374,32 @@ export const SecretItem = memo(
</IconButton> </IconButton>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<IconButton
className={twMerge(
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
hasReminder && "w-5 text-primary"
)}
variant="plain"
size="md"
ariaLabel="add-reminder"
>
<Tooltip content="Reminder">
<FontAwesomeIcon
onClick={() => {
if(!hasReminder) {
setCreateReminderFormOpen.on();
}
else {
setValue("reminderCron", null, {shouldDirty: true});
setValue("reminderNote", null, {shouldDirty: true});
}
}}
icon={faClock} />
</Tooltip>
</IconButton>
<Popover> <Popover>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
@@ -363,7 +409,7 @@ export const SecretItem = memo(
<PopoverTrigger asChild disabled={!isAllowed}> <PopoverTrigger asChild disabled={!isAllowed}>
<IconButton <IconButton
className={twMerge( className={twMerge(
"overflow-hidden w-0 p-0 group-hover:w-5 group-hover:mr-2 data-[state=open]:w-6", "w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
hasComment && "w-5 text-primary" hasComment && "w-5 text-primary"
)} )}
variant="plain" variant="plain"
@@ -398,7 +444,7 @@ export const SecretItem = memo(
{!isDirty ? ( {!isDirty ? (
<motion.div <motion.div
key="options" key="options"
className="h-10 flex items-center space-x-4 flex-shrink-0 px-3" className="flex h-10 flex-shrink-0 items-center space-x-4 px-3"
initial={{ x: 0, opacity: 0 }} initial={{ x: 0, opacity: 0 }}
animate={{ x: 0, opacity: 1 }} animate={{ x: 0, opacity: 1 }}
exit={{ x: 10, opacity: 0 }} exit={{ x: 10, opacity: 0 }}
@@ -408,7 +454,7 @@ export const SecretItem = memo(
ariaLabel="more" ariaLabel="more"
variant="plain" variant="plain"
size="md" size="md"
className="group-hover:opacity-100 opacity-0 p-0" className="p-0 opacity-0 group-hover:opacity-100"
onClick={() => onDetailViewSecret(secret)} onClick={() => onDetailViewSecret(secret)}
> >
<FontAwesomeIcon icon={faEllipsis} size="lg" /> <FontAwesomeIcon icon={faEllipsis} size="lg" />
@@ -426,7 +472,7 @@ export const SecretItem = memo(
variant="plain" variant="plain"
colorSchema="danger" colorSchema="danger"
size="md" size="md"
className="group-hover:opacity-100 opacity-0 p-0" className="p-0 opacity-0 group-hover:opacity-100"
onClick={() => onDeleteSecret(secret)} onClick={() => onDeleteSecret(secret)}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
@@ -438,7 +484,7 @@ export const SecretItem = memo(
) : ( ) : (
<motion.div <motion.div
key="options-save" key="options-save"
className="h-10 flex items-center space-x-4 flex-shrink-0 px-3" className="flex h-10 flex-shrink-0 items-center space-x-4 px-3"
initial={{ x: -10, opacity: 0 }} initial={{ x: -10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }} animate={{ x: 0, opacity: 1 }}
exit={{ x: -10, opacity: 0 }} exit={{ x: -10, opacity: 0 }}
@@ -450,13 +496,13 @@ export const SecretItem = memo(
type="submit" type="submit"
size="md" size="md"
className={twMerge( className={twMerge(
"group-hover:opacity-100 opacity-0 p-0 text-primary", "p-0 text-primary opacity-0 group-hover:opacity-100",
isDirty && "opacity-100" isDirty && "opacity-100"
)} )}
isDisabled={isSubmitting} isDisabled={isSubmitting}
> >
{isSubmitting ? ( {isSubmitting ? (
<Spinner className="w-4 h-4 p-0 m-0" /> <Spinner className="m-0 h-4 w-4 p-0" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} size="lg" className="text-primary" /> <FontAwesomeIcon icon={faCheck} size="lg" className="text-primary" />
)} )}
@@ -468,7 +514,7 @@ export const SecretItem = memo(
variant="plain" variant="plain"
size="md" size="md"
className={twMerge( className={twMerge(
"group-hover:opacity-100 opacity-0 p-0", "p-0 opacity-0 group-hover:opacity-100",
isDirty && "opacity-100" isDirty && "opacity-100"
)} )}
onClick={() => reset()} onClick={() => reset()}
@@ -483,6 +529,7 @@ export const SecretItem = memo(
</div> </div>
</div> </div>
</form> </form>
</>
); );
} }
); );
@@ -122,6 +122,8 @@ export const SecretListView = ({
{ {
value, value,
comment, comment,
reminderCron,
reminderNote,
tags, tags,
skipMultilineEncoding, skipMultilineEncoding,
newKey, newKey,
@@ -129,6 +131,8 @@ export const SecretListView = ({
}: Partial<{ }: Partial<{
value: string; value: string;
comment: string; comment: string;
reminderCron: string | null;
reminderNote: string | null;
tags: string[]; tags: string[];
skipMultilineEncoding: boolean; skipMultilineEncoding: boolean;
newKey: string; newKey: string;
@@ -159,6 +163,8 @@ export const SecretListView = ({
latestFileKey: decryptFileKey, latestFileKey: decryptFileKey,
tags, tags,
secretComment: comment, secretComment: comment,
secretReminderCron: reminderCron,
secretReminderNote: reminderNote,
skipMultilineEncoding, skipMultilineEncoding,
newSecretName: newKey newSecretName: newKey
}); });
@@ -188,14 +194,14 @@ export const SecretListView = ({
cb?: () => void cb?: () => void
) => { ) => {
const { key: oldKey } = orgSecret; const { key: oldKey } = orgSecret;
const { key, value, overrideAction, idOverride, valueOverride, tags, comment } = modSecret; const { key, value, overrideAction, idOverride, valueOverride, tags, comment, reminderCron, reminderNote } = modSecret;
const hasKeyChanged = oldKey !== key; const hasKeyChanged = oldKey !== key;
const tagIds = tags.map(({ _id }) => _id); const tagIds = tags.map(({ _id }) => _id);
const oldTagIds = orgSecret.tags.map(({ _id }) => _id); const oldTagIds = orgSecret.tags.map(({ _id }) => _id);
const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds); const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds);
const isSharedSecUnchanged = const isSharedSecUnchanged =
(["key", "value", "comment", "skipMultilineEncoding"] as const).every( (["key", "value", "comment", "skipMultilineEncoding", "reminderCron", "reminderNote"] as const).every(
(el) => orgSecret[el] === modSecret[el] (el) => orgSecret[el] === modSecret[el]
) && isSameTags; ) && isSameTags;
@@ -222,13 +228,14 @@ export const SecretListView = ({
value, value,
tags: tagIds, tags: tagIds,
comment, comment,
reminderCron,
reminderNote,
secretId: orgSecret._id, secretId: orgSecret._id,
newKey: hasKeyChanged ? key : undefined, newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding skipMultilineEncoding: modSecret.skipMultilineEncoding
}); });
if (cb) cb(); if (cb) cb();
} }
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
); );
@@ -20,6 +20,10 @@ export const formSchema = z.object({
overrideAction: z.string().trim().optional(), overrideAction: z.string().trim().optional(),
comment: z.string().trim().optional(), comment: z.string().trim().optional(),
skipMultilineEncoding: z.boolean().optional(), skipMultilineEncoding: z.boolean().optional(),
reminderCron: z.string().trim().nullable().optional(),
reminderNote: z.string().trim().nullable().optional(),
tags: z tags: z
.object({ .object({
_id: z.string(), _id: z.string(),