mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Reminders
This commit is contained in:
@@ -717,6 +717,8 @@ export const updateSecretHelper = async ({
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath,
|
||||
secretReminderCron,
|
||||
secretReminderNote,
|
||||
tags,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
@@ -781,6 +783,10 @@ export const updateSecretHelper = async ({
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
|
||||
secretReminderCron,
|
||||
secretReminderNote,
|
||||
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex: newSecretNameBlindIndex,
|
||||
secretKeyIV,
|
||||
|
||||
@@ -58,6 +58,10 @@ export interface UpdateSecretParams {
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
|
||||
secretReminderCron?: string | null;
|
||||
secretReminderNote?: string | null ;
|
||||
|
||||
skipMultilineEncoding?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@ export interface ISecret {
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: 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;
|
||||
algorithm: "aes-256-gcm";
|
||||
keyEncoding: "utf8" | "base64";
|
||||
@@ -118,10 +124,23 @@ const secretSchema = new Schema<ISecret>(
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
|
||||
secretReminderCron: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
secretReminderNote: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
|
||||
skipMultilineEncoding: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
|
||||
algorithm: {
|
||||
// the encryption algorithm used
|
||||
type: String,
|
||||
|
||||
60
backend/src/queues/reminders/sendSecretReminders.ts
Normal file
60
backend/src/queues/reminders/sendSecretReminders.ts
Normal file
@@ -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)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { AuthData } from "../interfaces/middleware";
|
||||
import { ActorType } from "../ee/models";
|
||||
import { z } from "zod";
|
||||
import { SECRET_PERSONAL, SECRET_SHARED } from "../variables";
|
||||
|
||||
import { isValidCron } from "cron-validator";
|
||||
/**
|
||||
* Validate authenticated clients for secrets with id [secretId] based
|
||||
* on any known permissions.
|
||||
@@ -260,6 +260,7 @@ export const CreateSecretRawV3 = z.object({
|
||||
.string()
|
||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
|
||||
secretComment: z.string().trim().optional().default(""),
|
||||
|
||||
skipMultilineEncoding: z.boolean().optional(),
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
|
||||
}),
|
||||
@@ -275,6 +276,7 @@ export const UpdateSecretByNameRawV3 = z.object({
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
|
||||
secretValue: z
|
||||
.string()
|
||||
.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(),
|
||||
secretCommentIV: 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(),
|
||||
skipMultilineEncoding: z.boolean().optional(),
|
||||
// to update secret name
|
||||
|
||||
@@ -139,6 +139,8 @@ export const useUpdateSecretV3 = ({
|
||||
latestFileKey,
|
||||
tags,
|
||||
secretComment,
|
||||
secretReminderCron,
|
||||
secretReminderNote,
|
||||
newSecretName,
|
||||
skipMultilineEncoding
|
||||
}) => {
|
||||
@@ -157,6 +159,8 @@ export const useUpdateSecretV3 = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretReminderNote,
|
||||
secretReminderCron,
|
||||
secretPath,
|
||||
secretId,
|
||||
...encryptSecret(randomBytes, newSecretName ?? secretName, secretValue, secretComment),
|
||||
|
||||
@@ -69,6 +69,8 @@ export const decryptSecrets = (
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
reminderCron: encSecret.secretReminderCron,
|
||||
reminderNote: encSecret.secretReminderNote,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt,
|
||||
version: encSecret.version,
|
||||
|
||||
@@ -20,6 +20,8 @@ export type EncryptedSecret = {
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
secretReminderCron?: string | null;
|
||||
secretReminderNote?: string | null;
|
||||
tags: WsTag[];
|
||||
};
|
||||
|
||||
@@ -29,6 +31,8 @@ export type DecryptedSecret = {
|
||||
key: string;
|
||||
value: string;
|
||||
comment: string;
|
||||
reminderCron?: string | null;
|
||||
reminderNote?: string | null;
|
||||
tags: WsTag[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -112,6 +116,8 @@ export type TUpdateSecretsV3DTO = {
|
||||
secretId?: string;
|
||||
secretValue: string;
|
||||
secretComment?: string;
|
||||
secretReminderCron?: string | null;
|
||||
secretReminderNote?: string | null;
|
||||
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 { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faCheck,
|
||||
faClock,
|
||||
faClose,
|
||||
faCodeBranch,
|
||||
faComment,
|
||||
@@ -17,7 +19,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
@@ -49,6 +50,8 @@ import { DecryptedSecret } from "@app/hooks/api/secrets/types";
|
||||
import { WsTag } from "@app/hooks/api/types";
|
||||
|
||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||
import { CreateReminderForm } from "./CreateReminderForm";
|
||||
|
||||
|
||||
type Props = {
|
||||
secret: DecryptedSecret;
|
||||
@@ -68,6 +71,8 @@ type Props = {
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const SecretItem = memo(
|
||||
({
|
||||
secret,
|
||||
@@ -111,6 +116,7 @@ export const SecretItem = memo(
|
||||
|
||||
const overrideAction = watch("overrideAction");
|
||||
const hasComment = Boolean(watch("comment"));
|
||||
const hasReminder = Boolean(watch("reminderCron"));
|
||||
|
||||
const selectedTags = watch("tags", []);
|
||||
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
||||
@@ -123,6 +129,7 @@ export const SecretItem = memo(
|
||||
});
|
||||
|
||||
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
|
||||
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isSecValueCopied) {
|
||||
@@ -181,17 +188,30 @@ export const SecretItem = memo(
|
||||
};
|
||||
|
||||
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)}>
|
||||
<div
|
||||
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"
|
||||
)}
|
||||
>
|
||||
<div className="flex group">
|
||||
<div className="group flex">
|
||||
<div
|
||||
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"
|
||||
)}
|
||||
>
|
||||
@@ -199,14 +219,14 @@ export const SecretItem = memo(
|
||||
id={`checkbox-${secret._id}`}
|
||||
isChecked={isSelected}
|
||||
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
|
||||
icon={faKey}
|
||||
className={twMerge("group-hover:hidden block ml-3", isSelected && "hidden")}
|
||||
className={twMerge("ml-3 block group-hover:hidden", isSelected && "hidden")}
|
||||
/>
|
||||
</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
|
||||
name="key"
|
||||
control={control}
|
||||
@@ -218,13 +238,13 @@ export const SecretItem = memo(
|
||||
variant="plain"
|
||||
isDisabled={isOverriden}
|
||||
{...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
|
||||
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}
|
||||
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">
|
||||
<IconButton
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
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}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSecValueCopied ? faCheck : faCopy} />
|
||||
@@ -283,7 +303,7 @@ export const SecretItem = memo(
|
||||
variant="plain"
|
||||
size="sm"
|
||||
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"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
@@ -310,7 +330,7 @@ export const SecretItem = memo(
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full mr-2"
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: tagColor || "#bec2c8" }}
|
||||
/>
|
||||
{name}
|
||||
@@ -346,7 +366,7 @@ export const SecretItem = memo(
|
||||
size="sm"
|
||||
onClick={handleOverrideClick}
|
||||
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"
|
||||
)}
|
||||
>
|
||||
@@ -354,6 +374,32 @@ export const SecretItem = memo(
|
||||
</IconButton>
|
||||
)}
|
||||
</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>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
@@ -363,7 +409,7 @@ export const SecretItem = memo(
|
||||
<PopoverTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
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"
|
||||
)}
|
||||
variant="plain"
|
||||
@@ -398,7 +444,7 @@ export const SecretItem = memo(
|
||||
{!isDirty ? (
|
||||
<motion.div
|
||||
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 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 10, opacity: 0 }}
|
||||
@@ -408,7 +454,7 @@ export const SecretItem = memo(
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="group-hover:opacity-100 opacity-0 p-0"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDetailViewSecret(secret)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsis} size="lg" />
|
||||
@@ -426,7 +472,7 @@ export const SecretItem = memo(
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="md"
|
||||
className="group-hover:opacity-100 opacity-0 p-0"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDeleteSecret(secret)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
@@ -438,7 +484,7 @@ export const SecretItem = memo(
|
||||
) : (
|
||||
<motion.div
|
||||
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 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -10, opacity: 0 }}
|
||||
@@ -450,13 +496,13 @@ export const SecretItem = memo(
|
||||
type="submit"
|
||||
size="md"
|
||||
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"
|
||||
)}
|
||||
isDisabled={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" />
|
||||
)}
|
||||
@@ -468,7 +514,7 @@ export const SecretItem = memo(
|
||||
variant="plain"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"group-hover:opacity-100 opacity-0 p-0",
|
||||
"p-0 opacity-0 group-hover:opacity-100",
|
||||
isDirty && "opacity-100"
|
||||
)}
|
||||
onClick={() => reset()}
|
||||
@@ -483,6 +529,7 @@ export const SecretItem = memo(
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -122,6 +122,8 @@ export const SecretListView = ({
|
||||
{
|
||||
value,
|
||||
comment,
|
||||
reminderCron,
|
||||
reminderNote,
|
||||
tags,
|
||||
skipMultilineEncoding,
|
||||
newKey,
|
||||
@@ -129,6 +131,8 @@ export const SecretListView = ({
|
||||
}: Partial<{
|
||||
value: string;
|
||||
comment: string;
|
||||
reminderCron: string | null;
|
||||
reminderNote: string | null;
|
||||
tags: string[];
|
||||
skipMultilineEncoding: boolean;
|
||||
newKey: string;
|
||||
@@ -159,6 +163,8 @@ export const SecretListView = ({
|
||||
latestFileKey: decryptFileKey,
|
||||
tags,
|
||||
secretComment: comment,
|
||||
secretReminderCron: reminderCron,
|
||||
secretReminderNote: reminderNote,
|
||||
skipMultilineEncoding,
|
||||
newSecretName: newKey
|
||||
});
|
||||
@@ -188,14 +194,14 @@ export const SecretListView = ({
|
||||
cb?: () => void
|
||||
) => {
|
||||
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 tagIds = tags.map(({ _id }) => _id);
|
||||
const oldTagIds = orgSecret.tags.map(({ _id }) => _id);
|
||||
const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds);
|
||||
const isSharedSecUnchanged =
|
||||
(["key", "value", "comment", "skipMultilineEncoding"] as const).every(
|
||||
(["key", "value", "comment", "skipMultilineEncoding", "reminderCron", "reminderNote"] as const).every(
|
||||
(el) => orgSecret[el] === modSecret[el]
|
||||
) && isSameTags;
|
||||
|
||||
@@ -222,13 +228,14 @@ export const SecretListView = ({
|
||||
value,
|
||||
tags: tagIds,
|
||||
comment,
|
||||
reminderCron,
|
||||
reminderNote,
|
||||
secretId: orgSecret._id,
|
||||
newKey: hasKeyChanged ? key : undefined,
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding
|
||||
});
|
||||
if (cb) cb();
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
|
||||
);
|
||||
|
||||
@@ -20,6 +20,10 @@ export const formSchema = z.object({
|
||||
overrideAction: z.string().trim().optional(),
|
||||
comment: z.string().trim().optional(),
|
||||
skipMultilineEncoding: z.boolean().optional(),
|
||||
|
||||
reminderCron: z.string().trim().nullable().optional(),
|
||||
reminderNote: z.string().trim().nullable().optional(),
|
||||
|
||||
tags: z
|
||||
.object({
|
||||
_id: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user