mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 10:18:16 +00:00
Merge remote-tracking branch 'origin/main' into feat/enforce-oidc-sso
This commit is contained in:
@@ -8,11 +8,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
import Error from "@app/components/basic/Error";
|
||||
import { RegionSelect } from "@app/components/navigation/RegionSelect";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { Button, IconButton, Input, Tooltip } from "@app/components/v2";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { useFetchServerStatus } from "@app/hooks/api";
|
||||
import { LoginMethod } from "@app/hooks/api/admin/types";
|
||||
@@ -166,70 +167,9 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||
Login to Infisical
|
||||
</h1>
|
||||
{shouldDisplayLoginMethod(LoginMethod.GOOGLE) && (
|
||||
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
|
||||
window.open(
|
||||
`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`
|
||||
);
|
||||
window.close();
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
|
||||
className="mx-0 h-10 w-full"
|
||||
>
|
||||
{t("login.continue-with-google")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{shouldDisplayLoginMethod(LoginMethod.GITHUB) && (
|
||||
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
|
||||
window.open(
|
||||
`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`
|
||||
);
|
||||
|
||||
window.close();
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
|
||||
className="mx-0 h-10 w-full"
|
||||
>
|
||||
Continue with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{shouldDisplayLoginMethod(LoginMethod.GITLAB) && (
|
||||
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
|
||||
window.open(
|
||||
`/api/v1/sso/redirect/gitlab${callbackPort ? `?callback_port=${callbackPort}` : ""}`
|
||||
);
|
||||
|
||||
window.close();
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
|
||||
className="mx-0 h-10 w-full"
|
||||
>
|
||||
Continue with GitLab
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<RegionSelect />
|
||||
{shouldDisplayLoginMethod(LoginMethod.SAML) && (
|
||||
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
@@ -273,6 +213,76 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 flex w-1/4 min-w-[21.2rem] gap-2 md:min-w-[20.1rem] lg:w-1/6">
|
||||
{shouldDisplayLoginMethod(LoginMethod.GOOGLE) && (
|
||||
<Tooltip position="bottom" content={t("login.continue-with-google")}>
|
||||
<IconButton
|
||||
ariaLabel={t("login.continue-with-google")}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
|
||||
window.open(
|
||||
`/api/v1/sso/redirect/google${
|
||||
callbackPort ? `?callback_port=${callbackPort}` : ""
|
||||
}`
|
||||
);
|
||||
window.close();
|
||||
}}
|
||||
className="h-10 w-full bg-mineshaft-600"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGoogle} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{shouldDisplayLoginMethod(LoginMethod.GITHUB) && (
|
||||
<Tooltip position="bottom" content="Continue with GitHub">
|
||||
<IconButton
|
||||
ariaLabel="Login continue with GitHub"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
|
||||
window.open(
|
||||
`/api/v1/sso/redirect/github${
|
||||
callbackPort ? `?callback_port=${callbackPort}` : ""
|
||||
}`
|
||||
);
|
||||
|
||||
window.close();
|
||||
}}
|
||||
className="h-10 w-full bg-mineshaft-600"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{shouldDisplayLoginMethod(LoginMethod.GITLAB) && (
|
||||
<Tooltip position="bottom" content="Continue with GitLab">
|
||||
<IconButton
|
||||
ariaLabel="Login continue with GitLab"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
|
||||
window.open(
|
||||
`/api/v1/sso/redirect/gitlab${
|
||||
callbackPort ? `?callback_port=${callbackPort}` : ""
|
||||
}`
|
||||
);
|
||||
|
||||
window.close();
|
||||
}}
|
||||
className="h-10 w-full bg-mineshaft-600"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGitlab} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{(!config.enabledLoginMethods ||
|
||||
(shouldDisplayLoginMethod(LoginMethod.EMAIL) && config.enabledLoginMethods.length > 1)) && (
|
||||
<div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
|
||||
@@ -316,7 +326,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
disabled={shouldShowCaptcha && captchaToken === ""}
|
||||
type="submit"
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Select
|
||||
value={role === "custom" ? (customRole?.slug as string) : role}
|
||||
isDisabled={!isAllowed}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
className="w-48 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
handleChangeRole({
|
||||
|
||||
+119
-94
@@ -21,7 +21,8 @@ import {
|
||||
useDeleteIdentityKubernetesAuth,
|
||||
useDeleteIdentityOidcAuth,
|
||||
useDeleteIdentityTokenAuth,
|
||||
useDeleteIdentityUniversalAuth} from "@app/hooks/api";
|
||||
useDeleteIdentityUniversalAuth
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -54,7 +55,10 @@ const identityAuthMethods = [
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
authMethod: yup.string().required("Auth method is required")
|
||||
authMethod: yup
|
||||
.mixed<IdentityAuthMethod>()
|
||||
.oneOf(Object.values(IdentityAuthMethod))
|
||||
.required("Auth method is required")
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -72,14 +76,26 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
|
||||
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
|
||||
|
||||
const { control, watch, setValue } = useForm<FormData>({
|
||||
const initialAuthMethod = popUp?.identityAuthMethod?.data?.authMethod;
|
||||
|
||||
const { control, watch, setValue, reset } = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH
|
||||
authMethod: initialAuthMethod
|
||||
}
|
||||
});
|
||||
|
||||
const identityAuthMethodData = popUp?.identityAuthMethod?.data as {
|
||||
useEffect(() => {
|
||||
// reset form on open
|
||||
if (popUp.identityAuthMethod.isOpen)
|
||||
reset({ authMethod: popUp?.identityAuthMethod?.data?.authMethod });
|
||||
}, [popUp.identityAuthMethod.isOpen]);
|
||||
|
||||
const identityAuthMethodData = {
|
||||
identityId: popUp?.identityAuthMethod.data?.identityId,
|
||||
name: popUp?.identityAuthMethod?.data?.name,
|
||||
authMethod: watch("authMethod")
|
||||
} as {
|
||||
identityId: string;
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
@@ -94,85 +110,10 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
setValue("authMethod", IdentityAuthMethod.UNIVERSAL_AUTH);
|
||||
}, [identityAuthMethodData?.authMethod]);
|
||||
|
||||
const authMethod = watch("authMethod");
|
||||
|
||||
const renderIdentityAuthForm = () => {
|
||||
switch (identityAuthMethodData?.authMethod ?? authMethod) {
|
||||
case IdentityAuthMethod.AWS_AUTH: {
|
||||
return (
|
||||
<IdentityAwsAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.KUBERNETES_AUTH: {
|
||||
return (
|
||||
<IdentityKubernetesAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.GCP_AUTH: {
|
||||
return (
|
||||
<IdentityGcpAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.AZURE_AUTH: {
|
||||
return (
|
||||
<IdentityAzureAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.UNIVERSAL_AUTH: {
|
||||
return (
|
||||
<IdentityUniversalAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.OIDC_AUTH: {
|
||||
return (
|
||||
<IdentityOidcAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.TOKEN_AUTH: {
|
||||
return (
|
||||
<IdentityTokenAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onRevokeAuthMethodSubmit = async () => {
|
||||
if (!identityAuthMethodData.authMethod) return;
|
||||
if (!orgId) return;
|
||||
const onRevokeAuthMethodSubmit = async (authMethod: IdentityAuthMethod) => {
|
||||
if (!orgId || !authMethod) return;
|
||||
try {
|
||||
console.log("onRevokeAuthMethodSubmit identityId: ", identityAuthMethodData);
|
||||
switch (identityAuthMethodData.authMethod) {
|
||||
switch (authMethod) {
|
||||
case IdentityAuthMethod.UNIVERSAL_AUTH: {
|
||||
await revokeUniversalAuth({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
@@ -227,9 +168,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: `Successfully removed ${
|
||||
identityAuthToNameMap[identityAuthMethodData.authMethod]
|
||||
} on ${identityAuthMethodData.name}`,
|
||||
text: `Successfully removed ${identityAuthToNameMap[authMethod]} on ${identityAuthMethodData.name}`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -238,13 +177,95 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to remove ${identityAuthToNameMap[identityAuthMethodData.authMethod]} on ${
|
||||
identityAuthMethodData.name
|
||||
}`,
|
||||
text: `Failed to remove ${identityAuthToNameMap[authMethod]} on ${identityAuthMethodData.name}`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
const renderIdentityAuthForm = () => {
|
||||
switch (identityAuthMethodData.authMethod) {
|
||||
case IdentityAuthMethod.AWS_AUTH: {
|
||||
return (
|
||||
<IdentityAwsAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.KUBERNETES_AUTH: {
|
||||
return (
|
||||
<IdentityKubernetesAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.GCP_AUTH: {
|
||||
return (
|
||||
<IdentityGcpAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.AZURE_AUTH: {
|
||||
return (
|
||||
<IdentityAzureAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.UNIVERSAL_AUTH: {
|
||||
return (
|
||||
<IdentityUniversalAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.OIDC_AUTH: {
|
||||
return (
|
||||
<IdentityOidcAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.TOKEN_AUTH: {
|
||||
return (
|
||||
<IdentityTokenAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -255,8 +276,10 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
>
|
||||
<ModalContent
|
||||
title={`${
|
||||
identityAuthMethodData?.authMethod ? "Update" : "Configure"
|
||||
} Identity Auth Method for ${identityAuthMethodData?.name ?? ""}`}
|
||||
identityAuthMethodData.authMethod === initialAuthMethod ? "Update" : "Configure"
|
||||
} Identity Auth Method for ${
|
||||
identityAuthToNameMap[identityAuthMethodData.authMethod!] ?? ""
|
||||
}`}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -267,9 +290,10 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
onValueChange={(e) => {
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-full"
|
||||
isDisabled={!!identityAuthMethodData?.authMethod}
|
||||
>
|
||||
{identityAuthMethods.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
@@ -295,7 +319,8 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
} on ${identityAuthMethodData?.name ?? ""}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={onRevokeAuthMethodSubmit}
|
||||
buttonText="Remove"
|
||||
onDeleteApproved={() => onRevokeAuthMethodSubmit(identityAuthMethodData.authMethod!)}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
+214
-180
@@ -6,7 +6,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityAwsAuth,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
@@ -64,12 +64,16 @@ type Props = {
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityAwsAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -78,7 +82,12 @@ export const IdentityAwsAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
|
||||
|
||||
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "");
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
});
|
||||
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -175,9 +184,7 @@ export const IdentityAwsAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${
|
||||
identityAuthMethodData?.authMethod ? "updated" : "configured"
|
||||
} auth method`,
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -191,189 +198,216 @@ export const IdentityAwsAuthForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="allowedPrincipalArns"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Principal ARNs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedAccountIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Account IDs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="123456789012, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="https://sts.amazonaws.com/"
|
||||
name="stsEndpoint"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="allowedPrincipalArns"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Principal ARNs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedAccountIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Account IDs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="123456789012, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="https://sts.amazonaws.com/"
|
||||
name="stsEndpoint"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
{identityAuthMethodData?.authMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+225
-181
@@ -6,7 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityAzureAuth,
|
||||
@@ -15,11 +15,11 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
tenantId: z.string(),
|
||||
tenantId: z.string().min(1),
|
||||
resource: z.string(),
|
||||
allowedServicePrincipalIds: z.string(),
|
||||
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
||||
@@ -52,12 +52,16 @@ type Props = {
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityAzureAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -66,12 +70,18 @@ export const IdentityAzureAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
|
||||
|
||||
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "");
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
});
|
||||
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
trigger,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -163,9 +173,7 @@ export const IdentityAzureAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${
|
||||
identityAuthMethodData?.authMethod ? "updated" : "configured"
|
||||
} auth method`,
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -179,189 +187,225 @@ export const IdentityAzureAuthForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="tenantId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Tenant ID"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="resource"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Resource / Audience"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="https://management.azure.com/" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedServicePrincipalIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Principal IDs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="tenantId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Tenant ID"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="resource"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Resource / Audience"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="https://management.azure.com/" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedServicePrincipalIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Principal IDs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
{identityAuthMethodData?.authMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
const result = await trigger();
|
||||
if (result) {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
} else {
|
||||
createNotification({
|
||||
text: "Please fill in all required fields",
|
||||
type: "error"
|
||||
});
|
||||
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+239
-194
@@ -6,7 +6,15 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityGcpAuth,
|
||||
@@ -15,7 +23,7 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -53,12 +61,16 @@ type Props = {
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityGcpAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData
|
||||
identityAuthMethodData,
|
||||
revokeAuth,
|
||||
initialAuthMethod
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -67,7 +79,11 @@ export const IdentityGcpAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
|
||||
|
||||
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "");
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
});
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -100,7 +116,7 @@ export const IdentityGcpAuthForm = ({
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
type: data.type,
|
||||
type: data.type || "gce",
|
||||
allowedServiceAccounts: data.allowedServiceAccounts,
|
||||
allowedProjects: data.allowedProjects,
|
||||
allowedZones: data.allowedZones,
|
||||
@@ -117,7 +133,7 @@ export const IdentityGcpAuthForm = ({
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
type: "iam",
|
||||
type: "gce",
|
||||
allowedServiceAccounts: "",
|
||||
allowedProjects: "",
|
||||
allowedZones: "",
|
||||
@@ -173,9 +189,7 @@ export const IdentityGcpAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${
|
||||
identityAuthMethodData?.authMethod ? "updated" : "configured"
|
||||
} auth method`,
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -189,213 +203,244 @@ export const IdentityGcpAuthForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="gce" key="gcp-type-gce">
|
||||
GCP ID Token Auth (Recommended)
|
||||
</SelectItem>
|
||||
<SelectItem value="iam" key="gcpiam">
|
||||
GCP IAM Auth
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="allowedServiceAccounts"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Account Emails"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="[email protected], [email protected]"
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{watchedType === "gce" && (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedProjects"
|
||||
name="type"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="gce" key="gce">
|
||||
GCP ID Token Auth (Recommended)
|
||||
</SelectItem>
|
||||
<SelectItem value="iam" key="iam">
|
||||
GCP IAM Auth
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="allowedServiceAccounts"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Projects"
|
||||
label="Allowed Service Account Emails"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="my-gcp-project, ..." />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="[email protected], [email protected]"
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "gce" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedZones"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Allowed Zones" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
{watchedType === "gce" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name="allowedProjects"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Projects"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="my-gcp-project, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<IconButton
|
||||
)}
|
||||
{watchedType === "gce" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedZones"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Zones"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
{identityAuthMethodData?.authMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
buttonText="Overwrite"
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={async () => {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+283
-231
@@ -6,7 +6,14 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityKubernetesAuth,
|
||||
@@ -15,12 +22,12 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
kubernetesHost: z.string(),
|
||||
tokenReviewerJwt: z.string(),
|
||||
kubernetesHost: z.string().min(1),
|
||||
tokenReviewerJwt: z.string().min(1),
|
||||
allowedNames: z.string(),
|
||||
allowedNamespaces: z.string(),
|
||||
allowedAudience: z.string(),
|
||||
@@ -55,12 +62,16 @@ type Props = {
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityKubernetesAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -69,12 +80,17 @@ export const IdentityKubernetesAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
|
||||
|
||||
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "");
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
});
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
trigger,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -184,9 +200,7 @@ export const IdentityKubernetesAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${
|
||||
identityAuthMethodData?.authMethod ? "updated" : "configured"
|
||||
} auth method`,
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -200,239 +214,277 @@ export const IdentityKubernetesAuthForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="kubernetesHost"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Kubernetes Host / Base Kubernetes API URL "
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running 'kubectl cluster-info'"
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tokenReviewerJwt"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Token Reviewer JWT"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods."
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="" type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Account Names"
|
||||
isError={Boolean(error)}
|
||||
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedNamespaces"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Namespaces"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any namespaces."
|
||||
>
|
||||
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedAudience"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Audience"
|
||||
isError={Boolean(error)} errorText={error?.message}
|
||||
tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="kubernetesHost"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Kubernetes Host / Base Kubernetes API URL "
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running 'kubectl cluster-info'"
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message} isError={Boolean(error)}
|
||||
tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server."
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
<Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tokenReviewerJwt"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Token Reviewer JWT"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods."
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="" type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Account Names"
|
||||
isError={Boolean(error)}
|
||||
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedNamespaces"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Namespaces"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any namespaces."
|
||||
>
|
||||
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedAudience"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Audience"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
|
||||
>
|
||||
<Input {...field} placeholder="" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server."
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
{identityAuthMethodData?.authMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
const result = await trigger();
|
||||
if (result) {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
} else {
|
||||
createNotification({
|
||||
text: "Please fill in all required fields",
|
||||
type: "error"
|
||||
});
|
||||
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+351
-300
@@ -7,13 +7,21 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z.object({
|
||||
accessTokenTrustedIps: z
|
||||
@@ -56,12 +64,16 @@ type Props = {
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityOidcAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -70,12 +82,17 @@ export const IdentityOidcAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
|
||||
|
||||
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "");
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
});
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
trigger,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -193,9 +210,7 @@ export const IdentityOidcAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${
|
||||
identityAuthMethodData?.authMethod ? "updated" : "configured"
|
||||
} auth method`,
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -209,314 +224,350 @@ export const IdentityOidcAuthForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="oidcDiscoveryUrl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="OIDC Discovery URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://token.actions.githubusercontent.com"
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundIssuer"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Issuer"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder="https://token.actions.githubusercontent.com"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundSubject"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subject"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Input {...field} type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundAudiences"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Audiences"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Input {...field} type="text" placeholder="service1, service2" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{boundClaimsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`boundClaims.${index}.key`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Claims" : undefined}
|
||||
icon={
|
||||
index === 0 ? (
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="oidcDiscoveryUrl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="OIDC Discovery URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://token.actions.githubusercontent.com"
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundIssuer"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Issuer"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder="https://token.actions.githubusercontent.com"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundSubject"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subject"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="property"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`boundClaims.${index}.value`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Input {...field} type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundAudiences"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Audiences"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="value1, value2"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Input {...field} type="text" placeholder="service1, service2" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{boundClaimsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`boundClaims.${index}.key`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Claims" : undefined}
|
||||
icon={
|
||||
index === 0 ? (
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="property"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`boundClaims.${index}.value`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="value1, value2"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onClick={() => removeBoundClaimField(index)}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
<IconButton
|
||||
onClick={() => removeBoundClaimField(index)}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
appendBoundClaimField({
|
||||
key: "",
|
||||
value: ""
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
Add Claims
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
appendBoundClaimField({
|
||||
key: "",
|
||||
value: ""
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add Claims
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
{identityAuthMethodData?.authMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
const result = await trigger();
|
||||
if (result) {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
} else {
|
||||
createNotification({
|
||||
text: "Please fill in all required fields",
|
||||
type: "error"
|
||||
});
|
||||
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -199,7 +199,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Select
|
||||
value={role === "custom" ? (customRole?.slug as string) : role}
|
||||
isDisabled={!isAllowed}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
className="w-48 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
handleChangeRole({
|
||||
|
||||
+173
-140
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityTokenAuth,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
useUpdateIdentityTokenAuth
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -47,12 +47,16 @@ type Props = {
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityTokenAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -61,7 +65,11 @@ export const IdentityTokenAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
|
||||
|
||||
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "");
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
});
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -116,164 +124,189 @@ export const IdentityTokenAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${
|
||||
identityAuthMethodData?.authMethod ? "updated" : "configured"
|
||||
} auth method`,
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
|
||||
text: `Failed to ${isCurrentAuthMethod ? "update" : "configure"} identity`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
{identityAuthMethodData?.authMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+230
-196
@@ -6,7 +6,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityUniversalAuth,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
@@ -70,19 +70,28 @@ type Props = {
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityUniversalAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const { subscription } = useSubscription();
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
|
||||
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "");
|
||||
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
});
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -181,9 +190,7 @@ export const IdentityUniversalAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${
|
||||
identityAuthMethodData?.authMethod ? "updated" : "configured"
|
||||
} auth method`,
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -203,216 +210,243 @@ export const IdentityUniversalAuthForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`clientSecretTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`clientSecretTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeClientSecretTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeClientSecretTrustedIp(index);
|
||||
appendClientSecretTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendClientSecretTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
{identityAuthMethodData?.authMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -212,7 +212,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
<Select
|
||||
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
className="w-48 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
onRoleChange(orgMembershipId, selectedRole)
|
||||
@@ -235,7 +235,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
serverDetails?.emailConfigured && (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
className="w-40"
|
||||
className="w-48"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => onResendInvite(email)}
|
||||
|
||||
@@ -96,8 +96,12 @@ export const OrgRoleTable = () => {
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => router.push(`/org/${orgId}/roles/${id}`)}
|
||||
>
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{name}
|
||||
</Td>
|
||||
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{slug}
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
DragEvent,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
accept?: string;
|
||||
onData: (file: File) => void;
|
||||
isSmaller: boolean;
|
||||
text?: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const GenericDropzone = forwardRef<HTMLInputElement, Props>(
|
||||
({ onData, isSmaller, text, isDisabled, accept }: Props, ref): JSX.Element => {
|
||||
const [isDragActive, setDragActive] = useToggle();
|
||||
const [selectedFileName, setSelectedFileName] = useState<string | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
|
||||
|
||||
const updateSelectedFileName = () => {
|
||||
if (inputRef.current?.files?.[0]) {
|
||||
setSelectedFileName(inputRef.current.files[0].name);
|
||||
} else {
|
||||
setSelectedFileName(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrag = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.type === "dragenter" || e.type === "dragover") {
|
||||
setDragActive.on();
|
||||
} else if (e.type === "dragleave") {
|
||||
setDragActive.off();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!e.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
setDragActive.off();
|
||||
const file = e.dataTransfer.files[0];
|
||||
onData(file);
|
||||
setSelectedFileName(file.name);
|
||||
};
|
||||
|
||||
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!e.target?.files?.[0]) {
|
||||
return;
|
||||
}
|
||||
onData(e.target.files[0]);
|
||||
updateSelectedFileName();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
updateSelectedFileName();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
className={twMerge(
|
||||
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 py-4 px-2 text-sm text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
||||
isDragActive && "opacity-100",
|
||||
!isSmaller && "mx-auto w-full max-w-3xl flex-col space-y-4 py-20"
|
||||
)}
|
||||
>
|
||||
{selectedFileName ? (
|
||||
<p>{selectedFileName}</p>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center space-y-2">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="">{text}</p>
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
disabled={isDisabled}
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||
accept={accept}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
GenericDropzone.displayName = "GenericDropzone";
|
||||
@@ -351,7 +351,7 @@ export const SecretOverviewPage = () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
await createSecretV3({
|
||||
const result = await createSecretV3({
|
||||
environment: env,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
@@ -360,10 +360,18 @@ export const SecretOverviewPage = () => {
|
||||
secretComment: "",
|
||||
type: SecretType.Shared
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created secret"
|
||||
});
|
||||
|
||||
if ("approval" in result) {
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Requested change has been sent for review"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created secret"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
@@ -388,7 +396,7 @@ export const SecretOverviewPage = () => {
|
||||
type = SecretType.Shared
|
||||
) => {
|
||||
try {
|
||||
await updateSecretV3({
|
||||
const result = await updateSecretV3({
|
||||
environment: env,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
@@ -396,10 +404,18 @@ export const SecretOverviewPage = () => {
|
||||
secretValue: value,
|
||||
type
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated secret"
|
||||
});
|
||||
|
||||
if ("approval" in result) {
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Requested change has been sent for review"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated secret"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
@@ -411,7 +427,7 @@ export const SecretOverviewPage = () => {
|
||||
|
||||
const handleSecretDelete = async (env: string, key: string, secretId?: string) => {
|
||||
try {
|
||||
await deleteSecretV3({
|
||||
const result = await deleteSecretV3({
|
||||
environment: env,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
@@ -419,10 +435,18 @@ export const SecretOverviewPage = () => {
|
||||
secretId,
|
||||
type: SecretType.Shared
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted secret"
|
||||
});
|
||||
|
||||
if ("approval" in result) {
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Requested change has been sent for review"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted secret"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
|
||||
+53
-19
@@ -122,42 +122,76 @@ export const CreateSecretForm = ({
|
||||
|
||||
const isEdit = getSecretByKey(environment, key) !== undefined;
|
||||
if (isEdit) {
|
||||
return updateSecretV3({
|
||||
return {
|
||||
...(await updateSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
type: SecretType.Shared
|
||||
})),
|
||||
environment
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...(await createSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
type: SecretType.Shared
|
||||
});
|
||||
}
|
||||
|
||||
return createSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
type: SecretType.Shared
|
||||
});
|
||||
})),
|
||||
environment
|
||||
};
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const isSecretsAdded = results.some((result) => result.status === "fulfilled");
|
||||
const forApprovalEnvs = results
|
||||
.map((result) =>
|
||||
result.status === "fulfilled" && "approval" in result.value
|
||||
? result.value.environment
|
||||
: undefined
|
||||
)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (isSecretsAdded) {
|
||||
const updatedEnvs = results
|
||||
.map((result) =>
|
||||
result.status === "fulfilled" && !("approval" in result.value)
|
||||
? result.value.environment
|
||||
: undefined
|
||||
)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (forApprovalEnvs.length) {
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: `Change request submitted for ${
|
||||
forApprovalEnvs.length > 1 ? "environments" : "environment"
|
||||
}: ${forApprovalEnvs.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
if (updatedEnvs.length) {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Secrets created successfully"
|
||||
text: `Secrets created in ${
|
||||
updatedEnvs.length > 1 ? "environments" : "environment"
|
||||
}: ${updatedEnvs.join(", ")}`
|
||||
});
|
||||
onClose();
|
||||
reset();
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!updatedEnvs.length && !forApprovalEnvs.length) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create secrets"
|
||||
});
|
||||
} else {
|
||||
onClose();
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ export const SecretOverviewTableRow = ({
|
||||
>
|
||||
<div className="h-full w-full border-r border-mineshaft-600 py-2.5 px-5">
|
||||
<div className="flex items-center space-x-5">
|
||||
<div className="text-blue-300/70">
|
||||
<div className="text-bunker-300">
|
||||
<Checkbox
|
||||
id={`checkbox-${secretKey}`}
|
||||
isChecked={isSelected}
|
||||
|
||||
+58
-16
@@ -1,6 +1,8 @@
|
||||
import { faFileImport, faFingerprint, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
folderCount?: number;
|
||||
importCount?: number;
|
||||
@@ -17,28 +19,68 @@ export const SecretTableResourceCount = ({
|
||||
return (
|
||||
<div className="flex items-center gap-2 divide-x divide-mineshaft-500 text-sm text-mineshaft-400">
|
||||
{importCount > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faFileImport} className=" text-green-700" />
|
||||
<span>{importCount}</span>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<p className="whitespace-nowrap text-center">
|
||||
Total import count{" "}
|
||||
<span className="text-center text-mineshaft-400">(matching filters)</span>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faFileImport} className=" text-green-700" />
|
||||
<span>{importCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{folderCount > 0 && (
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-yellow-700" />
|
||||
<span>{folderCount}</span>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<p className="whitespace-nowrap text-center">
|
||||
Total folder count{" "}
|
||||
<span className="text-center text-mineshaft-400">(matching filters)</span>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-yellow-700" />
|
||||
<span>{folderCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{dynamicSecretCount > 0 && (
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faFingerprint} className="text-yellow-700" />
|
||||
<span>{dynamicSecretCount}</span>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<p className="whitespace-nowrap text-center">
|
||||
Total dynamic secret count{" "}
|
||||
<span className="text-center text-mineshaft-400">(matching filters)</span>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faFingerprint} className="text-yellow-700" />
|
||||
<span>{dynamicSecretCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{secretCount > 0 && (
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faKey} className="text-bunker-300" />
|
||||
<span>{secretCount}</span>
|
||||
</div>
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<p className="whitespace-nowrap text-center">
|
||||
Total secret count{" "}
|
||||
<span className="text-center text-mineshaft-400">(matching filters)</span>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<FontAwesomeIcon icon={faKey} className="text-bunker-300" />
|
||||
<span>{secretCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import Link from "next/link";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useOrgPermission } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
import { SelectImportFromPlatformModal } from "./components/SelectImportFromPlatformModal";
|
||||
|
||||
export const ImportTab = () => {
|
||||
const { membership } = useOrgPermission();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Import from external source</p>
|
||||
|
||||
<div>
|
||||
<Link
|
||||
href="https://infisical.com/docs/documentation/guides/migrating-from-envkey"
|
||||
passHref
|
||||
>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 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>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopUpOpen("selectImportPlatform");
|
||||
}}
|
||||
isDisabled={membership?.role !== ProjectMembershipRole.Admin}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mb-4 text-gray-400">Import data from another platform to Infisical.</p>
|
||||
|
||||
<SelectImportFromPlatformModal
|
||||
isOpen={popUp.selectImportPlatform.isOpen}
|
||||
onToggle={(state) => handlePopUpToggle("selectImportPlatform", state)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import { useRef } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useImportEnvKey } from "@app/hooks/api/migration/mutations";
|
||||
import { GenericDropzone } from "@app/views/SecretMainPage/components/SecretDropzone/GenericDropzone";
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
encryptionKey: z.string().min(1),
|
||||
encryptedJson: z.object({
|
||||
nonce: z.string().min(1),
|
||||
data: z.string().min(1)
|
||||
})
|
||||
});
|
||||
|
||||
type TFormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const EnvKeyPlatformModal = ({ onClose }: Props) => {
|
||||
const fileUploadRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { mutateAsync: importEnvKey } = useImportEnvKey();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
setError,
|
||||
formState: { isLoading, isDirty, isSubmitting, isValid }
|
||||
} = useForm<TFormData>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const onSubmit = async (data: TFormData) => {
|
||||
if (!data.encryptedJson) {
|
||||
setError("encryptedJson", {
|
||||
type: "required",
|
||||
message: "File is required"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await importEnvKey({
|
||||
encryptedJson: data.encryptedJson,
|
||||
decryptionKey: data.encryptionKey
|
||||
});
|
||||
createNotification({
|
||||
text: "Data imported successfully.",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
onClose();
|
||||
reset();
|
||||
|
||||
if (fileUploadRef.current) {
|
||||
fileUploadRef.current.value = "";
|
||||
}
|
||||
} catch {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
const onImportFileDrop = (file?: File) => {
|
||||
const reader = new FileReader();
|
||||
if (!file) {
|
||||
createNotification({
|
||||
text: "No file selected.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
reader.onload = (event) => {
|
||||
if (!event?.target?.result) return;
|
||||
|
||||
const droppedFile = event.target.result.toString();
|
||||
const formattedData: Record<string, string> = JSON.parse(droppedFile);
|
||||
if (
|
||||
Object.keys(formattedData).includes("nonce") &&
|
||||
Object.keys(formattedData).includes("data")
|
||||
) {
|
||||
const data = {
|
||||
nonce: formattedData.nonce,
|
||||
data: formattedData.data
|
||||
};
|
||||
setValue("encryptedJson", data, { shouldDirty: true, shouldValidate: true });
|
||||
} else {
|
||||
setValue(
|
||||
"encryptedJson",
|
||||
{
|
||||
nonce: "",
|
||||
data: ""
|
||||
},
|
||||
{ shouldDirty: true, shouldValidate: true }
|
||||
);
|
||||
|
||||
if (fileUploadRef.current) {
|
||||
fileUploadRef.current.value = "";
|
||||
}
|
||||
createNotification({
|
||||
text: "Improper file format, please upload the EnvKey export.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||
<Controller
|
||||
control={control}
|
||||
name="encryptionKey"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Encryption key"
|
||||
isRequired
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input type="password" placeholder="" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<GenericDropzone
|
||||
ref={fileUploadRef}
|
||||
text="Select Env Key export file"
|
||||
onData={onImportFileDrop}
|
||||
isSmaller
|
||||
/>
|
||||
|
||||
<div className="mt-6 flex items-center space-x-4">
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={!isDirty || isSubmitting || isLoading || !isValid}
|
||||
>
|
||||
Import data
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { useState } from "react";
|
||||
import { faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
|
||||
import { EnvKeyPlatformModal } from "./EnvKeyPlatformModal";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
enum WizardSteps {
|
||||
SelectPlatform = "select-platform",
|
||||
PlatformInputs = "platform-inputs"
|
||||
}
|
||||
|
||||
const PLATFORM_LIST = [
|
||||
{
|
||||
icon: faKey,
|
||||
platform: "env-key",
|
||||
title: "Env Key"
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const SelectImportFromPlatformModal = ({ isOpen, onToggle }: Props) => {
|
||||
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectPlatform);
|
||||
const [selectedPlatform, setSelectedPlatform] = useState<(typeof PLATFORM_LIST)[number] | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const handleFormReset = (state: boolean = false) => {
|
||||
onToggle(state);
|
||||
setWizardStep(WizardSteps.SelectPlatform);
|
||||
setSelectedPlatform(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
|
||||
<ModalContent
|
||||
title={
|
||||
selectedPlatform ? `Import from ${selectedPlatform.title}` : "Import from external source"
|
||||
}
|
||||
className="my-4"
|
||||
>
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{wizardStep === WizardSteps.SelectPlatform && (
|
||||
<motion.div
|
||||
key="select-type-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<div className="mb-4 text-sm text-mineshaft-300">
|
||||
Select a platform to import from
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{PLATFORM_LIST.map((platform, idx) => (
|
||||
<div
|
||||
key={`platform-${idx + 1}`}
|
||||
className="flex h-28 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setSelectedPlatform(platform);
|
||||
setWizardStep(WizardSteps.PlatformInputs);
|
||||
}}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
setSelectedPlatform(platform);
|
||||
setWizardStep(WizardSteps.PlatformInputs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={platform.icon} size="lg" />
|
||||
<div className="whitespace-pre-wrap text-center text-sm">{platform.title}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.PlatformInputs &&
|
||||
selectedPlatform?.platform === "env-key" && (
|
||||
<motion.div
|
||||
key="env-key-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<EnvKeyPlatformModal onClose={() => handleFormReset(false)} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ImportTab } from "./ImportTab";
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { Badge, Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useAddExternalKms, useUpdateExternalKms } from "@app/hooks/api";
|
||||
import {
|
||||
@@ -244,7 +244,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
|
||||
>
|
||||
{AWS_REGIONS.map((awsRegion) => (
|
||||
<SelectItem value={awsRegion.slug} key={`kms-aws-region-${awsRegion.slug}`}>
|
||||
{awsRegion.name} ({awsRegion.slug})
|
||||
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -2,7 +2,11 @@ import { Fragment, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
|
||||
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
||||
import { ImportTab } from "../ImportTab";
|
||||
import { OrgAuthTab } from "../OrgAuthTab";
|
||||
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||
@@ -13,7 +17,8 @@ const tabs = [
|
||||
{ name: "Security", key: "tab-org-security" },
|
||||
{ name: "Encryption", key: "tab-org-encryption" },
|
||||
{ name: "Workflow Integrations", key: "workflow-integrations" },
|
||||
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
|
||||
{ name: "Audit Log Streams", key: "tag-audit-log-streams" },
|
||||
{ name: "Import", key: "tab-import" }
|
||||
];
|
||||
export const OrgTabGroup = () => {
|
||||
const { query } = useRouter();
|
||||
@@ -63,6 +68,11 @@ export const OrgTabGroup = () => {
|
||||
<Tab.Panel>
|
||||
<AuditLogStreamsTab />
|
||||
</Tab.Panel>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Workspace}>
|
||||
<Tab.Panel>
|
||||
<ImportTab />
|
||||
</Tab.Panel>
|
||||
</OrgPermissionCan>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user