diff --git a/frontend-v2/src/components/organization/CreateOrgModal/CreateOrgModal.tsx b/frontend-v2/src/components/organization/CreateOrgModal/CreateOrgModal.tsx new file mode 100644 index 000000000..c01d30dca --- /dev/null +++ b/frontend-v2/src/components/organization/CreateOrgModal/CreateOrgModal.tsx @@ -0,0 +1,115 @@ +import { FC } 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, Modal, ModalContent } from "@app/components/v2"; +import { useCreateOrg, useSelectOrganization } from "@app/hooks/api"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { useNavigate } from "@tanstack/react-router"; + +const schema = z + .object({ + name: z.string().nonempty({ message: "Name is required" }) + }) + .required(); + +export type FormData = z.infer; + +interface CreateOrgModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const CreateOrgModal: FC = ({ isOpen, onClose }) => { + const navigate = useNavigate(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "" + } + }); + + const { mutateAsync: createOrg } = useCreateOrg({ + invalidate: false + }); + const { mutateAsync: selectOrg } = useSelectOrganization(); + + const onFormSubmit = async ({ name }: FormData) => { + try { + const organization = await createOrg({ + name + }); + + await selectOrg({ + organizationId: organization.id + }); + + createNotification({ + text: "Successfully created organization", + type: "success" + }); + + navigate({ + to: `/organization/$organizationId/${ProjectType.SecretManager}` as const, + params: { + organizationId: organization.id + } + }); + + localStorage.setItem("orgData.id", organization.id); + + reset(); + onClose(); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to created organization", + type: "error" + }); + } + }; + + return ( + + +
+ ( + + + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend-v2/src/components/organization/CreateOrgModal/index.tsx b/frontend-v2/src/components/organization/CreateOrgModal/index.tsx new file mode 100644 index 000000000..7b794731a --- /dev/null +++ b/frontend-v2/src/components/organization/CreateOrgModal/index.tsx @@ -0,0 +1 @@ +export { CreateOrgModal } from "./CreateOrgModal"; diff --git a/frontend-v2/src/hooks/api/secretScanning/index.tsx b/frontend-v2/src/hooks/api/secretScanning/index.tsx new file mode 100644 index 000000000..a7f4c2a76 --- /dev/null +++ b/frontend-v2/src/hooks/api/secretScanning/index.tsx @@ -0,0 +1,10 @@ +export { + useCreateNewInstallationSession, + useLinkGitAppInstallationWithOrg, + useUpdateRiskStatus +} from "./mutation"; +export { + secretScanningQueryKeys, + useGetSecretScanningInstallationStatus, + useGetSecretScanningRisks +} from "./queries"; diff --git a/frontend-v2/src/hooks/api/secretScanning/mutation.ts b/frontend-v2/src/hooks/api/secretScanning/mutation.ts new file mode 100644 index 000000000..f469c872c --- /dev/null +++ b/frontend-v2/src/hooks/api/secretScanning/mutation.ts @@ -0,0 +1,45 @@ +import { useMutation } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { RiskStatus, TGitAppOrg, TSecretScanningGitRisks } from "./types"; + +export const useCreateNewInstallationSession = () => { + return useMutation<{ sessionId: string }, object, { organizationId: string }>({ + mutationFn: async (opt) => { + const { data } = await apiRequest.post( + "/api/v1/secret-scanning/create-installation-session/organization", + opt + ); + return data; + } + }); +}; + +export const useUpdateRiskStatus = () => { + return useMutation< + TSecretScanningGitRisks, + object, + { organizationId: string; riskId: string; status: RiskStatus } + >({ + mutationFn: async (opt) => { + const { data } = await apiRequest.post( + `/api/v1/secret-scanning/organization/${opt.organizationId}/risks/${opt.riskId}/status`, + { status: opt.status } + ); + return data; + } + }); +}; + +export const useLinkGitAppInstallationWithOrg = () => { + return useMutation({ + mutationFn: async (opt) => { + const { data } = await apiRequest.post( + "/api/v1/secret-scanning/link-installation", + opt + ); + return data; + } + }); +}; diff --git a/frontend-v2/src/hooks/api/secretScanning/queries.ts b/frontend-v2/src/hooks/api/secretScanning/queries.ts new file mode 100644 index 000000000..84ad6deda --- /dev/null +++ b/frontend-v2/src/hooks/api/secretScanning/queries.ts @@ -0,0 +1,34 @@ +import { apiRequest } from "@app/config/request"; +import { useQuery } from "@tanstack/react-query"; +import { TSecretScanningGitRisks } from "./types"; + +export const secretScanningQueryKeys = { + getInstallationStatus: (orgId: string) => ["secret-scanning-installation-status", { orgId }], + getRisksByOrganizatio: (orgId: string) => ["secret-scanning-risks", { orgId }] +}; + +const fetchSecretScanningInstallationStatus = async (organizationId: string) => { + const { data } = await apiRequest.get<{ appInstallationCompleted: boolean }>( + `/api/v1/secret-scanning/installation-status/organization/${organizationId}` + ); + return data; +}; + +export const useGetSecretScanningInstallationStatus = (orgId: string) => + useQuery({ + queryKey: secretScanningQueryKeys.getInstallationStatus(orgId), + queryFn: () => fetchSecretScanningInstallationStatus(orgId) + }); + +const fetchSecretScanningRisksByOrgId = async (oranizationId: string) => { + const { data } = await apiRequest.get( + `/api/v1/secret-scanning/organization/${oranizationId}/risks` + ); + return data; +}; + +export const useGetSecretScanningRisks = (orgId: string) => + useQuery({ + queryKey: secretScanningQueryKeys.getRisksByOrganizatio(orgId), + queryFn: () => fetchSecretScanningRisksByOrgId(orgId) + }); diff --git a/frontend-v2/src/hooks/api/secretScanning/types.ts b/frontend-v2/src/hooks/api/secretScanning/types.ts new file mode 100644 index 000000000..7c2cdc652 --- /dev/null +++ b/frontend-v2/src/hooks/api/secretScanning/types.ts @@ -0,0 +1,51 @@ +export enum RiskStatus { + RESOLVED_FALSE_POSITIVE = "RESOLVED_FALSE_POSITIVE", + RESOLVED_REVOKED = "RESOLVED_REVOKED", + RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED", + UNRESOLVED = "UNRESOLVED" +} + +export type TSecretScanningGitRisks = { + id: string; + description: string; + startLine: string; + endLine: string; + startColumn: string; + endColumn: string; + match: string; + secret: string; + file: string; + symlinkFile: string; + commit: string; + entropy: string; + author: string; + email: string; + date: string; + message: string; + tags: string[]; + ruleID: string; + fingerprint: string; + status: string; + isFalsePositive: boolean; // New field for marking risks as false positives + isResolved: boolean; // New field for marking risks as resolved + riskOwner: string | null; // New field for setting a risk owner (nullable string) + installationId: string; + repositoryId: string; + repositoryLink: string; + repositoryFullName: string; + pusher: { + name: string; + email: string; + }; + createdAt: string; + orgId: string; +}; + +export type TGitAppOrg = { + id: string; + installationId: string; + userId: string; + orgId: string; + createdAt: Date; + updatedAt: Date; +}; diff --git a/frontend-v2/src/layouts/OrganizationLayout/OrganizationLayout.tsx b/frontend-v2/src/layouts/OrganizationLayout/OrganizationLayout.tsx index 63604d224..b178f1dc2 100644 --- a/frontend-v2/src/layouts/OrganizationLayout/OrganizationLayout.tsx +++ b/frontend-v2/src/layouts/OrganizationLayout/OrganizationLayout.tsx @@ -5,6 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, Outlet } from "@tanstack/react-router"; import { Mfa } from "@app/components/auth/Mfa"; +import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Menu, MenuItem } from "@app/components/v2"; import { useOrganization, useUser } from "@app/context"; @@ -189,12 +190,10 @@ export const OrganizationLayout = () => { - { - // handlePopUpToggle("createOrg", false)} - // /> - } + handlePopUpToggle("createOrg", false)} + />
diff --git a/frontend-v2/src/routeTree.gen.ts b/frontend-v2/src/routeTree.gen.ts index 0c3b32964..ce5de828f 100644 --- a/frontend-v2/src/routeTree.gen.ts +++ b/frontend-v2/src/routeTree.gen.ts @@ -35,8 +35,22 @@ import { Route as authenticatePersonalSettingsIndexImport } from './routes/_auth import { Route as RestrictloginsignupLoginProviderSuccessImport } from './routes/_restrict_login_signup/login/provider/success' import { Route as RestrictloginsignupLoginProviderErrorImport } from './routes/_restrict_login_signup/login/provider/error' import { Route as authenticateOrgdetailsOrganizationIndexImport } from './routes/_authenticate/_org_details/organization/index' -import { Route as authenticateOrgdetailsOrganizationOrganizationIdIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/index' -import { Route as authenticateOrgdetailsOrganizationOrganizationIdSecretManagerImport } from './routes/_authenticate/_org_details/organization/$organizationId/secret-manager' +import { Route as authenticateOrgdetailsOrganizationNoneIndexImport } from './routes/_authenticate/_org_details/organization/none/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/settings/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/secret-sharing/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/secret-scanning/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/overview/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdMembersIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/members/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdBillingIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/billing/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/audit-logs/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdAdminIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/admin/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewImport } from './routes/_authenticate/_org_details/organization/$organizationId/secret-manager/overview' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewImport } from './routes/_authenticate/_org_details/organization/$organizationId/kms/overview' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewImport } from './routes/_authenticate/_org_details/organization/$organizationId/cert-manager/overview' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/roles/$roleId/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/memberships/$membershipId/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/index' +import { Route as authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexImport } from './routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/index' // Create Virtual Routes @@ -197,20 +211,134 @@ const authenticateOrgdetailsOrganizationIndexRoute = getParentRoute: () => authenticateOrgdetailsLayoutRoute, } as any) -const authenticateOrgdetailsOrganizationOrganizationIdIndexRoute = - authenticateOrgdetailsOrganizationOrganizationIdIndexImport.update({ - id: '/organization/$organizationId/', - path: '/organization/$organizationId/', +const authenticateOrgdetailsOrganizationNoneIndexRoute = + authenticateOrgdetailsOrganizationNoneIndexImport.update({ + id: '/organization/none/', + path: '/organization/none/', getParentRoute: () => authenticateOrgdetailsLayoutRoute, } as any) -const authenticateOrgdetailsOrganizationOrganizationIdSecretManagerRoute = - authenticateOrgdetailsOrganizationOrganizationIdSecretManagerImport.update({ - id: '/organization/$organizationId/secret-manager', - path: '/organization/$organizationId/secret-manager', +const authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexImport.update({ + id: '/organization/$organizationId/settings/', + path: '/organization/$organizationId/settings/', getParentRoute: () => authenticateOrgdetailsLayoutRoute, } as any) +const authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexImport.update( + { + id: '/organization/$organizationId/secret-sharing/', + path: '/organization/$organizationId/secret-sharing/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any, + ) + +const authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexImport.update( + { + id: '/organization/$organizationId/secret-scanning/', + path: '/organization/$organizationId/secret-scanning/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any, + ) + +const authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexImport.update({ + id: '/organization/$organizationId/overview/', + path: '/organization/$organizationId/overview/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any) + +const authenticateOrgdetailsOrganizationOrganizationIdMembersIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdMembersIndexImport.update({ + id: '/organization/$organizationId/members/', + path: '/organization/$organizationId/members/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any) + +const authenticateOrgdetailsOrganizationOrganizationIdBillingIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdBillingIndexImport.update({ + id: '/organization/$organizationId/billing/', + path: '/organization/$organizationId/billing/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any) + +const authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexImport.update({ + id: '/organization/$organizationId/audit-logs/', + path: '/organization/$organizationId/audit-logs/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any) + +const authenticateOrgdetailsOrganizationOrganizationIdAdminIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdAdminIndexImport.update({ + id: '/organization/$organizationId/admin/', + path: '/organization/$organizationId/admin/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any) + +const authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewRoute = + authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewImport.update( + { + id: '/organization/$organizationId/secret-manager/overview', + path: '/organization/$organizationId/secret-manager/overview', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any, + ) + +const authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewRoute = + authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewImport.update({ + id: '/organization/$organizationId/kms/overview', + path: '/organization/$organizationId/kms/overview', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any) + +const authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewRoute = + authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewImport.update( + { + id: '/organization/$organizationId/cert-manager/overview', + path: '/organization/$organizationId/cert-manager/overview', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any, + ) + +const authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexImport.update( + { + id: '/organization/$organizationId/roles/$roleId/', + path: '/organization/$organizationId/roles/$roleId/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any, + ) + +const authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexImport.update( + { + id: '/organization/$organizationId/memberships/$membershipId/', + path: '/organization/$organizationId/memberships/$membershipId/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any, + ) + +const authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexImport.update( + { + id: '/organization/$organizationId/identities/$identityId/', + path: '/organization/$organizationId/identities/$identityId/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any, + ) + +const authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexRoute = + authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexImport.update( + { + id: '/organization/$organizationId/groups/$groupId/', + path: '/organization/$organizationId/groups/$groupId/', + getParentRoute: () => authenticateOrgdetailsLayoutRoute, + } as any, + ) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -376,18 +504,116 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof authenticateOrgdetailsOrganizationIndexImport parentRoute: typeof authenticateOrgdetailsLayoutImport } - '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager': { - id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager' - path: '/organization/$organizationId/secret-manager' - fullPath: '/organization/$organizationId/secret-manager' - preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdSecretManagerImport + '/_authenticate/_org_details/_org-layout/organization/none/': { + id: '/_authenticate/_org_details/_org-layout/organization/none/' + path: '/organization/none' + fullPath: '/organization/none' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationNoneIndexImport parentRoute: typeof authenticateOrgdetailsLayoutImport } - '/_authenticate/_org_details/_org-layout/organization/$organizationId/': { - id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/' - path: '/organization/$organizationId' - fullPath: '/organization/$organizationId' - preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdIndexImport + '/_authenticate/_org_details/_org-layout/organization/$organizationId/cert-manager/overview': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/cert-manager/overview' + path: '/organization/$organizationId/cert-manager/overview' + fullPath: '/organization/$organizationId/cert-manager/overview' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/kms/overview': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/kms/overview' + path: '/organization/$organizationId/kms/overview' + fullPath: '/organization/$organizationId/kms/overview' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager/overview': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager/overview' + path: '/organization/$organizationId/secret-manager/overview' + fullPath: '/organization/$organizationId/secret-manager/overview' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/admin/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/admin/' + path: '/organization/$organizationId/admin' + fullPath: '/organization/$organizationId/admin' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdAdminIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/audit-logs/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/audit-logs/' + path: '/organization/$organizationId/audit-logs' + fullPath: '/organization/$organizationId/audit-logs' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/billing/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/billing/' + path: '/organization/$organizationId/billing' + fullPath: '/organization/$organizationId/billing' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdBillingIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/members/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/members/' + path: '/organization/$organizationId/members' + fullPath: '/organization/$organizationId/members' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdMembersIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/overview/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/overview/' + path: '/organization/$organizationId/overview' + fullPath: '/organization/$organizationId/overview' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-scanning/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-scanning/' + path: '/organization/$organizationId/secret-scanning' + fullPath: '/organization/$organizationId/secret-scanning' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-sharing/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-sharing/' + path: '/organization/$organizationId/secret-sharing' + fullPath: '/organization/$organizationId/secret-sharing' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/settings/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/settings/' + path: '/organization/$organizationId/settings' + fullPath: '/organization/$organizationId/settings' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/' + path: '/organization/$organizationId/groups/$groupId' + fullPath: '/organization/$organizationId/groups/$groupId' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/' + path: '/organization/$organizationId/identities/$identityId' + fullPath: '/organization/$organizationId/identities/$identityId' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/memberships/$membershipId/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/memberships/$membershipId/' + path: '/organization/$organizationId/memberships/$membershipId' + fullPath: '/organization/$organizationId/memberships/$membershipId' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexImport + parentRoute: typeof authenticateOrgdetailsLayoutImport + } + '/_authenticate/_org_details/_org-layout/organization/$organizationId/roles/$roleId/': { + id: '/_authenticate/_org_details/_org-layout/organization/$organizationId/roles/$roleId/' + path: '/organization/$organizationId/roles/$roleId' + fullPath: '/organization/$organizationId/roles/$roleId' + preLoaderRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexImport parentRoute: typeof authenticateOrgdetailsLayoutImport } } @@ -397,18 +623,60 @@ declare module '@tanstack/react-router' { interface authenticateOrgdetailsLayoutRouteChildren { authenticateOrgdetailsOrganizationIndexRoute: typeof authenticateOrgdetailsOrganizationIndexRoute - authenticateOrgdetailsOrganizationOrganizationIdSecretManagerRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdSecretManagerRoute - authenticateOrgdetailsOrganizationOrganizationIdIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdIndexRoute + authenticateOrgdetailsOrganizationNoneIndexRoute: typeof authenticateOrgdetailsOrganizationNoneIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewRoute + authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewRoute + authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewRoute + authenticateOrgdetailsOrganizationOrganizationIdAdminIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdAdminIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdBillingIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdBillingIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdMembersIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdMembersIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexRoute + authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexRoute: typeof authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexRoute } const authenticateOrgdetailsLayoutRouteChildren: authenticateOrgdetailsLayoutRouteChildren = { authenticateOrgdetailsOrganizationIndexRoute: authenticateOrgdetailsOrganizationIndexRoute, - authenticateOrgdetailsOrganizationOrganizationIdSecretManagerRoute: - authenticateOrgdetailsOrganizationOrganizationIdSecretManagerRoute, - authenticateOrgdetailsOrganizationOrganizationIdIndexRoute: - authenticateOrgdetailsOrganizationOrganizationIdIndexRoute, + authenticateOrgdetailsOrganizationNoneIndexRoute: + authenticateOrgdetailsOrganizationNoneIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewRoute: + authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewRoute, + authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewRoute: + authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewRoute, + authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewRoute: + authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewRoute, + authenticateOrgdetailsOrganizationOrganizationIdAdminIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdAdminIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdBillingIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdBillingIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdMembersIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdMembersIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexRoute, + authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexRoute: + authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexRoute, } const authenticateOrgdetailsLayoutRouteWithChildren = @@ -540,8 +808,22 @@ export interface FileRoutesByFullPath { '/login/sso': typeof RestrictloginsignupLoginSsoIndexRoute '/signup/sso': typeof RestrictloginsignupSignupSsoIndexRoute '/organization': typeof authenticateOrgdetailsOrganizationIndexRoute - '/organization/$organizationId/secret-manager': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretManagerRoute - '/organization/$organizationId': typeof authenticateOrgdetailsOrganizationOrganizationIdIndexRoute + '/organization/none': typeof authenticateOrgdetailsOrganizationNoneIndexRoute + '/organization/$organizationId/cert-manager/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewRoute + '/organization/$organizationId/kms/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewRoute + '/organization/$organizationId/secret-manager/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewRoute + '/organization/$organizationId/admin': typeof authenticateOrgdetailsOrganizationOrganizationIdAdminIndexRoute + '/organization/$organizationId/audit-logs': typeof authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexRoute + '/organization/$organizationId/billing': typeof authenticateOrgdetailsOrganizationOrganizationIdBillingIndexRoute + '/organization/$organizationId/members': typeof authenticateOrgdetailsOrganizationOrganizationIdMembersIndexRoute + '/organization/$organizationId/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexRoute + '/organization/$organizationId/secret-scanning': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexRoute + '/organization/$organizationId/secret-sharing': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexRoute + '/organization/$organizationId/settings': typeof authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexRoute + '/organization/$organizationId/groups/$groupId': typeof authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexRoute + '/organization/$organizationId/identities/$identityId': typeof authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexRoute + '/organization/$organizationId/memberships/$membershipId': typeof authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexRoute + '/organization/$organizationId/roles/$roleId': typeof authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexRoute } export interface FileRoutesByTo { @@ -563,8 +845,22 @@ export interface FileRoutesByTo { '/login/sso': typeof RestrictloginsignupLoginSsoIndexRoute '/signup/sso': typeof RestrictloginsignupSignupSsoIndexRoute '/organization': typeof authenticateOrgdetailsOrganizationIndexRoute - '/organization/$organizationId/secret-manager': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretManagerRoute - '/organization/$organizationId': typeof authenticateOrgdetailsOrganizationOrganizationIdIndexRoute + '/organization/none': typeof authenticateOrgdetailsOrganizationNoneIndexRoute + '/organization/$organizationId/cert-manager/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewRoute + '/organization/$organizationId/kms/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewRoute + '/organization/$organizationId/secret-manager/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewRoute + '/organization/$organizationId/admin': typeof authenticateOrgdetailsOrganizationOrganizationIdAdminIndexRoute + '/organization/$organizationId/audit-logs': typeof authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexRoute + '/organization/$organizationId/billing': typeof authenticateOrgdetailsOrganizationOrganizationIdBillingIndexRoute + '/organization/$organizationId/members': typeof authenticateOrgdetailsOrganizationOrganizationIdMembersIndexRoute + '/organization/$organizationId/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexRoute + '/organization/$organizationId/secret-scanning': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexRoute + '/organization/$organizationId/secret-sharing': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexRoute + '/organization/$organizationId/settings': typeof authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexRoute + '/organization/$organizationId/groups/$groupId': typeof authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexRoute + '/organization/$organizationId/identities/$identityId': typeof authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexRoute + '/organization/$organizationId/memberships/$membershipId': typeof authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexRoute + '/organization/$organizationId/roles/$roleId': typeof authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexRoute } export interface FileRoutesById { @@ -592,8 +888,22 @@ export interface FileRoutesById { '/_restrict_login_signup/login/sso/': typeof RestrictloginsignupLoginSsoIndexRoute '/_restrict_login_signup/signup/sso/': typeof RestrictloginsignupSignupSsoIndexRoute '/_authenticate/_org_details/_org-layout/organization/': typeof authenticateOrgdetailsOrganizationIndexRoute - '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretManagerRoute - '/_authenticate/_org_details/_org-layout/organization/$organizationId/': typeof authenticateOrgdetailsOrganizationOrganizationIdIndexRoute + '/_authenticate/_org_details/_org-layout/organization/none/': typeof authenticateOrgdetailsOrganizationNoneIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/cert-manager/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdCertManagerOverviewRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/kms/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdKmsOverviewRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager/overview': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretManagerOverviewRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/admin/': typeof authenticateOrgdetailsOrganizationOrganizationIdAdminIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/audit-logs/': typeof authenticateOrgdetailsOrganizationOrganizationIdAuditLogsIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/billing/': typeof authenticateOrgdetailsOrganizationOrganizationIdBillingIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/members/': typeof authenticateOrgdetailsOrganizationOrganizationIdMembersIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/overview/': typeof authenticateOrgdetailsOrganizationOrganizationIdOverviewIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-scanning/': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretScanningIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-sharing/': typeof authenticateOrgdetailsOrganizationOrganizationIdSecretSharingIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/settings/': typeof authenticateOrgdetailsOrganizationOrganizationIdSettingsIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/': typeof authenticateOrgdetailsOrganizationOrganizationIdGroupsGroupIdIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/': typeof authenticateOrgdetailsOrganizationOrganizationIdIdentitiesIdentityIdIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/memberships/$membershipId/': typeof authenticateOrgdetailsOrganizationOrganizationIdMembershipsMembershipIdIndexRoute + '/_authenticate/_org_details/_org-layout/organization/$organizationId/roles/$roleId/': typeof authenticateOrgdetailsOrganizationOrganizationIdRolesRoleIdIndexRoute } export interface FileRouteTypes { @@ -618,8 +928,22 @@ export interface FileRouteTypes { | '/login/sso' | '/signup/sso' | '/organization' - | '/organization/$organizationId/secret-manager' - | '/organization/$organizationId' + | '/organization/none' + | '/organization/$organizationId/cert-manager/overview' + | '/organization/$organizationId/kms/overview' + | '/organization/$organizationId/secret-manager/overview' + | '/organization/$organizationId/admin' + | '/organization/$organizationId/audit-logs' + | '/organization/$organizationId/billing' + | '/organization/$organizationId/members' + | '/organization/$organizationId/overview' + | '/organization/$organizationId/secret-scanning' + | '/organization/$organizationId/secret-sharing' + | '/organization/$organizationId/settings' + | '/organization/$organizationId/groups/$groupId' + | '/organization/$organizationId/identities/$identityId' + | '/organization/$organizationId/memberships/$membershipId' + | '/organization/$organizationId/roles/$roleId' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -640,8 +964,22 @@ export interface FileRouteTypes { | '/login/sso' | '/signup/sso' | '/organization' - | '/organization/$organizationId/secret-manager' - | '/organization/$organizationId' + | '/organization/none' + | '/organization/$organizationId/cert-manager/overview' + | '/organization/$organizationId/kms/overview' + | '/organization/$organizationId/secret-manager/overview' + | '/organization/$organizationId/admin' + | '/organization/$organizationId/audit-logs' + | '/organization/$organizationId/billing' + | '/organization/$organizationId/members' + | '/organization/$organizationId/overview' + | '/organization/$organizationId/secret-scanning' + | '/organization/$organizationId/secret-sharing' + | '/organization/$organizationId/settings' + | '/organization/$organizationId/groups/$groupId' + | '/organization/$organizationId/identities/$identityId' + | '/organization/$organizationId/memberships/$membershipId' + | '/organization/$organizationId/roles/$roleId' id: | '__root__' | '/' @@ -667,8 +1005,22 @@ export interface FileRouteTypes { | '/_restrict_login_signup/login/sso/' | '/_restrict_login_signup/signup/sso/' | '/_authenticate/_org_details/_org-layout/organization/' - | '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager' - | '/_authenticate/_org_details/_org-layout/organization/$organizationId/' + | '/_authenticate/_org_details/_org-layout/organization/none/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/cert-manager/overview' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/kms/overview' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager/overview' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/admin/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/audit-logs/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/billing/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/members/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/overview/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-scanning/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-sharing/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/settings/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/memberships/$membershipId/' + | '/_authenticate/_org_details/_org-layout/organization/$organizationId/roles/$roleId/' fileRoutesById: FileRoutesById } @@ -764,8 +1116,22 @@ export const routeTree = rootRoute "parent": "/_authenticate/_org_details", "children": [ "/_authenticate/_org_details/_org-layout/organization/", - "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager", - "/_authenticate/_org_details/_org-layout/organization/$organizationId/" + "/_authenticate/_org_details/_org-layout/organization/none/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/cert-manager/overview", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/kms/overview", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager/overview", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/admin/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/audit-logs/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/billing/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/members/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/overview/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-scanning/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-sharing/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/settings/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/memberships/$membershipId/", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/roles/$roleId/" ] }, "/_authenticate/personal-settings": { @@ -822,12 +1188,68 @@ export const routeTree = rootRoute "filePath": "_authenticate/_org_details/organization/index.tsx", "parent": "/_authenticate/_org_details/_org-layout" }, - "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager": { - "filePath": "_authenticate/_org_details/organization/$organizationId/secret-manager.tsx", + "/_authenticate/_org_details/_org-layout/organization/none/": { + "filePath": "_authenticate/_org_details/organization/none/index.tsx", "parent": "/_authenticate/_org_details/_org-layout" }, - "/_authenticate/_org_details/_org-layout/organization/$organizationId/": { - "filePath": "_authenticate/_org_details/organization/$organizationId/index.tsx", + "/_authenticate/_org_details/_org-layout/organization/$organizationId/cert-manager/overview": { + "filePath": "_authenticate/_org_details/organization/$organizationId/cert-manager/overview.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/kms/overview": { + "filePath": "_authenticate/_org_details/organization/$organizationId/kms/overview.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager/overview": { + "filePath": "_authenticate/_org_details/organization/$organizationId/secret-manager/overview.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/admin/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/admin/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/audit-logs/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/audit-logs/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/billing/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/billing/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/members/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/members/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/overview/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/overview/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-scanning/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/secret-scanning/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-sharing/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/secret-sharing/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/settings/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/settings/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/groups/$groupId/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/identities/$identityId/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/memberships/$membershipId/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/memberships/$membershipId/index.tsx", + "parent": "/_authenticate/_org_details/_org-layout" + }, + "/_authenticate/_org_details/_org-layout/organization/$organizationId/roles/$roleId/": { + "filePath": "_authenticate/_org_details/organization/$organizationId/roles/$roleId/index.tsx", "parent": "/_authenticate/_org_details/_org-layout" } } diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/admin/-components/OrgAdminProjects/OrgAdminProjects.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/admin/-components/OrgAdminProjects/OrgAdminProjects.tsx new file mode 100644 index 000000000..164c37a18 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/admin/-components/OrgAdminProjects/OrgAdminProjects.tsx @@ -0,0 +1,168 @@ +import { useState } from "react"; +import { faEllipsis, faMagnifyingGlass, faSignIn } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; +import { motion } from "framer-motion"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + Input, + Pagination, + Spinner, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { + OrgPermissionAdminConsoleAction, + OrgPermissionSubjects +} from "@app/context/OrgPermissionContext/types"; +import { withPermission } from "@app/hoc"; +import { useDebounce } from "@app/hooks"; +import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api"; +import { useNavigate } from "@tanstack/react-router"; +import { ProjectType } from "@app/hooks/api/workspace/types"; + +export const OrgAdminProjects = withPermission( + () => { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const [debouncedSearch] = useDebounce(search); + const [perPage, setPerPage] = useState(25); + const navigate = useNavigate(); + const orgAdminAccessProject = useOrgAdminAccessProject(); + + const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({ + offset: (page - 1) * perPage, + limit: perPage, + search: debouncedSearch || undefined + }); + + const projects = data?.projects || []; + const projectCount = data?.count || 0; + const isEmpty = !isProjectsLoading && projects.length === 0; + + const handleAccessProject = async (type: ProjectType, projectId: string) => { + try { + await orgAdminAccessProject.mutateAsync({ + projectId + }); + await navigate({ + to: `/${type}/$projectId/secrets/overview` as const, + params: { + projectId + } + }); + } catch { + createNotification({ + text: "Failed to access project", + type: "error" + }); + } + }; + + return ( + +
+
+

Projects

+
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search by project name" + /> + + + + + + + + + + + {isProjectsLoading && } + {!isProjectsLoading && + projects?.map(({ name, slug, createdAt, type, id }) => ( + + + + + + + ))} + +
NameSlugCreated At +
{name}{slug}{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")} +
+ + + + + + { + e.stopPropagation(); + e.preventDefault(); + handleAccessProject(type, id); + }} + icon={} + disabled={ + orgAdminAccessProject.variables?.projectId === id && + orgAdminAccessProject.isLoading + } + > + Access{" "} + {orgAdminAccessProject.variables?.projectId === id && + orgAdminAccessProject.isLoading && } + + + +
+
+ {!isProjectsLoading && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {isEmpty && } +
+
+
+
+ ); + }, + { + action: OrgPermissionAdminConsoleAction.AccessAllProjects, + subject: OrgPermissionSubjects.AdminConsole + } +); diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/admin/-components/OrgAdminProjects/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/admin/-components/OrgAdminProjects/index.tsx new file mode 100644 index 000000000..b331589a4 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/admin/-components/OrgAdminProjects/index.tsx @@ -0,0 +1 @@ +export { OrgAdminProjects } from "./OrgAdminProjects"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/admin/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/admin/index.tsx new file mode 100644 index 000000000..fd2868421 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/admin/index.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react' +import { Helmet } from 'react-helmet' +import { useTranslation } from 'react-i18next' +import { createFileRoute } from '@tanstack/react-router' + +import { Tab, TabList, TabPanel, Tabs } from '@app/components/v2' + +import { OrgAdminProjects } from './-components/OrgAdminProjects' + +enum TabSections { + Projects = 'projects', +} + +const OrgAdminPage = () => { + const { t } = useTranslation() + const [activeTab, setActiveTab] = useState(TabSections.Projects) + return ( + <> + + + {t('common.head-title', { title: t('settings.org.title') })} + + + +
+
+
+

+ Organization Admin Console +

+
+ setActiveTab(el as TabSections)} + > + + Projects + + + + + +
+
+ + ) +} + +export const Route = createFileRoute( + '/_authenticate/_org_details/_org-layout/organization/$organizationId/admin/', +)({ + component: OrgAdminPage, +}) diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsFilter.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsFilter.tsx new file mode 100644 index 000000000..1c644b821 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsFilter.tsx @@ -0,0 +1,325 @@ +/* eslint-disable no-nested-ternary */ +import { useEffect, useState } from "react"; +import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form"; +import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { + Button, + DatePicker, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + FilterableSelect, + FormControl, + Select, + SelectItem +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/api"; +import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants"; +import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums"; +import { Actor } from "@app/hooks/api/auditLogs/types"; + +import { AuditLogFilterFormData } from "./types"; + +const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value })); +const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({ + label, + value +})); + +type Props = { + presets?: { + actorId?: string; + eventType?: EventType[]; + }; + className?: string; + isOrgAuditLogs?: boolean; + setValue: UseFormSetValue; + control: Control; + reset: UseFormReset; + watch: UseFormWatch; +}; + +export const LogsFilter = ({ + presets, + isOrgAuditLogs, + className, + control, + setValue, + reset, + watch +}: Props) => { + const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); + const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false); + + const { data: workspaces = [] } = useGetUserWorkspaces(); + const { currentOrg } = useOrganization(); + + const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id); + + const { data, isLoading } = useGetAuditLogActorFilterOpts(workspaces?.[0]?.id ?? ""); + + useEffect(() => { + if (workspacesInOrg.length) { + setValue("project", workspacesInOrg[0]); + } + }, [workspaces]); + + const renderActorSelectItem = (actor: Actor) => { + switch (actor.type) { + case ActorType.USER: + return ( + + {actor.metadata.email} + + ); + case ActorType.SERVICE: + return ( + + {actor.metadata.name} + + ); + case ActorType.IDENTITY: + return ( + + {actor.metadata.name} + + ); + default: + return ( + + N/A + + ); + } + }; + + const selectedEventTypes = watch("eventType") as EventType[] | undefined; + + return ( +
+ {isOrgAuditLogs && workspacesInOrg.length > 0 && ( + ( + + ({ name, id }))} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + + )} + /> + )} +
+ ( + + + +
+ {selectedEventTypes?.length === 1 + ? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0]) + ?.label + : selectedEventTypes?.length === 0 + ? "All events" + : `${selectedEventTypes?.length} events selected`} + +
+
+ +
+ {eventTypes && eventTypes.length > 0 ? ( + eventTypes.map((eventType) => { + const isSelected = selectedEventTypes?.includes( + eventType.value as EventType + ); + + return ( + eventTypes.length > 1 && event.preventDefault()} + onClick={() => { + if (selectedEventTypes?.includes(eventType.value as EventType)) { + field.onChange( + selectedEventTypes?.filter((e: string) => e !== eventType.value) + ); + } else { + field.onChange([...(selectedEventTypes || []), eventType.value]); + } + }} + key={`event-type-${eventType.value}`} + icon={ + isSelected ? ( + + ) : ( +
+ ) + } + iconPos="left" + className="w-[28.4rem] text-sm" + > + {eventType.label} + + ); + }) + ) : ( +
+ )} +
+ + + + )} + /> + + {!isLoading && data && data.length > 0 && !presets?.actorId && ( + ( + + + + )} + /> + )} + ( + + + + )} + /> + { + return ( + + + + ); + }} + /> + { + return ( + + + + ); + }} + /> + +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsSection.tsx new file mode 100644 index 000000000..4ee895649 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsSection.tsx @@ -0,0 +1,115 @@ +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { UpgradePlanModal } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; +import { withPermission } from "@app/hoc"; +import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { LogsFilter } from "./LogsFilter"; +import { LogsTable, TAuditLogTableHeader } from "./LogsTable"; +import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types"; + +type Props = { + presets?: { + actorId?: string; + eventType?: EventType[]; + actorType?: ActorType; + startDate?: Date; + endDate?: Date; + eventMetadata?: Record; + }; + + showFilters?: boolean; + filterClassName?: string; + isOrgAuditLogs?: boolean; + showActorColumn?: boolean; + remappedHeaders?: Partial>; + refetchInterval?: number; +}; + +export const LogsSection = withPermission( + ({ + presets, + filterClassName, + remappedHeaders, + isOrgAuditLogs, + showActorColumn, + refetchInterval, + showFilters + }: Props) => { + const { subscription } = useSubscription(); + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); + + const { control, reset, watch, setValue } = useForm({ + resolver: zodResolver(auditLogFilterFormSchema), + defaultValues: { + project: null, + actor: presets?.actorId, + eventType: presets?.eventType || [], + page: 1, + perPage: 10, + startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)), // day before today + endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) // end of today + } + }); + + useEffect(() => { + if (subscription && !subscription.auditLogs) { + handlePopUpOpen("upgradePlan"); + } + }, [subscription]); + + const eventType = watch("eventType") as EventType[] | undefined; + const userAgentType = watch("userAgentType") as UserAgentType | undefined; + const actor = watch("actor"); + const projectId = watch("project")?.id; + + const startDate = watch("startDate"); + const endDate = watch("endDate"); + + return ( +
+ {showFilters && ( + + )} + + { + handlePopUpToggle("upgradePlan", isOpen); + }} + text="You can use audit logs if you switch to a paid Infisical plan." + /> +
+ ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.AuditLogs } +); diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsTable.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsTable.tsx new file mode 100644 index 000000000..350b477dd --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsTable.tsx @@ -0,0 +1,119 @@ +import { Fragment } from "react"; +import { faFile } from "@fortawesome/free-solid-svg-icons"; + +import { + Button, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useGetAuditLogs } from "@app/hooks/api"; +import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types"; + +import { LogsTableRow } from "./LogsTableRow"; + +type Props = { + isOrgAuditLogs?: boolean; + showActorColumn: boolean; + filter?: TGetAuditLogsFilter; + remappedHeaders?: Partial>; + refetchInterval?: number; +}; + +const AUDIT_LOG_LIMIT = 15; + +const TABLE_HEADERS = ["Timestamp", "Event", "Project", "Actor", "Source", "Metadata"] as const; +export type TAuditLogTableHeader = (typeof TABLE_HEADERS)[number]; + +export const LogsTable = ({ + showActorColumn, + isOrgAuditLogs, + filter, + remappedHeaders, + refetchInterval +}: Props) => { + // TODO(rbr): check this again, there was a filter with current workspace id + // Determine the project ID for filtering + const filterProjectId = + // Use the projectId from the filter if it exists + filter?.projectId || null; + + const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs( + { + ...filter, + limit: AUDIT_LOG_LIMIT + }, + filterProjectId, + { + refetchInterval + } + ); + + const isEmpty = !isLoading && !data?.pages?.[0].length; + + return ( +
+ + + + + {TABLE_HEADERS.map((header, idx) => { + if ( + (header === "Project" && !isOrgAuditLogs) || + (header === "Actor" && !showActorColumn) + ) { + return null; + } + + return ( + + ); + })} + + + + {!isLoading && + data?.pages?.map((group, i) => ( + + {group.map((auditLog) => ( + + ))} + + ))} + {isLoading && } + {isEmpty && ( + + + + )} + +
{remappedHeaders?.[header] || header}
+ +
+
+ {!isEmpty && ( + + )} +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsTableRow.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsTableRow.tsx new file mode 100644 index 000000000..4da98613e --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/LogsTableRow.tsx @@ -0,0 +1,101 @@ +import { Td, Tr } from "@app/components/v2"; +import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants"; +import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums"; +import { Actor, AuditLog } from "@app/hooks/api/auditLogs/types"; + +type Props = { + auditLog: AuditLog; + isOrgAuditLogs?: boolean; + showActorColumn: boolean; +}; + +export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => { + const renderActor = (actor: Actor) => { + if (!actor) { + return ; + } + + switch (actor.type) { + case ActorType.USER: + return ( + +

{actor.metadata.email}

+

User

+ + ); + case ActorType.SERVICE: + return ( + +

{`${actor.metadata.name}`}

+

Service token

+ + ); + case ActorType.IDENTITY: + return ( + +

{`${actor.metadata.name}`}

+

Machine Identity

+ + ); + default: + return ; + } + }; + + const formatDate = (dateToFormat: string) => { + const date = new Date(dateToFormat); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + + let hours = date.getHours(); + const minutes = String(date.getMinutes()).padStart(2, "0"); + + // convert from 24h to 12h format + const period = hours >= 12 ? "PM" : "AM"; + hours %= 12; + hours = hours || 12; // the hour '0' should be '12' + + const formattedDate = `${day}-${month}-${year} at ${hours}:${minutes} ${period}`; + return formattedDate; + }; + + const renderSource = () => { + const { event, actor } = auditLog; + + if (event.type === EventType.INTEGRATION_SYNCED) { + if (actor.type === ActorType.USER) { + return ( + +

Manually triggered by {actor.metadata.email}

+ + ); + } + + // Platform / automatic syncs + return ( + +

