mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
feat(user-auth): make users auth token expiration customizable for orgs
This commit is contained in:
@@ -111,7 +111,8 @@ export const useUpdateOrg = () => {
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
allowSecretSharingOutsideOrganization,
|
||||
bypassOrgAuthEnabled
|
||||
bypassOrgAuthEnabled,
|
||||
userTokenExpiration
|
||||
}) => {
|
||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||
name,
|
||||
@@ -122,7 +123,8 @@ export const useUpdateOrg = () => {
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
allowSecretSharingOutsideOrganization,
|
||||
bypassOrgAuthEnabled
|
||||
bypassOrgAuthEnabled,
|
||||
userTokenExpiration
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ export type Organization = {
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
shouldUseNewPrivilegeSystem: boolean;
|
||||
allowSecretSharingOutsideOrganization?: boolean;
|
||||
userTokenExpiration?: string;
|
||||
userRole: string;
|
||||
};
|
||||
|
||||
@@ -32,6 +33,7 @@ export type UpdateOrgDTO = {
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
allowSecretSharingOutsideOrganization?: boolean;
|
||||
bypassOrgAuthEnabled?: boolean;
|
||||
userTokenExpiration?: string;
|
||||
};
|
||||
|
||||
export type BillingDetails = {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { OrgLDAPSection } from "./OrgLDAPSection";
|
||||
import { OrgOIDCSection } from "./OrgOIDCSection";
|
||||
import { OrgScimSection } from "./OrgSCIMSection";
|
||||
import { OrgSSOSection } from "./OrgSSOSection";
|
||||
import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection";
|
||||
import { SSOModal } from "./SSOModal";
|
||||
|
||||
export const OrgAuthTab = withPermission(
|
||||
@@ -166,6 +167,7 @@ export const OrgAuthTab = withPermission(
|
||||
return (
|
||||
<>
|
||||
<OrgGenericAuthSection />
|
||||
<OrgUserAccessTokenLimitSection />
|
||||
{shouldShowCreateIdentityProviderView ? (
|
||||
createIdentityProviderView
|
||||
) : (
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
const formSchema = z.object({
|
||||
expirationValue: z.number().min(1, "Value must be at least 1"),
|
||||
expirationUnit: z.enum(["m", "h", "d", "w"], {
|
||||
invalid_type_error: "Please select a valid time unit"
|
||||
})
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
// Function to parse duration string like "30d" into value and unit
|
||||
const parseDuration = (duration: string): { value: number; unit: string } => {
|
||||
const match = duration.match(/^(\d+)([mhdw])$/);
|
||||
if (match) {
|
||||
return {
|
||||
value: parseInt(match[1], 10),
|
||||
unit: match[2]
|
||||
};
|
||||
}
|
||||
// Default to 30 days if invalid format
|
||||
return { value: 30, unit: "d" };
|
||||
};
|
||||
|
||||
// Function to format value and unit back to duration string
|
||||
const formatDuration = (value: number, unit: string): string => {
|
||||
return `${value}${unit}`;
|
||||
};
|
||||
|
||||
export const OrgUserAccessTokenLimitSection = () => {
|
||||
const { mutateAsync: updateUserTokenExpiration } = useUpdateOrg();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
// Parse the current duration or use default
|
||||
const currentDuration = parseDuration(currentOrg?.userTokenExpiration || "30d");
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting, isDirty },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
expirationValue: currentDuration.value,
|
||||
expirationUnit: currentDuration.unit as "m" | "h" | "d" | "w"
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentOrg) return null;
|
||||
|
||||
const handleUserTokenExpirationSubmit = async (formData: TForm) => {
|
||||
try {
|
||||
const userTokenExpiration = formatDuration(formData.expirationValue, formData.expirationUnit);
|
||||
|
||||
await updateUserTokenExpiration({
|
||||
userTokenExpiration,
|
||||
orgId: currentOrg.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated user token expiration",
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed updating user token expiration",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Units for the dropdown with readable labels
|
||||
const timeUnits = [
|
||||
{ value: "m", label: "Minutes" },
|
||||
{ value: "h", label: "Hours" },
|
||||
{ value: "d", label: "Days" },
|
||||
{ value: "w", label: "Weeks" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-6 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-semibold">User Token Expiration</p>
|
||||
</div>
|
||||
<p className="mb-4 mt-2 text-sm text-gray-400">
|
||||
This defines the maximum time a user token will be valid. After this time, the user will
|
||||
need to re-authenticate.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(handleUserTokenExpirationSubmit)} autoComplete="off">
|
||||
<div className="flex max-w-md gap-4">
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expirationValue"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Expiration value"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || 1)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expirationUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Time unit">
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
placeholder="Select time unit"
|
||||
>
|
||||
{timeUnits.map(({ value, label }) => (
|
||||
<SelectItem
|
||||
key={value}
|
||||
value={value}
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">{label}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
className="mt-4"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user