Merge branch 'main' into feature/secret-reference-shortcut

This commit is contained in:
Victor Santos
2025-11-07 12:26:06 -03:00
49 changed files with 1099 additions and 410 deletions

View File

@@ -159,8 +159,8 @@ export type TCreateCertificateDTO = {
ttl: string; // string compatible with ms
notBefore?: string;
notAfter?: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
keyUsages: string[];
extendedKeyUsages: string[];
};
export type TCreateCertificateResponse = {

View File

@@ -90,7 +90,12 @@ export const useCreateCertTemplateV2 = () => {
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
queryClient.invalidateQueries({
predicate: (query) => {
const [firstKey, queryProjectId] = query.queryKey;
return firstKey === "list-template" && queryProjectId === projectId;
}
});
}
});
};
@@ -107,7 +112,12 @@ export const useUpdateCertTemplateV2 = () => {
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
queryClient.invalidateQueries({
predicate: (query) => {
const [firstKey, queryProjectId] = query.queryKey;
return firstKey === "list-template" && queryProjectId === projectId;
}
});
}
});
};
@@ -127,7 +137,12 @@ export const useDeleteCertTemplateV2 = () => {
return data.certificateTemplate;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
queryClient.invalidateQueries({
predicate: (query) => {
const [firstKey, queryProjectId] = query.queryKey;
return firstKey === "list-template" && queryProjectId === projectId;
}
});
}
});
};

View File

@@ -109,6 +109,7 @@ export type SecretVersions = {
actorType?: string | null;
name?: string | null;
membershipId?: string | null;
groupId?: string | null;
} | null;
};

View File

