diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx index 87ca2deee..57dcc9be2 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from "react"; import { SingleValue } from "react-select"; -import { faCopy, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { faCopy, faQuestionCircle, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; import { z } from "zod"; @@ -8,14 +8,21 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, + Checkbox, FilterableSelect, FormLabel, IconButton, Input, - ModalClose + ModalClose, + Tooltip } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { useOrganization } from "@app/context"; +import { + OrgPermissionIdentityActions, + OrgPermissionSubjects, + useOrganization, + useOrgPermission +} from "@app/context"; import { useAddIdentityTokenAuth, useCreateTokenIdentityTokenAuth, @@ -27,7 +34,7 @@ import { slugSchema } from "@app/lib/schemas"; import { RelayOption } from "./RelayOption"; -const formSchema = z.object({ +const baseFormSchema = z.object({ name: slugSchema({ field: "name" }), instanceDomain: z.string().url("Must be a valid URL").or(z.literal("")), relay: z @@ -39,7 +46,10 @@ const formSchema = z.object({ { required_error: "Relay is required" } ) .nullable() - .refine((val) => val !== null, { message: "Relay is required" }), + .refine((val) => val !== null, { message: "Relay is required" }) +}); + +const formSchemaWithIdentity = baseFormSchema.extend({ identity: z .object( { @@ -52,6 +62,10 @@ const formSchema = z.object({ .refine((val) => val !== null, { message: "Identity is required" }) }); +const formSchemaWithToken = baseFormSchema.extend({ + identityToken: z.string().min(1, "Token is required") +}); + export const GatewayCliDeploymentMethod = () => { const { protocol, hostname, port } = window.location; const portSuffix = port && port !== "80" ? `:${port}` : ""; @@ -61,6 +75,7 @@ export const GatewayCliDeploymentMethod = () => { from: ROUTE_PATHS.Organization.NetworkingPage.path }); + const [autogenerateToken, setAutogenerateToken] = useState(true); const [step, setStep] = useState<"form" | "command">("form"); const [name, setName] = useState(""); const [instanceDomain, setInstanceDomain] = useState(siteURL); @@ -90,6 +105,12 @@ export const GatewayCliDeploymentMethod = () => { const { currentOrg } = useOrganization(); const organizationId = currentOrg?.id || ""; + const { permission } = useOrgPermission(); + const canCreateToken = permission.can( + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ); + const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } = useGetIdentityMembershipOrgs({ organizationId, @@ -105,48 +126,68 @@ export const GatewayCliDeploymentMethod = () => { const handleGenerateCommand = async () => { setFormErrors([]); - const validation = formSchema.safeParse({ name, relay, identity, instanceDomain }); - if (!validation.success) { - setFormErrors(validation.error.issues); - return; - } - const validatedIdentity = validation.data.identity; - - try { - const { data: identityTokenAuth } = await refetch(); - if (!identityTokenAuth) { - await addIdentityTokenAuth({ - identityId: validatedIdentity.id, - organizationId, - accessTokenTTL: 2592000, - accessTokenMaxTTL: 2592000, - accessTokenNumUsesLimit: 0, - accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] - }); - createNotification({ - text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.", - type: "warning" - }); + if (canCreateToken && autogenerateToken) { + const validation = formSchemaWithIdentity.safeParse({ + name, + relay, + identity, + instanceDomain + }); + if (!validation.success) { + setFormErrors(validation.error.issues); + return; } - const token = await createToken({ - identityId: validatedIdentity.id, - name: `gateway token for ${name} (autogenerated)` - }); - setIdentityToken(token.accessToken); - createNotification({ - text: "Automatically generated a token for the selected identity.", - type: "info" + const validatedIdentity = validation.data.identity; + + try { + const { data: identityTokenAuth } = await refetch(); + if (!identityTokenAuth) { + await addIdentityTokenAuth({ + identityId: validatedIdentity.id, + organizationId, + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0, + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + createNotification({ + text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.", + type: "warning" + }); + } + + const token = await createToken({ + identityId: validatedIdentity.id, + name: `gateway token for ${name} (autogenerated)` + }); + setIdentityToken(token.accessToken); + createNotification({ + text: "Automatically generated a token for the selected identity.", + type: "info" + }); + setStep("command"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to generate token for the selected identity", + type: "error" + }); + setIdentityToken(""); + } + } else { + const validation = formSchemaWithToken.safeParse({ + name, + relay, + identityToken, + instanceDomain }); + if (!validation.success) { + setFormErrors(validation.error.issues); + return; + } setStep("command"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to generate token for the selected identity", - type: "error" - }); - setIdentityToken(""); } }; @@ -239,8 +280,8 @@ export const GatewayCliDeploymentMethod = () => { {errors.relay &&

{errors.relay}

} { /> {errors.instanceDomain &&

{errors.instanceDomain}

} - - - setIdentity( - e as SingleValue<{ - id: string; - name: string; - }> - ) - } - isLoading={isIdentitiesLoading} - placeholder="Select identity..." - options={identityMembershipOrgs.map((membership) => membership.identity)} - getOptionValue={(option) => option.id} - getOptionLabel={(option) => option.name} - /> - {errors.identity &&

{errors.identity}

} + {canCreateToken && autogenerateToken ? ( + <> + + + setIdentity( + e as SingleValue<{ + id: string; + name: string; + }> + ) + } + isLoading={isIdentitiesLoading} + placeholder="Select identity..." + options={identityMembershipOrgs.map((membership) => membership.identity)} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + {errors.identity &&

{errors.identity}

} + + ) : ( + <> + + setIdentityToken(e.target.value)} + placeholder="Enter identity token..." + isError={Boolean(errors.identityToken)} + /> + {errors.identityToken &&

{errors.identityToken}

} + + )} + + {canCreateToken && ( +
+ { + setAutogenerateToken(Boolean(e)); + }} + id="autogenerate-token" + className="mr-2" + > +
+ Automatically enable token auth and generate a token for identity + + Token authentication will be automatically enabled for the selected identity if + it isn't already configured. By default, it will be configured to allow all IP + addresses with a token TTL of 30 days. You can manage these settings in Access + Control. +
+
A token will automatically be generated to be used with the CLI command. + + } + > + +
+
+
+
+ )}
-

{errors.identity &&

{errors.identity}

} ) : ( @@ -303,18 +295,42 @@ export const RelayCliDeploymentMethod = () => { placeholder="Enter identity token..." isError={Boolean(errors.identityToken)} /> - {canCreateToken && ( - - )} {errors.identityToken &&

{errors.identityToken}

} )} + {canCreateToken && ( +
+ { + setAutogenerateToken(Boolean(e)); + }} + id="autogenerate-token" + className="mr-2" + > +
+ Automatically enable token auth and generate a token for identity + + Token authentication will be automatically enabled for the selected identity if + it isn't already configured. By default, it will be configured to allow all IP + addresses with a token TTL of 30 days. You can manage these settings in Access + Control. +
+
A token will automatically be generated to be used with the CLI command. + + } + > + +
+
+
+
+ )} +