mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 16:27:40 +00:00
UI polish: Add better time inputs and tooltips
This commit is contained in:
@@ -22,3 +22,89 @@ export const formatDateTime = ({
|
|||||||
}
|
}
|
||||||
return format(date, dateFormat);
|
return format(date, dateFormat);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to convert duration to seconds
|
||||||
|
export const durationToSeconds = (
|
||||||
|
value: number,
|
||||||
|
unit: "s" | "m" | "h" | "d" | "w" | "y"
|
||||||
|
): number => {
|
||||||
|
switch (unit) {
|
||||||
|
case "s":
|
||||||
|
return value;
|
||||||
|
case "m":
|
||||||
|
return value * 60;
|
||||||
|
case "h":
|
||||||
|
return value * 60 * 60;
|
||||||
|
case "d":
|
||||||
|
return value * 60 * 60 * 24;
|
||||||
|
case "w":
|
||||||
|
return value * 60 * 60 * 24 * 7;
|
||||||
|
case "y":
|
||||||
|
return value * 60 * 60 * 24 * 365;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to convert seconds to value and unit
|
||||||
|
export const getObjectFromSeconds = (
|
||||||
|
totalSeconds: number,
|
||||||
|
activeUnits?: Array<"s" | "m" | "h" | "d" | "w" | "y">
|
||||||
|
): { value: number; unit: "s" | "m" | "h" | "d" | "w" | "y" } => {
|
||||||
|
const SECONDS_IN_MINUTE = 60;
|
||||||
|
const SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60;
|
||||||
|
const SECONDS_IN_DAY = SECONDS_IN_HOUR * 24;
|
||||||
|
const SECONDS_IN_WEEK = SECONDS_IN_DAY * 7;
|
||||||
|
const SECONDS_IN_YEAR = SECONDS_IN_DAY * 365;
|
||||||
|
|
||||||
|
const activeUnitsSet = activeUnits ? new Set(activeUnits) : null;
|
||||||
|
|
||||||
|
const isUnitActive = (unit: "s" | "m" | "h" | "d" | "w" | "y"): boolean => {
|
||||||
|
return activeUnitsSet ? activeUnitsSet.has(unit) : true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
isUnitActive("y") &&
|
||||||
|
totalSeconds >= SECONDS_IN_YEAR &&
|
||||||
|
totalSeconds % SECONDS_IN_YEAR === 0
|
||||||
|
) {
|
||||||
|
return { value: totalSeconds / SECONDS_IN_YEAR, unit: "y" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isUnitActive("w") &&
|
||||||
|
totalSeconds >= SECONDS_IN_WEEK &&
|
||||||
|
totalSeconds % SECONDS_IN_WEEK === 0
|
||||||
|
) {
|
||||||
|
return { value: totalSeconds / SECONDS_IN_WEEK, unit: "w" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUnitActive("d") && totalSeconds >= SECONDS_IN_DAY && totalSeconds % SECONDS_IN_DAY === 0) {
|
||||||
|
return { value: totalSeconds / SECONDS_IN_DAY, unit: "d" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isUnitActive("h") &&
|
||||||
|
totalSeconds >= SECONDS_IN_HOUR &&
|
||||||
|
totalSeconds % SECONDS_IN_HOUR === 0
|
||||||
|
) {
|
||||||
|
return { value: totalSeconds / SECONDS_IN_HOUR, unit: "h" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isUnitActive("m") &&
|
||||||
|
totalSeconds >= SECONDS_IN_MINUTE &&
|
||||||
|
totalSeconds % SECONDS_IN_MINUTE === 0
|
||||||
|
) {
|
||||||
|
return { value: totalSeconds / SECONDS_IN_MINUTE, unit: "m" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUnitActive("s") && totalSeconds >= 1) {
|
||||||
|
return { value: totalSeconds, unit: "s" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: 0,
|
||||||
|
unit: "s"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
+256
-79
@@ -11,6 +11,8 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
Switch,
|
Switch,
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
@@ -27,6 +29,7 @@ import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityFormTab } from "./types";
|
import { IdentityFormTab } from "./types";
|
||||||
|
import { durationToSeconds, getObjectFromSeconds } from "@app/helpers/datetime";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -69,20 +72,73 @@ const schema = z
|
|||||||
(value) => Number(value) <= 30 && Number(value) >= 1,
|
(value) => Number(value) <= 30 && Number(value) >= 1,
|
||||||
"Lockout threshold must be between 1 and 30"
|
"Lockout threshold must be between 1 and 30"
|
||||||
),
|
),
|
||||||
lockoutDuration: z
|
lockoutDurationValue: z.string(),
|
||||||
.string()
|
lockoutDurationUnit: z.enum(["s", "m", "h", "d"], {
|
||||||
.refine(
|
invalid_type_error: "Please select a valid time unit"
|
||||||
(value) => Number(value) <= 86400 && Number(value) >= 30,
|
}),
|
||||||
"Lockout duration must be between 30 seconds and 1 day"
|
lockoutCounterResetValue: z.string(),
|
||||||
),
|
lockoutCounterResetUnit: z.enum(["s", "m", "h"], {
|
||||||
lockoutCounterReset: z
|
invalid_type_error: "Please select a valid time unit"
|
||||||
.string()
|
})
|
||||||
.refine(
|
|
||||||
(value) => Number(value) <= 3600 && Number(value) >= 5,
|
|
||||||
"Lockout counter reset must be between 5 seconds and 1 hour"
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
const {
|
||||||
|
lockoutDurationValue,
|
||||||
|
lockoutCounterResetValue,
|
||||||
|
lockoutDurationUnit,
|
||||||
|
lockoutCounterResetUnit,
|
||||||
|
lockoutEnabled
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
if (!lockoutEnabled) return;
|
||||||
|
|
||||||
|
let isAnyParseError = false;
|
||||||
|
|
||||||
|
const parsedLockoutDuration = parseInt(lockoutDurationValue, 10);
|
||||||
|
if (isNaN(parsedLockoutDuration)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Lockout duration must be a number",
|
||||||
|
path: ["lockoutDurationValue"]
|
||||||
|
});
|
||||||
|
isAnyParseError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedLockoutCounterReset = parseInt(lockoutCounterResetValue, 10);
|
||||||
|
if (isNaN(parsedLockoutCounterReset)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Lockout counter reset must be a number",
|
||||||
|
path: ["lockoutCounterResetValue"]
|
||||||
|
});
|
||||||
|
isAnyParseError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnyParseError) return;
|
||||||
|
|
||||||
|
const lockoutDurationInSeconds = durationToSeconds(parsedLockoutDuration, lockoutDurationUnit);
|
||||||
|
const lockoutCounterResetInSeconds = durationToSeconds(
|
||||||
|
parsedLockoutCounterReset,
|
||||||
|
lockoutCounterResetUnit
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Lockout duration must be between 30 seconds and 1 day",
|
||||||
|
path: ["lockoutDurationValue"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lockoutCounterResetInSeconds > 3600 || lockoutCounterResetInSeconds < 5) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Lockout counter reset must be between 5 seconds and 1 hour",
|
||||||
|
path: ["lockoutCounterResetValue"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
@@ -130,8 +186,10 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenPeriod: "0",
|
accessTokenPeriod: "0",
|
||||||
lockoutEnabled: true,
|
lockoutEnabled: true,
|
||||||
lockoutThreshold: "3",
|
lockoutThreshold: "3",
|
||||||
lockoutDuration: "300",
|
lockoutDurationValue: "5",
|
||||||
lockoutCounterReset: "30"
|
lockoutDurationUnit: "m",
|
||||||
|
lockoutCounterResetValue: "30",
|
||||||
|
lockoutCounterResetUnit: "s"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -139,8 +197,10 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
|
|
||||||
const lockoutEnabled = watch("lockoutEnabled");
|
const lockoutEnabled = watch("lockoutEnabled");
|
||||||
const lockoutThreshold = watch("lockoutThreshold");
|
const lockoutThreshold = watch("lockoutThreshold");
|
||||||
const lockoutDuration = watch("lockoutDuration");
|
const lockoutDurationValue = watch("lockoutDurationValue");
|
||||||
const lockoutCounterReset = watch("lockoutCounterReset");
|
const lockoutDurationUnit = watch("lockoutDurationUnit");
|
||||||
|
const lockoutCounterResetValue = watch("lockoutCounterResetValue");
|
||||||
|
const lockoutCounterResetUnit = watch("lockoutCounterResetUnit");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fields: clientSecretTrustedIpsFields,
|
fields: clientSecretTrustedIpsFields,
|
||||||
@@ -155,6 +215,9 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
const lockoutDurationObj = getObjectFromSeconds(data.lockoutDuration);
|
||||||
|
const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterReset);
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
accessTokenTTL: String(data.accessTokenTTL),
|
accessTokenTTL: String(data.accessTokenTTL),
|
||||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||||
@@ -176,8 +239,10 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
),
|
),
|
||||||
lockoutEnabled: data.lockoutEnabled,
|
lockoutEnabled: data.lockoutEnabled,
|
||||||
lockoutThreshold: String(data.lockoutThreshold),
|
lockoutThreshold: String(data.lockoutThreshold),
|
||||||
lockoutDuration: String(data.lockoutDuration),
|
lockoutDurationValue: String(lockoutDurationObj.value),
|
||||||
lockoutCounterReset: String(data.lockoutCounterReset)
|
lockoutDurationUnit: lockoutDurationObj.unit as "s" | "m" | "h" | "d",
|
||||||
|
lockoutCounterResetValue: String(lockoutCounterResetObj.value),
|
||||||
|
lockoutCounterResetUnit: lockoutCounterResetObj.unit as "s" | "m" | "h"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
@@ -189,8 +254,10 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||||
lockoutEnabled: true,
|
lockoutEnabled: true,
|
||||||
lockoutThreshold: "3",
|
lockoutThreshold: "3",
|
||||||
lockoutDuration: "300",
|
lockoutDurationValue: "5",
|
||||||
lockoutCounterReset: "30"
|
lockoutDurationUnit: "m",
|
||||||
|
lockoutCounterResetValue: "30",
|
||||||
|
lockoutCounterResetUnit: "s"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@@ -204,12 +271,20 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenPeriod,
|
accessTokenPeriod,
|
||||||
lockoutEnabled,
|
lockoutEnabled,
|
||||||
lockoutThreshold,
|
lockoutThreshold,
|
||||||
lockoutDuration,
|
lockoutDurationValue,
|
||||||
lockoutCounterReset
|
lockoutDurationUnit,
|
||||||
|
lockoutCounterResetValue,
|
||||||
|
lockoutCounterResetUnit
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityId) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
|
const lockoutDuration = durationToSeconds(Number(lockoutDurationValue), lockoutDurationUnit);
|
||||||
|
const lockoutCounterReset = durationToSeconds(
|
||||||
|
Number(lockoutCounterResetValue),
|
||||||
|
lockoutCounterResetUnit
|
||||||
|
);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// update universal auth configuration
|
// update universal auth configuration
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
@@ -223,8 +298,8 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenPeriod: Number(accessTokenPeriod),
|
accessTokenPeriod: Number(accessTokenPeriod),
|
||||||
lockoutEnabled,
|
lockoutEnabled,
|
||||||
lockoutThreshold: Number(lockoutThreshold),
|
lockoutThreshold: Number(lockoutThreshold),
|
||||||
lockoutDuration: Number(lockoutDuration),
|
lockoutDuration,
|
||||||
lockoutCounterReset: Number(lockoutCounterReset)
|
lockoutCounterReset
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// create new universal auth configuration
|
// create new universal auth configuration
|
||||||
@@ -272,8 +347,10 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
: [
|
: [
|
||||||
"lockoutEnabled",
|
"lockoutEnabled",
|
||||||
"lockoutThreshold",
|
"lockoutThreshold",
|
||||||
"lockoutDuration",
|
"lockoutDurationValue",
|
||||||
"lockoutCounterReset"
|
"lockoutDurationUnit",
|
||||||
|
"lockoutCounterResetValue",
|
||||||
|
"lockoutCounterResetUnit"
|
||||||
].includes(Object.keys(fields)[0])
|
].includes(Object.keys(fields)[0])
|
||||||
? IdentityFormTab.Lockout
|
? IdentityFormTab.Lockout
|
||||||
: IdentityFormTab.Configuration
|
: IdentityFormTab.Configuration
|
||||||
@@ -362,7 +439,7 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl
|
||||||
helperText={`The lockout feature will prevent login attempts for ${lockoutDuration || 300} seconds after ${lockoutThreshold || 3} consecutive login failures. If ${lockoutCounterReset || 30} seconds pass after the most recent failure, the lockout counter resets.`}
|
helperText={`The lockout feature will prevent login attempts for ${lockoutDurationValue}${lockoutDurationUnit} after ${lockoutThreshold} consecutive login failures. If ${lockoutCounterResetValue}${lockoutCounterResetUnit} pass after the most recent failure, the lockout counter resets.`}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
@@ -380,57 +457,157 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<div className="flex flex-col gap-2">
|
||||||
control={control}
|
<Controller
|
||||||
name="lockoutThreshold"
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => {
|
name="lockoutThreshold"
|
||||||
return (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<FormControl
|
return (
|
||||||
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
|
<FormControl
|
||||||
label="Lockout Threshold"
|
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
|
||||||
isError={Boolean(error)}
|
label="Lockout Threshold"
|
||||||
errorText={error?.message}
|
isError={Boolean(error)}
|
||||||
tooltipText="The amount of times login must fail before locking the identity auth method"
|
errorText={error?.message}
|
||||||
>
|
tooltipText="The amount of times login must fail before locking the identity auth method"
|
||||||
<Input {...field} placeholder="3" isDisabled={!lockoutEnabled} />
|
>
|
||||||
</FormControl>
|
<Input
|
||||||
);
|
{...field}
|
||||||
}}
|
placeholder="Enter lockout threshold..."
|
||||||
/>
|
isDisabled={!lockoutEnabled}
|
||||||
<Controller
|
/>
|
||||||
control={control}
|
</FormControl>
|
||||||
name="lockoutDuration"
|
);
|
||||||
render={({ field, fieldState: { error } }) => {
|
}}
|
||||||
return (
|
/>
|
||||||
<FormControl
|
<div className="flex items-end gap-2">
|
||||||
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
|
<Controller
|
||||||
label="Lockout Duration (seconds)"
|
control={control}
|
||||||
isError={Boolean(error)}
|
name="lockoutDurationValue"
|
||||||
errorText={error?.message}
|
render={({ field, fieldState: { error } }) => {
|
||||||
tooltipText="How long an identity auth method lockout lasts"
|
return (
|
||||||
>
|
<FormControl
|
||||||
<Input {...field} placeholder="300" isDisabled={!lockoutEnabled} />
|
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
|
||||||
</FormControl>
|
label="Lockout Duration"
|
||||||
);
|
isError={Boolean(error)}
|
||||||
}}
|
errorText={error?.message}
|
||||||
/>
|
tooltipText="How long an identity auth method lockout lasts"
|
||||||
<Controller
|
>
|
||||||
control={control}
|
<Input
|
||||||
name="lockoutCounterReset"
|
{...field}
|
||||||
render={({ field, fieldState: { error } }) => {
|
placeholder="Enter lockout duration..."
|
||||||
return (
|
isDisabled={!lockoutEnabled}
|
||||||
<FormControl
|
/>
|
||||||
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
|
</FormControl>
|
||||||
label="Lockout Counter Reset (seconds)"
|
);
|
||||||
isError={Boolean(error)}
|
}}
|
||||||
errorText={error?.message}
|
/>
|
||||||
tooltipText="How long to wait from the most recent failed login until resetting the lockout counter"
|
<Controller
|
||||||
>
|
control={control}
|
||||||
<Input {...field} placeholder="30" isDisabled={!lockoutEnabled} />
|
name="lockoutDurationUnit"
|
||||||
</FormControl>
|
render={({ field, fieldState: { error } }) => (
|
||||||
);
|
<FormControl
|
||||||
}}
|
className={`mb-0 ${lockoutEnabled ? "" : "opacity-70"}`}
|
||||||
/>
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
isDisabled={!lockoutEnabled}
|
||||||
|
value={field.value}
|
||||||
|
className="min-w-32 pr-2"
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value="s"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Seconds</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="m"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Minutes</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="h"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Hours</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="d"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Days</div>
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="lockoutCounterResetValue"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
|
||||||
|
label="Lockout Counter Reset"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
tooltipText="How long to wait from the most recent failed login until resetting the lockout counter"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="Enter lockout counter reset..."
|
||||||
|
isDisabled={!lockoutEnabled}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="lockoutCounterResetUnit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className={`mb-0 ${lockoutEnabled ? "" : "opacity-70"}`}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
isDisabled={!lockoutEnabled}
|
||||||
|
value={field.value}
|
||||||
|
className="min-w-32 pr-2"
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value="s"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Seconds</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="m"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Minutes</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="h"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Hours</div>
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
|
|||||||
+1
-7
@@ -120,13 +120,7 @@ export const ViewIdentityUniversalAuthContent = ({
|
|||||||
isDisabled={!isAllowed || !lockedOutState || isClearLockoutsPending}
|
isDisabled={!isAllowed || !lockedOutState || isClearLockoutsPending}
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={clearLockouts}
|
onClick={clearLockouts}
|
||||||
leftIcon={
|
isLoading={isClearLockoutsPending}
|
||||||
isClearLockoutsPending ? (
|
|
||||||
<FontAwesomeIcon icon={faArrowsRotate} className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon icon={faFire} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
>
|
>
|
||||||
Clear All Lockouts
|
Clear All Lockouts
|
||||||
|
|||||||
+10
-72
@@ -8,63 +8,11 @@ import { OrgPermissionCan } from "@app/components/permissions";
|
|||||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { useUpdateOrg } from "@app/hooks/api";
|
import { useUpdateOrg } from "@app/hooks/api";
|
||||||
|
import { durationToSeconds, getObjectFromSeconds } from "@app/helpers/datetime";
|
||||||
|
|
||||||
const MAX_SHARED_SECRET_LIFETIME_SECONDS = 30 * 24 * 60 * 60; // 30 days in seconds
|
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 MIN_SHARED_SECRET_LIFETIME_SECONDS = 5 * 60; // 5 minutes in seconds
|
||||||
|
|
||||||
// Helper function to convert duration to seconds
|
|
||||||
const durationToSeconds = (value: number, unit: "m" | "h" | "d"): number => {
|
|
||||||
switch (unit) {
|
|
||||||
case "m":
|
|
||||||
return value * 60;
|
|
||||||
case "h":
|
|
||||||
return value * 60 * 60;
|
|
||||||
case "d":
|
|
||||||
return value * 60 * 60 * 24;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to convert seconds to form lifetime value and unit
|
|
||||||
const getFormLifetimeFromSeconds = (
|
|
||||||
totalSeconds: number | null | undefined
|
|
||||||
): { maxLifetimeValue: number; maxLifetimeUnit: "m" | "h" | "d" } => {
|
|
||||||
const DEFAULT_LIFETIME_VALUE = 30;
|
|
||||||
const DEFAULT_LIFETIME_UNIT = "d" as "m" | "h" | "d";
|
|
||||||
|
|
||||||
if (totalSeconds == null || totalSeconds <= 0) {
|
|
||||||
return {
|
|
||||||
maxLifetimeValue: DEFAULT_LIFETIME_VALUE,
|
|
||||||
maxLifetimeUnit: DEFAULT_LIFETIME_UNIT
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const secondsInDay = 24 * 60 * 60;
|
|
||||||
const secondsInHour = 60 * 60;
|
|
||||||
const secondsInMinute = 60;
|
|
||||||
|
|
||||||
if (totalSeconds % secondsInDay === 0) {
|
|
||||||
const value = totalSeconds / secondsInDay;
|
|
||||||
if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "d" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalSeconds % secondsInHour === 0) {
|
|
||||||
const value = totalSeconds / secondsInHour;
|
|
||||||
if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "h" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalSeconds % secondsInMinute === 0) {
|
|
||||||
const value = totalSeconds / secondsInMinute;
|
|
||||||
if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "m" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
maxLifetimeValue: DEFAULT_LIFETIME_VALUE,
|
|
||||||
maxLifetimeUnit: DEFAULT_LIFETIME_UNIT
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
maxLifetimeValue: z.number().min(1, "Value must be at least 1"),
|
maxLifetimeValue: z.number().min(1, "Value must be at least 1"),
|
||||||
@@ -79,32 +27,18 @@ const formSchema = z
|
|||||||
|
|
||||||
const durationInSeconds = durationToSeconds(maxLifetimeValue, maxLifetimeUnit);
|
const durationInSeconds = durationToSeconds(maxLifetimeValue, maxLifetimeUnit);
|
||||||
|
|
||||||
// Check max limit
|
|
||||||
if (durationInSeconds > MAX_SHARED_SECRET_LIFETIME_SECONDS) {
|
if (durationInSeconds > MAX_SHARED_SECRET_LIFETIME_SECONDS) {
|
||||||
let message = "Duration exceeds maximum allowed limit";
|
|
||||||
|
|
||||||
if (maxLifetimeUnit === "m") {
|
|
||||||
message = `Maximum allowed minutes is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / 60} (30 days)`;
|
|
||||||
} else if (maxLifetimeUnit === "h") {
|
|
||||||
message = `Maximum allowed hours is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / (60 * 60)} (30 days)`;
|
|
||||||
} else if (maxLifetimeUnit === "d") {
|
|
||||||
message = `Maximum allowed days is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / (24 * 60 * 60)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message,
|
message: "Duration exceeds a maximum of 30 days",
|
||||||
path: ["maxLifetimeValue"]
|
path: ["maxLifetimeValue"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check min limit
|
|
||||||
if (durationInSeconds < MIN_SHARED_SECRET_LIFETIME_SECONDS) {
|
if (durationInSeconds < MIN_SHARED_SECRET_LIFETIME_SECONDS) {
|
||||||
const message = `Duration must be at least ${MIN_SHARED_SECRET_LIFETIME_SECONDS / 60} minutes`; // 5 minutes
|
|
||||||
|
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message,
|
message: "Duration must be at least 5 minutes",
|
||||||
path: ["maxLifetimeValue"]
|
path: ["maxLifetimeValue"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -122,10 +56,14 @@ export const OrgSecretShareLimitSection = () => {
|
|||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
const getDefaultFormValues = () => {
|
const getDefaultFormValues = () => {
|
||||||
const initialLifetime = getFormLifetimeFromSeconds(currentOrg?.maxSharedSecretLifetime);
|
const initialLifetime = getObjectFromSeconds(currentOrg?.maxSharedSecretLifetime, [
|
||||||
|
"m",
|
||||||
|
"h",
|
||||||
|
"d"
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
maxLifetimeValue: initialLifetime.maxLifetimeValue,
|
maxLifetimeValue: initialLifetime.value,
|
||||||
maxLifetimeUnit: initialLifetime.maxLifetimeUnit,
|
maxLifetimeUnit: initialLifetime.unit as "m" | "h" | "d",
|
||||||
maxViewLimit: currentOrg?.maxSharedSecretViewLimit?.toString() || "1",
|
maxViewLimit: currentOrg?.maxSharedSecretViewLimit?.toString() || "1",
|
||||||
shouldLimitView: Boolean(currentOrg?.maxSharedSecretViewLimit)
|
shouldLimitView: Boolean(currentOrg?.maxSharedSecretViewLimit)
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user