@@ -94,9 +94,9 @@ const createSchema = (shouldShowSubjectSection: boolean) => {
export type FormData = z.infer<ReturnType<typeof createSchema>>;
type Props = {
popUp: UsePopUpState<["certificateIssuance"]>;
popUp: UsePopUpState<["issueCertificate"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["certificateIssuance"]>,
popUpName: keyof UsePopUpState<["issueCertificate"]>,
state?: boolean
) => void;
profileId?: string;
@@ -115,7 +115,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const { currentProject } = useProject();
const inputSerialNumber =
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || "";
(popUp?.issueCertificate?.data as { serialNumber: string })?.serialNumber || "";
const sanitizedSerialNumber = inputSerialNumber.replace(/[^a-fA-F0-9:]/g, "");
const { data: cert } = useGetCert(sanitizedSerialNumber);
@@ -181,7 +181,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
} = useCertificateTemplate(
templateData,
actualSelectedProfile,
popUp?.certificateIssuance?.isOpen || false,
popUp?.issueCertificate?.isOpen || false,
setValue,
watch
);
@@ -227,10 +227,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
}, [cert, reset]);
useEffect(() => {
if (popUp?.certificateIssuance?.isOpen && profileId && !cert) {
if (popUp?.issueCertificate?.isOpen && profileId && !cert) {
setValue("profileId", profileId);
}
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
}, [popUp?.issueCertificate?.isOpen, profileId, cert, setValue]);
const onFormSubmit = useCallback(
async ({
@@ -332,9 +332,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
return (
<Modal
isOpen={popUp?.certificateIssuance?.isOpen}
isOpen={popUp?.issueCertificate?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("certificateIssuance", isOpen);
handlePopUpToggle("issueCertificate", isOpen);
if (!isOpen) {
resetAllState();
}
@@ -503,7 +503,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
colorSchema="secondary"
variant="plain"
onClick={() => {
handlePopUpToggle("certificateIssuance", false);
handlePopUpToggle("issueCertificate", false);
}}
>
Cancel

View File

@@ -75,6 +75,7 @@ export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["certificate"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void;
preselectedTemplate?: { id: string; name: string };
};
type TCertificateDetails = {
@@ -86,7 +87,7 @@ type TCertificateDetails = {
const CERT_TEMPLATE_NONE_VALUE = "none";
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
export const CertificateModal = ({ popUp, handlePopUpToggle, preselectedTemplate }: Props) => {
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const { currentProject } = useProject();
const { data: cert } = useGetCert(
@@ -147,13 +148,15 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
(cert.extendedKeyUsages || []).map((name) => [name, true])
)
});
} else {
} else if (popUp?.certificate?.isOpen) {
const templateId = preselectedTemplate?.id || CERT_TEMPLATE_NONE_VALUE;
reset({
caId: "",
commonName: "",
subjectAltNames: "",
ttl: "",
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE,
certificateTemplateId: templateId,
keyUsages: {
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true
@@ -161,7 +164,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
extendedKeyUsages: {}
});
}
}, [cert]);
}, [cert, preselectedTemplate, popUp?.certificate?.isOpen]);
useEffect(() => {
if (!cert && selectedCertTemplate) {
@@ -198,10 +201,14 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
.map(([key]) =>
key === CertKeyUsage.CRL_SIGN
? "cRLSign"
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
.map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
});
reset();
@@ -269,15 +276,25 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
isRequired
>
<Select
defaultValue={field.value}
{...field}
value={field.value}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(cert)}
isDisabled={Boolean(cert) || Boolean(preselectedTemplate)}
>
<SelectItem value={CERT_TEMPLATE_NONE_VALUE} key="cert-template-none">
None
</SelectItem>
{preselectedTemplate &&
!templatesData?.certificateTemplates?.find(
(t) => t.id === preselectedTemplate.id
) && (
<SelectItem
value={preselectedTemplate.id}
key={`cert-template-preselected-${preselectedTemplate.id}`}
>
{preselectedTemplate.name}
</SelectItem>
)}
{(templatesData?.certificateTemplates || []).map(({ id, name }) => (
<SelectItem value={id} key={`cert-template-${id}`}>
{name}

View File

@@ -17,7 +17,6 @@ import { CertificateImportModal } from "./CertificateImportModal";
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal";
import { CertificateModal } from "./CertificateModal";
import { CertificateRenewalModal } from "./CertificateRenewalModal";
import { CertificateRevocationModal } from "./CertificateRevocationModal";
import { CertificatesTable } from "./CertificatesTable";
@@ -26,12 +25,8 @@ export const CertificatesSection = () => {
const { currentProject } = useProject();
const { mutateAsync: deleteCert } = useDeleteCert();
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
const isLegacyTemplatesEnabled = true;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"certificateIssuance",
"certificate",
"issueCertificate",
"certificateImport",
"certificateCert",
"deleteCertificate",
@@ -76,9 +71,7 @@ export const CertificatesSection = () => {
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() =>
handlePopUpOpen(isLegacyTemplatesEnabled ? "certificate" : "certificateIssuance")
}
onClick={() => handlePopUpOpen("issueCertificate")}
isDisabled={!isAllowed}
>
Issue
@@ -88,11 +81,7 @@ export const CertificatesSection = () => {
</ProjectPermissionCan>
</div>
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
{isLegacyTemplatesEnabled ? (
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
) : (
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
)}
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />

View File

@@ -64,7 +64,7 @@ type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
[
"certificate",
"issueCertificate",
"deleteCertificate",
"revokeCertificate",
"certificateCert",
@@ -297,7 +297,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () =>
handlePopUpOpen("certificate", {
handlePopUpOpen("issueCertificate", {
serialNumber: certificate.serialNumber
})
}

View File

@@ -5,6 +5,7 @@ import {
faCertificate,
faCog,
faEllipsis,
faFileContract,
faPencil,
faPlus,
faTrash
@@ -40,6 +41,7 @@ import {
Tr
} from "@app/components/v2";
import {
ProjectPermissionCertificateActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub,
useProject,
@@ -50,6 +52,7 @@ import { useDeleteCertTemplateV2 } from "@app/hooks/api";
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
import { ProjectType } from "@app/hooks/api/projects/types";
import { CertificateModal } from "../CertificatesPage/components/CertificateModal";
import { CertificateTemplateEnrollmentModal } from "../CertificatesPage/components/CertificateTemplateEnrollmentModal";
import { PkiTemplateForm } from "./components/PkiTemplateForm";
@@ -64,7 +67,8 @@ export const PkiTemplateListPage = () => {
"certificateTemplate",
"deleteTemplate",
"enrollmentOptions",
"estUpgradePlan"
"estUpgradePlan",
"certificateFromTemplate"
] as const);
const { subscription } = useSubscription();
@@ -160,6 +164,27 @@ export const PkiTemplateListPage = () => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Create}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("certificateFromTemplate", template);
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faFileContract} />}
>
Issue Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Edit}
a={ProjectPermissionSub.CertificateTemplates}
@@ -284,6 +309,16 @@ export const PkiTemplateListPage = () => {
</ModalContent>
</Modal>
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateModal
popUp={{
certificate: {
isOpen: popUp.certificateFromTemplate.isOpen,
data: popUp.certificateFromTemplate.data
}
}}
handlePopUpToggle={(_, state) => handlePopUpToggle("certificateFromTemplate", state)}
preselectedTemplate={popUp.certificateFromTemplate.data}
/>
</div>
<UpgradePlanModal
isOpen={popUp.estUpgradePlan.isOpen}

View File

@@ -44,7 +44,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
const { data: caData } = useGetCaById(profile.caId);
const { popUp, handlePopUpToggle } = usePopUp(["certificateIssuance"] as const);
const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -147,7 +147,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpToggle("certificateIssuance");
handlePopUpToggle("issueCertificate");
}}
icon={<FontAwesomeIcon icon={faPlus} />}
>

View File

@@ -251,7 +251,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
resolver: zodResolver(templateSchema),
defaultValues: getDefaultValues()
defaultValues: getDefaultValues(),
mode: "onChange",
reValidateMode: "onChange",
criteriaMode: "all"
});
useEffect(() => {

View File

@@ -1,7 +1,8 @@
/* eslint-disable no-nested-ternary */
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { MultiValue, SingleValue } from "react-select";
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
@@ -11,13 +12,10 @@ import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FilterableSelect,
FormControl,
Input,
Select,
SelectItem
Input
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useOrganization } from "@app/context";
@@ -132,7 +130,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="mt-4 overflow-visible py-4">
<form onSubmit={handleSubmit(setFilter)}>
<div className="flex min-w-64 flex-col font-inter">
<div className="flex max-w-96 min-w-96 flex-col font-inter">
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2">
@@ -173,76 +171,24 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
name="eventType"
render={({ field }) => (
<FormControl>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="inline-flex thin-scrollbar w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal whitespace-nowrap text-bunker-200 outline-hidden data-placeholder:text-mineshaft-200">
{selectedEventTypes?.length === 1
? filteredEventTypes.find(
(eventType) => eventType.value === selectedEventTypes[0]
)?.label
: selectedEventTypes?.length === 0
? "All events"
: `${selectedEventTypes?.length} events selected`}
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={2}
className="z-100 max-h-80 thin-scrollbar overflow-hidden"
>
<div className="max-h-80 overflow-y-auto">
{filteredEventTypes.length > 0 ? (
filteredEventTypes.map((eventType) => {
const isSelected = selectedEventTypes?.includes(
eventType.value as EventType
);
return (
<DropdownMenuItem
onSelect={(event) =>
filteredEventTypes.length > 1 && event.preventDefault()
}
onClick={() => {
if (
selectedEventTypes?.includes(eventType.value as EventType)
) {
field.onChange(
selectedEventTypes?.filter(
(e: string) => e !== eventType.value
)
);
} else {
field.onChange([
...(selectedEventTypes || []),
eventType.value
]);
}
}}
key={`event-type-${eventType.value}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{eventType.label}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
<FilterableSelect
value={filteredEventTypes.filter((eventType) =>
field.value.includes(eventType.value as EventType)
)}
isMulti
isClearable
onChange={(options) =>
field.onChange(
(options as MultiValue<(typeof filteredEventTypes)[number]>).map(
(option) => option.value
)
)
}
placeholder="All events"
options={filteredEventTypes}
getOptionValue={(option) => option.value}
getOptionLabel={(option) => option.label}
/>
</FormControl>
)}
/>
@@ -256,31 +202,27 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
<Controller
control={control}
name="userAgentType"
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="w-full"
>
<Select
{...field}
value={value === undefined ? "all" : value}
onValueChange={(e) => {
if (e === "all") onChange(undefined);
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
}}
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
position="popper"
>
<SelectItem value="all" key="all">
All sources
</SelectItem>
{userAgentTypes.map(({ label, value: userAgent }) => (
<SelectItem value={userAgent} key={label}>
{label}
</SelectItem>
))}
</Select>
<FilterableSelect
value={
userAgentTypes.find(
(userAgentType) => value === (userAgentType.value as UserAgentType)
) ?? null
}
isClearable
onChange={(option) =>
onChange((option as SingleValue<(typeof userAgentTypes)[number]>)?.value)
}
placeholder="All sources"
options={userAgentTypes}
getOptionValue={(option) => option.value}
getOptionLabel={(option) => option.label}
/>
</FormControl>
)}
/>

View File

@@ -36,7 +36,6 @@ import { RelayOption } from "./RelayOption";
const baseFormSchema = z.object({
name: slugSchema({ field: "name" }),
instanceDomain: z.string().url("Must be a valid URL").or(z.literal("")),
relay: z
.object(
{
@@ -78,7 +77,6 @@ export const GatewayCliDeploymentMethod = () => {
const [autogenerateToken, setAutogenerateToken] = useState(true);
const [step, setStep] = useState<"form" | "command">("form");
const [name, setName] = useState("");
const [instanceDomain, setInstanceDomain] = useState(siteURL);
const [relay, setRelay] = useState<null | {
id: string;
name: string;
@@ -131,8 +129,7 @@ export const GatewayCliDeploymentMethod = () => {
const validation = formSchemaWithIdentity.safeParse({
name,
relay,
identity,
instanceDomain
identity
});
if (!validation.success) {
setFormErrors(validation.error.issues);
@@ -175,8 +172,7 @@ export const GatewayCliDeploymentMethod = () => {
const validation = formSchemaWithToken.safeParse({
name,
relay,
identityToken,
instanceDomain
identityToken
});
if (!validation.success) {
setFormErrors(validation.error.issues);
@@ -187,11 +183,10 @@ export const GatewayCliDeploymentMethod = () => {
};
const command = useMemo(() => {
const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : "";
return `infisical gateway start --name=${name} --relay=${
relay?.name || ""
}${domainFlag} --token=${identityToken}`;
}, [name, relay, identityToken, instanceDomain]);
} --domain=${siteURL} --token=${identityToken}`;
}, [name, relay, identityToken, siteURL]);
if (step === "command") {
return (
@@ -274,19 +269,6 @@ export const GatewayCliDeploymentMethod = () => {
/>
{errors.relay && <p className="mt-1 text-sm text-red">{errors.relay}</p>}
<FormLabel
label="Infisical Instance Host Address"
tooltipText="The host address of the infisical instance that's accessible by the gateway."
className="mt-4"
/>
<Input
value={instanceDomain}
onChange={(e) => setInstanceDomain(e.target.value)}
placeholder="https://app.infisical.com"
isError={Boolean(errors.instanceDomain)}
/>
{errors.instanceDomain && <p className="mt-1 text-sm text-red">{errors.instanceDomain}</p>}
{canCreateToken && autogenerateToken ? (
<>
<FormLabel

View File

@@ -31,8 +31,7 @@ import { slugSchema } from "@app/lib/schemas";
const baseFormSchema = z.object({
name: slugSchema({ field: "name" }),
host: z.string().min(1, "Host is required"),
instanceDomain: z.string().url("Must be a valid URL").or(z.literal(""))
host: z.string().min(1, "Host is required")
});
const formSchemaWithIdentity = baseFormSchema.extend({
@@ -62,7 +61,6 @@ export const RelayCliDeploymentMethod = () => {
const [name, setName] = useState("");
const [host, setHost] = useState("");
const [instanceDomain, setInstanceDomain] = useState(siteURL);
const [identity, setIdentity] = useState<null | {
id: string;
name: string;
@@ -106,7 +104,7 @@ export const RelayCliDeploymentMethod = () => {
setFormErrors([]);
if (canCreateToken && autogenerateToken) {
const validation = formSchemaWithIdentity.safeParse({ name, host, instanceDomain, identity });
const validation = formSchemaWithIdentity.safeParse({ name, host, identity });
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
@@ -148,7 +146,6 @@ export const RelayCliDeploymentMethod = () => {
const validation = formSchemaWithToken.safeParse({
name,
host,
instanceDomain,
identityToken
});
if (!validation.success) {
@@ -169,9 +166,8 @@ export const RelayCliDeploymentMethod = () => {
};
const command = useMemo(() => {
const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : "";
return `infisical relay start --name=${name}${domainFlag} --host=${host} --token=${identityToken}`;
}, [name, instanceDomain, host, identityToken]);
return `infisical relay start --name=${name} --domain=${siteURL} --host=${host} --token=${identityToken}`;
}, [name, siteURL, host, identityToken]);
if (step === "command") {
return (
@@ -239,19 +235,6 @@ export const RelayCliDeploymentMethod = () => {
/>
{errors.host && <p className="mt-1 text-sm text-red">{errors.host}</p>}
<FormLabel
label="Infisical Instance Host Address"
tooltipText="The host address of the infisical instance that's accessible by the relay."
className="mt-4"
/>
<Input
value={instanceDomain}
onChange={(e) => setInstanceDomain(e.target.value)}
placeholder="https://app.infisical.com"
isError={Boolean(errors.instanceDomain)}
/>
{errors.instanceDomain && <p className="mt-1 text-sm text-red">{errors.instanceDomain}</p>}
{canCreateToken && autogenerateToken ? (
<>
<FormLabel

View File

@@ -4,6 +4,7 @@ import { Modal, ModalContent } from "@app/components/v2";
import { RelayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect";
import { RelayCliDeploymentMethod } from "./RelayCliDeploymentMethod";
import { RelayTerraformDeploymentMethod } from "./RelayTerraformDeploymentMethod";
type Props = {
isOpen: boolean;
@@ -11,7 +12,12 @@ type Props = {
};
export const RelayDeploymentInfoMap = {
cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod }
cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod },
terraform: {
name: "Terraform",
image: "Terraform.png",
component: RelayTerraformDeploymentMethod
}
} as const;
export type RelayDeploymentMethod = keyof typeof RelayDeploymentInfoMap;

View File

@@ -0,0 +1,509 @@
import { useMemo, useState } from "react";
import { SingleValue } from "react-select";
import { faCopy, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FilterableSelect,
FormLabel,
IconButton,
Input,
ModalClose,
Tooltip
} from "@app/components/v2";
import {
OrgPermissionIdentityActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import {
useAddIdentityTokenAuth,
useCreateTokenIdentityTokenAuth,
useGetIdentityMembershipOrgs,
useGetIdentityTokenAuth
} from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
const baseFormSchema = z.object({
name: slugSchema({ field: "name" })
});
const formSchemaWithIdentity = baseFormSchema.extend({
identity: z
.object(
{
id: z.string(),
name: z.string()
},
{ required_error: "Identity is required" }
)
.nullable()
.refine((val) => val !== null, { message: "Identity is required" })
});
const formSchemaWithToken = baseFormSchema.extend({
identityToken: z.string().min(1, "Token is required")
});
const ec2FormSchema = z.object({
awsRegion: z.string().min(1, "AWS Region is required"),
vpcId: z.string().min(1, "VPC ID is required"),
ami: z.string().min(1, "AMI ID is required"),
subnetId: z.string().min(1, "Subnet ID is required")
});
export const RelayTerraformDeploymentMethod = () => {
const { protocol, hostname, port } = window.location;
const portSuffix = port && port !== "80" ? `:${port}` : "";
const siteURL = `${protocol}//${hostname}${portSuffix}`;
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const [autogenerateToken, setAutogenerateToken] = useState(true);
const [step, setStep] = useState<"form" | "command">("form");
const [name, setName] = useState("");
const [identity, setIdentity] = useState<null | {
id: string;
name: string;
}>(null);
const [identityToken, setIdentityToken] = useState("");
const [formErrors, setFormErrors] = useState<z.ZodIssue[]>([]);
const [awsRegion, setAwsRegion] = useState("us-east-1");
const [vpcId, setVpcId] = useState("");
const [ami, setAmi] = useState("ami-01b2110eef525172b");
const [subnetId, setSubnetId] = useState("");
const errors = useMemo(() => {
const errorMap: Record<string, string | undefined> = {};
formErrors.forEach((issue) => {
if (issue.path.length > 0) {
errorMap[String(issue.path[0])] = issue.message;
}
});
return errorMap;
}, [formErrors]);
const { currentOrg } = useOrganization();
const organizationId = currentOrg?.id || "";
const { permission } = useOrgPermission();
const canCreateToken = permission.can(
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity
);
const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } =
useGetIdentityMembershipOrgs({
organizationId,
limit: 20000
});
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || [];
const { mutateAsync: createToken, isPending: isCreatingToken } =
useCreateTokenIdentityTokenAuth();
const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } =
useAddIdentityTokenAuth();
const { refetch } = useGetIdentityTokenAuth(identity?.id ?? "");
const handleGenerateCommand = async () => {
setFormErrors([]);
if (canCreateToken && autogenerateToken) {
const validation = formSchemaWithIdentity.safeParse({ name, identity });
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
if (selectedTabIndex === 0) {
const ec2Validation = ec2FormSchema.safeParse({ awsRegion, vpcId, ami, subnetId });
if (!ec2Validation.success) {
setFormErrors(ec2Validation.error.issues);
return;
}
}
const validatedIdentity = validation.data.identity;
try {
const { data: identityTokenAuth } = await refetch();
if (!identityTokenAuth) {
await addIdentityTokenAuth({
identityId: validatedIdentity.id,
organizationId,
accessTokenTTL: 2592000,
accessTokenMaxTTL: 2592000,
accessTokenNumUsesLimit: 0,
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
createNotification({
text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.",
type: "warning"
});
}
const token = await createToken({
identityId: validatedIdentity.id,
name: `relay token for ${name} (autogenerated)`
});
setIdentityToken(token.accessToken);
createNotification({
text: "Automatically generated a token for the selected identity.",
type: "info"
});
setStep("command");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to generate token for the selected identity",
type: "error"
});
setIdentityToken("");
}
} else {
const validation = formSchemaWithToken.safeParse({
name,
identityToken
});
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
if (selectedTabIndex === 0) {
const ec2Validation = ec2FormSchema.safeParse({ awsRegion, vpcId, ami, subnetId });
if (!ec2Validation.success) {
setFormErrors(ec2Validation.error.issues);
return;
}
}
setStep("command");
}
};
const handleIdentityChange = (
selectedIdentity: SingleValue<{
id: string;
name: string;
}>
) => {
setIdentity(selectedIdentity);
};
const terraformCommand = useMemo(() => {
return `terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "${awsRegion}"
}
# Security Group for the Infisical Relay instance
resource "aws_security_group" "infisical_relay_sg" {
name = "${name}-relay-sg"
description = "Allows inbound traffic for Infisical Relay and SSH"
vpc_id = "${vpcId}"
# Inbound: Allows the Infisical platform to securely communicate with the Relay server.
ingress {
from_port = 8443
to_port = 8443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Inbound: Allows Infisical Gateway to securely communicate via the Relay.
ingress {
from_port = 2222
to_port = 2222
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Inbound: Allows secure shell (SSH) access for administration.
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Restrict this to your IP in production
}
# Outbound: Allows the Relay server to make necessary outbound connections to the Infisical platform.
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${name}-relay-sg"
}
}
# Elastic IP for a static public IP address
resource "aws_eip" "infisical_relay_eip" {
tags = {
Name = "${name}-relay-eip"
}
}
# EC2 instance to run Infisical Relay
module "infisical_relay_instance" {
source = "terraform-aws-modules/ec2-instance/aws"
version = "~> 5.6"
name = "${name}-relay-instance"
ami = "${ami}"
instance_type = "t3.micro"
subnet_id = "${subnetId}"
vpc_security_group_ids = [aws_security_group.infisical_relay_sg.id]
associate_public_ip_address = false # We are using an Elastic IP instead
user_data = <<-EOT
#!/bin/bash
set -e
# Install Infisical CLI
curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash
apt-get update && apt-get install -y infisical
# Install the relay as a systemd service.
# This example uses a Machine Identity token for authentication via the INFISICAL_TOKEN environment variable.
#
# Note: For production environments, you might consider fetching the token from AWS Parameter Store or AWS Secrets Manager.
export INFISICAL_TOKEN="${identityToken}"
sudo -E infisical relay systemd install \\
--name "${name}" \\
--domain "${siteURL}" \\
--host "\${aws_eip.infisical_relay_eip.public_ip}"
# Start and enable the service to run on boot
sudo systemctl start infisical-relay
sudo systemctl enable infisical-relay
EOT
}
# Associate the Elastic IP with the EC2 instance
resource "aws_eip_association" "eip_assoc" {
instance_id = module.infisical_relay_instance.id
allocation_id = aws_eip.infisical_relay_eip.id
}
`;
}, [name, siteURL, identityToken, awsRegion, vpcId, ami, subnetId]);
if (step === "command") {
return (
<>
<div className="mb-2 flex items-center justify-between">
<span>Terraform Configuration</span>
<IconButton
ariaLabel="copy"
variant="outline_bg"
colorSchema="secondary"
onClick={() => {
navigator.clipboard.writeText(terraformCommand);
createNotification({
text: "Terraform configuration copied to clipboard",
type: "info"
});
}}
className="w-10"
>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</div>
<div className="h-80 overflow-y-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 font-mono text-sm text-bunker-300">
<pre>
<code>{terraformCommand}</code>
</pre>
</div>
<div className="mt-6 flex items-center">
<ModalClose asChild>
<Button className="mr-4" size="sm" colorSchema="secondary">
Done
</Button>
</ModalClose>
</div>
</>
);
}
return (
<>
<FormLabel label="Name" tooltipText="The name for your relay." />
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter relay name..."
isError={Boolean(errors.name)}
/>
{errors.name && <p className="mt-1 text-sm text-red">{errors.name}</p>}
{canCreateToken && autogenerateToken ? (
<>
<FormLabel
label="Identity"
tooltipText="The identity that your relay will use for authentication."
className="mt-4"
/>
<FilterableSelect
value={identity}
onChange={(e) =>
handleIdentityChange(
e as SingleValue<{
id: string;
name: string;
}>
)
}
isLoading={isIdentitiesLoading}
placeholder="Select identity..."
options={identityMembershipOrgs.map((membership) => membership.identity)}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
{errors.identity && <p className="mt-1 text-sm text-red">{errors.identity}</p>}
</>
) : (
<>
<FormLabel
label="Identity Token"
tooltipText="The identity token that your relay will use for authentication."
className="mt-4"
/>
<Input
value={identityToken}
onChange={(e) => setIdentityToken(e.target.value)}
placeholder="Enter identity token..."
isError={Boolean(errors.identityToken)}
/>
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
</>
)}
{canCreateToken && (
<div className="mt-2">
<Checkbox
isChecked={autogenerateToken}
onCheckedChange={(e) => {
setAutogenerateToken(Boolean(e));
}}
id="autogenerate-token"
className="mr-2"
>
<div className="flex items-center">
<span>Automatically enable token auth and generate a token for identity</span>
<Tooltip
className="max-w-md"
content={
<>
Token authentication will be automatically enabled for the selected identity if
it isn&apos;t already configured. By default, it will be configured to allow all
IP addresses with a token TTL of 30 days. You can manage these settings in
Access Control.
<br />
<br />A token will automatically be generated to be used with the CLI command.
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="mt-0.5 ml-1" />
</Tooltip>
</div>
</Checkbox>
</div>
)}
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List className="-pb-1 mt-4 mb-6 w-full border-b-2 border-mineshaft-600">
<Tab
className={({ selected }) =>
`-mb-[0.14rem] px-4 py-2 text-sm font-medium whitespace-nowrap outline-hidden disabled:opacity-60 ${
selected ? "border-b-2 border-mineshaft-300 text-mineshaft-200" : "text-bunker-300"
}`
}
>
EC2
</Tab>
</Tab.List>
<Tab.Panels className="mb-4 rounded-sm border border-mineshaft-600 bg-mineshaft-700/70 p-3">
<Tab.Panel>
<FormLabel label="AWS Region" />
<FilterableSelect
value={AWS_REGIONS.find((r) => r.slug === awsRegion)}
onChange={(selected) => {
if (selected) {
setAwsRegion((selected as SingleValue<{ slug: string; name: string }>)!.slug);
}
}}
options={AWS_REGIONS}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
/>
{errors.awsRegion && <p className="mt-1 text-sm text-red">{errors.awsRegion}</p>}
<FormLabel label="VPC ID" className="mt-4" />
<Input
value={vpcId}
onChange={(e) => setVpcId(e.target.value)}
placeholder="vpc-..."
isError={Boolean(errors.vpcId)}
/>
{errors.vpcId && <p className="mt-1 text-sm text-red">{errors.vpcId}</p>}
<FormLabel
label="AMI ID"
tooltipText="The ID of the Amazon Machine Image (AMI) for the EC2 linux instance."
className="mt-4"
/>
<Input
value={ami}
onChange={(e) => setAmi(e.target.value)}
placeholder="ami-..."
isError={Boolean(errors.ami)}
/>
{errors.ami && <p className="mt-1 text-sm text-red">{errors.ami}</p>}
<FormLabel label="Subnet ID" className="mt-4" />
<Input
value={subnetId}
onChange={(e) => setSubnetId(e.target.value)}
placeholder="subnet-..."
isError={Boolean(errors.subnetId)}
/>
{errors.subnetId && <p className="mt-1 text-sm text-red">{errors.subnetId}</p>}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
colorSchema="secondary"
onClick={handleGenerateCommand}
isLoading={isCreatingToken || isAddingTokenAuth}
>
Continue
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};

View File

@@ -1,116 +0,0 @@
import { useState } from "react";
import { HighlightText } from "@app/components/v2/HighlightText";
import { PamResourceType } from "@app/hooks/api/pam";
type TableLog = {
command?: string;
data_rows: Record<string, string | number | null>[];
total_rows?: number;
};
export const PamSessionLogOutput = ({
content,
resourceType,
search
}: {
content: string;
resourceType: PamResourceType;
search: string;
}) => {
const [isRawView, setIsRawView] = useState(false);
let parsedContent: TableLog | null = null;
if (resourceType === PamResourceType.Postgres || resourceType === PamResourceType.MySQL) {
try {
const parsed = JSON.parse(content);
if (
parsed &&
typeof parsed === "object" &&
!Array.isArray(parsed) &&
parsed.data_rows &&
Array.isArray(parsed.data_rows) &&
parsed.data_rows.length > 0 &&
typeof parsed.data_rows[0] === "object" &&
parsed.data_rows[0] !== null
) {
parsedContent = parsed;
}
} catch {
// Not a valid JSON or doesn't match structure, will render as plain text
}
}
if (parsedContent) {
const headers = Object.keys(parsedContent.data_rows[0]);
return (
<div className="font-sans">
{isRawView ? (
<div className="font-mono break-all whitespace-pre-wrap">
<HighlightText text={content} highlight={search} />
</div>
) : (
<>
{parsedContent.command && (
<div className="mb-2 font-mono">{`> ${parsedContent.command}`}</div>
)}
<div className="overflow-x-auto rounded-md border border-mineshaft-600">
<table className="w-full min-w-max text-left">
<thead className="bg-mineshaft-800">
<tr className="border-b border-mineshaft-600">
{headers.map((header) => (
<th key={header} className="p-2 font-semibold text-mineshaft-200 capitalize">
<HighlightText text={header.replace(/_/g, " ")} highlight={search} />
</th>
))}
</tr>
</thead>
<tbody>
{parsedContent.data_rows.map((row, rowIndex) => (
<tr
// eslint-disable-next-line react/no-array-index-key
key={`row-${rowIndex}`}
className="border-b border-mineshaft-700 bg-mineshaft-900 last:border-b-0 hover:bg-mineshaft-800/50"
>
{headers.map((header) => (
<td key={header} className="p-2">
<HighlightText text={String(row[header] ?? "")} highlight={search} />
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</>
)}
<div className="mt-2 flex items-center">
<button
type="button"
className="cursor-pointer text-sm text-bunker-400 underline"
onClick={(e) => {
e.stopPropagation();
setIsRawView((v) => !v);
}}
>
{isRawView ? "View Formatted" : "View Raw"}
</button>
{parsedContent.total_rows !== undefined && (
<div className="ml-auto text-right text-sm text-bunker-400">
Total rows: {parsedContent.total_rows}
</div>
)}
</div>
</div>
);
}
return (
<div className="font-mono break-all whitespace-pre-wrap">
<HighlightText text={content} highlight={search} />
</div>
);
};

View File

@@ -7,7 +7,6 @@ import { Input } from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText";
import { TPamSession } from "@app/hooks/api/pam";
import { PamSessionLogOutput } from "./PamSessionLogOutput";
import { formatLogContent } from "./PamSessionLogsSection.utils";
type Props = {
@@ -104,20 +103,9 @@ export const PamSessionLogsSection = ({ session }: Props) => {
>
<div className="overflow-hidden">
{log.output && (
<>
<div className="mt-2 flex items-center gap-2">
<div className="h-px w-full bg-mineshaft-400" />
<span className="text-xs text-mineshaft-400">OUTPUT</span>
<div className="h-px w-full bg-mineshaft-400" />
</div>
<div className="pt-2 text-bunker-300">
<PamSessionLogOutput
content={log.output}
resourceType={session.resourceType}
search={search}
/>
</div>
</>
<div className="pt-2 text-bunker-300">
<HighlightText text={log.output} highlight={search} />
</div>
)}
</div>
</div>

View File

@@ -25,14 +25,12 @@ import {
useProject
} from "@app/context";
import {
useAddUsersToOrg,
useAddUserToWsNonE2EE,
useGetOrgUsers,
useGetProjectRoles,
useGetWorkspaceUsers
} from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/projects/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = z.object({
@@ -86,7 +84,6 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
defaultValues: { orgMemberships: [], projectRoleSlugs: [] }
});
const { mutateAsync: addMemberToOrg } = useAddUsersToOrg();
const { mutateAsync: addUserToProject } = useAddUserToWsNonE2EE();
useEffect(() => {
@@ -140,13 +137,6 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
return;
}
if (newInvitees.length) {
await addMemberToOrg({
inviteeEmails: newInvitees,
organizationId: orgId,
organizationRoleSlug: ProjectMembershipRole.Member // only applies to new invites
});
}
if (newInvitees.length || inviteeEmails.length) {
await addUserToProject({
usernames: [...inviteeEmails, ...newInvitees],

View File

@@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useEffect, useMemo } from "react";
import {
faArrowDown,
faArrowUp,
@@ -7,6 +7,7 @@ import {
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import {
@@ -48,6 +49,7 @@ enum GroupMembersOrderBy {
}
export const GroupMembersTable = ({ groupMembership }: Props) => {
const navigate = useNavigate();
const {
search,
setSearch,
@@ -62,6 +64,21 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
initPerPage: getUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, 20)
});
// this handles links from secret versions when the actor is in a group membership
const { username, ...restSearch } = useSearch({
strict: false
});
useEffect(() => {
if (username) {
setSearch(username);
navigate({
to: ".",
replace: true,
search: restSearch
});
}
}, [username]);
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["assumePrivileges"] as const);
const handlePerPageChange = (newPerPage: number) => {

View File

@@ -1,4 +1,6 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { ProjectAccessControlTabs } from "@app/types/project";
@@ -8,6 +10,11 @@ export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
validateSearch: zodValidator(
z.object({
username: z.string().optional().catch(undefined)
})
),
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [

View File

@@ -40,7 +40,7 @@ import {
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination } from "@app/hooks";
import { usePagination, useResetPageHelper } from "@app/hooks";
import {
useGetSecretApprovalRequestCount,
useGetSecretApprovalRequests,
@@ -99,6 +99,12 @@ export const SecretApprovalRequest = () => {
const totalApprovalCount = data?.totalCount ?? 0;
const secretApprovalRequests = data?.approvals ?? [];
useResetPageHelper({
totalCount: totalApprovalCount,
offset,
setPage
});
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
useGetSecretApprovalRequestCount({ projectId });
const { user: userSession } = useUser();
@@ -107,7 +113,7 @@ export const SecretApprovalRequest = () => {
});
const { permission } = useProjectPermission();
const { data: members } = useGetWorkspaceUsers(projectId);
const { data: members } = useGetWorkspaceUsers(projectId, true);
const isSecretApprovalScreen = Boolean(selectedApprovalId);
const { requestId } = search;

View File

@@ -221,9 +221,7 @@ export const SecretApprovalRequestChangeItem = ({
<div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Multi-line Encoding</div>
<div className="text-sm">
{secretVersion?.skipMultilineEncoding?.toString() || (
<span className="text-sm text-mineshaft-300">-</span>
)}{" "}
{secretVersion?.skipMultilineEncoding?.toString() || "false"}
</div>
</div>
</div>
@@ -366,9 +364,8 @@ export const SecretApprovalRequestChangeItem = ({
<div className="text-sm font-medium text-mineshaft-300">Multi-line Encoding</div>
<div className="text-sm">
{newVersion?.skipMultilineEncoding?.toString() ??
secretVersion?.skipMultilineEncoding?.toString() ?? (
<span className="text-sm text-mineshaft-300">-</span>
)}{" "}
secretVersion?.skipMultilineEncoding?.toString() ??
"false"}
</div>
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { useState } from "react";
import { faEye } from "@fortawesome/free-regular-svg-icons";
import {
faArrowRotateRight,
faBan,
faDesktop,
faEyeSlash,
faServer,
@@ -68,10 +69,14 @@ export const SecretVersionItem = ({
const getLinkToModifyHistoryEntity = (
actorId: string,
actorType: string,
membershipId: string | null = ""
membershipId: string | null = "",
groupId: string | null = "",
actorName: string | null = ""
) => {
switch (actorType) {
case ActorType.USER:
if (groupId)
return `/projects/secret-management/${currentProject.id}/groups/${groupId}?username=${actorName}`;
return `/projects/secret-management/${currentProject.id}/members/${membershipId}`;
case ActorType.IDENTITY:
return `/projects/secret-management/${currentProject.id}/identities/${actorId}`;
@@ -83,10 +88,26 @@ export const SecretVersionItem = ({
const onModifyHistoryClick = (
actorId: string | undefined | null,
actorType: string | undefined | null,
membershipId: string | undefined | null
membershipId: string | undefined | null,
groupId: string | undefined | null,
actorName: string | undefined | null
) => {
if (!membershipId) {
createNotification({
type: "info",
text: `This ${actorType === ActorType.USER ? "user" : "identity"} is no longer a member of this project.`
});
return;
}
if (actorType && actorId && actorType !== ActorType.PLATFORM) {
const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId);
const redirectLink = getLinkToModifyHistoryEntity(
actorId,
actorType,
membershipId,
groupId,
actorName
);
if (redirectLink) {
navigate({ to: redirectLink });
}
@@ -157,15 +178,35 @@ export const SecretVersionItem = ({
<div className="flex flex-row">
<div className="flex w-fit flex-row text-sm">
Modified by:
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
<Tooltip
className="z-[100] max-w-sm"
content={
getModifiedByName(actor.actorType, actor.name) +
(!actor.membershipId && actor.actorId ? " (Removed from project)" : "")
}
>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
onClick={() =>
onModifyHistoryClick(actor.actorId, actor.actorType, actor.membershipId)
onClick={
actor.membershipId
? () =>
onModifyHistoryClick(
actor.actorId,
actor.actorType,
actor.membershipId,
actor.groupId,
actor.name
)
: undefined
}
className="cursor-pointer"
className={actor.membershipId ? "cursor-pointer" : undefined}
>
<FontAwesomeIcon icon={getModifiedByIcon(actor.actorType)} className="ml-2" />
{!actor.membershipId &&
actor.actorType &&
[ActorType.USER, ActorType.IDENTITY].includes(
actor.actorType as ActorType
) && <FontAwesomeIcon className="ml-1 text-mineshaft-400" icon={faBan} />}
</div>
</Tooltip>
</div>