mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 20:28:17 +00:00
fix: add secret settings tab to secret sharing page
This commit is contained in:
@@ -6,11 +6,13 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
|
||||
import { SecretSharingSettingsTab } from "./components/SecretSharingSettings/SecretSharingSettingsTab";
|
||||
import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab";
|
||||
|
||||
enum SecretSharingPageTabs {
|
||||
ShareSecret = "share-secret",
|
||||
RequestSecret = "request-secret"
|
||||
RequestSecret = "request-secret",
|
||||
Settings = "settings"
|
||||
}
|
||||
|
||||
export const ShareSecretSection = () => {
|
||||
@@ -46,6 +48,11 @@ export const ShareSecretSection = () => {
|
||||
<Tab variant={tabVariant} value={SecretSharingPageTabs.RequestSecret}>
|
||||
Request Secrets
|
||||
</Tab>
|
||||
{!isSubOrganization && (
|
||||
<Tab variant={tabVariant} value={SecretSharingPageTabs.Settings}>
|
||||
Settings
|
||||
</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
<TabPanel value={SecretSharingPageTabs.ShareSecret}>
|
||||
<ShareSecretTab />
|
||||
@@ -53,6 +60,9 @@ export const ShareSecretSection = () => {
|
||||
<TabPanel value={SecretSharingPageTabs.RequestSecret}>
|
||||
<RequestSecretTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={SecretSharingPageTabs.Settings}>
|
||||
<SecretSharingSettingsTab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { getObjectFromSeconds } from "@app/helpers/datetime";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
const MAX_SHARED_SECRET_LIFETIME_SECONDS = 30 * 24 * 60 * 60; // 30 days in seconds
|
||||
const MIN_SHARED_SECRET_LIFETIME_SECONDS = 5 * 60; // 5 minutes in seconds
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
maxLifetimeValue: z.number().min(1, "Value must be at least 1"),
|
||||
maxLifetimeUnit: z.enum(["m", "h", "d"], {
|
||||
invalid_type_error: "Please select a valid time unit"
|
||||
}),
|
||||
maxViewLimit: z.string(),
|
||||
shouldLimitView: z.boolean()
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const { maxLifetimeValue, maxLifetimeUnit } = data;
|
||||
|
||||
const durationInSeconds = ms(`${maxLifetimeValue}${maxLifetimeUnit}`) / 1000;
|
||||
|
||||
if (durationInSeconds > MAX_SHARED_SECRET_LIFETIME_SECONDS) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Duration exceeds a maximum of 30 days",
|
||||
path: ["maxLifetimeValue"]
|
||||
});
|
||||
}
|
||||
|
||||
if (durationInSeconds < MIN_SHARED_SECRET_LIFETIME_SECONDS) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Duration must be at least 5 minutes",
|
||||
path: ["maxLifetimeValue"]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
const viewLimitOptions = [
|
||||
{ label: "Unlimited", value: false },
|
||||
{ label: "Limited", value: true }
|
||||
];
|
||||
|
||||
export const OrgSecretShareLimitSection = () => {
|
||||
const { mutateAsync } = useUpdateOrg();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const getDefaultFormValues = () => {
|
||||
const initialLifetime = getObjectFromSeconds(currentOrg?.maxSharedSecretLifetime, [
|
||||
"m",
|
||||
"h",
|
||||
"d"
|
||||
]);
|
||||
return {
|
||||
maxLifetimeValue: initialLifetime.value,
|
||||
maxLifetimeUnit: initialLifetime.unit as "m" | "h" | "d",
|
||||
maxViewLimit: currentOrg?.maxSharedSecretViewLimit?.toString() || "1",
|
||||
shouldLimitView: Boolean(currentOrg?.maxSharedSecretViewLimit)
|
||||
};
|
||||
};
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting, isDirty },
|
||||
watch,
|
||||
handleSubmit,
|
||||
reset
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: getDefaultFormValues()
|
||||
});
|
||||
|
||||
const shouldLimitView = watch("shouldLimitView");
|
||||
|
||||
useEffect(() => {
|
||||
if (currentOrg) {
|
||||
reset(getDefaultFormValues());
|
||||
}
|
||||
}, [currentOrg, reset]);
|
||||
|
||||
const handleFormSubmit = async (formData: TForm) => {
|
||||
try {
|
||||
const maxSharedSecretLifetimeSeconds =
|
||||
ms(`${formData.maxLifetimeValue}${formData.maxLifetimeUnit}`) / 1000;
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
maxSharedSecretViewLimit: formData.shouldLimitView ? Number(formData.maxViewLimit) : null,
|
||||
maxSharedSecretLifetime: maxSharedSecretLifetimeSeconds
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated secret share limits",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset(formData);
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to update secret share limits",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Units for the dropdown with readable labels
|
||||
const timeUnits = [
|
||||
{ value: "m", label: "Minutes" },
|
||||
{ value: "h", label: "Hours" },
|
||||
{ value: "d", label: "Days" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-medium">Secret Share Limits</p>
|
||||
</div>
|
||||
<p className="mt-2 mb-4 text-sm text-gray-400">
|
||||
These settings establish the maximum limits for all Shared Secret parameters within this
|
||||
organization. Shared secrets cannot be created with values exceeding these limits.
|
||||
</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} autoComplete="off">
|
||||
<div className="flex max-w-sm gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxLifetimeValue"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The max amount of time that can be set before the secret share link expires."
|
||||
label="Max Lifetime"
|
||||
className="w-full"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
field.onChange(val === "" ? "" : parseInt(val, 10));
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxLifetimeUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Time unit"
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
className="pr-2"
|
||||
onValueChange={field.onChange}
|
||||
placeholder="Select time unit"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{timeUnits.map(({ value, label }) => (
|
||||
<SelectItem
|
||||
key={value}
|
||||
value={value}
|
||||
className="relative py-2 pr-8 pl-6 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">{label}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex max-w-sm items-end gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldLimitView"
|
||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Max Views"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-48"
|
||||
>
|
||||
<Select
|
||||
defaultValue={value.toString()}
|
||||
value={value.toString()}
|
||||
onValueChange={(e) => onChange(e === "true")}
|
||||
className="w-full"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isDisabled={!isAllowed}
|
||||
{...field}
|
||||
>
|
||||
{viewLimitOptions.map(({ label, value: viewLimitValue }) => (
|
||||
<SelectItem
|
||||
value={viewLimitValue.toString()}
|
||||
key={viewLimitValue.toString()}
|
||||
>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{shouldLimitView && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxViewLimit"
|
||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-48"
|
||||
>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
{...field}
|
||||
min={1}
|
||||
max={1000}
|
||||
type="number"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={!isDirty || !isAllowed}
|
||||
className="mt-4"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Switch } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
export const SecretSharingAllowShareToAnyone = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync } = useUpdateOrg();
|
||||
|
||||
const handleSecretSharingToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
allowSecretSharingOutsideOrganization: value
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${value ? "enabled" : "disabled"} secret sharing to members outside of this organization`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: (err as { response: { data: { message: string } } }).response.data.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<div className="py-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<h3 className="text-md text-mineshaft-100">
|
||||
Allow sharing secrets to members outside of this organization
|
||||
</h3>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enable-secret-sharing-outside-org"
|
||||
onCheckedChange={(value) => handleSecretSharingToggle(value)}
|
||||
isChecked={currentOrg?.allowSecretSharingOutsideOrganization ?? false}
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
If enabled, team members will be able to share secrets to members outside of this
|
||||
organization
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { OrgSecretShareLimitSection } from "./OrgSecretShareLimitSection";
|
||||
import { SecretSharingAllowShareToAnyone } from "./SecretSharingAllowShareToAnyone";
|
||||
|
||||
export const SecretSharingSettingsTab = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<SecretSharingAllowShareToAnyone />
|
||||
<OrgSecretShareLimitSection />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgPermissionSecretShareAction.ManageSettings,
|
||||
subject: OrgPermissionSubjects.SecretShare
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user