mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 09:28:52 +00:00
Merge pull request #4685 from Infisical/ENG-3925
feat(pam): account credential rotation
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export const UNCHANGED_PASSWORD_SENTINEL = "__INFISICAL_UNCHANGED__";
|
||||
@@ -3,6 +3,7 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TPamResourceOption } from "./types/resource-options";
|
||||
import { PamResourceType } from "./enums";
|
||||
import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types";
|
||||
|
||||
export const pamKeys = {
|
||||
@@ -12,6 +13,12 @@ export const pamKeys = {
|
||||
session: () => [...pamKeys.all, "session"] as const,
|
||||
listResourceOptions: () => [...pamKeys.resource(), "options"] as const,
|
||||
listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId],
|
||||
getResource: (resourceType: string, resourceId: string) => [
|
||||
...pamKeys.resource(),
|
||||
"get",
|
||||
resourceType,
|
||||
resourceId
|
||||
],
|
||||
listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId],
|
||||
getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId],
|
||||
listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId]
|
||||
@@ -68,6 +75,28 @@ export const useListPamResources = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetPamResourceById = (
|
||||
resourceType?: PamResourceType,
|
||||
resourceId?: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<TPamResource, unknown, TPamResource, ReturnType<typeof pamKeys.getResource>>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: pamKeys.getResource(resourceType || "", resourceId || ""),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ resource: TPamResource }>(
|
||||
`/api/v1/pam/resources/${resourceType}/${resourceId}`
|
||||
);
|
||||
|
||||
return data.resource;
|
||||
},
|
||||
enabled: !!resourceId && !!resourceType && (options?.enabled ?? true),
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
// Accounts
|
||||
export const useListPamAccounts = (
|
||||
projectId: string,
|
||||
|
||||
@@ -9,9 +9,13 @@ export interface TBasePamAccount {
|
||||
id: string;
|
||||
name: string;
|
||||
resourceType: PamResourceType;
|
||||
rotationCredentialsConfigured: boolean;
|
||||
};
|
||||
name: string;
|
||||
description?: string | null;
|
||||
rotationEnabled: boolean;
|
||||
rotationIntervalSeconds?: number | null;
|
||||
lastRotatedAt?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TBasePamResource } from "./base-resource";
|
||||
// Resources
|
||||
export type TPostgresResource = TBasePamResource & { resourceType: PamResourceType.Postgres } & {
|
||||
connectionDetails: TBaseSqlConnectionDetails;
|
||||
rotationAccountCredentials?: TBaseSqlCredentials | null;
|
||||
};
|
||||
|
||||
// Accounts
|
||||
|
||||
+9
-6
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -29,6 +30,8 @@ const formSchema = z.object({
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const isUpdate = Boolean(auditLogStream);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
@@ -96,10 +99,10 @@ export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: P
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="Bearer <token>"
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
onFocus={(e) => {
|
||||
placeholder="Bearer <token>"
|
||||
onFocus={() => {
|
||||
if (
|
||||
auditLogStream &&
|
||||
auditLogStream.credentials.headers[i] &&
|
||||
@@ -108,9 +111,9 @@ export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: P
|
||||
) {
|
||||
field.onChange("");
|
||||
}
|
||||
e.target.type = "text";
|
||||
setShowPassword(true);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
onBlur={() => {
|
||||
if (
|
||||
auditLogStream &&
|
||||
auditLogStream.credentials.headers[i] &&
|
||||
@@ -119,7 +122,7 @@ export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: P
|
||||
) {
|
||||
field.onChange("******");
|
||||
}
|
||||
e.target.type = "password";
|
||||
setShowPassword(false);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
+15
-5
@@ -34,10 +34,11 @@ const CreateForm = ({
|
||||
}: CreateFormProps) => {
|
||||
const createPamAccount = useCreatePamAccount();
|
||||
|
||||
console.log({ folderId });
|
||||
|
||||
const onSubmit = async (
|
||||
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
|
||||
formData: DiscriminativePick<
|
||||
TPamAccount,
|
||||
"name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds"
|
||||
>
|
||||
) => {
|
||||
try {
|
||||
const account = await createPamAccount.mutateAsync({
|
||||
@@ -64,7 +65,13 @@ const CreateForm = ({
|
||||
|
||||
switch (resourceType) {
|
||||
case PamResourceType.Postgres:
|
||||
return <PostgresAccountForm onSubmit={onSubmit} />;
|
||||
return (
|
||||
<PostgresAccountForm
|
||||
onSubmit={onSubmit}
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unhandled resource: ${resourceType}`);
|
||||
}
|
||||
@@ -74,7 +81,10 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
|
||||
const updatePamAccount = useUpdatePamAccount();
|
||||
|
||||
const onSubmit = async (
|
||||
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
|
||||
formData: DiscriminativePick<
|
||||
TPamAccount,
|
||||
"name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds"
|
||||
>
|
||||
) => {
|
||||
try {
|
||||
const updatedAccount = await updatePamAccount.mutateAsync({
|
||||
|
||||
+24
-4
@@ -1,26 +1,31 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { TPostgresAccount } from "@app/hooks/api/pam";
|
||||
import { PamResourceType, TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam";
|
||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||
|
||||
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
|
||||
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
||||
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||
import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields";
|
||||
|
||||
type Props = {
|
||||
account?: TPostgresAccount;
|
||||
resourceId?: string;
|
||||
resourceType?: PamResourceType;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = genericAccountFieldsSchema.extend({
|
||||
const formSchema = genericAccountFieldsSchema.extend(rotateAccountFieldsSchema.shape).extend({
|
||||
credentials: BaseSqlAccountSchema
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
|
||||
export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(account);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
@@ -30,7 +35,7 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
|
||||
...account,
|
||||
credentials: {
|
||||
...account.credentials,
|
||||
password: "******"
|
||||
password: UNCHANGED_PASSWORD_SENTINEL
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
@@ -41,6 +46,20 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
const [rotationCredentialsConfigured, setRotationCredentialsConfigured] = useState(false);
|
||||
|
||||
const { data: resource } = useGetPamResourceById(resourceType, resourceId, {
|
||||
enabled: !account && !!resourceId && !!resourceType
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
setRotationCredentialsConfigured(account.resource.rotationCredentialsConfigured);
|
||||
} else {
|
||||
setRotationCredentialsConfigured(!!resource?.rotationAccountCredentials);
|
||||
}
|
||||
}, [account, resource]);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
@@ -50,6 +69,7 @@ export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
|
||||
>
|
||||
<GenericAccountFields />
|
||||
<SqlAccountFields isUpdate={isUpdate} />
|
||||
<RotateAccountFields rotationCredentialsConfigured={rotationCredentialsConfigured} />
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { z } from "zod";
|
||||
|
||||
import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/components/v2";
|
||||
|
||||
export const rotateAccountFieldsSchema = z.object({
|
||||
rotationEnabled: z.boolean(),
|
||||
rotationIntervalSeconds: z.number().nullable().optional()
|
||||
});
|
||||
|
||||
export const RotateAccountFields = ({
|
||||
rotationCredentialsConfigured
|
||||
}: {
|
||||
rotationCredentialsConfigured: boolean;
|
||||
}) => {
|
||||
const { control, watch } = useFormContext<{
|
||||
rotationEnabled: boolean;
|
||||
rotationIntervalSeconds?: number | null;
|
||||
}>();
|
||||
|
||||
const rotationEnabled = watch("rotationEnabled");
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
isDisabled={rotationCredentialsConfigured}
|
||||
content="The resource which owns this account does not have rotation credentials configured."
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-9 w-fit items-center gap-3",
|
||||
!rotationCredentialsConfigured && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="rotationEnabled"
|
||||
defaultValue={false}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="mb-0">
|
||||
<Switch
|
||||
className="ml-0 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="rotation-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
isDisabled={!rotationCredentialsConfigured}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<span className="text-sm">Rotate Credentials Every</span>
|
||||
|
||||
<Controller
|
||||
name="rotationIntervalSeconds"
|
||||
control={control}
|
||||
defaultValue={2592000}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
className="mb-0"
|
||||
>
|
||||
<Select
|
||||
value={(value || 2592000).toString()}
|
||||
onValueChange={(val) => onChange(parseInt(val, 10))}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select an interval..."
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isDisabled={!rotationEnabled || !rotationCredentialsConfigured}
|
||||
dropdownContainerStyle={{
|
||||
width: "130px"
|
||||
}}
|
||||
>
|
||||
<SelectItem value="2592000">30 Days</SelectItem>
|
||||
<SelectItem value="604800">7 Days</SelectItem>
|
||||
<SelectItem value="259200">3 Days</SelectItem>
|
||||
<SelectItem value="86400">1 Day</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
+20
-9
@@ -1,9 +1,19 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input } from "@app/components/v2";
|
||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||
|
||||
export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
const { control } = useFormContext();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const password = useWatch({ control, name: "credentials.password" });
|
||||
|
||||
useEffect(() => {
|
||||
if (password === UNCHANGED_PASSWORD_SENTINEL) {
|
||||
setShowPassword(false);
|
||||
}
|
||||
}, [password]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
@@ -17,7 +27,7 @@ export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
isError={Boolean(error?.message)}
|
||||
label="Username"
|
||||
>
|
||||
<Input {...field} />
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -33,18 +43,19 @@ export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
onFocus={(e) => {
|
||||
if (isUpdate && field.value === "******") {
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
onFocus={() => {
|
||||
if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
|
||||
field.onChange("");
|
||||
}
|
||||
e.target.type = "text";
|
||||
setShowPassword(true);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
onBlur={() => {
|
||||
if (isUpdate && field.value === "") {
|
||||
field.onChange("******");
|
||||
field.onChange(UNCHANGED_PASSWORD_SENTINEL);
|
||||
}
|
||||
e.target.type = "password";
|
||||
setShowPassword(false);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
+6
-2
@@ -5,6 +5,10 @@ export const BaseSqlAccountSchema = z.object({
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username required")
|
||||
.max(255, "Username must be 255 characters or less"),
|
||||
password: z.string().trim().min(1, "Password required")
|
||||
.max(63, "Username must be 63 characters or less"),
|
||||
password: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Password required")
|
||||
.max(256, "Password must be 256 characters or less")
|
||||
});
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
faEllipsisV,
|
||||
faFolder,
|
||||
faRightToBracket,
|
||||
faRotate,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -100,6 +102,12 @@ export const PamAccountRow = ({
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
{account.lastRotatedAt && (
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 bg-orange/20 whitespace-nowrap text-orange">
|
||||
<FontAwesomeIcon icon={faRotate} />
|
||||
<span>Rotated {formatDistance(new Date(), account.lastRotatedAt)} ago</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
|
||||
+27
-12
@@ -5,9 +5,12 @@ import { z } from "zod";
|
||||
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { PamResourceType, TPostgresResource } from "@app/hooks/api/pam";
|
||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||
import { BaseSqlAccountSchema } from "@app/pages/pam/PamAccountsPage/components/PamAccountForm/shared/sql-account-schemas";
|
||||
|
||||
import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas";
|
||||
import { SqlResourceFields } from "./shared/SqlResourceFields";
|
||||
import { SqlRotateAccountFields } from "./shared/SqlRotateAccountFields";
|
||||
import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields";
|
||||
|
||||
type Props = {
|
||||
@@ -17,7 +20,8 @@ type Props = {
|
||||
|
||||
const formSchema = genericResourceFieldsSchema.extend({
|
||||
resourceType: z.literal(PamResourceType.Postgres),
|
||||
connectionDetails: BaseSqlResourceSchema
|
||||
connectionDetails: BaseSqlResourceSchema,
|
||||
rotationAccountCredentials: BaseSqlAccountSchema.nullable().optional()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
@@ -28,17 +32,27 @@ export const PostgresResourceForm = ({ resource, onSubmit }: Props) => {
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: resource ?? {
|
||||
resourceType: PamResourceType.Postgres,
|
||||
connectionDetails: {
|
||||
host: "",
|
||||
port: 5432,
|
||||
database: "default",
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: undefined
|
||||
}
|
||||
}
|
||||
defaultValues: resource
|
||||
? {
|
||||
...resource,
|
||||
rotationAccountCredentials: resource.rotationAccountCredentials
|
||||
? {
|
||||
...resource.rotationAccountCredentials,
|
||||
password: UNCHANGED_PASSWORD_SENTINEL
|
||||
}
|
||||
: resource.rotationAccountCredentials
|
||||
}
|
||||
: {
|
||||
resourceType: PamResourceType.Postgres,
|
||||
connectionDetails: {
|
||||
host: "",
|
||||
port: 5432,
|
||||
database: "default",
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -59,6 +73,7 @@ export const PostgresResourceForm = ({ resource, onSubmit }: Props) => {
|
||||
selectedTabIndex={selectedTabIndex}
|
||||
setSelectedTabIndex={setSelectedTabIndex}
|
||||
/>
|
||||
<SqlRotateAccountFields isUpdate={isUpdate} />
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
FormControl,
|
||||
Input
|
||||
} from "@app/components/v2";
|
||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||
|
||||
export const SqlRotateAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
const { control } = useFormContext();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const password = useWatch({ control, name: "credentials.password" });
|
||||
|
||||
useEffect(() => {
|
||||
if (password === UNCHANGED_PASSWORD_SENTINEL) {
|
||||
setShowPassword(false);
|
||||
}
|
||||
}, [password]);
|
||||
|
||||
return (
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
<div className="order-1 ml-3">Rotation Account</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent childrenClassName="px-0 py-0">
|
||||
<p className="mb-2 text-xs">
|
||||
Credentials of the privileged account which will be used for rotating other accounts
|
||||
under this resource
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Controller
|
||||
name="rotationAccountCredentials.username"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0 flex-1"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Username"
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="rotationAccountCredentials.password"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0 flex-1"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Password"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
onFocus={() => {
|
||||
if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
|
||||
field.onChange("");
|
||||
}
|
||||
setShowPassword(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (isUpdate && field.value === "") {
|
||||
field.onChange(UNCHANGED_PASSWORD_SENTINEL);
|
||||
}
|
||||
setShowPassword(false);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user