mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 10:35:17 +00:00
Merge branch 'heads/main' into daniel/request-ids
This commit is contained in:
@@ -4,13 +4,15 @@ import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toas
|
||||
export type TNotification = {
|
||||
title?: string;
|
||||
text: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const NotificationContent = ({ title, text }: TNotification) => {
|
||||
export const NotificationContent = ({ title, text, children }: TNotification) => {
|
||||
return (
|
||||
<div className="msg-container">
|
||||
{title && <div className="text-md mb-1 font-medium">{title}</div>}
|
||||
<div className={title ? "text-sm" : "text-md"}>{text}</div>
|
||||
<div className={title ? "text-sm text-neutral-400" : "text-md"}>{text}</div>
|
||||
{children && <div className="mt-2">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -23,7 +25,13 @@ export const createNotification = (
|
||||
position: "bottom-right",
|
||||
...toastProps,
|
||||
theme: "dark",
|
||||
type: myProps?.type || "info",
|
||||
type: myProps?.type || "info"
|
||||
});
|
||||
|
||||
export const NotificationContainer = () => <ToastContainer pauseOnHover toastClassName="border border-mineshaft-500" style={{ width: "400px" }} />;
|
||||
export const NotificationContainer = () => (
|
||||
<ToastContainer
|
||||
pauseOnHover
|
||||
toastClassName="border border-mineshaft-500"
|
||||
style={{ width: "400px" }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,15 @@ export enum PermissionConditionOperators {
|
||||
$GLOB = "$glob"
|
||||
}
|
||||
|
||||
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
|
||||
[PermissionConditionOperators.$EQ]: "equal to",
|
||||
[PermissionConditionOperators.$IN]: "contains",
|
||||
[PermissionConditionOperators.$ALL]: "contains all",
|
||||
[PermissionConditionOperators.$NEQ]: "not equal to",
|
||||
[PermissionConditionOperators.$GLOB]: "matches glob pattern",
|
||||
[PermissionConditionOperators.$REGEX]: "matches regex pattern"
|
||||
};
|
||||
|
||||
export type TPermissionConditionOperators = {
|
||||
[PermissionConditionOperators.$IN]: string[];
|
||||
[PermissionConditionOperators.$ALL]: string[];
|
||||
|
||||
@@ -9,7 +9,7 @@ export type TGetAuditLogsFilter = {
|
||||
eventMetadata?: Record<string, string>;
|
||||
actorType?: ActorType;
|
||||
projectId?: string;
|
||||
actorId?: string; // user ID format
|
||||
actor?: string; // user ID format
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
limit: number;
|
||||
|
||||
@@ -28,7 +28,8 @@ export enum DynamicSecretProviders {
|
||||
AzureEntraId = "azure-entra-id",
|
||||
Ldap = "ldap",
|
||||
SapHana = "sap-hana",
|
||||
Snowflake = "snowflake"
|
||||
Snowflake = "snowflake",
|
||||
Totp = "totp"
|
||||
}
|
||||
|
||||
export enum SqlProviders {
|
||||
@@ -230,6 +231,21 @@ export type TDynamicSecretProvider =
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.Totp;
|
||||
inputs:
|
||||
| {
|
||||
configType: "url";
|
||||
url: string;
|
||||
}
|
||||
| {
|
||||
configType: "manual";
|
||||
secret: string;
|
||||
period?: number;
|
||||
algorithm?: string;
|
||||
digits?: number;
|
||||
};
|
||||
};
|
||||
export type TCreateDynamicSecretDTO = {
|
||||
projectSlug: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PureAbility } from "@casl/ability";
|
||||
import { ZodIssue } from "zod";
|
||||
|
||||
export type { TAccessApprovalPolicy } from "./accessApproval/types";
|
||||
@@ -53,10 +54,21 @@ export type TApiErrors =
|
||||
requestId: string;
|
||||
error: ApiErrorTypes.ValidationError;
|
||||
message: ZodIssue[];
|
||||
statusCode: 401;
|
||||
}
|
||||
| {
|
||||
requestId: string;
|
||||
error: ApiErrorTypes.UnauthorizedError;
|
||||
message: string;
|
||||
statusCode: 401;
|
||||
}
|
||||
| {
|
||||
requestId: string;
|
||||
error: ApiErrorTypes.ForbiddenError;
|
||||
message: string;
|
||||
details: PureAbility["rules"];
|
||||
statusCode: 403;
|
||||
}
|
||||
| { requestId: string; error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 403 }
|
||||
| { requestId: string; error: ApiErrorTypes.UnauthorizedError; message: string; statusCode: 401 }
|
||||
| {
|
||||
requestId: string;
|
||||
statusCode: 400;
|
||||
|
||||
+152
-24
@@ -3,6 +3,14 @@ import axios from "axios";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
|
||||
// akhilmhdh: doing individual imports to avoid cyclic import error
|
||||
import { Button } from "./components/v2/Button";
|
||||
import { Modal, ModalContent, ModalTrigger } from "./components/v2/Modal";
|
||||
import { Table, TableContainer, TBody, Td, Th, THead, Tr } from "./components/v2/Table";
|
||||
import {
|
||||
formatedConditionsOperatorNames,
|
||||
PermissionConditionOperators
|
||||
} from "./context/ProjectPermissionContext/types";
|
||||
import { ApiErrorTypes, TApiErrors } from "./hooks/api/types";
|
||||
|
||||
// this is saved in react-query cache
|
||||
@@ -10,41 +18,161 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
|
||||
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
|
||||
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
|
||||
|
||||
const camelCaseToSpaces = (input: string) => {
|
||||
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
|
||||
};
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const serverResponse = error.response?.data as TApiErrors;
|
||||
if (serverResponse?.error === ApiErrorTypes.ValidationError) {
|
||||
createNotification({
|
||||
title: "Validation Error",
|
||||
type: "error",
|
||||
text: (
|
||||
<div>
|
||||
{serverResponse.message?.map(({ message, path }) => (
|
||||
<div className="flex space-y-2" key={path.join(".")}>
|
||||
<div>
|
||||
Field <i>{path.join(".")}</i> {message.toLowerCase()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2">Request ID: {serverResponse.requestId}</div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
createNotification(
|
||||
{
|
||||
title: "Validation Error",
|
||||
type: "error",
|
||||
text: (
|
||||
<div>
|
||||
<p>Please check the input and try again.</p>
|
||||
<p className="mt-2 text-xs">Request ID: {serverResponse.requestId}</p>
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<Modal>
|
||||
<ModalTrigger>
|
||||
<Button variant="outline_bg" size="xs">
|
||||
Show more
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent title="Validation Error Details">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Field</Th>
|
||||
<Th>Issue</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{serverResponse.message?.map(({ message, path }) => (
|
||||
<Tr key={path.join(".")}>
|
||||
<Td>{path.join(".")}</Td>
|
||||
<Td>{message.toLowerCase()}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
},
|
||||
{ closeOnClick: false }
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
|
||||
createNotification(
|
||||
{
|
||||
title: "Forbidden Access",
|
||||
type: "error",
|
||||
|
||||
const title =
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
serverResponse.statusCode === 403
|
||||
? "Forbidden Access"
|
||||
: serverResponse.statusCode === 401
|
||||
? "Unauthorized Access"
|
||||
: "Bad Request";
|
||||
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`,
|
||||
children: serverResponse?.details?.length ? (
|
||||
<Modal>
|
||||
<ModalTrigger>
|
||||
<Button variant="outline_bg" size="xs">
|
||||
Show more
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
title="Validation Rules"
|
||||
subTitle="Please review the allowed rules below."
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{serverResponse.details?.map((el, index) => {
|
||||
const hasConditions = Object.keys(el.conditions || {}).length;
|
||||
return (
|
||||
<div
|
||||
key={`Forbidden-error-details-${index + 1}`}
|
||||
className="rounded-md border border-gray-600 p-4"
|
||||
>
|
||||
<div>
|
||||
{el.inverted ? "Cannot" : "Can"}{" "}
|
||||
<span className="text-yellow-600">
|
||||
{el.action.toString().replaceAll(",", ", ")}
|
||||
</span>{" "}
|
||||
{el.subject.toString()} {hasConditions && "with conditions:"}
|
||||
</div>
|
||||
{hasConditions && (
|
||||
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
|
||||
{Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
|
||||
const operators = (
|
||||
el.conditions as Record<
|
||||
string,
|
||||
| string
|
||||
| { [K in PermissionConditionOperators]: string | string[] }
|
||||
>
|
||||
)[field];
|
||||
|
||||
const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
|
||||
if (typeof operators === "string") {
|
||||
return (
|
||||
<li
|
||||
key={`Forbidden-error-details-${index + 1}-${
|
||||
fieldIndex + 1
|
||||
}`}
|
||||
>
|
||||
<span className="font-bold capitalize">
|
||||
{formattedFieldName}
|
||||
</span>{" "}
|
||||
<span className="text-mineshaft-200">equal to</span>{" "}
|
||||
<span className="text-yellow-600">{operators}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return Object.keys(operators).map((operator, operatorIndex) => (
|
||||
<li
|
||||
key={`Forbidden-error-details-${index + 1}-${
|
||||
fieldIndex + 1
|
||||
}-${operatorIndex + 1}`}
|
||||
>
|
||||
<span className="font-bold capitalize">
|
||||
{formattedFieldName}
|
||||
</span>{" "}
|
||||
<span className="text-mineshaft-200">
|
||||
{
|
||||
formatedConditionsOperatorNames[
|
||||
operator as PermissionConditionOperators
|
||||
]
|
||||
}
|
||||
</span>{" "}
|
||||
<span className="text-yellow-600">
|
||||
{operators[
|
||||
operator as PermissionConditionOperators
|
||||
].toString()}
|
||||
</span>
|
||||
</li>
|
||||
));
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
) : undefined
|
||||
},
|
||||
{ closeOnClick: false }
|
||||
);
|
||||
return;
|
||||
}
|
||||
createNotification({
|
||||
title,
|
||||
title: "Bad Request",
|
||||
type: "error",
|
||||
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`
|
||||
});
|
||||
|
||||
@@ -13,6 +13,14 @@ html {
|
||||
@apply rounded-md;
|
||||
}
|
||||
|
||||
.Toastify__toast-body {
|
||||
@apply items-start;
|
||||
}
|
||||
|
||||
.Toastify__toast-icon {
|
||||
@apply w-4 pt-1;
|
||||
}
|
||||
|
||||
.rdp-day,
|
||||
.rdp-nav_button {
|
||||
@apply rounded-md hover:text-mineshaft-500;
|
||||
|
||||
@@ -98,7 +98,7 @@ export const LogsSection = ({
|
||||
userAgentType,
|
||||
startDate,
|
||||
endDate,
|
||||
actorId: actor
|
||||
actor
|
||||
}}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
|
||||
+25
-1
@@ -11,7 +11,7 @@ import {
|
||||
SiSnowflake
|
||||
} from "react-icons/si";
|
||||
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faDatabase } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
@@ -31,6 +31,7 @@ import { RabbitMqInputForm } from "./RabbitMqInputForm";
|
||||
import { RedisInputForm } from "./RedisInputForm";
|
||||
import { SapHanaInputForm } from "./SapHanaInputForm";
|
||||
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
|
||||
import { TotpInputForm } from "./TotpInputForm";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
@@ -110,6 +111,11 @@ const DYNAMIC_SECRET_LIST = [
|
||||
icon: <SiSnowflake size="1.5rem" />,
|
||||
provider: DynamicSecretProviders.Snowflake,
|
||||
title: "Snowflake"
|
||||
},
|
||||
{
|
||||
icon: <FontAwesomeIcon icon={faClock} size="lg" />,
|
||||
provider: DynamicSecretProviders.Totp,
|
||||
title: "TOTP"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -405,6 +411,24 @@ export const CreateDynamicSecretForm = ({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.Totp && (
|
||||
<motion.div
|
||||
key="dynamic-totp-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<TotpInputForm
|
||||
onCompleted={handleFormReset}
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Link from "next/link";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
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 { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
enum ConfigType {
|
||||
URL = "url",
|
||||
MANUAL = "manual"
|
||||
}
|
||||
|
||||
enum TotpAlgorithm {
|
||||
SHA1 = "sha1",
|
||||
SHA256 = "sha256",
|
||||
SHA512 = "sha512"
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.discriminatedUnion("configType", [
|
||||
z.object({
|
||||
configType: z.literal(ConfigType.URL),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((val) => {
|
||||
const urlObj = new URL(val);
|
||||
const secret = urlObj.searchParams.get("secret");
|
||||
|
||||
return Boolean(secret);
|
||||
}, "OTP URL must contain secret field")
|
||||
}),
|
||||
z.object({
|
||||
configType: z.literal(ConfigType.MANUAL),
|
||||
secret: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.transform((val) => val.replace(/\s+/g, "")),
|
||||
period: z.number().optional(),
|
||||
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
|
||||
digits: z.number().optional()
|
||||
})
|
||||
]),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onCompleted: () => void;
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
export const TotpInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: {
|
||||
configType: ConfigType.URL
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const selectedConfigType = watch("provider.configType");
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, provider }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Totp, inputs: provider },
|
||||
maxTTL: "24h",
|
||||
name,
|
||||
path: secretPath,
|
||||
defaultTTL: "1m",
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
});
|
||||
onCompleted();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: err instanceof Error ? err.message : "Failed to create dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<Link
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
|
||||
passHref
|
||||
>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.configType"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Configuration Type"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
className="w-full"
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
}}
|
||||
dropdownContainerClassName="max-w-full"
|
||||
>
|
||||
<SelectItem value={ConfigType.URL} key="config-type-url">
|
||||
URL
|
||||
</SelectItem>
|
||||
<SelectItem value={ConfigType.MANUAL} key="config-type-manual">
|
||||
Manual
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedConfigType === ConfigType.URL && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.url"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="OTP URL"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="otpauth://" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectedConfigType === ConfigType.MANUAL && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.secret"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Key"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.period"
|
||||
defaultValue={30}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Period"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.digits"
|
||||
defaultValue={6}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Digits"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.algorithm"
|
||||
defaultValue={TotpAlgorithm.SHA1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Algorithm"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
className="w-full"
|
||||
dropdownContainerClassName="max-w-full"
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
}}
|
||||
>
|
||||
<SelectItem value={TotpAlgorithm.SHA1} key="algorithm-sha-1">
|
||||
SHA1
|
||||
</SelectItem>
|
||||
<SelectItem value={TotpAlgorithm.SHA256} key="algorithm-sha-256">
|
||||
SHA256
|
||||
</SelectItem>
|
||||
<SelectItem value={TotpAlgorithm.SHA512} key="algorithm-sha-512">
|
||||
SHA512
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<p className="mb-8 text-sm font-normal text-gray-400">
|
||||
The period, digits, and algorithm values can remain at their defaults unless
|
||||
your TOTP provider specifies otherwise.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -154,7 +154,7 @@ export const CreateSecretForm = ({
|
||||
isMulti
|
||||
name="tagIds"
|
||||
isDisabled={!canReadTags}
|
||||
isLoading={isTagsLoading}
|
||||
isLoading={isTagsLoading && canReadTags}
|
||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
|
||||
+129
-7
@@ -1,6 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faClock, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
@@ -9,8 +9,16 @@ import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, IconButton, Input, SecretInput, Tooltip } from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
SecretInput,
|
||||
Spinner,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useTimedReset, useToggle } from "@app/hooks";
|
||||
import { useCreateDynamicSecretLease } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
@@ -54,7 +62,76 @@ const OutputDisplay = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
|
||||
const TotpOutputDisplay = ({
|
||||
totp,
|
||||
remainingSeconds,
|
||||
triggerLeaseRegeneration
|
||||
}: {
|
||||
totp: string;
|
||||
remainingSeconds: number;
|
||||
triggerLeaseRegeneration: (details: { ttl?: string }) => Promise<void>;
|
||||
}) => {
|
||||
const [remainingTime, setRemainingTime] = useState(remainingSeconds);
|
||||
const [shouldShowRegenerate, setShouldShowRegenerate] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
setRemainingTime(remainingSeconds);
|
||||
setShouldShowRegenerate.off();
|
||||
|
||||
// Set up countdown interval
|
||||
const intervalId = setInterval(() => {
|
||||
setRemainingTime((prevTime) => {
|
||||
if (prevTime <= 1) {
|
||||
clearInterval(intervalId);
|
||||
setShouldShowRegenerate.on();
|
||||
return 0;
|
||||
}
|
||||
return prevTime - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// Cleanup interval on unmount or when totp changes
|
||||
return () => clearInterval(intervalId);
|
||||
}, [totp, remainingSeconds]);
|
||||
|
||||
return (
|
||||
<div className="h-36">
|
||||
<OutputDisplay label="Time-based one-time password" value={totp} />
|
||||
{remainingTime > 0 ? (
|
||||
<div
|
||||
className={`ml-2 flex items-center text-sm ${
|
||||
remainingTime < 10 ? "text-red-500" : "text-yellow-500"
|
||||
} transition-colors duration-500`}
|
||||
>
|
||||
<FontAwesomeIcon className="mr-1" icon={faClock} size="sm" />
|
||||
<span>
|
||||
Expires in {remainingTime} {remainingTime > 1 ? "seconds" : "second"}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-2 flex items-center text-sm text-red-500">
|
||||
<FontAwesomeIcon className="mr-1" icon={faClock} size="sm" />
|
||||
Expired
|
||||
</div>
|
||||
)}
|
||||
{shouldShowRegenerate && (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="mt-2"
|
||||
onClick={() => triggerLeaseRegeneration({})}
|
||||
>
|
||||
Regenerate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOutputForm = (
|
||||
provider: DynamicSecretProviders,
|
||||
data: unknown,
|
||||
triggerLeaseRegeneration: (details: { ttl?: string }) => Promise<void>
|
||||
) => {
|
||||
if (
|
||||
provider === DynamicSecretProviders.SqlDatabase ||
|
||||
provider === DynamicSecretProviders.Cassandra ||
|
||||
@@ -242,11 +319,29 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (provider === DynamicSecretProviders.Totp) {
|
||||
const { TOTP, TIME_REMAINING } = data as {
|
||||
TOTP: string;
|
||||
TIME_REMAINING: number;
|
||||
};
|
||||
|
||||
return (
|
||||
<TotpOutputDisplay
|
||||
totp={TOTP}
|
||||
remainingSeconds={TIME_REMAINING}
|
||||
triggerLeaseRegeneration={triggerLeaseRegeneration}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -259,6 +354,8 @@ type Props = {
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
const PROVIDERS_WITH_AUTOGENERATE_SUPPORT = [DynamicSecretProviders.Totp];
|
||||
|
||||
export const CreateDynamicSecretLease = ({
|
||||
onClose,
|
||||
projectSlug,
|
||||
@@ -277,6 +374,9 @@ export const CreateDynamicSecretLease = ({
|
||||
ttl: "1h"
|
||||
}
|
||||
});
|
||||
const [isPreloading, setIsPreloading] = useToggle(
|
||||
PROVIDERS_WITH_AUTOGENERATE_SUPPORT.includes(provider)
|
||||
);
|
||||
|
||||
const createDynamicSecretLease = useCreateDynamicSecretLease();
|
||||
|
||||
@@ -290,10 +390,13 @@ export const CreateDynamicSecretLease = ({
|
||||
ttl,
|
||||
dynamicSecretName
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully leased dynamic secret"
|
||||
});
|
||||
|
||||
setIsPreloading.off();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
@@ -303,8 +406,23 @@ export const CreateDynamicSecretLease = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleLeaseRegeneration = async (data: { ttl?: string }) => {
|
||||
setIsPreloading.on();
|
||||
handleDynamicSecretLeaseCreate(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (provider === DynamicSecretProviders.Totp) {
|
||||
handleDynamicSecretLeaseCreate({});
|
||||
}
|
||||
}, [provider]);
|
||||
|
||||
const isOutputMode = Boolean(createDynamicSecretLease?.data);
|
||||
|
||||
if (isPreloading) {
|
||||
return <Spinner className="mx-auto h-40 text-mineshaft-700" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
@@ -350,7 +468,11 @@ export const CreateDynamicSecretLease = ({
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
{renderOutputForm(provider, createDynamicSecretLease.data?.data)}
|
||||
{renderOutputForm(
|
||||
provider,
|
||||
createDynamicSecretLease.data?.data,
|
||||
handleLeaseRegeneration
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
+10
@@ -101,10 +101,20 @@ export const DynamicSecretListView = ({
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
// no lease view for TOTP because it's irrelevant
|
||||
if (secret.type === DynamicSecretProviders.Totp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.key === "Enter" && !isRevoking)
|
||||
handlePopUpOpen("dynamicSecretLeases", secret.id);
|
||||
}}
|
||||
onClick={() => {
|
||||
// no lease view for TOTP because it's irrelevant
|
||||
if (secret.type === DynamicSecretProviders.Totp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRevoking) {
|
||||
handlePopUpOpen("dynamicSecretLeases", secret.id);
|
||||
}
|
||||
|
||||
+18
@@ -17,6 +17,7 @@ import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProv
|
||||
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
|
||||
import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm";
|
||||
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
||||
import { EditDynamicSecretTotpForm } from "./EditDynamicSecretTotpForm";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
@@ -276,6 +277,23 @@ export const EditDynamicSecretForm = ({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{dynamicSecretDetails?.type === DynamicSecretProviders.Totp && (
|
||||
<motion.div
|
||||
key="totp-edit"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<EditDynamicSecretTotpForm
|
||||
onClose={onClose}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecret={dynamicSecretDetails}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Link from "next/link";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
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 { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
enum ConfigType {
|
||||
URL = "url",
|
||||
MANUAL = "manual"
|
||||
}
|
||||
|
||||
enum TotpAlgorithm {
|
||||
SHA1 = "sha1",
|
||||
SHA256 = "sha256",
|
||||
SHA512 = "sha512"
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
.discriminatedUnion("configType", [
|
||||
z.object({
|
||||
configType: z.literal(ConfigType.URL),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((val) => {
|
||||
const urlObj = new URL(val);
|
||||
const secret = urlObj.searchParams.get("secret");
|
||||
|
||||
return Boolean(secret);
|
||||
}, "OTP URL must contain secret field")
|
||||
}),
|
||||
z.object({
|
||||
configType: z.literal(ConfigType.MANUAL),
|
||||
secret: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.transform((val) => val.replace(/\s+/g, "")),
|
||||
period: z.number().optional(),
|
||||
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
|
||||
digits: z.number().optional()
|
||||
})
|
||||
])
|
||||
.optional(),
|
||||
newName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
export const EditDynamicSecretTotpForm = ({
|
||||
onClose,
|
||||
dynamicSecret,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
watch,
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
newName: dynamicSecret.name,
|
||||
inputs: dynamicSecret.inputs as TForm["inputs"]
|
||||
}
|
||||
});
|
||||
|
||||
const selectedConfigType = watch("inputs.configType");
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, newName }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
path: secretPath,
|
||||
projectSlug,
|
||||
environmentSlug: environment,
|
||||
data: {
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated dynamic secret"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: err instanceof Error ? err.message : "Failed to update dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="newName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<Link
|
||||
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
|
||||
passHref
|
||||
>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.configType"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Configuration Type"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
className="w-full"
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
}}
|
||||
dropdownContainerClassName="max-w-full"
|
||||
>
|
||||
<SelectItem value={ConfigType.URL} key="config-type-url">
|
||||
URL
|
||||
</SelectItem>
|
||||
<SelectItem value={ConfigType.MANUAL} key="config-type-manual">
|
||||
Manual
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedConfigType === ConfigType.URL && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.url"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="OTP URL"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectedConfigType === ConfigType.MANUAL && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.secret"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Key"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.period"
|
||||
defaultValue={30}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Period"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.digits"
|
||||
defaultValue={6}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Digits"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.algorithm"
|
||||
defaultValue={TotpAlgorithm.SHA1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Algorithm"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
className="w-full"
|
||||
dropdownContainerClassName="max-w-full"
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
}}
|
||||
>
|
||||
<SelectItem value={TotpAlgorithm.SHA1} key="algorithm-sha-1">
|
||||
SHA1
|
||||
</SelectItem>
|
||||
<SelectItem value={TotpAlgorithm.SHA256} key="algorithm-sha-256">
|
||||
SHA256
|
||||
</SelectItem>
|
||||
<SelectItem value={TotpAlgorithm.SHA512} key="algorithm-sha-512">
|
||||
SHA512
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<p className="mb-8 text-sm font-normal text-gray-400">
|
||||
The period, digits, and algorithm values can remain at their defaults unless
|
||||
your TOTP provider specifies otherwise.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -867,7 +867,7 @@ export const SecretOverviewPage = () => {
|
||||
<div className="thin-scrollbar mt-4">
|
||||
<TableContainer
|
||||
onScroll={(e) => setScrollOffset(e.currentTarget.scrollLeft)}
|
||||
className="thin-scrollbar"
|
||||
className="thin-scrollbar rounded-b-none"
|
||||
>
|
||||
<Table>
|
||||
<THead>
|
||||
|
||||
+1
-1
@@ -255,7 +255,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
||||
isMulti
|
||||
name="tagIds"
|
||||
isDisabled={!canReadTags}
|
||||
isLoading={isTagsLoading}
|
||||
isLoading={isTagsLoading && canReadTags}
|
||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
|
||||
Reference in New Issue
Block a user