From 74fc3b8cd11997e102e3e8f580247eac6bde92f3 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Wed, 3 Dec 2025 19:23:15 +0100 Subject: [PATCH] Rewrite identity and access management Signed-off-by: Bryan Frimin --- .gitattributes | 2 +- apps/console/package.json | 1 + apps/console/src/components/PageError.tsx | 2 +- apps/console/src/environments.ts | 18 + apps/console/src/layouts/EmployeeLayout.tsx | 9 +- apps/console/src/layouts/MainLayout.tsx | 4 +- .../src/layouts/PublicTrustCenterLayout.tsx | 18 +- apps/console/src/main.tsx | 11 +- apps/console/src/pages/APIKeysPage.tsx | 241 +- .../src/pages/DocumentSigningRequestsPage.tsx | 59 +- .../src/pages/auth/ForgotPasswordPage.tsx | 20 +- apps/console/src/pages/auth/LoginPage.tsx | 32 +- apps/console/src/pages/auth/RegisterPage.tsx | 18 +- .../src/pages/auth/ResetPasswordPage.tsx | 41 +- .../pages/auth/SignupFromInvitationPage.tsx | 32 +- apps/console/src/providers/RelayProviders.tsx | 242 - apps/console/src/routes.tsx | 102 +- apps/console/src/routes/assetRoutes.ts | 10 +- apps/console/src/routes/auditRoutes.ts | 6 +- .../src/routes/continualImprovementRoutes.ts | 10 +- apps/console/src/routes/dataRoutes.ts | 10 +- apps/console/src/routes/documentsRoutes.ts | 6 +- apps/console/src/routes/frameworkRoutes.ts | 8 +- apps/console/src/routes/measureRoutes.ts | 6 +- apps/console/src/routes/meetingsRoutes.ts | 6 +- .../console/src/routes/nonconformityRoutes.ts | 10 +- apps/console/src/routes/obligationRoutes.ts | 10 +- apps/console/src/routes/peopleRoutes.ts | 6 +- .../src/routes/processingActivityRoutes.ts | 10 +- apps/console/src/routes/riskRoutes.ts | 10 +- apps/console/src/routes/snapshotsRoutes.ts | 6 +- apps/console/src/routes/taskRoutes.ts | 4 +- apps/console/src/routes/trustCenterRoutes.ts | 4 +- apps/console/src/routes/vendorRoutes.ts | 10 +- apps/console/vite.config.ts | 1 + apps/trust/src/providers/RelayProviders.tsx | 10 +- apps/trust/src/routes.tsx | 29 +- cfg/dev.yaml | 27 +- compose.yaml | 1 - go.sum | 4 - package-lock.json | 1969 +- package.json | 3 - packages/emails/package.json | 1 + packages/relay/package.json | 31 + packages/relay/src/errors.ts | 53 + packages/relay/src/fetch.ts | 112 + packages/relay/src/index.ts | 2 + packages/relay/tsconfig.json | 6 + pkg/auth/access.go | 88 - pkg/auth/saml_cleanup.go | 109 - pkg/auth/saml_configuration_service.go | 288 - pkg/auth/saml_metadata.go | 204 - pkg/auth/saml_service.go | 556 - pkg/auth/saml_validator.go | 103 - pkg/auth/service.go | 1687 -- pkg/authz/service.go | 933 - pkg/baseurl/baseurl.go | 17 +- pkg/bearertoken/bearertoken.go | 120 + pkg/bearertoken/bearertoken_test.go | 298 + pkg/coredata/asset.go | 24 +- pkg/coredata/audit.go | 24 +- pkg/coredata/control.go | 28 +- pkg/coredata/control_document.go | 16 +- pkg/coredata/custom_domain.go | 20 +- pkg/coredata/document.go | 20 +- pkg/coredata/document_version.go | 35 +- pkg/coredata/document_version_signature.go | 27 +- pkg/coredata/errors.go | 11 + pkg/coredata/evidence.go | 20 +- pkg/coredata/expire_reason.go | 11 + pkg/coredata/file.go | 22 +- pkg/coredata/framework.go | 34 +- pkg/coredata/invitation.go | 37 +- pkg/coredata/measure.go | 22 +- pkg/coredata/meeting.go | 22 +- pkg/coredata/membership.go | 124 +- pkg/coredata/migrations/20251205T120000Z.sql | 9 + pkg/coredata/migrations/20251205T140000Z.sql | 2 + pkg/coredata/migrations/20251205T150000Z.sql | 8 + pkg/coredata/migrations/20251212T120000Z.sql | 2 + pkg/coredata/organization.go | 29 +- pkg/coredata/organization_context.go | 12 +- pkg/coredata/people.go | 34 +- pkg/coredata/report.go | 2 +- pkg/coredata/risk.go | 18 +- pkg/coredata/saml_assertion.go | 23 +- pkg/coredata/saml_configuration.go | 133 +- .../saml_configuration_order_field.go | 40 + pkg/coredata/saml_request.go | 28 +- pkg/coredata/scope.go | 6 +- pkg/coredata/session.go | 211 +- pkg/coredata/session_order_field.go | 11 + pkg/coredata/state_of_applicability.go | 30 +- pkg/coredata/task.go | 22 +- pkg/coredata/trust_center.go | 24 +- pkg/coredata/trust_center_access.go | 24 +- pkg/coredata/trust_center_document_access.go | 34 +- pkg/coredata/trust_center_reference.go | 22 +- pkg/coredata/user.go | 106 +- pkg/coredata/user_api_key.go | 78 +- pkg/coredata/user_api_key_order_field.go | 40 + pkg/coredata/vendor.go | 18 +- .../vendor_business_associate_agreement.go | 24 +- pkg/coredata/vendor_contact.go | 10 +- pkg/coredata/vendor_data_privacy_agreement.go | 24 +- pkg/coredata/vendor_service.go | 10 +- pkg/filemanager/service.go | 14 +- pkg/iam/access_management_service.go | 204 + pkg/iam/account_service.go | 748 + pkg/iam/action_registry.go | 41 + pkg/iam/api_key.go | 78 + pkg/iam/auth_service.go | 488 + pkg/iam/errors.go | 289 + pkg/iam/organization_service.go | 1106 + pkg/{authz => iam}/permissions.go | 4 +- .../saml_mapper.go => iam/saml/attributes.go} | 151 +- pkg/iam/saml/errors.go | 90 + pkg/iam/saml/gc.go | 97 + pkg/iam/saml/saml_metadata.go | 87 + pkg/iam/saml/service.go | 402 + pkg/iam/saml/sp.go | 68 + pkg/{auth => iam}/saml_config_validator.go | 2 +- pkg/iam/service.go | 194 + pkg/iam/session_service.go | 266 + .../validators.go} | 32 +- pkg/probo/document_service.go | 20 +- pkg/probo/organization_service.go | 35 - pkg/probo/service.go | 8 - pkg/probod/auth_config.go | 1 + pkg/probod/probod.go | 176 +- pkg/securetoken/securetoken.go | 91 + pkg/server/api/api.go | 181 +- .../api/connect/v1/api_key_middleware.go | 94 + pkg/server/api/connect/v1/ctxkey.go | 19 + pkg/server/api/connect/v1/gqlgen.yaml | 36 + pkg/server/api/connect/v1/graphql_handler.go | 102 + pkg/server/api/connect/v1/httpctx.go | 51 + pkg/server/api/connect/v1/resolver.go | 66 + pkg/server/api/connect/v1/saml_handler.go | 96 + pkg/server/api/connect/v1/schema.graphql | 769 + pkg/server/api/connect/v1/schema/schema.go | 20067 ++++++++++++++++ .../api/connect/v1/session_middleware.go | 123 + pkg/server/api/connect/v1/types/identity.go | 27 + .../v1/types/invitation.go | 44 +- .../v1/types/membership.go | 40 +- .../types.go => connect/v1/types/order_by.go} | 21 +- .../api/connect/v1/types/organization.go | 32 + pkg/server/api/connect/v1/types/page_info.go | 30 + .../api/connect/v1/types/personal_api_key.go | 69 + .../connect/v1/types/saml_configuration.go | 82 + .../user.go => connect/v1/types/session.go} | 53 +- pkg/server/api/connect/v1/types/types.go | 966 + pkg/server/api/connect/v1/v1_resolver.go | 930 + pkg/server/api/console/v1/resolver.go | 278 +- pkg/server/api/console/v1/schema.graphql | 598 +- pkg/server/api/console/v1/schema/schema.go | 10241 +------- .../api/console/v1/types/organization.go | 25 - pkg/server/api/console/v1/types/people.go | 1 - .../console/v1/types/saml_configuration.go | 44 - .../v1/types/state_of_applicability.go | 8 +- pkg/server/api/console/v1/types/task.go | 10 +- pkg/server/api/console/v1/types/types.go | 370 +- pkg/server/api/console/v1/v1_resolver.go | 2760 +-- pkg/server/api/mcp/mcputils/recovery.go | 8 +- pkg/server/api/mcp/v1/middleware.go | 37 +- pkg/server/api/mcp/v1/resolver.go | 33 +- pkg/server/api/mcp/v1/schema.resolvers.go | 169 +- pkg/server/api/mcp/v1/v1_handler.go | 21 +- pkg/server/api/trust/v1/resolver.go | 249 +- .../trust/v1/trust_center_access_handler.go | 46 +- pkg/server/api/trust/v1/v1_resolver.go | 1067 +- pkg/server/auth/accept_invitation_handler.go | 94 - pkg/server/auth/apikey.go | 120 - pkg/server/auth/auth.go | 77 - pkg/server/auth/auth_middleware.go | 89 - pkg/server/auth/create_api_key_handler.go | 145 - pkg/server/auth/delete_api_key_handler.go | 82 - pkg/server/auth/forget_password_handler.go | 56 - pkg/server/auth/get_api_key_handler.go | 65 - pkg/server/auth/list_api_keys_handler.go | 93 - pkg/server/auth/list_invitations_handler.go | 87 - pkg/server/auth/list_organizations_handler.go | 146 - pkg/server/auth/organization_logo_handler.go | 58 - pkg/server/auth/reset_password_handler.go | 70 - pkg/server/auth/saml_acs_handler.go | 122 - pkg/server/auth/saml_check_sso_handler.go | 96 - pkg/server/auth/saml_login_handler.go | 65 - pkg/server/auth/sign_in_handler.go | 102 - pkg/server/auth/sign_out_handler.go | 61 - pkg/server/auth/sign_up_handler.go | 95 - .../auth/signup_from_invitation_handler.go | 76 - pkg/server/auth/update_api_key_handler.go | 159 - pkg/server/authz/authz.go | 121 - pkg/server/authz/permissions_handler.go | 76 - pkg/server/gqlutils/handler.go | 71 + pkg/server/gqlutils/recovery.go | 42 +- pkg/server/server.go | 94 +- pkg/server/session/session.go | 149 - .../{token.go => statelesstoken.go} | 0 pkg/trust/service.go | 63 +- pkg/trust/trust_center_access_service.go | 248 +- 201 files changed, 32895 insertions(+), 23649 deletions(-) create mode 100644 apps/console/src/environments.ts delete mode 100644 apps/console/src/providers/RelayProviders.tsx create mode 100644 packages/relay/package.json create mode 100644 packages/relay/src/errors.ts create mode 100644 packages/relay/src/fetch.ts create mode 100644 packages/relay/src/index.ts create mode 100644 packages/relay/tsconfig.json delete mode 100644 pkg/auth/access.go delete mode 100644 pkg/auth/saml_cleanup.go delete mode 100644 pkg/auth/saml_configuration_service.go delete mode 100644 pkg/auth/saml_metadata.go delete mode 100644 pkg/auth/saml_service.go delete mode 100644 pkg/auth/saml_validator.go delete mode 100644 pkg/auth/service.go delete mode 100644 pkg/authz/service.go create mode 100644 pkg/bearertoken/bearertoken.go create mode 100644 pkg/bearertoken/bearertoken_test.go create mode 100644 pkg/coredata/errors.go create mode 100644 pkg/coredata/expire_reason.go create mode 100644 pkg/coredata/migrations/20251205T120000Z.sql create mode 100644 pkg/coredata/migrations/20251205T140000Z.sql create mode 100644 pkg/coredata/migrations/20251205T150000Z.sql create mode 100644 pkg/coredata/migrations/20251212T120000Z.sql create mode 100644 pkg/coredata/saml_configuration_order_field.go create mode 100644 pkg/coredata/user_api_key_order_field.go create mode 100644 pkg/iam/access_management_service.go create mode 100644 pkg/iam/account_service.go create mode 100644 pkg/iam/action_registry.go create mode 100644 pkg/iam/api_key.go create mode 100644 pkg/iam/auth_service.go create mode 100644 pkg/iam/errors.go create mode 100644 pkg/iam/organization_service.go rename pkg/{authz => iam}/permissions.go (99%) rename pkg/{auth/saml_mapper.go => iam/saml/attributes.go} (65%) create mode 100644 pkg/iam/saml/errors.go create mode 100644 pkg/iam/saml/gc.go create mode 100644 pkg/iam/saml/saml_metadata.go create mode 100644 pkg/iam/saml/service.go create mode 100644 pkg/iam/saml/sp.go rename pkg/{auth => iam}/saml_config_validator.go (99%) create mode 100644 pkg/iam/service.go create mode 100644 pkg/iam/session_service.go rename pkg/{server/auth/saml_metadata_handler.go => iam/validators.go} (55%) create mode 100644 pkg/securetoken/securetoken.go create mode 100644 pkg/server/api/connect/v1/api_key_middleware.go create mode 100644 pkg/server/api/connect/v1/ctxkey.go create mode 100644 pkg/server/api/connect/v1/gqlgen.yaml create mode 100644 pkg/server/api/connect/v1/graphql_handler.go create mode 100644 pkg/server/api/connect/v1/httpctx.go create mode 100644 pkg/server/api/connect/v1/resolver.go create mode 100644 pkg/server/api/connect/v1/saml_handler.go create mode 100644 pkg/server/api/connect/v1/schema.graphql create mode 100644 pkg/server/api/connect/v1/schema/schema.go create mode 100644 pkg/server/api/connect/v1/session_middleware.go create mode 100644 pkg/server/api/connect/v1/types/identity.go rename pkg/server/api/{console => connect}/v1/types/invitation.go (61%) rename pkg/server/api/{console => connect}/v1/types/membership.go (64%) rename pkg/server/api/{mcp/v1/types.go => connect/v1/types/order_by.go} (73%) create mode 100644 pkg/server/api/connect/v1/types/organization.go create mode 100644 pkg/server/api/connect/v1/types/page_info.go create mode 100644 pkg/server/api/connect/v1/types/personal_api_key.go create mode 100644 pkg/server/api/connect/v1/types/saml_configuration.go rename pkg/server/api/{console/v1/types/user.go => connect/v1/types/session.go} (54%) create mode 100644 pkg/server/api/connect/v1/types/types.go create mode 100644 pkg/server/api/connect/v1/v1_resolver.go delete mode 100644 pkg/server/api/console/v1/types/saml_configuration.go delete mode 100644 pkg/server/auth/accept_invitation_handler.go delete mode 100644 pkg/server/auth/apikey.go delete mode 100644 pkg/server/auth/auth.go delete mode 100644 pkg/server/auth/auth_middleware.go delete mode 100644 pkg/server/auth/create_api_key_handler.go delete mode 100644 pkg/server/auth/delete_api_key_handler.go delete mode 100644 pkg/server/auth/forget_password_handler.go delete mode 100644 pkg/server/auth/get_api_key_handler.go delete mode 100644 pkg/server/auth/list_api_keys_handler.go delete mode 100644 pkg/server/auth/list_invitations_handler.go delete mode 100644 pkg/server/auth/list_organizations_handler.go delete mode 100644 pkg/server/auth/organization_logo_handler.go delete mode 100644 pkg/server/auth/reset_password_handler.go delete mode 100644 pkg/server/auth/saml_acs_handler.go delete mode 100644 pkg/server/auth/saml_check_sso_handler.go delete mode 100644 pkg/server/auth/saml_login_handler.go delete mode 100644 pkg/server/auth/sign_in_handler.go delete mode 100644 pkg/server/auth/sign_out_handler.go delete mode 100644 pkg/server/auth/sign_up_handler.go delete mode 100644 pkg/server/auth/signup_from_invitation_handler.go delete mode 100644 pkg/server/auth/update_api_key_handler.go delete mode 100644 pkg/server/authz/authz.go delete mode 100644 pkg/server/authz/permissions_handler.go create mode 100644 pkg/server/gqlutils/handler.go delete mode 100644 pkg/server/session/session.go rename pkg/statelesstoken/{token.go => statelesstoken.go} (100%) diff --git a/.gitattributes b/.gitattributes index ccc5c1871..2cc76dc72 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -pkg/server/api/console/v1/v1_resolver.go linguist-generated=false +pkg/server/api/console/v1/v1_resolver.go linguist-detectable diff --git a/apps/console/package.json b/apps/console/package.json index f21835b5f..6f8c77c89 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -18,6 +18,7 @@ "@probo/hooks": "1.0.0", "@probo/i18n": "1.0.0", "@probo/react-lazy": "1.0.0", + "@probo/relay": "^1.0.0", "@probo/routes": "^1.0.0", "@probo/ui": "1.0.0", "@probo/vendors": "0.0.1", diff --git a/apps/console/src/components/PageError.tsx b/apps/console/src/components/PageError.tsx index 0e9ff3356..4b64f1682 100644 --- a/apps/console/src/components/PageError.tsx +++ b/apps/console/src/components/PageError.tsx @@ -2,7 +2,7 @@ import { useLocation, useRouteError } from "react-router"; import { IconPageCross } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { useEffect, useRef } from "react"; -import { AuthenticationRequiredError } from "/providers/RelayProviders"; +import { AuthenticationRequiredError } from "@probo/relay"; const classNames = { wrapper: "py-10 text-center space-y-2 ", diff --git a/apps/console/src/environments.ts b/apps/console/src/environments.ts new file mode 100644 index 000000000..2e1ac42c4 --- /dev/null +++ b/apps/console/src/environments.ts @@ -0,0 +1,18 @@ +import { Environment, Network, RecordSource, Store } from "relay-runtime"; +import { makeFetchQuery } from "@probo/relay"; + +const source = new RecordSource(); +const store = new Store(source, { + queryCacheExpirationTime: 1 * 60 * 1000, + gcReleaseBufferSize: 20, +}); + +export const consoleEnvironment = new Environment({ + network: Network.create(makeFetchQuery("/api/console/v1/query")), + store, +}); + +export const connectEnvironment = new Environment({ + network: Network.create(makeFetchQuery("/api/connect/v1/query")), + store, +}); diff --git a/apps/console/src/layouts/EmployeeLayout.tsx b/apps/console/src/layouts/EmployeeLayout.tsx index 359a85b7b..82b7a7173 100644 --- a/apps/console/src/layouts/EmployeeLayout.tsx +++ b/apps/console/src/layouts/EmployeeLayout.tsx @@ -32,7 +32,6 @@ import type { EmployeeLayoutQuery as EmployeeLayoutQueryType } from "./__generat import { Suspense, useState, useEffect, useMemo, use } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { PageError } from "/components/PageError"; -import { buildEndpoint } from "/providers/RelayProviders"; import { PermissionsProvider } from "/providers/PermissionsProvider"; import { PermissionsContext } from "/providers/PermissionsContext"; @@ -71,11 +70,7 @@ export function EmployeeLayout() { ); } -function EmployeeLayoutContent({ - organizationId, -}: { - organizationId: string; -}) { +function EmployeeLayoutContent({ organizationId }: { organizationId: string }) { const data = useLazyLoadQuery(EmployeeLayoutQuery, { organizationId, }); @@ -352,7 +347,7 @@ function UserDropdown({ organizationId }: { organizationId: string }) { ) => { e.preventDefault(); - fetch(buildEndpoint("/connect/logout"), { + fetch("/connect/logout", { method: "DELETE", headers: { "Content-Type": "application/json", diff --git a/apps/console/src/layouts/MainLayout.tsx b/apps/console/src/layouts/MainLayout.tsx index aa9766d35..6ca0ed74d 100644 --- a/apps/console/src/layouts/MainLayout.tsx +++ b/apps/console/src/layouts/MainLayout.tsx @@ -50,7 +50,6 @@ import { graphql } from "relay-runtime"; import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql"; import { PageError } from "/components/PageError"; import { PermissionsProvider } from "/providers/PermissionsProvider"; -import { buildEndpoint } from "/providers/RelayProviders"; import { PermissionsContext } from "/providers/PermissionsContext"; const MainLayoutQuery = graphql` @@ -283,7 +282,7 @@ function UserDropdown({ organizationId }: { organizationId: string }) { ) => { e.preventDefault(); - fetch(buildEndpoint("/connect/logout"), { + fetch("/connect/logout", { method: "DELETE", headers: { "Content-Type": "application/json", @@ -371,7 +370,6 @@ interface InvitationsResponse { invitations: Invitation[]; } - function OrganizationSelector({ currentOrganization, }: { diff --git a/apps/console/src/layouts/PublicTrustCenterLayout.tsx b/apps/console/src/layouts/PublicTrustCenterLayout.tsx index 7d9e821b3..9c51cdbaa 100644 --- a/apps/console/src/layouts/PublicTrustCenterLayout.tsx +++ b/apps/console/src/layouts/PublicTrustCenterLayout.tsx @@ -1,7 +1,6 @@ import { Outlet } from "react-router"; import { Logo, Button, IconArrowBoxLeft, useToast } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; -import { buildEndpoint } from "/providers/RelayProviders"; import type { ReactNode } from "react"; type Props = { @@ -11,18 +10,23 @@ type Props = { isAuthenticated?: boolean; }; -export function PublicTrustCenterLayout({ organizationName, organizationLogo, children, isAuthenticated }: Props) { +export function PublicTrustCenterLayout({ + organizationName, + organizationLogo, + children, + isAuthenticated, +}: Props) { const { __ } = useTranslate(); const { toast } = useToast(); - const handleLogout = async () => { + const handleLogout = async () => { try { - const response = await fetch(buildEndpoint('/api/trust/v1/auth/logout'), { - method: 'DELETE', + const response = await fetch("/api/trust/v1/auth/logout", { + method: "DELETE", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - credentials: 'include', + credentials: "include", }); if (!response.ok) { diff --git a/apps/console/src/main.tsx b/apps/console/src/main.tsx index 879d96c1a..1a490ab75 100644 --- a/apps/console/src/main.tsx +++ b/apps/console/src/main.tsx @@ -1,7 +1,6 @@ import { createRoot } from "react-dom/client"; import "./index.css"; import { App } from "./App"; -import { RelayProvider } from "./providers/RelayProviders"; import { TranslatorProvider } from "./providers/TranslatorProvider"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -15,10 +14,8 @@ const queryClient = new QueryClient({ createRoot(document.getElementById("root")!).render( - - - - - - , + + + + ); diff --git a/apps/console/src/pages/APIKeysPage.tsx b/apps/console/src/pages/APIKeysPage.tsx index c0bd6661f..60d17001b 100644 --- a/apps/console/src/pages/APIKeysPage.tsx +++ b/apps/console/src/pages/APIKeysPage.tsx @@ -31,7 +31,7 @@ import { formatDate } from "@probo/helpers"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { Controller } from "react-hook-form"; import { z } from "zod"; -import { UnAuthenticatedError } from "/providers/RelayProviders"; +import { UnAuthenticatedError } from "@probo/relay"; interface APIKey { id: string; @@ -56,10 +56,14 @@ interface Organization { const createSchema = z.object({ name: z.string().min(1, "Name is required"), expiresIn: z.enum(["1month", "3months", "6months", "1year"]), - organizations: z.array(z.object({ - organizationId: z.string(), - role: z.string(), - })).min(1, "At least one organization is required"), + organizations: z + .array( + z.object({ + organizationId: z.string(), + role: z.string(), + }) + ) + .min(1, "At least one organization is required"), }); type CreateFormData = z.infer; @@ -79,19 +83,24 @@ export default function APIKeysPage() { const [isCreating, setIsCreating] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [isDeleting, setIsDeleting] = useState(false); - const [selectedOrganizations, setSelectedOrganizations] = useState([]); - const [organizationRoles, setOrganizationRoles] = useState>({}); + const [selectedOrganizations, setSelectedOrganizations] = useState( + [] + ); + const [organizationRoles, setOrganizationRoles] = useState< + Record + >({}); const [editingAPIKey, setEditingAPIKey] = useState(null); const [editingName, setEditingName] = useState(""); const [error, setError] = useState(null); - const { formState, handleSubmit, reset, control, setValue, register } = useFormWithSchema(createSchema, { - defaultValues: { - name: new Date().toISOString().split('T')[0], - expiresIn: "1month", - organizations: [], - }, - }); + const { formState, handleSubmit, reset, control, setValue, register } = + useFormWithSchema(createSchema, { + defaultValues: { + name: new Date().toISOString().split("T")[0], + expiresIn: "1month", + organizations: [], + }, + }); if (error) { throw error; @@ -99,14 +108,16 @@ export default function APIKeysPage() { const fetchAPIKeys = async () => { try { - const response = await fetch('/connect/api-keys', { credentials: 'include' }); + const response = await fetch("/connect/api-keys", { + credentials: "include", + }); if (!response.ok) { - throw new Error('Failed to fetch API keys'); + throw new Error("Failed to fetch API keys"); } const data: { apiKeys: APIKey[] } = await response.json(); setApiKeys(data.apiKeys); } catch (err) { - console.error('Failed to fetch API keys:', err); + console.error("Failed to fetch API keys:", err); toast({ title: __("Error"), description: __("Failed to load API keys"), @@ -119,32 +130,40 @@ export default function APIKeysPage() { const fetchData = async () => { try { const [apiKeysResponse, organizationsResponse] = await Promise.all([ - fetch('/connect/api-keys', { credentials: 'include' }), - fetch('/connect/organizations?role=OWNER', { credentials: 'include' }), + fetch("/connect/api-keys", { credentials: "include" }), + fetch("/connect/organizations?role=OWNER", { + credentials: "include", + }), ]); - if (apiKeysResponse.status === 401 || organizationsResponse.status === 401) { + if ( + apiKeysResponse.status === 401 || + organizationsResponse.status === 401 + ) { setError(new UnAuthenticatedError()); return; } if (!apiKeysResponse.ok) { - throw new Error('Failed to fetch API keys'); + throw new Error("Failed to fetch API keys"); } if (!organizationsResponse.ok) { - throw new Error('Failed to fetch organizations'); + throw new Error("Failed to fetch organizations"); } const apiKeysData: { apiKeys: APIKey[] } = await apiKeysResponse.json(); - const orgsData: { organizations: Organization[] } = await organizationsResponse.json(); + const orgsData: { organizations: Organization[] } = + await organizationsResponse.json(); - const authenticatedOrgs = orgsData.organizations.filter(org => org.authStatus === "authenticated"); + const authenticatedOrgs = orgsData.organizations.filter( + (org) => org.authStatus === "authenticated" + ); setApiKeys(apiKeysData.apiKeys); setOrganizations(authenticatedOrgs); } catch (err) { - console.error('Failed to fetch data:', err); + console.error("Failed to fetch data:", err); toast({ title: __("Error"), description: __("Failed to load data"), @@ -179,12 +198,12 @@ export default function APIKeysPage() { setIsCreating(true); try { - const response = await fetch('/connect/api-keys', { - method: 'POST', + const response = await fetch("/connect/api-keys", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - credentials: 'include', + credentials: "include", body: JSON.stringify({ name: formData.name, expiresAt: expiresAt.toISOString(), @@ -193,7 +212,7 @@ export default function APIKeysPage() { }); if (!response.ok) { - throw new Error('Failed to create API key'); + throw new Error("Failed to create API key"); } const data: { apiKey: APIKey; key: string } = await response.json(); @@ -224,9 +243,9 @@ export default function APIKeysPage() { const handleEdit = (apiKey: APIKey) => { setEditingAPIKey(apiKey); setEditingName(apiKey.name); - const orgIds = apiKey.organizations.map(org => org.organizationId); + const orgIds = apiKey.organizations.map((org) => org.organizationId); const roles: Record = {}; - apiKey.organizations.forEach(org => { + apiKey.organizations.forEach((org) => { roles[org.organizationId] = org.role; }); setSelectedOrganizations(orgIds); @@ -239,16 +258,16 @@ export default function APIKeysPage() { setIsUpdating(true); try { - const response = await fetch('/connect/api-keys', { - method: 'PUT', + const response = await fetch("/connect/api-keys", { + method: "PUT", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - credentials: 'include', + credentials: "include", body: JSON.stringify({ id: editingAPIKey.id, name: editingName, - organizations: selectedOrganizations.map(id => ({ + organizations: selectedOrganizations.map((id) => ({ organizationId: id, role: organizationRoles[id] || "FULL", })), @@ -256,7 +275,7 @@ export default function APIKeysPage() { }); if (!response.ok) { - throw new Error('Failed to update API key'); + throw new Error("Failed to update API key"); } await fetchAPIKeys(); @@ -286,20 +305,20 @@ export default function APIKeysPage() { async () => { setIsDeleting(true); try { - const response = await fetch('/connect/api-keys', { - method: 'DELETE', + const response = await fetch("/connect/api-keys", { + method: "DELETE", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - credentials: 'include', + credentials: "include", body: JSON.stringify({ id }), }); if (!response.ok) { - throw new Error('Failed to delete API key'); + throw new Error("Failed to delete API key"); } - setApiKeys(apiKeys.filter(key => key.id !== id)); + setApiKeys(apiKeys.filter((key) => key.id !== id)); toast({ title: __("Success"), description: __("API Key deleted successfully"), @@ -317,7 +336,9 @@ export default function APIKeysPage() { } }, { - message: __(`Are you sure you want to delete the API key "${name}"? This action cannot be undone.`), + message: __( + `Are you sure you want to delete the API key "${name}"? This action cannot be undone.` + ), } ); }; @@ -326,11 +347,11 @@ export default function APIKeysPage() { setIsLoadingKey(true); try { const response = await fetch(`/connect/api-keys/${id}`, { - credentials: 'include', + credentials: "include", }); if (!response.ok) { - throw new Error('Failed to load API key'); + throw new Error("Failed to load API key"); } const data: { key: string } = await response.json(); @@ -400,7 +421,7 @@ export default function APIKeysPage() { return ( -
+

{apiKey.name}

@@ -464,7 +485,9 @@ export default function APIKeysPage() { {__("Create an API key")}

- {__("Generate a new API key for programmatic access to your organization")} + {__( + "Generate a new API key for programmatic access to your organization" + )}

)}
@@ -556,9 +582,7 @@ export default function APIKeysPage() { {__("Name")} {__("Role")} -
- {__("Access")} -
+
{__("Access")}
@@ -570,30 +594,33 @@ export default function APIKeysPage() { {org.name}
- -
- {selectedOrganizations.includes(org.id) ? ( - - ) : ( - - )} -
- + +
+ {selectedOrganizations.includes(org.id) ? ( + + ) : ( + + )} +
+
id !== org.id); + newSelected = selectedOrganizations.filter( + (id) => id !== org.id + ); delete newRoles[org.id]; } setSelectedOrganizations(newSelected); setOrganizationRoles(newRoles); setValue( "organizations", - newSelected.map(id => ({ + newSelected.map((id) => ({ organizationId: id, - role: newRoles[id] || "FULL" + role: newRoles[id] || "FULL", })) ); }} @@ -660,14 +692,15 @@ export default function APIKeysPage() { type="button" variant="tertiary" onClick={() => { - const allSelected = selectedOrganizations.length === organizations.length; + const allSelected = + selectedOrganizations.length === organizations.length; if (allSelected) { setSelectedOrganizations([]); setOrganizationRoles({}); } else { - const allOrgIds = organizations.map(org => org.id); + const allOrgIds = organizations.map((org) => org.id); const newRoles: Record = {}; - allOrgIds.forEach(id => { + allOrgIds.forEach((id) => { newRoles[id] = organizationRoles[id] || "FULL"; }); setSelectedOrganizations(allOrgIds); @@ -676,7 +709,9 @@ export default function APIKeysPage() { }} className="text-xs h-7 min-h-7" > - {selectedOrganizations.length === organizations.length ? __("Clear All") : __("Select All")} + {selectedOrganizations.length === organizations.length + ? __("Clear All") + : __("Select All")} )}
@@ -692,9 +727,7 @@ export default function APIKeysPage() { {__("Name")} {__("Role")} -
- {__("Access")} -
+
{__("Access")}
@@ -712,7 +745,10 @@ export default function APIKeysPage() {