- Swap to using ms in some frontend areas - Rename button from "Clear

All Lockouts" to "Reset All Lockouts" - Add a tooltip to the red lock
icon on auth row - Make the red lock icon go away after resetting all
lockouts
This commit is contained in:
x032205
2025-08-27 04:47:21 -04:00
parent f52dbaa2f2
commit 8d6461b01d
8 changed files with 37 additions and 52 deletions
-23
View File
@@ -23,29 +23,6 @@ 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 // Helper function to convert seconds to value and unit
export const getObjectFromSeconds = ( export const getObjectFromSeconds = (
totalSeconds: number, totalSeconds: number,
@@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -20,7 +21,7 @@ import {
Tabs Tabs
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { durationToSeconds, getObjectFromSeconds } from "@app/helpers/datetime"; import { getObjectFromSeconds } from "@app/helpers/datetime";
import { import {
useAddIdentityUniversalAuth, useAddIdentityUniversalAuth,
useGetIdentityUniversalAuth, useGetIdentityUniversalAuth,
@@ -117,11 +118,9 @@ const schema = z
if (isAnyParseError) return; if (isAnyParseError) return;
const lockoutDurationInSeconds = durationToSeconds(parsedLockoutDuration, lockoutDurationUnit); const lockoutDurationInSeconds = ms(`${parsedLockoutDuration}${lockoutDurationUnit}`) / 1000;
const lockoutCounterResetInSeconds = durationToSeconds( const lockoutCounterResetInSeconds =
parsedLockoutCounterReset, ms(`${parsedLockoutCounterReset}${lockoutCounterResetUnit}`) / 1000;
lockoutCounterResetUnit
);
if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) { if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) {
ctx.addIssue({ ctx.addIssue({
@@ -279,14 +278,9 @@ export const IdentityUniversalAuthForm = ({
try { try {
if (!identityId) return; if (!identityId) return;
const lockoutDurationSeconds = durationToSeconds( const lockoutDurationSeconds = ms(`${lockoutDurationValue}${lockoutDurationUnit}`) / 1000;
Number(lockoutDurationValue), const lockoutCounterResetSeconds =
lockoutDurationUnit ms(`${lockoutCounterResetValue}${lockoutCounterResetUnit}`) / 1000;
);
const lockoutCounterResetSeconds = durationToSeconds(
Number(lockoutCounterResetValue),
lockoutCounterResetUnit
);
if (data) { if (data) {
// update universal auth configuration // update universal auth configuration
@@ -118,6 +118,7 @@ const Page = () => {
authMethod={popUp.viewAuthMethod.data?.authMethod} authMethod={popUp.viewAuthMethod.data?.authMethod}
lockedOut={popUp.viewAuthMethod.data?.lockedOut || false} lockedOut={popUp.viewAuthMethod.data?.lockedOut || false}
identityId={identityId} identityId={identityId}
onResetAllLockouts={popUp.viewAuthMethod.data?.refetchIdentity}
/> />
</div> </div>
); );
@@ -2,7 +2,7 @@ import { faCog, faLock, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button, Tooltip } from "@app/components/v2";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api"; import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -16,7 +16,7 @@ type Props = {
}; };
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => { export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => {
const { data } = useGetIdentityById(identityId); const { data, refetch } = useGetIdentityById(identityId);
return data ? ( return data ? (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
@@ -31,7 +31,8 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P
onClick={() => onClick={() =>
handlePopUpOpen("viewAuthMethod", { handlePopUpOpen("viewAuthMethod", {
authMethod, authMethod,
lockedOut: data.identity.activeLockoutAuthMethods.includes(authMethod) lockedOut: data.identity.activeLockoutAuthMethods.includes(authMethod),
refetchIdentity: refetch
}) })
} }
type="button" type="button"
@@ -40,7 +41,9 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P
<span>{identityAuthToNameMap[authMethod]}</span> <span>{identityAuthToNameMap[authMethod]}</span>
<div className="flex gap-2"> <div className="flex gap-2">
{data.identity.activeLockoutAuthMethods.includes(authMethod) && ( {data.identity.activeLockoutAuthMethods.includes(authMethod) && (
<FontAwesomeIcon icon={faLock} size="xs" className="text-red-400/50" /> <Tooltip content="Auth method has active lockouts">
<FontAwesomeIcon icon={faLock} size="xs" className="text-red-400/50" />
</Tooltip>
)} )}
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" /> <FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" />
</div> </div>
@@ -41,6 +41,7 @@ type Props = {
isOpen: boolean; isOpen: boolean;
onOpenChange: (isOpen: boolean) => void; onOpenChange: (isOpen: boolean) => void;
onDeleteAuthMethod: () => void; onDeleteAuthMethod: () => void;
onResetAllLockouts: () => void;
}; };
type TRevokeOptions = { type TRevokeOptions = {
@@ -52,8 +53,12 @@ export const Content = ({
identityId, identityId,
authMethod, authMethod,
lockedOut, lockedOut,
onDeleteAuthMethod onDeleteAuthMethod,
}: Pick<Props, "authMethod" | "lockedOut" | "identityId" | "onDeleteAuthMethod">) => { onResetAllLockouts
}: Pick<
Props,
"authMethod" | "lockedOut" | "identityId" | "onDeleteAuthMethod" | "onResetAllLockouts"
>) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -161,6 +166,7 @@ export const Content = ({
<Component <Component
identityId={identityId} identityId={identityId}
onDelete={handleDelete} onDelete={handleDelete}
onResetAllLockouts={onResetAllLockouts}
popUp={popUp} popUp={popUp}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
@@ -188,7 +194,8 @@ export const ViewIdentityAuthModal = ({
onOpenChange, onOpenChange,
authMethod, authMethod,
identityId, identityId,
lockedOut lockedOut,
onResetAllLockouts
}: Omit<Props, "onDeleteAuthMethod">) => { }: Omit<Props, "onDeleteAuthMethod">) => {
if (!identityId || !authMethod) return null; if (!identityId || !authMethod) return null;
@@ -200,6 +207,7 @@ export const ViewIdentityAuthModal = ({
authMethod={authMethod} authMethod={authMethod}
lockedOut={lockedOut} lockedOut={lockedOut}
onDeleteAuthMethod={() => onOpenChange(false)} onDeleteAuthMethod={() => onOpenChange(false)}
onResetAllLockouts={() => onResetAllLockouts()}
/> />
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -26,7 +26,8 @@ export const ViewIdentityUniversalAuthContent = ({
handlePopUpOpen, handlePopUpOpen,
onDelete, onDelete,
popUp, popUp,
lockedOut lockedOut,
onResetAllLockouts
}: ViewAuthMethodProps) => { }: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityUniversalAuth(identityId); const { data, isPending } = useGetIdentityUniversalAuth(identityId);
const { data: clientSecrets = [], isPending: clientSecretsPending } = const { data: clientSecrets = [], isPending: clientSecretsPending } =
@@ -48,6 +49,7 @@ export const ViewIdentityUniversalAuthContent = ({
type: "success" type: "success"
}); });
setLockedOutState(false); setLockedOutState(false);
onResetAllLockouts();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
createNotification({ createNotification({
@@ -124,7 +126,7 @@ export const ViewIdentityUniversalAuthContent = ({
isLoading={isClearLockoutsPending} isLoading={isClearLockoutsPending}
colorSchema="secondary" colorSchema="secondary"
> >
Clear All Lockouts Reset All Lockouts
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
@@ -10,4 +10,5 @@ export type ViewAuthMethodProps = {
) => void; ) => void;
popUp: UsePopUpState<["revokeAuthMethod", "upgradePlan", "identityAuthMethod"]>; popUp: UsePopUpState<["revokeAuthMethod", "upgradePlan", "identityAuthMethod"]>;
lockedOut: boolean; lockedOut: boolean;
onResetAllLockouts: () => void;
}; };
@@ -1,13 +1,14 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; 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 { durationToSeconds, getObjectFromSeconds } from "@app/helpers/datetime"; import { getObjectFromSeconds } from "@app/helpers/datetime";
import { useUpdateOrg } from "@app/hooks/api"; import { useUpdateOrg } from "@app/hooks/api";
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
@@ -25,7 +26,7 @@ const formSchema = z
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
const { maxLifetimeValue, maxLifetimeUnit } = data; const { maxLifetimeValue, maxLifetimeUnit } = data;
const durationInSeconds = durationToSeconds(maxLifetimeValue, maxLifetimeUnit); const durationInSeconds = ms(`${maxLifetimeValue}${maxLifetimeUnit}`) / 1000;
if (durationInSeconds > MAX_SHARED_SECRET_LIFETIME_SECONDS) { if (durationInSeconds > MAX_SHARED_SECRET_LIFETIME_SECONDS) {
ctx.addIssue({ ctx.addIssue({
@@ -90,10 +91,8 @@ export const OrgSecretShareLimitSection = () => {
const handleFormSubmit = async (formData: TForm) => { const handleFormSubmit = async (formData: TForm) => {
try { try {
const maxSharedSecretLifetimeSeconds = durationToSeconds( const maxSharedSecretLifetimeSeconds =
formData.maxLifetimeValue, ms(`${formData.maxLifetimeValue}${formData.maxLifetimeUnit}`) / 1000;
formData.maxLifetimeUnit
);
await mutateAsync({ await mutateAsync({
orgId: currentOrg.id, orgId: currentOrg.id,