misc: initial ui work

This commit is contained in:
Sheen Capadngan
2024-12-11 03:40:21 +08:00
parent 56aab172d3
commit 9d9f6ec268
10 changed files with 937 additions and 11 deletions

View File

@@ -7,7 +7,8 @@ export const buildAuthMethods = ({
kubernetesId,
oidcId,
azureId,
tokenId
tokenId,
jwtId
}: {
uaId?: string;
gcpId?: string;
@@ -16,6 +17,7 @@ export const buildAuthMethods = ({
oidcId?: string;
azureId?: string;
tokenId?: string;
jwtId?: string;
}) => {
return [
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
@@ -24,6 +26,7 @@ export const buildAuthMethods = ({
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null]
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
...[jwtId ? IdentityAuthMethod.JWT_AUTH : null]
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
};

View File

@@ -6,6 +6,7 @@ import {
TIdentityAwsAuths,
TIdentityAzureAuths,
TIdentityGcpAuths,
TIdentityJwtAuths,
TIdentityKubernetesAuths,
TIdentityOidcAuths,
TIdentityOrgMemberships,
@@ -70,6 +71,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityTokenAuth}.identityId`
)
.leftJoin<TIdentityJwtAuths>(
TableName.IdentityJwtAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityJwtAuth}.identityId`
)
.select(
selectAllTableCols(TableName.IdentityOrgMembership),
@@ -81,6 +87,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
db.ref("name").withSchema(TableName.Identity)
);
@@ -183,6 +190,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId",
`${TableName.IdentityTokenAuth}.identityId`
)
.leftJoin<TIdentityJwtAuths>(
TableName.IdentityJwtAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityJwtAuth}.identityId`
)
.select(
db.ref("id").withSchema("paginatedIdentity"),
@@ -200,7 +212,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
)
// cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
@@ -237,6 +250,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
uaId,
awsId,
gcpId,
jwtId,
kubernetesId,
oidcId,
azureId,
@@ -271,7 +285,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
kubernetesId,
oidcId,
azureId,
tokenId
tokenId,
jwtId
})
}
}),

View File

@@ -7,5 +7,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.GCP_AUTH]: "GCP Auth",
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth",
[IdentityAuthMethod.AZURE_AUTH]: "Azure Auth",
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth"
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth",
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth"
};

View File

@@ -5,5 +5,11 @@ export enum IdentityAuthMethod {
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth",
OIDC_AUTH = "oidc-auth"
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth"
}
export enum IdentityJwtConfigurationType {
JWKS = "jwks",
STATIC = "static"
}

View File

@@ -4,6 +4,7 @@ export {
useAddIdentityAwsAuth,
useAddIdentityAzureAuth,
useAddIdentityGcpAuth,
useAddIdentityJwtAuth,
useAddIdentityKubernetesAuth,
useAddIdentityOidcAuth,
useAddIdentityTokenAuth,
@@ -15,6 +16,7 @@ export {
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityJwtAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
@@ -25,20 +27,24 @@ export {
useUpdateIdentityAwsAuth,
useUpdateIdentityAzureAuth,
useUpdateIdentityGcpAuth,
useUpdateIdentityJwtAuth,
useUpdateIdentityKubernetesAuth,
useUpdateIdentityOidcAuth,
useUpdateIdentityTokenAuth,
useUpdateIdentityTokenAuthToken,
useUpdateIdentityUniversalAuth} from "./mutations";
useUpdateIdentityUniversalAuth
} from "./mutations";
export {
useGetIdentityAwsAuth,
useGetIdentityAzureAuth,
useGetIdentityById,
useGetIdentityGcpAuth,
useGetIdentityJwtAuth,
useGetIdentityKubernetesAuth,
useGetIdentityOidcAuth,
useGetIdentityProjectMemberships,
useGetIdentityTokenAuth,
useGetIdentityTokensTokenAuth,
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets} from "./queries";
useGetIdentityUniversalAuthClientSecrets
} from "./queries";

View File

@@ -8,6 +8,7 @@ import {
AddIdentityAwsAuthDTO,
AddIdentityAzureAuthDTO,
AddIdentityGcpAuthDTO,
AddIdentityJwtAuthDTO,
AddIdentityKubernetesAuthDTO,
AddIdentityOidcAuthDTO,
AddIdentityTokenAuthDTO,
@@ -22,6 +23,7 @@ import {
DeleteIdentityAzureAuthDTO,
DeleteIdentityDTO,
DeleteIdentityGcpAuthDTO,
DeleteIdentityJwtAuthDTO,
DeleteIdentityKubernetesAuthDTO,
DeleteIdentityOidcAuthDTO,
DeleteIdentityTokenAuthDTO,
@@ -32,6 +34,7 @@ import {
IdentityAwsAuth,
IdentityAzureAuth,
IdentityGcpAuth,
IdentityJwtAuth,
IdentityKubernetesAuth,
IdentityOidcAuth,
IdentityTokenAuth,
@@ -42,6 +45,7 @@ import {
UpdateIdentityAzureAuthDTO,
UpdateIdentityDTO,
UpdateIdentityGcpAuthDTO,
UpdateIdentityJwtAuthDTO,
UpdateIdentityKubernetesAuthDTO,
UpdateIdentityOidcAuthDTO,
UpdateIdentityTokenAuthDTO,
@@ -518,6 +522,118 @@ export const useDeleteIdentityOidcAuth = () => {
}
});
};
export const useUpdateIdentityJwtAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityJwtAuth, {}, UpdateIdentityJwtAuthDTO>({
mutationFn: async ({
identityId,
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject
}) => {
const {
data: { identityJwtAuth }
} = await apiRequest.patch<{ identityJwtAuth: IdentityJwtAuth }>(
`/api/v1/auth/jwt-auth/identities/${identityId}`,
{
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityJwtAuth;
},
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityJwtAuth(identityId));
}
});
};
export const useAddIdentityJwtAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityJwtAuth, {}, AddIdentityJwtAuthDTO>({
mutationFn: async ({
identityId,
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityJwtAuth }
} = await apiRequest.post<{ identityJwtAuth: IdentityJwtAuth }>(
`/api/v1/auth/jwt-auth/identities/${identityId}`,
{
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityJwtAuth;
},
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityJwtAuth(identityId));
}
});
};
export const useDeleteIdentityJwtAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityTokenAuth, {}, DeleteIdentityJwtAuthDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { identityJwtAuth }
} = await apiRequest.delete(`/api/v1/auth/jwt-auth/identities/${identityId}`);
return identityJwtAuth;
},
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityJwtAuth(identityId));
}
});
};
export const useAddIdentityAzureAuth = () => {
const queryClient = useQueryClient();

View File

@@ -8,6 +8,7 @@ import {
IdentityAwsAuth,
IdentityAzureAuth,
IdentityGcpAuth,
IdentityJwtAuth,
IdentityKubernetesAuth,
IdentityMembership,
IdentityMembershipOrg,
@@ -29,6 +30,7 @@ export const identitiesKeys = {
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const,
getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const,
getIdentityJwtAuth: (identityId: string) => [{ identityId }, "identity-jwt-auth"] as const,
getIdentityTokensTokenAuth: (identityId: string) =>
[{ identityId }, "identity-tokens-token-auth"] as const,
getIdentityProjectMemberships: (identityId: string) =>
@@ -276,3 +278,30 @@ export const useGetIdentityOidcAuth = (
enabled: Boolean(identityId) && (options?.enabled ?? true)
});
};
export const useGetIdentityJwtAuth = (
identityId: string,
options?: UseQueryOptions<
IdentityJwtAuth,
unknown,
IdentityJwtAuth,
ReturnType<typeof identitiesKeys.getIdentityJwtAuth>
>
) => {
return useQuery({
queryKey: identitiesKeys.getIdentityJwtAuth(identityId),
queryFn: async () => {
const {
data: { identityJwtAuth }
} = await apiRequest.get<{ identityJwtAuth: IdentityJwtAuth }>(
`/api/v1/auth/jwt-auth/identities/${identityId}`
);
return identityJwtAuth;
},
staleTime: 0,
cacheTime: 0,
...options,
enabled: Boolean(identityId) && (options?.enabled ?? true)
});
};

View File

@@ -1,6 +1,6 @@
import { TOrgRole } from "../roles/types";
import { ProjectUserMembershipTemporaryMode, Workspace } from "../workspace/types";
import { IdentityAuthMethod } from "./enums";
import { IdentityAuthMethod, IdentityJwtConfigurationType } from "./enums";
export type IdentityTrustedIp = {
id: string;
@@ -446,6 +446,65 @@ export type DeleteIdentityTokenAuthDTO = {
identityId: string;
};
export type IdentityJwtAuth = {
identityId: string;
configurationType: IdentityJwtConfigurationType;
jwksUrl: string;
jwksCaCert: string;
publicKeys: string[];
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: IdentityTrustedIp[];
};
export type AddIdentityJwtAuthDTO = {
organizationId: string;
identityId: string;
configurationType: string;
jwksUrl?: string;
jwksCaCert: string;
publicKeys?: string[];
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: {
ipAddress: string;
}[];
};
export type UpdateIdentityJwtAuthDTO = {
organizationId: string;
identityId: string;
configurationType?: string;
jwksUrl?: string;
jwksCaCert?: string;
publicKeys?: string[];
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: {
ipAddress: string;
}[];
};
export type DeleteIdentityJwtAuthDTO = {
organizationId: string;
identityId: string;
};
export type CreateTokenIdentityTokenAuthDTO = {
identityId: string;
name: string;

View File

@@ -23,12 +23,17 @@ import {
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import {
IdentityAuthMethod,
identityAuthToNameMap,
useDeleteIdentityJwtAuth
} from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
import { IdentityJwtAuthForm } from "./IdentityJwtAuthForm";
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
@@ -68,7 +73,11 @@ const identityAuthMethods = [
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH },
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH },
{
label: "JWT Auth",
value: IdentityAuthMethod.JWT_AUTH
}
];
const schema = yup
@@ -100,6 +109,7 @@ export const IdentityAuthMethodModalContent = ({
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth();
const { control, watch } = useForm<FormData>({
resolver: yupResolver(schema),
@@ -216,6 +226,17 @@ export const IdentityAuthMethodModalContent = ({
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.JWT_AUTH]: {
revokeMethod: revokeJwtAuth,
render: () => (
<IdentityJwtAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
}
};

View File

@@ -0,0 +1,670 @@
import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { useAddIdentityJwtAuth, useUpdateIdentityJwtAuth } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
import { useGetIdentityJwtAuth } from "@app/hooks/api/identities/queries";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const commonSchema = z.object({
accessTokenTrustedIps: z
.array(
z.object({
ipAddress: z.string().max(50)
})
)
.min(1),
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
message: "Access Token TTL cannot be greater than 315360000"
}),
accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, {
message: "Access Token Max TTL cannot be greater than 315360000"
}),
accessTokenNumUsesLimit: z.string(),
boundIssuer: z.string().trim().default(""),
boundAudiences: z.string().optional().default(""),
boundClaims: z.array(
z.object({
key: z.string(),
value: z.string()
})
),
boundSubject: z.string().optional().default("")
});
const schema = z.discriminatedUnion("configurationType", [
z
.object({
configurationType: z.literal(IdentityJwtConfigurationType.JWKS),
jwksUrl: z.string().trim().url(),
jwksCaCert: z.string().trim().default(""),
publicKeys: z
.object({
value: z.string()
})
.array()
.optional()
})
.merge(commonSchema),
z
.object({
configurationType: z.literal(IdentityJwtConfigurationType.STATIC),
jwksUrl: z.string().trim().optional(),
jwksCaCert: z.string().trim().optional().default(""),
publicKeys: z
.object({
value: z.string().min(1)
})
.array()
.min(1)
})
.merge(commonSchema)
]);
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
};
export const IdentityJwtAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { subscription } = useSubscription();
const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth();
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityJwtAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isUpdate
});
const {
watch,
control,
handleSubmit,
reset,
setValue,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
configurationType: IdentityJwtConfigurationType.JWKS
}
});
const selectedConfigurationType = watch("configurationType") as IdentityJwtConfigurationType;
const {
fields: publicKeyFields,
append: appendPublicKeyFields,
remove: removePublicKeyFields
} = useFieldArray({
control,
name: "publicKeys"
});
const {
fields: boundClaimsFields,
append: appendBoundClaimField,
remove: removeBoundClaimField
} = useFieldArray({
control,
name: "boundClaims"
});
const {
fields: accessTokenTrustedIpsFields,
append: appendAccessTokenTrustedIp,
remove: removeAccessTokenTrustedIp
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
useEffect(() => {
if (data) {
reset({
configurationType: data.configurationType,
jwksUrl: data.jwksUrl,
jwksCaCert: data.jwksCaCert,
publicKeys: data.publicKeys.map((pk) => ({
value: pk
})),
boundIssuer: data.boundIssuer,
boundAudiences: data.boundAudiences,
boundClaims: Object.entries(data.boundClaims).map(([key, value]) => ({
key,
value
})),
boundSubject: data.boundSubject,
accessTokenTTL: String(data.accessTokenTTL),
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
({ ipAddress, prefix }: IdentityTrustedIp) => {
return {
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
};
}
)
});
} else {
reset({
configurationType: IdentityJwtConfigurationType.JWKS,
jwksUrl: "",
jwksCaCert: "",
boundIssuer: "",
boundAudiences: "",
boundClaims: [],
boundSubject: "",
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
}
}, [data]);
const onFormSubmit = async ({
accessTokenTrustedIps,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject
}: FormData) => {
try {
if (!identityAuthMethodData) {
return;
}
if (data) {
await updateMutateAsync({
identityId: identityAuthMethodData.identityId,
organizationId: orgId,
configurationType,
jwksUrl,
jwksCaCert,
publicKeys: publicKeys?.map((field) => field.value).filter(Boolean),
boundIssuer,
boundAudiences,
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
boundSubject,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
} else {
await addMutateAsync({
identityId: identityAuthMethodData.identityId,
configurationType,
jwksUrl,
jwksCaCert,
publicKeys: publicKeys?.map((field) => field.value).filter(Boolean),
boundIssuer,
boundAudiences,
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
boundSubject,
organizationId: orgId,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
}
handlePopUpToggle("identityAuthMethod", false);
createNotification({
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success"
});
reset();
} catch (err) {
createNotification({
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="configurationType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
if (e === IdentityJwtConfigurationType.JWKS) {
setValue("publicKeys", []);
} else {
setValue("publicKeys", [
{
value: ""
}
]);
setValue("jwksUrl", "");
setValue("jwksCaCert", "");
}
onChange(e);
}}
className="w-full"
>
<SelectItem value={IdentityJwtConfigurationType.JWKS} key="jwks">
JWKS
</SelectItem>
<SelectItem value={IdentityJwtConfigurationType.STATIC} key="static">
Static
</SelectItem>
</Select>
</FormControl>
)}
/>
{selectedConfigurationType === IdentityJwtConfigurationType.JWKS && (
<>
<Controller
control={control}
name="jwksUrl"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="JWKS URL"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="jwksCaCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="JWKS CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
</>
)}
{selectedConfigurationType === IdentityJwtConfigurationType.STATIC && (
<>
{publicKeyFields.map(({ id }, index) => (
<div key={id} className="flex gap-2">
<Controller
control={control}
name={`publicKeys.${index}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-grow"
label={`Public Key ${index + 1}`}
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN PUBLIC KEY----- ..." />
</FormControl>
)}
/>
<IconButton
onClick={() => removePublicKeyFields(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={() =>
appendPublicKeyFields({
value: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add Public Key
</Button>
</div>
</>
)}
<Controller
control={control}
name="boundIssuer"
render={({ field, fieldState: { error } }) => (
<FormControl label="Issuer" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} type="text" />
</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}
>
<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"
>
<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"
>
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;
}
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) {
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}
>
{isUpdate ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);
};