Automatically synced by Infisical

+ + ); + } + + return ( + +

{userAgentTTypeoNameMap[auditLog.userAgentType]}

+

{auditLog.ipAddress}

+ + ); + }; + + return ( + + {formatDate(auditLog.createdAt)} + {`${eventToNameMap[auditLog.event.type]}`} + {isOrgAuditLogs && {auditLog?.projectName ?? auditLog?.projectId ?? "N/A"}} + {showActorColumn && renderActor(auditLog.actor)} + {renderSource()} + {JSON.stringify(auditLog.event.metadata || {})} + + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/index.tsx new file mode 100644 index 000000000..e15301e9b --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/index.tsx @@ -0,0 +1 @@ +export { LogsSection } from "./LogsSection"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/types.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/types.tsx new file mode 100644 index 000000000..c1c1ef921 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/-components/types.tsx @@ -0,0 +1,37 @@ +import { z } from "zod"; + +import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; + +// TODO(rbr): test this schema +export const auditLogFilterFormSchema = z + .object({ + eventMetadata: z.object({}).optional(), + project: z.object({ id: z.string(), name: z.string() }).optional().nullable(), + eventType: z.nativeEnum(EventType).array(), + actor: z.string().optional(), + userAgentType: z.nativeEnum(UserAgentType), + startDate: z.date().optional(), + endDate: z.date().optional(), + page: z.coerce.number().optional(), + perPage: z.coerce.number().optional() + }) + .superRefine((el, ctx) => { + if (el.endDate && el.startDate && el.endDate < el.startDate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["endDate"], + message: "End date cannot be before start date" + }); + } + }); + +export type AuditLogFilterFormData = z.infer; + +export type SetValueType = ( + name: keyof AuditLogFilterFormData, + value: any, + options?: { + shouldValidate?: boolean; + shouldDirty?: boolean; + } +) => void; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/index.tsx new file mode 100644 index 000000000..229bb9717 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/audit-logs/index.tsx @@ -0,0 +1,31 @@ +import { Helmet } from "react-helmet"; +import { createFileRoute } from "@tanstack/react-router"; + +import { LogsSection } from "./-components"; + +const AuditLogsPage = () => { + return ( +
+ + Infisical | Audit Logs + + + +
+
+
+

Audit Logs

+
+
+ +
+
+
+ ); +}; + +export const Route = createFileRoute( + "/_authenticate/_org_details/_org-layout/organization/$organizationId/audit-logs/" +)({ + component: AuditLogsPage +}); diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/BillingCloudTab.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/BillingCloudTab.tsx new file mode 100644 index 000000000..6085b90c6 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/BillingCloudTab.tsx @@ -0,0 +1,11 @@ +import { CurrentPlanSection } from "./CurrentPlanSection"; +import { PreviewSection } from "./PreviewSection"; + +export const BillingCloudTab = () => { + return ( +
+ + +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/CurrentPlanSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/CurrentPlanSection.tsx new file mode 100644 index 000000000..a3135d85f --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/CurrentPlanSection.tsx @@ -0,0 +1,72 @@ +import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useGetOrgPlanTable } from "@app/hooks/api"; + +export const CurrentPlanSection = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgPlanTable(currentOrg?.id ?? ""); + + const displayCell = (value: null | number | string | boolean) => { + if (value === null) return "-"; + + if (typeof value === "boolean") { + if (value) return ; + + return ; + } + + return value; + }; + + return ( +
+

Current usage

+ + + + + + + + + + + {!isLoading && + data && + data?.rows?.length > 0 && + data.rows.map(({ name, allowed, used }) => { + return ( + + + + + + ); + })} + {isLoading && } + {!isLoading && data && data?.rows?.length === 0 && ( + + + + )} + +
FeatureAllowedUsed
{name}{displayCell(allowed)}{used}
+ +
+
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/ManagePlansModal.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/ManagePlansModal.tsx new file mode 100644 index 000000000..453404d35 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/ManagePlansModal.tsx @@ -0,0 +1,62 @@ +import { Fragment } from "react"; +import { Tab } from "@headlessui/react"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { ManagePlansTable } from "./ManagePlansTable"; + +type Props = { + popUp: UsePopUpState<["managePlan"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["managePlan"]>, state?: boolean) => void; +}; + +export const ManagePlansModal = ({ popUp, handlePopUpToggle }: Props) => { + return ( + { + handlePopUpToggle("managePlan", isOpen); + }} + > + + + + + {({ selected }) => ( + + )} + + + {({ selected }) => ( + + )} + + + + + + + + + + + + + + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/ManagePlansTable.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/ManagePlansTable.tsx new file mode 100644 index 000000000..b37c22012 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/ManagePlansTable.tsx @@ -0,0 +1,138 @@ +import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { useCreateCustomerPortalSession, useGetOrgPlansTable } from "@app/hooks/api"; + +type Props = { + billingCycle: "monthly" | "yearly"; +}; + +export const ManagePlansTable = ({ billingCycle }: Props) => { + const { currentOrg } = useOrganization(); + const { subscription } = useSubscription(); + const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({ + organizationId: currentOrg?.id ?? "", + billingCycle + }); + const createCustomerPortalSession = useCreateCustomerPortalSession(); + + const displayCell = (value: null | number | string | boolean) => { + if (value === null) return "Unlimited"; + + if (typeof value === "boolean") { + if (value) return ; + + return ; + } + + return value; + }; + + return ( + + + + {subscription && !isTableDataLoading && tableData && ( + + + {tableData.head.map(({ name, priceLine }) => { + return ( + + ); + })} + + )} + + + {subscription && + !isTableDataLoading && + tableData && + tableData.rows.map(({ name, starter, pro, enterprise }) => { + return ( + + + + + + + ); + })} + {isTableDataLoading && } + {!isTableDataLoading && tableData?.rows.length === 0 && ( + + + + )} + {subscription && !isTableDataLoading && tableData && ( + + + ) : ( + + ); + })} + + )} + +
Feature / Limit +

{name}

+

{priceLine}

+
{displayCell(name)}{displayCell(starter)}{displayCell(pro)}{displayCell(enterprise)}
+ +
+ {tableData.head.map(({ slug, tier }) => { + const isCurrentPlan = slug === subscription.slug; + let subscriptionText = "Upgrade"; + + if (subscription.tier > tier) { + subscriptionText = "Downgrade"; + } + + if (tier === 3) { + subscriptionText = "Contact sales"; + } + + return isCurrentPlan ? ( + + + + +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/PreviewSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/PreviewSection.tsx new file mode 100644 index 000000000..a7c4cb443 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/PreviewSection.tsx @@ -0,0 +1,172 @@ +import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button } from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useSubscription +} from "@app/context"; +import { + useCreateCustomerPortalSession, + useGetOrgPlanBillingInfo, + useGetOrgTrialUrl +} from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { ManagePlansModal } from "./ManagePlansModal"; + +export const PreviewSection = () => { + const { currentOrg } = useOrganization(); + const { subscription } = useSubscription(); + const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?.id ?? ""); + const getOrgTrialUrl = useGetOrgTrialUrl(); + const createCustomerPortalSession = useCreateCustomerPortalSession(); + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["managePlan"] as const); + + const formatAmount = (amount: number) => { + const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", { + style: "currency", + currency: "USD" + }); + + return formattedTotal; + }; + + const formatDate = (date: number) => { + const createdDate = new Date(date * 1000); + const day: number = createdDate.getDate(); + const month: number = createdDate.getMonth() + 1; + const year: number = createdDate.getFullYear(); + const formattedDate: string = `${day}/${month}/${year}`; + + return formattedDate; + }; + + function formatPlanSlug(slug: string) { + return slug.replace(/(\b[a-z])/g, (match) => match.toUpperCase()).replace(/-/g, " "); + } + + const handleUpgradeBtnClick = async () => { + try { + if (!subscription || !currentOrg) return; + + if (!subscription.has_used_trial) { + // direct user to start pro trial + const url = await getOrgTrialUrl.mutateAsync({ + orgId: currentOrg.id, + success_url: window.location.href + }); + + window.location.href = url; + } else { + // open compare plans modal + handlePopUpOpen("managePlan"); + } + } catch (err) { + console.error(err); + } + }; + + return ( +
+ {subscription && + subscription?.slug !== "enterprise" && + subscription?.slug !== "pro" && + subscription?.slug !== "pro-annual" && ( +
+
+
+

+ Unleash the full power of{" "} + + Infisical + +

+

+ Get unlimited members, projects, RBAC, smart alerts, and so much more. +

+
+ + {(isAllowed) => ( + + )} + +
+
+
+ Want to learn more?{" "} +
+ +
+
+ )} + {!isLoading && subscription && data && ( +
+
+

Current plan

+

+ {`${formatPlanSlug(subscription.slug)} ${ + subscription.status === "trialing" ? "(Trial)" : "" + }`} +

+ + {(isAllowed) => ( + + )} + +
+
+

Price

+

+ {subscription.status === "trialing" + ? "$0.00 / month" + : `${formatAmount(data.amount)} / ${data.interval}`} +

+
+
+

Subscription renews on

+

+ {formatDate(data.currentPeriodEnd)} +

+
+
+ )} + +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/index.tsx new file mode 100644 index 000000000..341251e99 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingCloudTab/index.tsx @@ -0,0 +1 @@ +export { BillingCloudTab } from "./BillingCloudTab"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/BillingDetailsTab.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/BillingDetailsTab.tsx new file mode 100644 index 000000000..f422fe046 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/BillingDetailsTab.tsx @@ -0,0 +1,15 @@ +import { CompanyNameSection } from "./CompanyNameSection"; +import { InvoiceEmailSection } from "./InvoiceEmailSection"; +import { PmtMethodsSection } from "./PmtMethodsSection"; +import { TaxIDSection } from "./TaxIDSection"; + +export const BillingDetailsTab = () => { + return ( + <> + + + + + + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/CompanyNameSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/CompanyNameSection.tsx new file mode 100644 index 000000000..10df8e43a --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/CompanyNameSection.tsx @@ -0,0 +1,91 @@ +import { useEffect } 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 { OrgPermissionCan } from "@app/components/permissions"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api"; + +const schema = z + .object({ + name: z.string() + }) + .required(); + +export const CompanyNameSection = () => { + const { currentOrg } = useOrganization(); + const { reset, control, handleSubmit } = useForm({ + defaultValues: { + name: "" + }, + resolver: zodResolver(schema) + }); + const { data } = useGetOrgBillingDetails(currentOrg?.id ?? ""); + const { mutateAsync, isLoading } = useUpdateOrgBillingDetails(); + + useEffect(() => { + if (data) { + reset({ + name: data?.name ?? "" + }); + } + }, [data]); + + const onFormSubmit = async ({ name }: { name: string }) => { + try { + if (!currentOrg?.id) return; + if (name === "") return; + await mutateAsync({ + name, + organizationId: currentOrg.id + }); + + createNotification({ + text: "Successfully updated business name", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to update business name", + type: "error" + }); + } + }; + + return ( +
+

Business name

+
+ ( + + + + )} + control={control} + name="name" + /> +
+ + {(isAllowed) => ( + + )} + +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/InvoiceEmailSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/InvoiceEmailSection.tsx new file mode 100644 index 000000000..951c2bc33 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/InvoiceEmailSection.tsx @@ -0,0 +1,92 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api"; + +const schema = z + .object({ + email: z.string() + }) + .required(); + +export const InvoiceEmailSection = () => { + const { currentOrg } = useOrganization(); + const { reset, control, handleSubmit } = useForm({ + defaultValues: { + email: "" + }, + resolver: yupResolver(schema) + }); + const { data } = useGetOrgBillingDetails(currentOrg?.id ?? ""); + const { mutateAsync, isLoading } = useUpdateOrgBillingDetails(); + + useEffect(() => { + if (data) { + reset({ + email: data?.email ?? "" + }); + } + }, [data]); + + const onFormSubmit = async ({ email }: { email: string }) => { + try { + if (!currentOrg?.id) return; + if (email === "") return; + + await mutateAsync({ + email, + organizationId: currentOrg.id + }); + + createNotification({ + text: "Successfully updated invoice email recipient", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to update invoice email recipient", + type: "error" + }); + } + }; + + return ( +
+

Invoice email recipient

+
+ ( + + + + )} + control={control} + name="email" + /> +
+ + {(isAllowed) => ( + + )} + +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/PmtMethodsSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/PmtMethodsSection.tsx new file mode 100644 index 000000000..266e52999 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/PmtMethodsSection.tsx @@ -0,0 +1,47 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { useAddOrgPmtMethod } from "@app/hooks/api"; + +import { PmtMethodsTable } from "./PmtMethodsTable"; + +export const PmtMethodsSection = () => { + const { currentOrg } = useOrganization(); + const { mutateAsync, isLoading } = useAddOrgPmtMethod(); + + const handleAddPmtMethodBtnClick = async () => { + if (!currentOrg?.id) return; + const url = await mutateAsync({ + organizationId: currentOrg.id, + success_url: window.location.href, + cancel_url: window.location.href + }); + + window.location.href = url; + }; + + return ( +
+
+

Payment methods

+ + {(isAllowed) => ( + + )} + +
+ +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/PmtMethodsTable.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/PmtMethodsTable.tsx new file mode 100644 index 000000000..8dfe7e605 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/PmtMethodsTable.tsx @@ -0,0 +1,117 @@ +import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + DeleteActionModal, + EmptyState, + IconButton, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api"; + +export const PmtMethodsTable = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgPmtMethods(currentOrg?.id ?? ""); + const deleteOrgPmtMethod = useDeleteOrgPmtMethod(); + const { handlePopUpOpen, handlePopUpClose, handlePopUpToggle, popUp } = usePopUp([ + "removeCard" + ] as const); + + const pmtMethodToRemove = popUp.removeCard.data as { id: string; last4: string } | undefined; + + const handleDeletePmtMethodBtnClick = async () => { + if (!currentOrg?.id || !pmtMethodToRemove) return; + try { + await deleteOrgPmtMethod.mutateAsync({ + organizationId: currentOrg.id, + pmtMethodId: pmtMethodToRemove.id + }); + createNotification({ + type: "success", + text: "Successfully removed payment method" + }); + handlePopUpClose("removeCard"); + } catch (error: any) { + createNotification({ + type: "error", + text: error.message ?? "Error removing payment method" + }); + } + }; + + return ( + <> + + + + + + + + + + + + {!isLoading && + data && + data?.length > 0 && + data.map(({ _id: id, brand, exp_month, exp_year, funding, last4 }) => ( + + + + + + + + ))} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
BrandTypeLast 4 DigitsExpiration +
{brand.charAt(0).toUpperCase() + brand.slice(1)}{funding.charAt(0).toUpperCase() + funding.slice(1)}{last4}{`${exp_month}/${exp_year}`} + + {(isAllowed) => ( + handlePopUpOpen("removeCard", { id, last4 })} + size="lg" + isDisabled={!isAllowed} + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + + )} + +
+ +
+
+ handlePopUpToggle("removeCard", isOpen)} + title={`Remove payment method ending in *${pmtMethodToRemove?.last4}?`} + onDeleteApproved={handleDeletePmtMethodBtnClick} + /> + + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/TaxIDModal.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/TaxIDModal.tsx new file mode 100644 index 000000000..3d0e889f5 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/TaxIDModal.tsx @@ -0,0 +1,182 @@ +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, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useAddOrgTaxId } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const taxIDTypes = [ + { label: "Australia ABN", value: "au_abn" }, + { label: "Australia ARN", value: "au_arn" }, + { label: "Bulgaria UIC", value: "bg_uic" }, + { label: "Brazil CNPJ", value: "br_cnpj" }, + { label: "Brazil CPF", value: "br_cpf" }, + { label: "Canada BN", value: "ca_bn" }, + { label: "Canada GST/HST", value: "ca_gst_hst" }, + { label: "Canada PST BC", value: "ca_pst_bc" }, + { label: "Canada PST MB", value: "ca_pst_mb" }, + { label: "Canada PST SK", value: "ca_pst_sk" }, + { label: "Canada QST", value: "ca_qst" }, + { label: "Switzerland VAT", value: "ch_vat" }, + { label: "Chile TIN", value: "cl_tin" }, + { label: "Egypt TIN", value: "eg_tin" }, + { label: "Spain CIF", value: "es_cif" }, + { label: "EU OSS VAT", value: "eu_oss_vat" }, + { label: "EU VAT", value: "eu_vat" }, + { label: "GB VAT", value: "gb_vat" }, + { label: "Georgia VAT", value: "ge_vat" }, + { label: "Hong Kong BR", value: "hk_br" }, + { label: "Hungary TIN", value: "hu_tin" }, + { label: "Indonesia NPWP", value: "id_npwp" }, + { label: "Israel VAT", value: "il_vat" }, + { label: "India GST", value: "in_gst" }, + { label: "Iceland VAT", value: "is_vat" }, + { label: "Japan CN", value: "jp_cn" }, + { label: "Japan RN", value: "jp_rn" }, + { label: "Japan TRN", value: "jp_trn" }, + { label: "Kenya PIN", value: "ke_pin" }, + { label: "South Korea BRN", value: "kr_brn" }, + { label: "Liechtenstein UID", value: "li_uid" }, + { label: "Mexico RFC", value: "mx_rfc" }, + { label: "Malaysia FRP", value: "my_frp" }, + { label: "Malaysia ITN", value: "my_itn" }, + { label: "Malaysia SST", value: "my_sst" }, + { label: "Norway VAT", value: "no_vat" }, + { label: "New Zealand GST", value: "nz_gst" }, + { label: "Philippines TIN", value: "ph_tin" }, + { label: "Russia INN", value: "ru_inn" }, + { label: "Russia KPP", value: "ru_kpp" }, + { label: "Saudi Arabia VAT", value: "sa_vat" }, + { label: "Singapore GST", value: "sg_gst" }, + { label: "Singapore UEN", value: "sg_uen" }, + { label: "Slovenia TIN", value: "si_tin" }, + { label: "Thailand VAT", value: "th_vat" }, + { label: "Turkey TIN", value: "tr_tin" }, + { label: "Taiwan VAT", value: "tw_vat" }, + { label: "Ukraine VAT", value: "ua_vat" }, + { label: "US EIN", value: "us_ein" }, + { label: "South Africa VAT", value: "za_vat" } +]; + +const schema = z + .object({ + type: z.string(), + value: z.string() + }) + .required(); + +export type AddTaxIDFormData = z.infer; + +type Props = { + popUp: UsePopUpState<["addTaxID"]>; + handlePopUpClose: (popUpName: keyof UsePopUpState<["addTaxID"]>) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["addTaxID"]>, state?: boolean) => void; +}; + +export const TaxIDModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { + const { currentOrg } = useOrganization(); + const addOrgTaxId = useAddOrgTaxId(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema) + }); + + const onTaxIDModalSubmit = async ({ type, value }: AddTaxIDFormData) => { + try { + if (!currentOrg?.id) return; + await addOrgTaxId.mutateAsync({ + organizationId: currentOrg.id, + type, + value + }); + + createNotification({ + text: "Successfully added Tax ID", + type: "success" + }); + handlePopUpClose("addTaxID"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to add Tax ID", + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("addTaxID", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/TaxIDSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/TaxIDSection.tsx new file mode 100644 index 000000000..ecb876aa5 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/TaxIDSection.tsx @@ -0,0 +1,42 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { TaxIDModal } from "./TaxIDModal"; +import { TaxIDTable } from "./TaxIDTable"; + +export const TaxIDSection = () => { + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "addTaxID" + ] as const); + + return ( +
+
+

Tax ID

+ + {(isAllowed) => ( + + )} + +
+ + +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/TaxIDTable.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/TaxIDTable.tsx new file mode 100644 index 000000000..7a6362da2 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/TaxIDTable.tsx @@ -0,0 +1,139 @@ +import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { + EmptyState, + IconButton, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api"; + +const taxIDTypeLabelMap: { [key: string]: string } = { + au_abn: "Australia ABN", + au_arn: "Australia ARN", + bg_uic: "Bulgaria UIC", + br_cnpj: "Brazil CNPJ", + br_cpf: "Brazil CPF", + ca_bn: "Canada BN", + ca_gst_hst: "Canada GST/HST", + ca_pst_bc: "Canada PST BC", + ca_pst_mb: "Canada PST MB", + ca_pst_sk: "Canada PST SK", + ca_qst: "Canada QST", + ch_vat: "Switzerland VAT", + cl_tin: "Chile TIN", + eg_tin: "Egypt TIN", + es_cif: "Spain CIF", + eu_oss_vat: "EU OSS VAT", + eu_vat: "EU VAT", + gb_vat: "GB VAT", + ge_vat: "Georgia VAT", + hk_br: "Hong Kong BR", + hu_tin: "Hungary TIN", + id_npwp: "Indonesia NPWP", + il_vat: "Israel VAT", + in_gst: "India GST", + is_vat: "Iceland VAT", + jp_cn: "Japan CN", + jp_rn: "Japan RN", + jp_trn: "Japan TRN", + ke_pin: "Kenya PIN", + kr_brn: "South Korea BRN", + li_uid: "Liechtenstein UID", + mx_rfc: "Mexico RFC", + my_frp: "Malaysia FRP", + my_itn: "Malaysia ITN", + my_sst: "Malaysia SST", + no_vat: "Norway VAT", + nz_gst: "New Zealand GST", + ph_tin: "Philippines TIN", + ru_inn: "Russia INN", + ru_kpp: "Russia KPP", + sa_vat: "Saudi Arabia VAT", + sg_gst: "Singapore GST", + sg_uen: "Singapore UEN", + si_tin: "Slovenia TIN", + th_vat: "Thailand VAT", + tr_tin: "Turkey TIN", + tw_vat: "Taiwan VAT", + ua_vat: "Ukraine VAT", + us_ein: "US EIN", + za_vat: "South Africa VAT" +}; + +export const TaxIDTable = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgTaxIds(currentOrg?.id ?? ""); + const deleteOrgTaxId = useDeleteOrgTaxId(); + + const handleDeleteTaxIdBtnClick = async (taxId: string) => { + if (!currentOrg?.id) return; + await deleteOrgTaxId.mutateAsync({ + organizationId: currentOrg.id, + taxId + }); + }; + + return ( + + + + + + + + + + {!isLoading && + data && + data?.length > 0 && + data.map(({ id, type, value }) => ( + + + + + + ))} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
TypeValue +
{taxIDTypeLabelMap[type]}{value} + + {(isAllowed) => ( + { + await handleDeleteTaxIdBtnClick(id); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + isDisabled={!isAllowed} + > + + + )} + +
+ +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/index.tsx new file mode 100644 index 000000000..346c0f7d3 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingDetailsTab/index.tsx @@ -0,0 +1 @@ +export { BillingDetailsTab } from "./BillingDetailsTab"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingReceiptsTab/BillingReceiptsTab.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingReceiptsTab/BillingReceiptsTab.tsx new file mode 100644 index 000000000..0ae6d2c77 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingReceiptsTab/BillingReceiptsTab.tsx @@ -0,0 +1,10 @@ +import { InvoicesTable } from "./InvoicesTable"; + +export const BillingReceiptsTab = () => { + return ( +
+

Invoices

+ +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingReceiptsTab/InvoicesTable.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingReceiptsTab/InvoicesTable.tsx new file mode 100644 index 000000000..bc6032a67 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingReceiptsTab/InvoicesTable.tsx @@ -0,0 +1,80 @@ +import { faDownload, faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + EmptyState, + IconButton, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useGetOrgInvoices } from "@app/hooks/api"; + +export const InvoicesTable = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgInvoices(currentOrg?.id ?? ""); + return ( + + + + + + + + + + + + {!isLoading && + data && + data?.length > 0 && + data.map(({ id, created, paid, number, total, invoice_pdf }) => { + const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", { + style: "currency", + currency: "USD" + }); + const createdDate = new Date(created * 1000); + const day: number = createdDate.getDate(); + const month: number = createdDate.getMonth() + 1; + const year: number = createdDate.getFullYear(); + const formattedDate: string = `${day}/${month}/${year}`; + + return ( + + + + + + + + ); + })} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
Invoice #DateStatusAmount +
{number}{formattedDate}{paid ? "Paid" : "Not Paid"}{formattedTotal} + window.open(invoice_pdf)} + size="lg" + variant="plain" + ariaLabel="update" + > + + +
+ +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingReceiptsTab/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingReceiptsTab/index.tsx new file mode 100644 index 000000000..eec019952 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingReceiptsTab/index.tsx @@ -0,0 +1 @@ +export { BillingReceiptsTab } from "./BillingReceiptsTab"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingSelfHostedTab/BillingSelfHostedTab.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingSelfHostedTab/BillingSelfHostedTab.tsx new file mode 100644 index 000000000..c5a8d8f94 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingSelfHostedTab/BillingSelfHostedTab.tsx @@ -0,0 +1,9 @@ +import { LicensesSection } from "./LicensesSection"; + +export const BillingSelfHostedTab = () => { + return ( +
+ +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingSelfHostedTab/LicensesSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingSelfHostedTab/LicensesSection.tsx new file mode 100644 index 000000000..919d8eabb --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingSelfHostedTab/LicensesSection.tsx @@ -0,0 +1,63 @@ +import { faFileContract } from "@fortawesome/free-solid-svg-icons"; + +import { + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useGetOrgLicenses } from "@app/hooks/api"; + +export const LicensesSection = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgLicenses(currentOrg?.id ?? ""); + + return ( +
+

Enterprise licenses

+ + + + + + + + + + + + {!isLoading && + data && + data?.length > 0 && + data.map(({ id, licenseKey, isActivated, createdAt, expiresAt }) => { + const formattedCreatedAt = new Date(createdAt).toISOString().split("T")[0]; + const formattedExpiresAt = new Date(expiresAt).toISOString().split("T")[0]; + return ( + + + + + + + ); + })} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
License KeyStatusIssued DateExpiry Date
{licenseKey}{isActivated ? "Active" : "Inactive"}{formattedCreatedAt}{formattedExpiresAt}
+ +
+
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingSelfHostedTab/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingSelfHostedTab/index.tsx new file mode 100644 index 000000000..da9282157 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingSelfHostedTab/index.tsx @@ -0,0 +1 @@ +export * from "./BillingSelfHostedTab"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingTabGroup/BillingTabGroup.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingTabGroup/BillingTabGroup.tsx new file mode 100644 index 000000000..6d8b59d2a --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingTabGroup/BillingTabGroup.tsx @@ -0,0 +1,42 @@ +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { withPermission } from "@app/hoc"; + +import { BillingCloudTab } from "../BillingCloudTab"; +import { BillingDetailsTab } from "../BillingDetailsTab"; +import { BillingReceiptsTab } from "../BillingReceiptsTab"; +import { BillingSelfHostedTab } from "../BillingSelfHostedTab"; + +const tabs = [ + { name: "Infisical Cloud", key: "tab-infisical-cloud" }, + { name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" }, + { name: "Receipts", key: "tab-receipts" }, + { name: "Billing details", key: "tab-billing-details" } +]; + +export const BillingTabGroup = withPermission( + () => { + return ( + + + {tabs.map((tab) => ( + {tab.name} + ))} + + + + + + + + + + + + + + + ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Billing } +); diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingTabGroup/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingTabGroup/index.tsx new file mode 100644 index 000000000..8ad1fb5b8 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/BillingTabGroup/index.tsx @@ -0,0 +1 @@ +export { BillingTabGroup } from "./BillingTabGroup"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/index.tsx new file mode 100644 index 000000000..8ad1fb5b8 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/-components/index.tsx @@ -0,0 +1 @@ +export { BillingTabGroup } from "./BillingTabGroup"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/index.tsx new file mode 100644 index 000000000..dc2a0de6a --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/billing/index.tsx @@ -0,0 +1,52 @@ +import { Helmet } from 'react-helmet' +import { useTranslation } from 'react-i18next' +import { createFileRoute } from '@tanstack/react-router' + +import { OrgPermissionActions, OrgPermissionSubjects } from '@app/context' +import { withPermission } from '@app/hoc' + +import { BillingTabGroup } from './-components' + +const BillngPage = withPermission( + () => { + const { t } = useTranslation() + return ( +
+
+
+

+ {t('billing.title')} +

+
+
+ +
+
+ ) + }, + { + action: OrgPermissionActions.Read, + subject: OrgPermissionSubjects.Billing, + }, +) + +const BillingRoute = () => { + const { t } = useTranslation() + + return ( +
+ + {t('common.head-title', { title: t('billing.title') })} + + + + +
+ ) +} + +export const Route = createFileRoute( + '/_authenticate/_org_details/_org-layout/organization/$organizationId/billing/', +)({ + component: BillingRoute, +}) diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/cert-manager/overview.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/cert-manager/overview.tsx new file mode 100644 index 000000000..5be0fdc59 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/cert-manager/overview.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { ProjectType } from "@app/hooks/api/workspace/types"; + +import { ProductOverview } from "../secret-manager/overview"; + +const CertManagerOverviewPage = () => ; + +export const Route = createFileRoute( + "/_authenticate/_org_details/_org-layout/organization/$organizationId/cert-manager/overview" +)({ + component: CertManagerOverviewPage +}); diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/AddGroupMemberModal.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/AddGroupMemberModal.tsx new file mode 100644 index 000000000..38cf33c2d --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/AddGroupMemberModal.tsx @@ -0,0 +1,172 @@ +import { useState } from "react"; +import { faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Button, + EmptyState, + Input, + Modal, + ModalContent, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useDebounce, useResetPageHelper } from "@app/hooks"; +import { useAddUserToGroup, useListGroupUsers } from "@app/hooks/api"; +import { EFilterReturnedUsers } from "@app/hooks/api/groups/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + popUp: UsePopUpState<["addGroupMembers"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["addGroupMembers"]>, state?: boolean) => void; +}; + +export const AddGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => { + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(10); + const [searchMemberFilter, setSearchMemberFilter] = useState(""); + const [debouncedSearch] = useDebounce(searchMemberFilter); + + const popUpData = popUp?.addGroupMembers?.data as { + groupId: string; + slug: string; + }; + + const offset = (page - 1) * perPage; + const { data, isLoading } = useListGroupUsers({ + id: popUpData?.groupId, + groupSlug: popUpData?.slug, + offset, + limit: perPage, + search: debouncedSearch, + filter: EFilterReturnedUsers.NON_MEMBERS + }); + + const { totalCount = 0 } = data ?? {}; + + useResetPageHelper({ + totalCount, + offset, + setPage + }); + + const { mutateAsync: addUserToGroupMutateAsync } = useAddUserToGroup(); + + const handleAddMember = async (username: string) => { + try { + if (!popUpData?.slug) { + createNotification({ + text: "Some data is missing, please refresh the page and try again", + type: "error" + }); + return; + } + + await addUserToGroupMutateAsync({ + groupId: popUpData.groupId, + username, + slug: popUpData.slug + }); + + createNotification({ + text: "Successfully assigned user to the group", + type: "success" + }); + } catch { + createNotification({ + text: "Failed to assign user to the group", + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("addGroupMembers", isOpen); + }} + > + + setSearchMemberFilter(e.target.value)} + leftIcon={} + placeholder="Search members..." + /> + + + + + + + + + {isLoading && } + {!isLoading && + data?.users?.map(({ id, firstName, lastName, username }) => { + return ( + + + + + ); + })} + +
User +
+

{`${firstName ?? "-"} ${lastName ?? ""}`}

+

{username}

+
+ + {(isAllowed) => { + return ( + + ); + }} + +
+ {!isLoading && totalCount > 0 && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {!isLoading && !data?.users?.length && ( + + )} +
+
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupCreateUpdateModal.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupCreateUpdateModal.tsx new file mode 100644 index 000000000..42f2c73aa --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupCreateUpdateModal.tsx @@ -0,0 +1,192 @@ +import { useEffect } 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, + FilterableSelect, + FormControl, + Input, + Modal, + ModalContent +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { findOrgMembershipRole } from "@app/helpers/roles"; +import { useCreateGroup, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const GroupFormSchema = z.object({ + name: z.string().min(1, "Name cannot be empty").max(50, "Name must be 50 characters or fewer"), + slug: z + .string() + .min(5, "Slug must be at least 5 characters long") + .max(36, "Slug must be 36 characters or fewer"), + role: z.object({ name: z.string(), slug: z.string() }) +}); + +export type TGroupFormData = z.infer; + +type Props = { + popUp: UsePopUpState<["groupCreateUpdate"]>; + handlePopUpClose: (popUpName: keyof UsePopUpState<["groupCreateUpdate"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["groupCreateUpdate"]>, + state?: boolean + ) => void; +}; + +export const GroupCreateUpdateModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { + const { currentOrg } = useOrganization(); + const { data: roles } = useGetOrgRoles(currentOrg?.id || ""); + const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateGroup(); + const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateGroup(); + + const { control, handleSubmit, reset } = useForm({ + resolver: zodResolver(GroupFormSchema) + }); + + useEffect(() => { + const group = popUp?.groupCreateUpdate?.data as { + groupId: string; + name: string; + slug: string; + role: string; + customRole: { + name: string; + slug: string; + }; + }; + + if (!roles?.length) return; + + if (group) { + reset({ + name: group.name, + slug: group.slug, + role: group?.customRole ?? findOrgMembershipRole(roles, group.role) + }); + } else { + reset({ + name: "", + slug: "", + role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole) + }); + } + }, [popUp?.groupCreateUpdate?.data, roles]); + + const onGroupModalSubmit = async ({ name, slug, role }: TGroupFormData) => { + try { + if (!currentOrg?.id) return; + + const group = popUp?.groupCreateUpdate?.data as { + groupId: string; + name: string; + slug: string; + }; + + if (group) { + await updateMutateAsync({ + id: group.groupId, + name, + slug, + role: role.slug || undefined + }); + } else { + await createMutateAsync({ + name, + slug, + organizationId: currentOrg.id, + role: role.slug || undefined + }); + } + handlePopUpToggle("groupCreateUpdate", false); + reset(); + + createNotification({ + text: `Successfully ${popUp?.groupCreateUpdate?.data ? "updated" : "created"} group`, + type: "success" + }); + } catch { + createNotification({ + text: `Failed to ${popUp?.groupCreateUpdate?.data ? "updated" : "created"} group`, + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("groupCreateUpdate", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + option.slug} + getOptionLabel={(option) => option.name} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupDetailsSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupDetailsSection.tsx new file mode 100644 index 000000000..2b4674007 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupDetailsSection.tsx @@ -0,0 +1,88 @@ +import { faPencil } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { IconButton, Spinner, Tooltip } from "@app/components/v2"; +import { CopyButton } from "@app/components/v2/CopyButton"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useGetGroupById } from "@app/hooks/api/"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + groupId: string; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["groupCreateUpdate"]>, data?: object) => void; +}; + +export const GroupDetailsSection = ({ groupId, handlePopUpOpen }: Props) => { + const { data, isLoading } = useGetGroupById(groupId); + + if (isLoading) return ; + + return data ? ( +
+
+

Group Details

+ + {(isAllowed) => { + return ( + + { + handlePopUpOpen("groupCreateUpdate", { + groupId, + name: data.group.name, + slug: data.group.slug, + role: data.group.role + }); + }} + > + + + + ); + }} + +
+
+
+

Group ID

+
+

{data.group.id}

+ +
+
+
+

Name

+

{data.group.name}

+
+
+

Slug

+
+

{data.group.slug}

+ +
+
+
+

Organization Role

+

{data.group.role}

+
+
+

Created At

+

+ {new Date(data.group.createdAt).toLocaleString()} +

+
+
+
+ ) : ( +
+
+

Group data not found

+
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/GroupMembersSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/GroupMembersSection.tsx new file mode 100644 index 000000000..33a5e1f54 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/GroupMembersSection.tsx @@ -0,0 +1,90 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal, IconButton } from "@app/components/v2"; +import { useRemoveUserFromGroup } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { AddGroupMembersModal } from "../AddGroupMemberModal"; +import { GroupMembersTable } from "./GroupMembersTable"; + +type Props = { + groupId: string; + groupSlug: string; +}; + +export const GroupMembersSection = ({ groupId, groupSlug }: Props) => { + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "addGroupMembers", + "removeMemberFromGroup" + ] as const); + + const { mutateAsync: removeUserFromGroupMutateAsync } = useRemoveUserFromGroup(); + const handleRemoveUserFromGroup = async (username: string) => { + try { + await removeUserFromGroupMutateAsync({ + groupId, + username, + slug: groupSlug + }); + + createNotification({ + text: `Successfully removed user ${username} from the group`, + type: "success" + }); + + handlePopUpToggle("removeMemberFromGroup", false); + } catch { + createNotification({ + text: `Failed to remove user ${username} from the group`, + type: "error" + }); + } + }; + + return ( +
+
+

Group Members

+ { + handlePopUpOpen("addGroupMembers", { + groupId, + slug: groupSlug + }); + }} + > + + +
+
+ +
+ + handlePopUpToggle("removeMemberFromGroup", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const userData = popUp?.removeMemberFromGroup?.data as { + username: string; + id: string; + }; + + return handleRemoveUserFromGroup(userData.username); + }} + /> +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/GroupMembersTable.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/GroupMembersTable.tsx new file mode 100644 index 000000000..8c3268dab --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/GroupMembersTable.tsx @@ -0,0 +1,195 @@ +import { useMemo } from "react"; +import { + faArrowDown, + faArrowUp, + faFolder, + faMagnifyingGlass, + faSearch +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Button, + EmptyState, + IconButton, + Input, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Th, + THead, + Tr +} from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { usePagination, useResetPageHelper } from "@app/hooks"; +import { useListGroupUsers } from "@app/hooks/api"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { EFilterReturnedUsers } from "@app/hooks/api/groups/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { GroupMembershipRow } from "./GroupMembershipRow"; + +type Props = { + groupId: string; + groupSlug: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeMemberFromGroup", "addGroupMembers"]>, + data?: object + ) => void; +}; + +enum GroupMembersOrderBy { + Name = "name" +} + +export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props) => { + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + toggleOrderDirection + } = usePagination(GroupMembersOrderBy.Name, { initPerPage: 10 }); + + const { data: groupMemberships, isLoading } = useListGroupUsers({ + id: groupId, + groupSlug, + offset, + limit: perPage, + search, + filter: EFilterReturnedUsers.EXISTING_MEMBERS + }); + + const filteredGroupMemberships = useMemo(() => { + return groupMemberships && groupMemberships?.users + ? groupMemberships?.users + ?.filter((membership) => { + const userSearchString = `${membership.firstName && membership.firstName} ${ + membership.lastName && membership.lastName + } ${membership.email && membership.email} ${ + membership.username && membership.username + }`; + return userSearchString.toLowerCase().includes(search.trim().toLowerCase()); + }) + .sort((a, b) => { + const [membershipOne, membershipTwo] = + orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + const membershipOneComparisonString = membershipOne.firstName + ? membershipOne.firstName + : membershipOne.email; + + const membershipTwoComparisonString = membershipTwo.firstName + ? membershipTwo.firstName + : membershipTwo.email; + + const comparison = membershipOneComparisonString + .toLowerCase() + .localeCompare(membershipTwoComparisonString.toLowerCase()); + + return comparison; + }) + : []; + }, [groupMemberships, orderDirection, search]); + + useResetPageHelper({ + totalCount: filteredGroupMemberships?.length, + offset, + setPage + }); + + return ( +
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search users..." + /> + + + + + + + + + + + {isLoading && } + {!isLoading && + filteredGroupMemberships.slice(offset, perPage * page).map((userGroupMembership) => { + return ( + + ); + })} + +
+
+ Name + + + +
+
EmailAdded On +
+ {Boolean(filteredGroupMemberships.length) && ( + + )} + {!isLoading && !filteredGroupMemberships?.length && ( + + )} + {!groupMemberships?.users.length && ( + + {(isAllowed) => ( +
+ +
+ )} +
+ )} +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/GroupMembershipRow.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/GroupMembershipRow.tsx new file mode 100644 index 000000000..2aa9f4ef5 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/GroupMembershipRow.tsx @@ -0,0 +1,56 @@ +import { faUserMinus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { TGroupUser } from "@app/hooks/api/groups/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + user: TGroupUser; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeMemberFromGroup"]>, + data?: object + ) => void; +}; + +export const GroupMembershipRow = ({ + user: { firstName, lastName, username, joinedGroupAt, email, id }, + handlePopUpOpen +}: Props) => { + return ( + + +

{`${firstName ?? "-"} ${lastName ?? ""}`}

+ + +

{email}

+ + + +

{new Date(joinedGroupAt).toLocaleDateString()}

+
+ + + + {(isAllowed) => { + return ( + + handlePopUpOpen("removeMemberFromGroup", { username })} + variant="plain" + colorSchema="danger" + > + + + + ); + }} + + + + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/index.tsx new file mode 100644 index 000000000..70c696609 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/GroupMembersSection/index.tsx @@ -0,0 +1 @@ +export { GroupMembersSection } from "./GroupMembersSection"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/index.tsx new file mode 100644 index 000000000..003c47910 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/-components/index.tsx @@ -0,0 +1 @@ +export { GroupDetailsSection } from "./GroupDetailsSection"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/index.tsx new file mode 100644 index 000000000..ed2867b09 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/groups/$groupId/index.tsx @@ -0,0 +1,245 @@ +import { Helmet } from 'react-helmet' +import { useTranslation } from 'react-i18next' +import { faChevronLeft, faEllipsis } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' +import { twMerge } from 'tailwind-merge' + +import { createNotification } from '@app/components/notifications' +import { OrgPermissionCan } from '@app/components/permissions' +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Spinner, + Tooltip, + UpgradePlanModal, +} from '@app/components/v2' +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, +} from '@app/context' +import { withPermission } from '@app/hoc' +import { useDeleteGroup } from '@app/hooks/api' +import { useGetGroupById } from '@app/hooks/api/groups/queries' +import { usePopUp } from '@app/hooks/usePopUp' + +import { GroupCreateUpdateModal } from './-components/GroupCreateUpdateModal' +import { GroupMembersSection } from './-components/GroupMembersSection' +import { GroupDetailsSection } from './-components' + +export enum TabSections { + Member = 'members', + Groups = 'groups', + Roles = 'roles', + Identities = 'identities', +} + +const GroupPage = withPermission( + () => { + const navigate = useNavigate() + const params = useParams({ + from: '/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId', + }) + const groupId = params.groupId as string + const { currentOrg } = useOrganization() + + const { data, isLoading } = useGetGroupById(groupId) + + const { mutateAsync: deleteMutateAsync } = useDeleteGroup() + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = + usePopUp(['groupCreateUpdate', 'deleteGroup', 'upgradePlan'] as const) + + const onDeleteGroupSubmit = async ({ + name, + id, + }: { + name: string + id: string + }) => { + try { + await deleteMutateAsync({ + id, + }) + createNotification({ + text: `Successfully deleted the ${name} group`, + type: 'success', + }) + navigate({ + to: '/organization/$organizationId/members' as const, + params: { + organizationId: currentOrg.id, + }, + search: { + selectedTab: TabSections.Groups, + }, + }) + } catch (err) { + console.error(err) + createNotification({ + text: `Failed to delete the ${name} group`, + type: 'error', + }) + } + + handlePopUpClose('deleteGroup') + } + + if (isLoading) return + + return ( +
+ {data && ( +
+ +
+

+ {data.group.name} +

+ + +
+ + + +
+
+ + + {(isAllowed) => ( + { + handlePopUpOpen('groupCreateUpdate', { + groupId, + name: data.group.name, + slug: data.group.slug, + role: data.group.role, + }) + }} + disabled={!isAllowed} + > + Edit Group + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen('deleteGroup', { + id: groupId, + name: data.group.name, + }) + }} + disabled={!isAllowed} + > + Delete Group + + )} + + +
+
+
+
+ +
+ +
+
+ )} + + handlePopUpToggle('deleteGroup', isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteGroupSubmit( + popUp?.deleteGroup?.data as { name: string; id: string }, + ) + } + /> + handlePopUpToggle('upgradePlan', isOpen)} + text={ + (popUp.upgradePlan?.data as { description: string })?.description + } + /> +
+ ) + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Groups }, +) + +const GroupDetailPage = () => { + const { t } = useTranslation() + return ( + <> + + + {t('common.head-title', { title: t('settings.org.title') })} + + + + + + ) +} + +export const Route = createFileRoute( + '/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/', +)({ + component: GroupDetailPage, +}) diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx new file mode 100644 index 000000000..23f72d7da --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx @@ -0,0 +1,157 @@ +import { useEffect } from "react"; +import { faPencil, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, IconButton, Select, SelectItem, Tooltip } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useGetIdentityById } from "@app/hooks/api"; +import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; +import { Identity } from "@app/hooks/api/identities/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { IdentityClientSecrets } from "./IdentityClientSecrets"; +import { IdentityTokens } from "./IdentityTokens"; + +type Props = { + identityId: string; + setSelectedAuthMethod: (authMethod: Identity["authMethods"][number] | null) => void; + selectedAuthMethod: Identity["authMethods"][number] | null; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState< + [ + "clientSecret", + "identityAuthMethod", + "revokeClientSecret", + "token", + "revokeToken", + "universalAuthClientSecret", + "tokenList" + ] + >, + data?: object + ) => void; +}; + +export const IdentityAuthenticationSection = ({ + identityId, + setSelectedAuthMethod, + selectedAuthMethod, + handlePopUpOpen +}: Props) => { + const { data } = useGetIdentityById(identityId); + + useEffect(() => { + if (!data?.identity) return; + + if (data.identity.authMethods?.length) { + setSelectedAuthMethod(data.identity.authMethods[0]); + } + + // eslint-disable-next-line consistent-return + return () => setSelectedAuthMethod(null); + }, [data?.identity]); + + return data ? ( +
+
+

Authentication

+ + + {(isAllowed) => { + return ( + + + handlePopUpOpen("identityAuthMethod", { + identityId, + name: data.identity.name, + allAuthMethods: data.identity.authMethods + }) + } + > + + + + ); + }} + +
+ {data.identity.authMethods.length > 0 ? ( + <> +
+
+

Auth Method

+
+
+
+ +
+
+ + { + handlePopUpOpen("identityAuthMethod", { + identityId, + name: data.identity.name, + authMethod: selectedAuthMethod, + allAuthMethods: data.identity.authMethods + }); + }} + ariaLabel="copy icon" + variant="plain" + className="group relative" + > + + + {" "} +
+
+
+ {selectedAuthMethod === IdentityAuthMethod.UNIVERSAL_AUTH && ( + + )} + {selectedAuthMethod === IdentityAuthMethod.TOKEN_AUTH && ( + + )} + + ) : ( +
+

+ No authentication methods configured. Get started by creating a new auth method. +

+ +
+ )} +
+ ) : ( +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/IdentityClientSecrets.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/IdentityClientSecrets.tsx new file mode 100644 index 000000000..a68b99faf --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/IdentityClientSecrets.tsx @@ -0,0 +1,142 @@ +import { faCheck, faCopy, faKey, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, IconButton, Tooltip } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useTimedReset } from "@app/hooks"; +import { + useGetIdentityById, + useGetIdentityUniversalAuth, + useGetIdentityUniversalAuthClientSecrets +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + identityId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState< + ["clientSecret", "revokeClientSecret", "universalAuthClientSecret"] + >, + data?: object + ) => void; +}; + +const SHOW_LIMIT = 3; + +export const IdentityClientSecrets = ({ identityId, handlePopUpOpen }: Props) => { + const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset({ + initialState: "Copy Client ID to clipboard" + }); + + const { data } = useGetIdentityById(identityId); + const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(identityId); + const { data: clientSecrets } = useGetIdentityUniversalAuthClientSecrets(identityId); + return ( +
+
+

Client ID

+
+

{identityUniversalAuth?.clientId ?? ""}

+
+ + { + navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? ""); + setCopyTextClientId("Copied"); + }} + > + + + +
+
+
+ {clientSecrets?.length ? ( +
+

{`Client Secrets (${clientSecrets.length})`}

+ +
+ ) : ( +
+ )} + {clientSecrets + ?.slice(0, SHOW_LIMIT) + .map(({ id, clientSecretTTL, clientSecretPrefix, createdAt }) => { + let expiresAt; + if (clientSecretTTL > 0) { + expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000); + } + + return ( +
+
+ +
+

+ {`${clientSecretPrefix}****`} +

+

+ {expiresAt ? `Expires on ${format(expiresAt, "yyyy-MM-dd")}` : "No Expiry"} +

+
+
+
+ + { + handlePopUpOpen("revokeClientSecret", { + clientSecretId: id, + clientSecretPrefix + }); + }} + > + + + +
+
+ ); + })} + + {(isAllowed) => { + return ( + + ); + }} + +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/IdentityTokens.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/IdentityTokens.tsx new file mode 100644 index 000000000..18b61b0ca --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/IdentityTokens.tsx @@ -0,0 +1,121 @@ +import { faEllipsis, faKey } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip +} from "@app/components/v2"; +import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + identityId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["token", "tokenList", "revokeToken"]>, + data?: object + ) => void; +}; + +export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => { + const { data } = useGetIdentityById(identityId); + const { data: tokens } = useGetIdentityTokensTokenAuth(identityId); + return ( +
+ {tokens?.length ? ( +
+

{`Access Tokens (${tokens.length})`}

+ +
+ ) : ( +
+ )} + {tokens?.map((token) => { + const expiresAt = new Date( + new Date(token.createdAt).getTime() + token.accessTokenMaxTTL * 1000 + ); + return ( +
+
+ +
+

+ {token.name ? token.name : "-"} +

+

+ {token.isAccessTokenRevoked + ? "Revoked" + : `Expires on ${format(expiresAt, "yyyy-MM-dd")}`} +

+
+
+ + +
+ + + +
+
+ + { + handlePopUpOpen("token", { + identityId, + tokenId: token.id, + name: token.name + }); + }} + > + Edit Token + + {!token.isAccessTokenRevoked && ( + { + handlePopUpOpen("revokeToken", { + identityId, + tokenId: token.id, + name: token.name + }); + }} + > + Revoke Token + + )} + +
+
+ ); + })} + +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/index.tsx new file mode 100644 index 000000000..401c930d8 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityAuthenticationSection/index.tsx @@ -0,0 +1 @@ +export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityClientSecretModal.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityClientSecretModal.tsx new file mode 100644 index 000000000..102a31adf --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityClientSecretModal.tsx @@ -0,0 +1,182 @@ +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faCheck, faCopy } 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, + Modal, + ModalContent, + Tooltip +} from "@app/components/v2"; +import { useTimedReset } from "@app/hooks"; +import { useCreateIdentityUniversalAuthClientSecret } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z + .object({ + description: z.string(), + ttl: z.string().refine((val) => Number(val) <= 315360000, { + message: "TTL cannot be greater than 315360000" + }), + numUsesLimit: z.string() + }) + .required(); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["clientSecret"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["clientSecret"]>, state?: boolean) => void; +}; + +export const IdentityClientSecretModal = ({ popUp, handlePopUpToggle }: Props) => { + const { mutateAsync: createClientSecret } = useCreateIdentityUniversalAuthClientSecret(); + const [token, setToken] = useState(""); + const [copyTextToken, isCopyingToken, setCopyTextToken] = useTimedReset({ + initialState: "Copy to clipboard" + }); + const hasToken = Boolean(token); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + description: "", + ttl: "", + numUsesLimit: "" + } + }); + + const popUpData = popUp?.clientSecret?.data as { + identityId: string; + }; + + const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => { + const { clientSecret } = await createClientSecret({ + identityId: popUpData.identityId, + description, + ttl: Number(ttl), + numUsesLimit: Number(numUsesLimit) + }); + + setToken(clientSecret); + + createNotification({ + text: "Successfully created client secret", + type: "success" + }); + + reset(); + }; + + return ( + { + handlePopUpToggle("clientSecret", isOpen); + reset(); + setToken(""); + }} + > + + {!hasToken ? ( +
+ ( + + + + )} + /> + ( + +
+ +
+
+ )} + /> + ( + + + + )} + /> +
+ + +
+ + ) : ( +
+

{token}

+ + { + navigator.clipboard.writeText(token); + setCopyTextToken("Copied"); + }} + > + + + +
+ )} +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityDetailsSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityDetailsSection.tsx new file mode 100644 index 000000000..4b25475cc --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityDetailsSection.tsx @@ -0,0 +1,118 @@ +import { faCheck, faCopy, faKey, faPencil } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { IconButton, Tag, Tooltip } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useTimedReset } from "@app/hooks"; +import { useGetIdentityById } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + identityId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "token", "clientSecret"]>, + data?: object + ) => void; +}; + +export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) => { + const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + const { data } = useGetIdentityById(identityId); + return data ? ( +
+
+

Identity Details

+ + {(isAllowed) => { + return ( + + { + handlePopUpOpen("identity", { + identityId, + name: data.identity.name, + role: data.role, + customRole: data.customRole, + metadata: data.metadata + }); + }} + > + + + + ); + }} + +
+
+
+

Identity ID

+
+

{data.identity.id}

+
+ + { + navigator.clipboard.writeText(data.identity.id); + setCopyTextId("Copied"); + }} + > + + + +
+
+
+
+

Name

+

{data.identity.name}

+
+
+

Organization Role

+

{data.role}

+
+
+

Metadata

+ {data?.metadata?.length ? ( +
+ {data.metadata?.map((el) => ( +
+ + +
{el.key}
+
+ +
+ {el.value} +
+
+
+ ))} +
+ ) : ( +

-

+ )} +
+
+
+ ) : ( +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityAddToProjectModal.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityAddToProjectModal.tsx new file mode 100644 index 000000000..c6597a5eb --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityAddToProjectModal.tsx @@ -0,0 +1,185 @@ +import { useMemo } 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, + FilterableSelect, + FormControl, + Modal, + ModalClose, + ModalContent +} from "@app/components/v2"; +import { useOrganization, useWorkspace } from "@app/context"; +import { + useAddIdentityToWorkspace, + useGetIdentityProjectMemberships, + useGetProjectRoles, + useGetWorkspaceById +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z + .object({ + project: z.object({ name: z.string(), id: z.string() }), + role: z.object({ name: z.string(), slug: z.string() }) + }) + .required(); + +type FormData = z.infer; + +type Props = { + identityId: string; + popUp: UsePopUpState<["addIdentityToProject"]>; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["addIdentityToProject"]>, + state?: boolean + ) => void; +}; + +// TODO: eventually refactor to support adding to multiple projects at once? would lose role granularity unique to project + +const Content = ({ identityId, handlePopUpToggle }: Omit) => { + const { currentOrg } = useOrganization(); + const { workspaces } = useWorkspace(); + const { mutateAsync: addIdentityToWorkspace } = useAddIdentityToWorkspace(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting }, + watch + } = useForm({ + resolver: zodResolver(schema) + }); + + const projectId = watch("project")?.id; + const { data: projectMemberships } = useGetIdentityProjectMemberships(identityId); + const { data: project, isLoading: isProjectLoading } = useGetWorkspaceById(projectId); + const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(project?.id ?? ""); + + const filteredWorkspaces = useMemo(() => { + const wsWorkspaceIds = new Map(); + + projectMemberships?.forEach((projectMembership) => { + wsWorkspaceIds.set(projectMembership.project.id, true); + }); + + return (workspaces || []).filter( + ({ id, orgId }) => !wsWorkspaceIds.has(id) && orgId === currentOrg?.id + ); + }, [workspaces, projectMemberships]); + + const onFormSubmit = async ({ project: selectedProject, role }: FormData) => { + try { + await addIdentityToWorkspace({ + workspaceId: selectedProject.id, + identityId, + role: role.slug || undefined + }); + + createNotification({ + text: "Successfully added identity to project", + type: "success" + }); + + reset(); + handlePopUpToggle("addIdentityToProject", false); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to add identity to project"; + + createNotification({ + text, + type: "error" + }); + } + }; + + const isProjectSelected = Boolean(projectId); + + return ( +
+ ( + + option.id} + getOptionLabel={(option) => option.name} + isLoading={isProjectSelected && isProjectLoading} + /> + + )} + /> + ( + + option.slug} + getOptionLabel={(option) => option.name} + /> + + )} + /> +
+ + + + +
+ + ); +}; + +export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle }: Props) => { + return ( + { + handlePopUpToggle("addIdentityToProject", isOpen); + }} + > + + + + + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityProjectRow.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityProjectRow.tsx new file mode 100644 index 000000000..0c892afcd --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityProjectRow.tsx @@ -0,0 +1,111 @@ +import { useMemo } from "react"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; +import { format } from "date-fns"; + +import { createNotification } from "@app/components/notifications"; +import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { IdentityMembership } from "@app/hooks/api/identities/types"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +export enum TabSections { + Member = "members", + Groups = "groups", + Roles = "roles", + Identities = "identities" +} + +type Props = { + membership: IdentityMembership; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeIdentityFromProject"]>, + data?: object + ) => void; +}; + +const formatRoleName = (role: string, customRoleName?: string) => { + if (role === ProjectMembershipRole.Custom) return customRoleName; + if (role === ProjectMembershipRole.Admin) return "Admin"; + if (role === ProjectMembershipRole.Member) return "Developer"; + if (role === ProjectMembershipRole.Viewer) return "Viewer"; + if (role === ProjectMembershipRole.NoAccess) return "No Access"; + return role; +}; + +export const IdentityProjectRow = ({ + membership: { id, createdAt, identity, project, roles }, + handlePopUpOpen +}: Props) => { + const { workspaces } = useWorkspace(); + const navigate = useNavigate(); + + const isAccessible = useMemo(() => { + const workspaceIds = new Map(); + + workspaces?.forEach((workspace) => { + workspaceIds.set(workspace.id, true); + }); + + return workspaceIds.has(project.id); + }, [workspaces, project]); + + return ( + { + if (isAccessible) { + navigate({ + to: `/${project?.type}/${project.id}/members` as const, + search: { + selectedTab: TabSections.Identities + } + }); + return; + } + + createNotification({ + text: "Unable to access project", + type: "error" + }); + }} + > + {project.name} + + {project.type} + + {`${formatRoleName(roles[0].role, roles[0].customRoleName)}${ + roles.length > 1 ? ` (+${roles.length - 1})` : "" + }`} + {format(new Date(createdAt), "yyyy-MM-dd")} + + {isAccessible && ( +
+ + { + e.stopPropagation(); + handlePopUpOpen("removeIdentityFromProject", { + identityId: identity.id, + identityName: identity.name, + projectId: project.id, + projectName: project.name + }); + }} + > + + + +
+ )} + + + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityProjectsSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityProjectsSection.tsx new file mode 100644 index 000000000..b0c13009d --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityProjectsSection.tsx @@ -0,0 +1,92 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal, IconButton } from "@app/components/v2"; +import { useDeleteIdentityFromWorkspace } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { IdentityAddToProjectModal } from "./IdentityAddToProjectModal"; +import { IdentityProjectsTable } from "./IdentityProjectsTable"; + +type Props = { + identityId: string; +}; + +export const IdentityProjectsSection = ({ identityId }: Props) => { + const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "addIdentityToProject", + "removeIdentityFromProject" + ] as const); + + const onRemoveIdentitySubmit = async (id: string, projectId: string) => { + try { + await deleteMutateAsync({ + identityId: id, + workspaceId: projectId + }); + + createNotification({ + text: "Successfully removed identity from project", + type: "success" + }); + + handlePopUpClose("removeIdentityFromProject"); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to remove identity from project"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( +
+
+

Projects

+ { + handlePopUpOpen("addIdentityToProject"); + }} + > + + +
+
+ +
+ handlePopUpToggle("removeIdentityFromProject", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const popupData = popUp?.removeIdentityFromProject?.data as { + identityId: string; + projectId: string; + }; + + return onRemoveIdentitySubmit(popupData.identityId, popupData.projectId); + }} + /> + +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityProjectsTable.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityProjectsTable.tsx new file mode 100644 index 000000000..4f9191f3a --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/IdentityProjectsTable.tsx @@ -0,0 +1,151 @@ +import { useMemo } from "react"; +import { + faArrowDown, + faArrowUp, + faFolder, + faMagnifyingGlass, + faSearch +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + EmptyState, + IconButton, + Input, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Th, + THead, + Tr +} from "@app/components/v2"; +import { usePagination, useResetPageHelper } from "@app/hooks"; +import { useGetIdentityProjectMemberships } from "@app/hooks/api"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { IdentityProjectRow } from "./IdentityProjectRow"; + +type Props = { + identityId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeIdentityFromProject"]>, + data?: object + ) => void; +}; + +enum IdentityProjectsOrderBy { + Name = "name" +} + +export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) => { + const { data: projectMemberships = [], isLoading } = useGetIdentityProjectMemberships(identityId); + + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + toggleOrderDirection + } = usePagination(IdentityProjectsOrderBy.Name, { initPerPage: 10 }); + + const filteredProjectMemberships = useMemo( + () => + projectMemberships + ?.filter((membership) => + membership.project.name.toLowerCase().includes(search.trim().toLowerCase()) + ) + .sort((a, b) => { + const [membershipOne, membershipTwo] = + orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + return membershipOne.project.name + .toLowerCase() + .localeCompare(membershipTwo.project.name.toLowerCase()); + }), + [projectMemberships, orderDirection, search] + ); + + useResetPageHelper({ + totalCount: filteredProjectMemberships.length, + offset, + setPage + }); + + return ( +
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search projects..." + /> + + + + + + + + + + + + + {isLoading && } + {!isLoading && + filteredProjectMemberships.slice(offset, perPage * page).map((membership) => { + return ( + + ); + })} + +
+
+ Name + + + +
+
TypeRoleAdded On +
+ {Boolean(filteredProjectMemberships.length) && ( + + )} + {!isLoading && !filteredProjectMemberships?.length && ( + + )} +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/index.tsx new file mode 100644 index 000000000..4eb7d40a9 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityProjectsSection/index.tsx @@ -0,0 +1 @@ +export { IdentityProjectsSection } from "./IdentityProjectsSection"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityTokenListModal.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityTokenListModal.tsx new file mode 100644 index 000000000..f0336061b --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityTokenListModal.tsx @@ -0,0 +1,270 @@ +import { useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format } from "date-fns"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + EmptyState, + FormControl, + IconButton, + Input, + Modal, + ModalContent, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { + useCreateTokenIdentityTokenAuth, + useGetIdentityTokensTokenAuth, + useGetIdentityUniversalAuthClientSecrets +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z.object({ + name: z.string() +}); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["tokenList", "revokeToken"]>; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["revokeToken"]>, data?: object) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["tokenList", "revokeToken"]>, + state?: boolean + ) => void; +}; + +export const IdentityTokenListModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { + const { t } = useTranslation(); + + const [token, setToken] = useState(""); + const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false); + const [isClientIdCopied, setIsClientIdCopied] = useToggle(false); + + const popUpData = popUp?.tokenList?.data as { + identityId: string; + name: string; + }; + + const { data: tokens } = useGetIdentityTokensTokenAuth(popUpData?.identityId ?? ""); + const { data, isLoading } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? ""); + + const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "" + } + }); + + useEffect(() => { + let timer: NodeJS.Timeout; + if (isClientSecretCopied) { + timer = setTimeout(() => setIsClientSecretCopied.off(), 2000); + } + + return () => clearTimeout(timer); + }, [isClientSecretCopied]); + + useEffect(() => { + let timer: NodeJS.Timeout; + if (isClientIdCopied) { + timer = setTimeout(() => setIsClientIdCopied.off(), 2000); + } + + return () => clearTimeout(timer); + }, [isClientIdCopied]); + + const onFormSubmit = async ({ name }: FormData) => { + try { + if (!popUpData?.identityId) return; + + const newTokenData = await createToken({ + identityId: popUpData.identityId, + name + }); + + setToken(newTokenData.accessToken); + + createNotification({ + text: "Successfully created token", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to create token", + type: "error" + }); + } + }; + + const hasToken = Boolean(token); + + return ( + { + handlePopUpToggle("tokenList", isOpen); + reset(); + setToken(""); + }} + > + +

New Token

+ {hasToken ? ( +
+
+

We will only show this token once

+ +
+
+

{token}

+ { + navigator.clipboard.writeText(token); + setIsClientSecretCopied.on(); + }} + > + + + {t("common.click-to-copy")} + + +
+
+ ) : ( +
+ ( + +
+ + +
+
+ )} + /> + + )} +

Tokens

+ + + + + + + + + + + + {isLoading && } + {!isLoading && + tokens?.map( + ({ + id, + createdAt, + name, + accessTokenNumUses, + accessTokenNumUsesLimit, + accessTokenMaxTTL, + isAccessTokenRevoked + }) => { + const expiresAt = new Date( + new Date(createdAt).getTime() + accessTokenMaxTTL * 1000 + ); + + return ( + + + + + + + + ); + } + )} + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
nameNum UsesCreated AtMax Expires At +
{name === "" ? "-" : name}{`${accessTokenNumUses}${ + accessTokenNumUsesLimit ? `/${accessTokenNumUsesLimit}` : "" + }`}{format(new Date(createdAt), "yyyy-MM-dd")} + {isAccessTokenRevoked ? "Revoked" : `${format(expiresAt, "yyyy-MM-dd")}`} + + {!isAccessTokenRevoked && ( + { + handlePopUpOpen("revokeToken", { + identityId: popUpData?.identityId, + tokenId: id, + name + }); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + > + + + )} +
+ +
+
+
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityTokenModal.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityTokenModal.tsx new file mode 100644 index 000000000..c188680eb --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/IdentityTokenModal.tsx @@ -0,0 +1,182 @@ +import { useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faCheck, faCopy } 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, + Modal, + ModalContent, + Tooltip +} from "@app/components/v2"; +import { useTimedReset } from "@app/hooks"; +import { useCreateTokenIdentityTokenAuth, useUpdateIdentityTokenAuthToken } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z + .object({ + name: z.string() + }) + .required(); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["token"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["token"]>, state?: boolean) => void; +}; + +export const IdentityTokenModal = ({ popUp, handlePopUpToggle }: Props) => { + const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); + const { mutateAsync: updateToken } = useUpdateIdentityTokenAuthToken(); + const [token, setToken] = useState(""); + const [copyTextToken, isCopyingToken, setCopyTextToken] = useTimedReset({ + initialState: "Copy to clipboard" + }); + const hasToken = Boolean(token); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "" + } + }); + + const tokenData = popUp?.token?.data as { + identityId: string; + tokenId?: string; + name?: string; + }; + + useEffect(() => { + if (tokenData?.tokenId && tokenData?.name) { + reset({ + name: tokenData.name + }); + } else { + reset({ + name: "" + }); + } + }, [popUp?.token?.data]); + + const onFormSubmit = async ({ name }: FormData) => { + try { + if (tokenData?.tokenId) { + // update + + await updateToken({ + identityId: tokenData.identityId, + tokenId: tokenData.tokenId, + name + }); + + handlePopUpToggle("token", false); + } else { + // create + + const newTokenData = await createToken({ + identityId: tokenData.identityId, + name + }); + + setToken(newTokenData.accessToken); + } + + createNotification({ + text: `Successfully ${popUp?.token?.data ? "updated" : "created"} token`, + type: "success" + }); + + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = + error?.response?.data?.message ?? + `Failed to ${popUp?.token?.data ? "update" : "create"} token`; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("token", isOpen); + reset(); + setToken(""); + }} + > + + {!hasToken ? ( +
+ ( + + + + )} + /> +
+ + +
+ + ) : ( +
+

{token}

+ + { + navigator.clipboard.writeText(token); + setCopyTextToken("Copied"); + }} + > + + + +
+ )} +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/index.tsx new file mode 100644 index 000000000..8fcbcfc21 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/-components/index.tsx @@ -0,0 +1,6 @@ +export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection/IdentityAuthenticationSection"; +export { IdentityClientSecretModal } from "./IdentityClientSecretModal"; +export { IdentityDetailsSection } from "./IdentityDetailsSection"; +export { IdentityProjectsSection } from "./IdentityProjectsSection/IdentityProjectsSection"; +export { IdentityTokenListModal } from "./IdentityTokenListModal"; +export { IdentityTokenModal } from "./IdentityTokenModal"; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/index.tsx new file mode 100644 index 000000000..f5e20cd97 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/identities/$identityId/index.tsx @@ -0,0 +1,415 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useState } from 'react' +import { Helmet } from 'react-helmet' +import { useTranslation } from 'react-i18next' +import { faChevronLeft, faEllipsis } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' +import { twMerge } from 'tailwind-merge' + +import { createNotification } from '@app/components/notifications' +import { OrgPermissionCan } from '@app/components/permissions' +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip, + UpgradePlanModal, +} from '@app/components/v2' +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, +} from '@app/context' +import { withPermission } from '@app/hoc' +import { + IdentityAuthMethod, + useDeleteIdentity, + useGetIdentityById, + useRevokeIdentityTokenAuthToken, + useRevokeIdentityUniversalAuthClientSecret, +} from '@app/hooks/api' +import { Identity } from '@app/hooks/api/identities/types' +import { usePopUp } from '@app/hooks/usePopUp' +import { TabSections } from '@app/types/org' + +import { IdentityAuthMethodModal } from '../../members/-components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal' +import { IdentityModal } from '../../members/-components/OrgIdentityTab/components/IdentitySection/IdentityModal' +import { IdentityUniversalAuthClientSecretModal } from '../../members/-components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal' +import { + IdentityAuthenticationSection, + IdentityClientSecretModal, + IdentityDetailsSection, + IdentityProjectsSection, + IdentityTokenListModal, + IdentityTokenModal, +} from './-components' + +export const IdentitySection = withPermission( + () => { + const navigate = useNavigate() + const params = useParams({ + from: '/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId', + }) + const identityId = params.identityId as string + const { currentOrg } = useOrganization() + const orgId = currentOrg?.id || '' + const { data } = useGetIdentityById(identityId) + const { mutateAsync: deleteIdentity } = useDeleteIdentity() + const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken() + const { mutateAsync: revokeClientSecret } = + useRevokeIdentityUniversalAuthClientSecret() + + const [selectedAuthMethod, setSelectedAuthMethod] = useState< + Identity['authMethods'][number] | null + >(null) + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = + usePopUp([ + 'identity', + 'deleteIdentity', + 'identityAuthMethod', + 'revokeAuthMethod', + 'token', + 'tokenList', + 'revokeToken', + 'clientSecret', + 'revokeClientSecret', + 'universalAuthClientSecret', // list of client secrets + 'upgradePlan', + ] as const) + + const onDeleteIdentitySubmit = async (id: string) => { + try { + await deleteIdentity({ + identityId: id, + organizationId: orgId, + }) + + createNotification({ + text: 'Successfully deleted identity', + type: 'success', + }) + + handlePopUpClose('deleteIdentity') + navigate({ + to: `/organization/${orgId}/members` as const, + search: { + selectedTab: TabSections.Identities, + }, + }) + } catch (err) { + console.error(err) + const error = err as any + const text = + error?.response?.data?.message ?? 'Failed to delete identity' + + createNotification({ + text, + type: 'error', + }) + } + } + + const onRevokeTokenSubmit = async ({ + identityId: parentIdentityId, + tokenId, + name, + }: { + identityId: string + tokenId: string + name: string + }) => { + try { + await revokeToken({ + identityId: parentIdentityId, + tokenId, + }) + + handlePopUpClose('revokeToken') + + createNotification({ + text: `Successfully revoked token ${name ?? ''}`, + type: 'success', + }) + } catch (err) { + console.error(err) + const error = err as any + const text = + error?.response?.data?.message ?? 'Failed to delete identity' + + createNotification({ + text, + type: 'error', + }) + } + } + + const onDeleteClientSecretSubmit = async ({ + clientSecretId, + }: { + clientSecretId: string + }) => { + try { + if ( + !data?.identity.id || + selectedAuthMethod !== IdentityAuthMethod.UNIVERSAL_AUTH + ) + return + + await revokeClientSecret({ + identityId: data?.identity.id, + clientSecretId, + }) + + handlePopUpToggle('revokeClientSecret', false) + + createNotification({ + text: 'Successfully deleted client secret', + type: 'success', + }) + } catch (err) { + console.error(err) + createNotification({ + text: 'Failed to delete client secret', + type: 'error', + }) + } + } + + return ( +
+ {data && ( +
+ +
+

+ {data.identity.name} +

+ + +
+ + + +
+
+ + + {(isAllowed) => ( + { + handlePopUpOpen('identity', { + identityId, + name: data.identity.name, + role: data.role, + customRole: data.customRole, + }) + }} + disabled={!isAllowed} + > + Edit Identity + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen('identityAuthMethod', { + identityId, + name: data.identity.name, + allAuthMethods: data.identity.authMethods, + }) + }} + disabled={!isAllowed} + > + Add new auth method + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen('deleteIdentity', { + identityId, + name: data.identity.name, + }) + }} + disabled={!isAllowed} + > + Delete Identity + + )} + + +
+
+
+
+ + +
+ +
+
+ )} + + + + + + + handlePopUpToggle('upgradePlan', isOpen)} + text={ + (popUp.upgradePlan?.data as { description: string })?.description + } + /> + handlePopUpToggle('deleteIdentity', isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteIdentitySubmit( + (popUp?.deleteIdentity?.data as { identityId: string }) + ?.identityId, + ) + } + /> + handlePopUpToggle('revokeToken', isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const revokeTokenData = popUp?.revokeToken?.data as { + identityId: string + tokenId: string + name: string + } + + return onRevokeTokenSubmit(revokeTokenData) + }} + /> + handlePopUpToggle('revokeClientSecret', isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const deleteClientSecretData = popUp?.revokeClientSecret?.data as { + clientSecretId: string + clientSecretPrefix: string + } + + return onDeleteClientSecretSubmit({ + clientSecretId: deleteClientSecretData.clientSecretId, + }) + }} + /> +
+ ) + }, + { + action: OrgPermissionActions.Read, + subject: OrgPermissionSubjects.Identity, + }, +) + +const IdentityDetailPage = () => { + const { t } = useTranslation() + return ( + <> + + + {t('common.head-title', { title: t('settings.org.title') })} + + + + + + ) +} + +export const Route = createFileRoute( + '/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/', +)({ + component: IdentityDetailPage, +}) diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/index.tsx deleted file mode 100644 index d1bd04080..000000000 --- a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { - createFileRoute, - useRouteContext, - useRouter, -} from '@tanstack/react-router' - -function RouteComponent() { - const user = useRouteContext({ - from: '/_authenticate', - select: (el) => el.user, - }) - const router = useRouter() - - return ( -
- Hello {user?.email}! - -
- ) -} - -export const Route = createFileRoute( - '/_authenticate/_org_details/_org-layout/organization/$organizationId/', -)({ - component: RouteComponent, -}) diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/kms/overview.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/kms/overview.tsx new file mode 100644 index 000000000..cb8b5f9d4 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/kms/overview.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { ProjectType } from '@app/hooks/api/workspace/types' + +import { ProductOverview } from '../secret-manager/overview' + +const KeyManagerOverviewPage = () => + +export const Route = createFileRoute( + '/_authenticate/_org_details/_org-layout/organization/$organizationId/kms/overview', +)({ + component: KeyManagerOverviewPage, +}) diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/OrgGroupsTab.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/OrgGroupsTab.tsx new file mode 100644 index 000000000..428ee68d8 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/OrgGroupsTab.tsx @@ -0,0 +1,18 @@ +import { motion } from "framer-motion"; + +import { OrgGroupsSection } from "./components"; + +export const OrgGroupsTab = () => { + return ( + + + + ); +}; + diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx new file mode 100644 index 000000000..aaec0a595 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx @@ -0,0 +1,189 @@ +import { useEffect } 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, + FilterableSelect, + FormControl, + Input, + Modal, + ModalContent +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { findOrgMembershipRole } from "@app/helpers/roles"; +import { useCreateGroup, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const GroupFormSchema = z.object({ + name: z.string().min(1, "Name cannot be empty").max(50, "Name must be 50 characters or fewer"), + slug: z + .string() + .min(5, "Slug must be at least 5 characters long") + .max(36, "Slug must be 36 characters or fewer"), + role: z.object({ name: z.string(), slug: z.string() }) +}); + +export type TGroupFormData = z.infer; + +type Props = { + popUp: UsePopUpState<["group"]>; + handlePopUpClose: (popUpName: keyof UsePopUpState<["group"]>) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void; +}; + +export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => { + const { currentOrg } = useOrganization(); + const { data: roles } = useGetOrgRoles(currentOrg?.id || ""); + const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateGroup(); + const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateGroup(); + + const { control, handleSubmit, reset } = useForm({ + resolver: zodResolver(GroupFormSchema) + }); + + useEffect(() => { + const group = popUp?.group?.data as { + groupId: string; + name: string; + slug: string; + role: string; + customRole: { + name: string; + slug: string; + }; + }; + + if (!roles?.length) return; + + if (group) { + reset({ + name: group.name, + slug: group.slug, + role: group?.customRole ?? findOrgMembershipRole(roles, group.role) + }); + } else { + reset({ + name: "", + slug: "", + role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole) + }); + } + }, [popUp?.group?.data, roles]); + + const onGroupModalSubmit = async ({ name, slug, role }: TGroupFormData) => { + try { + if (!currentOrg?.id) return; + + const group = popUp?.group?.data as { + groupId: string; + name: string; + slug: string; + }; + + if (group) { + await updateMutateAsync({ + id: group.groupId, + name, + slug, + role: role.slug || undefined + }); + } else { + await createMutateAsync({ + name, + slug, + organizationId: currentOrg.id, + role: role.slug || undefined + }); + } + handlePopUpToggle("group", false); + reset(); + + createNotification({ + text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`, + type: "success" + }); + } catch { + createNotification({ + text: `Failed to ${popUp?.group?.data ? "updated" : "created"} group`, + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("group", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + option.slug} + getOptionLabel={(option) => option.name} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx new file mode 100644 index 000000000..9c3949150 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -0,0 +1,98 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; +import { useDeleteGroup } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { OrgGroupModal } from "./OrgGroupModal"; +import { OrgGroupsTable } from "./OrgGroupsTable"; + +export const OrgGroupsSection = () => { + const { subscription } = useSubscription(); + const { mutateAsync: deleteMutateAsync } = useDeleteGroup(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "group", + "groupMembers", + "deleteGroup", + "upgradePlan" + ] as const); + + const handleAddGroupModal = () => { + if (!subscription?.groups) { + handlePopUpOpen("upgradePlan", { + description: + "You can manage users more efficiently with groups if you upgrade your Infisical plan." + }); + } else { + handlePopUpOpen("group"); + } + }; + + const onDeleteGroupSubmit = async ({ name, groupId }: { name: string; groupId: string }) => { + try { + await deleteMutateAsync({ + id: groupId + }); + createNotification({ + text: `Successfully deleted the group named ${name}`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to delete the group named ${name}`, + type: "error" + }); + } + + handlePopUpClose("deleteGroup"); + }; + + return ( +
+
+

Groups

+ + {(isAllowed) => ( + + )} + +
+ + + handlePopUpToggle("deleteGroup", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteGroupSubmit(popUp?.deleteGroup?.data as { name: string; groupId: string }) + } + /> + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> +
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx new file mode 100644 index 000000000..6edcf9ba8 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -0,0 +1,397 @@ +import { useMemo } from "react"; +import { + faArrowDown, + faArrowUp, + faEllipsis, + faMagnifyingGlass, + faSearch, + faUsers +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + IconButton, + Input, + Pagination, + Select, + SelectItem, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { usePagination, useResetPageHelper } from "@app/hooks"; +import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; +import { useNavigate } from "@tanstack/react-router"; + +type Props = { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["group", "deleteGroup", "groupMembers"]>, + data?: { + groupId?: string; + name?: string; + slug?: string; + role?: string; + customRole?: { + name: string; + slug: string; + }; + } + ) => void; +}; + +enum GroupsOrderBy { + Name = "name", + Slug = "slug", + Role = "role" +} + +export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { + const navigate = useNavigate(); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { isLoading, data: groups = [] } = useGetOrganizationGroups(orgId); + const { mutateAsync: updateMutateAsync } = useUpdateGroup(); + + const { data: roles } = useGetOrgRoles(orgId); + + const handleChangeRole = async ({ id, role }: { id: string; role: string }) => { + try { + await updateMutateAsync({ + id, + role + }); + + createNotification({ + text: "Successfully updated group role", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to update group role", + type: "error" + }); + } + }; + + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + orderBy, + setOrderBy, + setOrderDirection, + toggleOrderDirection + } = usePagination(GroupsOrderBy.Name, { initPerPage: 20 }); + + const filteredGroups = useMemo(() => { + const filtered = search + ? groups?.filter( + ({ name, slug }) => + name.toLowerCase().includes(search.toLowerCase()) || + slug.toLowerCase().includes(search.toLowerCase()) + ) + : groups; + + const ordered = filtered?.sort((a, b) => { + switch (orderBy) { + case GroupsOrderBy.Role: { + const aValue = a.role === "custom" ? (a.customRole?.name as string) : a.role; + const bValue = b.role === "custom" ? (b.customRole?.name as string) : b.role; + + return aValue.toLowerCase().localeCompare(bValue.toLowerCase()); + } + default: + return a[orderBy].toLowerCase().localeCompare(b[orderBy].toLowerCase()); + } + }); + + return orderDirection === OrderByDirection.ASC ? ordered : ordered?.reverse(); + }, [search, groups, orderBy, orderDirection]); + + const handleSort = (column: GroupsOrderBy) => { + if (column === orderBy) { + toggleOrderDirection(); + return; + } + + setOrderBy(column); + setOrderDirection(OrderByDirection.ASC); + }; + + useResetPageHelper({ + totalCount: filteredGroups.length, + offset, + setPage + }); + + return ( +
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search groups..." + /> + + + + + + + + + + + {isLoading && } + {!isLoading && + filteredGroups + .slice(offset, perPage * page) + .map(({ id, name, slug, role, customRole }) => { + return ( + + navigate({ + to: "/organization/$organizationId/groups/$groupId", + params: { + organizationId: currentOrg.id, + groupId: id + } + }) + } + className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700" + key={`org-group-${id}`} + > + + + + + + ); + })} + +
+
+ Name + handleSort(GroupsOrderBy.Name)} + > + + +
+
+
+ Slug + handleSort(GroupsOrderBy.Slug)} + > + + +
+
+
+ Role + handleSort(GroupsOrderBy.Role)} + > + + +
+
+
{name}{slug} + + {(isAllowed) => { + return ( + + ); + }} + + + + +
+ +
+
+ + { + e.stopPropagation(); + createNotification({ + text: "Copied group ID to clipboard", + type: "info" + }); + navigator.clipboard.writeText(id); + }} + > + Copy Group ID + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("group", { + groupId: id, + name, + slug, + role, + customRole + }); + }} + disabled={!isAllowed} + > + Edit Group + + )} + + + {(isAllowed) => ( + + navigate({ + to: "/organization/$organizationId/groups/$groupId", + params: { + organizationId: currentOrg.id, + groupId: id + } + }) + } + disabled={!isAllowed} + > + Manage Members + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteGroup", { + groupId: id, + name + }); + }} + disabled={!isAllowed} + > + Delete Group + + )} + + +
+
+ {Boolean(filteredGroups.length) && ( + + )} + {!isLoading && !filteredGroups?.length && ( + + )} +
+
+ ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/index.tsx new file mode 100644 index 000000000..ca3caabea --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/OrgGroupsSection/index.tsx @@ -0,0 +1 @@ +export { OrgGroupsSection } from "./OrgGroupsSection"; \ No newline at end of file diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/index.tsx new file mode 100644 index 000000000..ca3caabea --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/components/index.tsx @@ -0,0 +1 @@ +export { OrgGroupsSection } from "./OrgGroupsSection"; \ No newline at end of file diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/index.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/index.tsx new file mode 100644 index 000000000..69936a978 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgGroupsTab/index.tsx @@ -0,0 +1 @@ +export { OrgGroupsTab } from "./OrgGroupsTab"; \ No newline at end of file diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/OrgIdentityTab.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/OrgIdentityTab.tsx new file mode 100644 index 000000000..251b0b831 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/OrgIdentityTab.tsx @@ -0,0 +1,17 @@ +import { motion } from "framer-motion"; + +import { IdentitySection } from "./components"; + +export const OrgIdentityTab = () => { + return ( + + + + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx new file mode 100644 index 000000000..70128fa1a --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent"; + +type Props = { + popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>, + state?: boolean + ) => void; +}; + +export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { + const [selectedAuthMethod, setSelectedAuthMethod] = useState(null); + + const initialAuthMethod = popUp?.identityAuthMethod?.data?.authMethod; + + const isSelectedAuthAlreadyConfigured = + popUp?.identityAuthMethod?.data?.allAuthMethods?.includes(selectedAuthMethod); + + return ( + { + handlePopUpToggle("identityAuthMethod", isOpen); + }} + > + + + + + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx new file mode 100644 index 000000000..1df129486 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx @@ -0,0 +1,335 @@ +import { useCallback } 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 { + Badge, + DeleteActionModal, + FormControl, + Select, + SelectItem, + Tooltip, + UpgradePlanModal +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useDeleteIdentityAwsAuth, + useDeleteIdentityAzureAuth, + useDeleteIdentityGcpAuth, + useDeleteIdentityKubernetesAuth, + useDeleteIdentityOidcAuth, + useDeleteIdentityTokenAuth, + useDeleteIdentityUniversalAuth +} from "@app/hooks/api"; +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"; +import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; + +type Props = { + popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>, + state?: boolean + ) => void; + + identity: { + name: string; + id: string; + authMethods: IdentityAuthMethod[]; + }; + initialAuthMethod: IdentityAuthMethod; + setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void; +}; + +type TRevokeOptions = { + identityId: string; + organizationId: string; +}; + +type TRevokeMethods = { + revokeMethod: (revokeOptions: TRevokeOptions) => Promise; + render: () => JSX.Element; +}; + +const identityAuthMethods = [ + { label: "Token Auth", value: IdentityAuthMethod.TOKEN_AUTH }, + { label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }, + { label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH }, + { 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: "JWT Auth", + value: IdentityAuthMethod.JWT_AUTH + } +]; + +const schema = z + .object({ + authMethod: z.nativeEnum(IdentityAuthMethod) + }) + .required(); + +export type FormData = z.infer; + +export const IdentityAuthMethodModalContent = ({ + popUp, + handlePopUpOpen, + handlePopUpToggle, + identity, + initialAuthMethod, + setSelectedAuthMethod +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + + const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth(); + const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth(); + const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth(); + const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth(); + const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth(); + const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); + const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); + const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth(); + + const { control, watch } = useForm({ + resolver: zodResolver(schema), + defaultValues: async () => { + let authMethod = initialAuthMethod; + + if (!authMethod) { + const firstAuthMethodNotConfiguredAuthMethod = identityAuthMethods.find( + ({ value }) => !identity?.authMethods?.includes(value) + ); + + if (firstAuthMethodNotConfiguredAuthMethod) { + authMethod = firstAuthMethodNotConfiguredAuthMethod.value; + } + } + + setSelectedAuthMethod(authMethod); + return { + authMethod + }; + } + }); + + const watchedAuthMethod = watch("authMethod"); + + const identityAuthMethodData = { + identityId: identity.id, + name: identity.name, + authMethod: watch("authMethod"), + configuredAuthMethods: identity.authMethods + } as { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + configuredAuthMethods?: IdentityAuthMethod[]; + }; + + const isSelectedAuthAlreadyConfigured = + identityAuthMethodData?.configuredAuthMethods?.includes(watchedAuthMethod); + + const methodMap: Record = { + [IdentityAuthMethod.UNIVERSAL_AUTH]: { + revokeMethod: revokeUniversalAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.OIDC_AUTH]: { + revokeMethod: revokeOidcAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.TOKEN_AUTH]: { + revokeMethod: revokeTokenAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.AZURE_AUTH]: { + revokeMethod: revokeAzureAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.GCP_AUTH]: { + revokeMethod: revokeGcpAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.KUBERNETES_AUTH]: { + revokeMethod: revokeKubernetesAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.AWS_AUTH]: { + revokeMethod: revokeAwsAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.JWT_AUTH]: { + revokeMethod: revokeJwtAuth, + render: () => ( + + ) + } + }; + + const isAlreadyConfigured = useCallback((method: IdentityAuthMethod) => { + return identityAuthMethodData?.configuredAuthMethods?.includes(method); + }, []); + + const selectedMethodItem = methodMap[identityAuthMethodData.authMethod!]; + + return ( + <> + ( + + + + )} + /> + {selectedMethodItem?.render ? selectedMethodItem.render() :
} + handlePopUpToggle("upgradePlan", isOpen)} + text="You can use IP allowlisting if you switch to Infisical's Pro plan." + /> + handlePopUpToggle("revokeAuthMethod", isOpen)} + deleteKey="confirm" + buttonText="Remove" + onDeleteApproved={async () => { + if (!identityAuthMethodData.authMethod || !orgId || !selectedMethodItem) { + return; + } + + try { + await selectedMethodItem.revokeMethod({ + identityId: identityAuthMethodData.identityId, + organizationId: orgId + }); + + createNotification({ + text: "Successfully removed auth method", + type: "success" + }); + + handlePopUpToggle("revokeAuthMethod", false); + handlePopUpToggle("identityAuthMethod", false); + } catch { + createNotification({ + text: "Failed to remove auth method", + type: "error" + }); + } + }} + /> + + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx new file mode 100644 index 000000000..59528e3a7 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx @@ -0,0 +1,377 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +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 } from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { + useAddIdentityAwsAuth, + useGetIdentityAwsAuth, + useUpdateIdentityAwsAuth +} 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"; + +const schema = z + .object({ + stsEndpoint: z.string(), + allowedPrincipalArns: z.string(), + allowedAccountIds: z.string(), + accessTokenTTL: z + .string() + .refine( + (value) => Number(value) <= 315360000, + "Access Token TTL cannot be greater than 315360000" + ), + accessTokenMaxTTL: z + .string() + .refine( + (value) => Number(value) <= 315360000, + "Access Token Max TTL cannot be greater than 315360000" + ), + accessTokenNumUsesLimit: z.string(), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().max(50) + }) + .array() + .min(1) + }) + .required(); + +export type FormData = z.infer; + +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 IdentityAwsAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth(); + + const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes( + identityAuthMethodData.authMethod! || "" + ); + const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", { + enabled: isUpdate + }); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + stsEndpoint: "https://sts.amazonaws.com/", + allowedPrincipalArns: "", + allowedAccountIds: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + stsEndpoint: data.stsEndpoint, + allowedPrincipalArns: data.allowedPrincipalArns, + allowedAccountIds: data.allowedAccountIds, + 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({ + stsEndpoint: "https://sts.amazonaws.com/", + allowedPrincipalArns: "", + allowedAccountIds: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + allowedPrincipalArns, + allowedAccountIds, + stsEndpoint, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityAuthMethodData) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + identityId: identityAuthMethodData.identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + stsEndpoint: stsEndpoint || "", + allowedPrincipalArns: allowedPrincipalArns || "", + allowedAccountIds: allowedAccountIds || "", + 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 { + createNotification({ + text: `Failed to ${isUpdate ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeAccessTokenTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+
+ + + +
+ {isUpdate && ( + + )} +
+ + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx new file mode 100644 index 000000000..7af879888 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx @@ -0,0 +1,373 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +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 } from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { + useAddIdentityAzureAuth, + useGetIdentityAzureAuth, + useUpdateIdentityAzureAuth +} 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"; + +const schema = z + .object({ + tenantId: z.string().min(1), + resource: z.string(), + allowedServicePrincipalIds: z.string(), + 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(), + accessTokenTrustedIps: z + .array( + z.object({ + ipAddress: z.string().max(50) + }) + ) + .min(1) + }) + .required(); + +export type FormData = z.infer; + +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 IdentityAzureAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth(); + + const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes( + identityAuthMethodData.authMethod! || "" + ); + const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", { + enabled: isUpdate + }); + + const { + control, + handleSubmit, + reset, + + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + tenantId: "", + resource: "https://management.azure.com/", + allowedServicePrincipalIds: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + tenantId: data.tenantId, + resource: data.resource, + allowedServicePrincipalIds: data.allowedServicePrincipalIds, + 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({ + tenantId: "", + resource: "https://management.azure.com/", + allowedServicePrincipalIds: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + tenantId, + resource, + allowedServicePrincipalIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityAuthMethodData) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + tenantId, + resource, + allowedServicePrincipalIds, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + tenantId: tenantId || "", + resource: resource || "", + allowedServicePrincipalIds: allowedServicePrincipalIds || "", + 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 { + createNotification({ + text: `Failed to ${isUpdate ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeAccessTokenTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+
+ + + +
+ {isUpdate && ( + + )} +
+ + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx new file mode 100644 index 000000000..0cccb8213 --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx @@ -0,0 +1,406 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +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 } from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { + useAddIdentityGcpAuth, + useGetIdentityGcpAuth, + useUpdateIdentityGcpAuth +} 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"; + +const schema = z + .object({ + type: z.enum(["iam", "gce"]), + allowedServiceAccounts: z.string(), + allowedProjects: z.string(), + allowedZones: z.string(), + 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(), + accessTokenTrustedIps: z + .array( + z.object({ + ipAddress: z.string().max(50) + }) + ) + .min(1) + }) + .required(); + +export type FormData = z.infer; + +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 IdentityGcpAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth(); + + const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes( + identityAuthMethodData.authMethod! || "" + ); + const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", { + enabled: isUpdate + }); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting }, + watch + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + type: "gce", + allowedServiceAccounts: "", + allowedProjects: "", + allowedZones: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const watchedType = watch("type"); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + type: data.type || "gce", + allowedServiceAccounts: data.allowedServiceAccounts, + allowedProjects: data.allowedProjects, + allowedZones: data.allowedZones, + 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({ + type: "gce", + allowedServiceAccounts: "", + allowedProjects: "", + allowedZones: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + type, + allowedServiceAccounts, + allowedProjects, + allowedZones, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityAuthMethodData) return; + + if (data) { + await updateMutateAsync({ + identityId: identityAuthMethodData.identityId, + organizationId: orgId, + type, + allowedServiceAccounts, + allowedProjects, + allowedZones, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + identityId: identityAuthMethodData.identityId, + organizationId: orgId, + type, + allowedServiceAccounts: allowedServiceAccounts || "", + allowedProjects: allowedProjects || "", + allowedZones: allowedZones || "", + 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 { + createNotification({ + text: `Failed to ${isUpdate ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + {watchedType === "gce" && ( + ( + + + + )} + /> + )} + {watchedType === "gce" && ( + ( + + + + )} + /> + )} + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeAccessTokenTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+
+ + + +
+ {isUpdate && ( + + )} +
+ + ); +}; diff --git a/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx new file mode 100644 index 000000000..e975b272d --- /dev/null +++ b/frontend-v2/src/routes/_authenticate/_org_details/organization/$organizationId/members/-components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx @@ -0,0 +1,688 @@ +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; + +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({ + 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 { + createNotification({ + text: `Failed to ${isUpdate ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + {selectedConfigurationType === IdentityJwtConfigurationType.JWKS && ( + <> + ( + + + + )} + /> + ( + +