diff --git a/GNUmakefile b/GNUmakefile index c175f1788..5b392343e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -199,3 +199,4 @@ compose/pebble/certs/rootCA.pem: localhost 127.0.0.1 ::1 pebble $(CP) "$$($(MKCERT) -CAROOT)/rootCA.pem" compose/pebble/certs/rootCA.pem $(CP) "$$($(MKCERT) -CAROOT)/rootCA-key.pem" compose/pebble/certs/rootCA-key.pem + diff --git a/apps/console/src/components/PageError.tsx b/apps/console/src/components/PageError.tsx index a8c414fce..a315db75d 100644 --- a/apps/console/src/components/PageError.tsx +++ b/apps/console/src/components/PageError.tsx @@ -2,6 +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"; const classNames = { wrapper: "py-10 text-center space-y-2 ", @@ -32,6 +33,27 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) { } }, [location, resetErrorBoundary]); + useEffect(() => { + if (error instanceof AuthenticationRequiredError) { + window.location.href = error.redirectUrl; + } + }, [error]); + + if (error instanceof AuthenticationRequiredError) { + return ( +
+

+ {__("Additional authentication required")} +

+

+ {error.requiresSaml + ? __("Redirecting to SAML authentication...") + : __("Redirecting to login...")} +

+
+ ); + } + if (!error || (error && error.toString().includes("PAGE_NOT_FOUND"))) { return (
diff --git a/apps/console/src/hooks/graph/OrganizationGraph.ts b/apps/console/src/hooks/graph/OrganizationGraph.ts index 843af3484..2d4330cb5 100644 --- a/apps/console/src/hooks/graph/OrganizationGraph.ts +++ b/apps/console/src/hooks/graph/OrganizationGraph.ts @@ -10,8 +10,6 @@ export const organizationViewQuery = graphql` id name ...SettingsPageFragment - ...SettingsPageMembershipsFragment - ...SettingsPageInvitationsFragment } } } diff --git a/apps/console/src/hooks/graph/SAMLConfigurationGraph.ts b/apps/console/src/hooks/graph/SAMLConfigurationGraph.ts new file mode 100644 index 000000000..4e3767fa3 --- /dev/null +++ b/apps/console/src/hooks/graph/SAMLConfigurationGraph.ts @@ -0,0 +1,205 @@ +import { graphql } from "relay-runtime"; +import { useMutationWithToasts } from "../useMutationWithToasts"; +import type { SAMLConfigurationGraphCreateMutation } from "./__generated__/SAMLConfigurationGraphCreateMutation.graphql"; +import type { SAMLConfigurationGraphUpdateMutation } from "./__generated__/SAMLConfigurationGraphUpdateMutation.graphql"; +import type { SAMLConfigurationGraphDeleteMutation } from "./__generated__/SAMLConfigurationGraphDeleteMutation.graphql"; +import type { SAMLConfigurationGraphEnableMutation } from "./__generated__/SAMLConfigurationGraphEnableMutation.graphql"; +import type { SAMLConfigurationGraphDisableMutation } from "./__generated__/SAMLConfigurationGraphDisableMutation.graphql"; +import type { SAMLConfigurationGraphInitiateDomainVerificationMutation } from "./__generated__/SAMLConfigurationGraphInitiateDomainVerificationMutation.graphql"; +import type { SAMLConfigurationGraphVerifyDomainMutation } from "./__generated__/SAMLConfigurationGraphVerifyDomainMutation.graphql"; + +const createSAMLConfigurationMutation = graphql` + mutation SAMLConfigurationGraphCreateMutation( + $input: CreateSAMLConfigurationInput! + ) { + createSAMLConfiguration(input: $input) { + samlConfiguration { + id + enabled + emailDomain + enforcementPolicy + spEntityId + spAcsUrl + spMetadataUrl + testLoginUrl + idpEntityId + idpSsoUrl + idpCertificate + idpMetadataUrl + attributeEmail + attributeFirstname + attributeLastname + attributeRole + defaultRole + autoSignupEnabled + createdAt + updatedAt + } + } + } +`; + +const updateSAMLConfigurationMutation = graphql` + mutation SAMLConfigurationGraphUpdateMutation( + $input: UpdateSAMLConfigurationInput! + ) { + updateSAMLConfiguration(input: $input) { + samlConfiguration { + id + enabled + emailDomain + enforcementPolicy + spEntityId + spAcsUrl + spMetadataUrl + testLoginUrl + idpEntityId + idpSsoUrl + idpCertificate + idpMetadataUrl + attributeEmail + attributeFirstname + attributeLastname + attributeRole + defaultRole + autoSignupEnabled + createdAt + updatedAt + } + } + } +`; + +const deleteSAMLConfigurationMutation = graphql` + mutation SAMLConfigurationGraphDeleteMutation( + $input: DeleteSAMLConfigurationInput! + ) { + deleteSAMLConfiguration(input: $input) { + deletedSAMLConfigurationId + } + } +`; + +const enableSAMLMutation = graphql` + mutation SAMLConfigurationGraphEnableMutation($input: EnableSAMLInput!) { + enableSAML(input: $input) { + samlConfiguration { + id + enabled + } + } + } +`; + +const disableSAMLMutation = graphql` + mutation SAMLConfigurationGraphDisableMutation($input: DisableSAMLInput!) { + disableSAML(input: $input) { + samlConfiguration { + id + enabled + } + } + } +`; + +const initiateDomainVerificationMutation = graphql` + mutation SAMLConfigurationGraphInitiateDomainVerificationMutation( + $input: InitiateDomainVerificationInput! + ) { + initiateDomainVerification(input: $input) { + samlConfiguration { + id + emailDomain + domainVerified + domainVerificationToken + domainVerifiedAt + } + dnsRecord + } + } +`; + +const verifyDomainMutation = graphql` + mutation SAMLConfigurationGraphVerifyDomainMutation( + $input: VerifyDomainInput! + ) { + verifyDomain(input: $input) { + samlConfiguration { + id + domainVerified + domainVerifiedAt + } + verified + } + } +`; + +export function useCreateSAMLConfigurationMutation() { + return useMutationWithToasts( + createSAMLConfigurationMutation, + { + successMessage: "SAML configuration created successfully.", + errorMessage: "Failed to create SAML configuration. Please try again.", + } + ); +} + +export function useUpdateSAMLConfigurationMutation() { + return useMutationWithToasts( + updateSAMLConfigurationMutation, + { + successMessage: "SAML configuration updated successfully.", + errorMessage: "Failed to update SAML configuration. Please try again.", + } + ); +} + +export function useDeleteSAMLConfigurationMutation() { + return useMutationWithToasts( + deleteSAMLConfigurationMutation, + { + successMessage: "SAML configuration deleted successfully.", + errorMessage: "Failed to delete SAML configuration. Please try again.", + } + ); +} + +export function useEnableSAMLMutation() { + return useMutationWithToasts( + enableSAMLMutation, + { + successMessage: "SAML enabled successfully.", + errorMessage: "Failed to enable SAML. Please try again.", + } + ); +} + +export function useDisableSAMLMutation() { + return useMutationWithToasts( + disableSAMLMutation, + { + successMessage: "SAML disabled successfully.", + errorMessage: "Failed to disable SAML. Please try again.", + } + ); +} + +export function useInitiateDomainVerificationMutation() { + return useMutationWithToasts( + initiateDomainVerificationMutation, + { + successMessage: "Domain verification initiated. Please add the DNS record.", + errorMessage: "Failed to initiate domain verification. Please try again.", + } + ); +} + +export function useVerifyDomainMutation() { + return useMutationWithToasts( + verifyDomainMutation, + { + successMessage: "Domain verified successfully!", + errorMessage: "Domain verification failed. Please ensure the DNS record is properly configured.", + } + ); +} diff --git a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts index 5f51da2a5..846b7d3b9 100644 --- a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<220f85396d6d4c5c04f730d07aaa42e1>> * @lightSyntaxTransform * @nogrep */ @@ -17,7 +17,7 @@ export type OrganizationGraph_ViewQuery$data = { readonly node: { readonly id?: string; readonly name?: string; - readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment" | "SettingsPageInvitationsFragment" | "SettingsPageMembershipsFragment">; + readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment">; }; }; export type OrganizationGraph_ViewQuery = { @@ -82,52 +82,50 @@ v7 = { "name": "updatedAt", "storageKey": null }, -v8 = { - "kind": "Literal", - "name": "first", - "value": 20 -}, -v9 = { - "kind": "Literal", - "name": "orderBy", - "value": { - "direction": "ASC", - "field": "CREATED_AT" +v8 = [ + { + "kind": "Literal", + "name": "first", + "value": 20 + }, + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "CREATED_AT" + } } -}, -v10 = [ - (v8/*: any*/), - (v9/*: any*/) ], -v11 = { +v9 = { "alias": null, "args": null, "kind": "ScalarField", "name": "totalCount", "storageKey": null }, -v12 = { +v10 = { "alias": null, "args": null, "kind": "ScalarField", "name": "fullName", "storageKey": null }, -v13 = { +v11 = { "alias": null, "args": null, "kind": "ScalarField", "name": "role", "storageKey": null }, -v14 = { +v12 = { "alias": null, "args": null, "kind": "ScalarField", "name": "cursor", "storageKey": null }, -v15 = { +v13 = { "alias": null, "args": null, "concreteType": "PageInfo", @@ -166,7 +164,7 @@ v15 = { ], "storageKey": null }, -v16 = { +v14 = { "kind": "ClientExtension", "selections": [ { @@ -178,19 +176,8 @@ v16 = { } ] }, -v17 = [ - { - "kind": "Literal", - "name": "filter", - "value": { - "statuses": [ - "PENDING", - "EXPIRED" - ] - } - }, - (v8/*: any*/), - (v9/*: any*/) +v15 = [ + "orderBy" ]; return { "fragment": { @@ -216,16 +203,6 @@ return { "args": null, "kind": "FragmentSpread", "name": "SettingsPageFragment" - }, - { - "args": null, - "kind": "FragmentSpread", - "name": "SettingsPageMembershipsFragment" - }, - { - "args": null, - "kind": "FragmentSpread", - "name": "SettingsPageInvitationsFragment" } ], "type": "Organization", @@ -294,6 +271,146 @@ return { "name": "headquarterAddress", "storageKey": null }, + (v6/*: any*/), + (v7/*: any*/), + { + "alias": null, + "args": (v8/*: any*/), + "concreteType": "MembershipConnection", + "kind": "LinkedField", + "name": "memberships", + "plural": false, + "selections": [ + (v9/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "MembershipEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v10/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "emailAddress", + "storageKey": null + }, + (v11/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "authMethod", + "storageKey": null + }, + (v6/*: any*/), + (v4/*: any*/) + ], + "storageKey": null + }, + (v12/*: any*/) + ], + "storageKey": null + }, + (v13/*: any*/), + (v14/*: any*/) + ], + "storageKey": "memberships(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" + }, + { + "alias": null, + "args": (v8/*: any*/), + "filters": (v15/*: any*/), + "handle": "connection", + "key": "MembersSettingsTabMemberships_memberships", + "kind": "LinkedHandle", + "name": "memberships" + }, + { + "alias": null, + "args": (v8/*: any*/), + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "invitations", + "plural": false, + "selections": [ + (v9/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "InvitationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v10/*: any*/), + (v5/*: any*/), + (v11/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + (v6/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "acceptedAt", + "storageKey": null + }, + (v4/*: any*/) + ], + "storageKey": null + }, + (v12/*: any*/) + ], + "storageKey": null + }, + (v13/*: any*/), + (v14/*: any*/) + ], + "storageKey": "invitations(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" + }, + { + "alias": null, + "args": (v8/*: any*/), + "filters": (v15/*: any*/), + "handle": "connection", + "key": "MembersSettingsTabInvitations_invitations", + "kind": "LinkedHandle", + "name": "invitations" + }, { "alias": null, "args": null, @@ -369,143 +486,157 @@ return { ], "storageKey": null }, - (v6/*: any*/), - (v7/*: any*/), { "alias": null, - "args": (v10/*: any*/), - "concreteType": "MembershipConnection", + "args": null, + "concreteType": "SAMLConfiguration", "kind": "LinkedField", - "name": "memberships", - "plural": false, + "name": "samlConfigurations", + "plural": true, "selections": [ - (v11/*: any*/), + (v2/*: any*/), { "alias": null, "args": null, - "concreteType": "MembershipEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Membership", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - (v12/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "emailAddress", - "storageKey": null - }, - (v13/*: any*/), - (v6/*: any*/), - (v4/*: any*/) - ], - "storageKey": null - }, - (v14/*: any*/) - ], + "kind": "ScalarField", + "name": "enabled", "storageKey": null }, - (v15/*: any*/), - (v16/*: any*/) - ], - "storageKey": "memberships(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" - }, - { - "alias": null, - "args": (v10/*: any*/), - "filters": [ - "orderBy" - ], - "handle": "connection", - "key": "SettingsPageMemberships_memberships", - "kind": "LinkedHandle", - "name": "memberships" - }, - { - "alias": null, - "args": (v17/*: any*/), - "concreteType": "InvitationConnection", - "kind": "LinkedField", - "name": "invitations", - "plural": false, - "selections": [ - (v11/*: any*/), { "alias": null, "args": null, - "concreteType": "InvitationEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Invitation", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - (v5/*: any*/), - (v12/*: any*/), - (v13/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "status", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "expiresAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "acceptedAt", - "storageKey": null - }, - (v6/*: any*/), - (v4/*: any*/) - ], - "storageKey": null - }, - (v14/*: any*/) - ], + "kind": "ScalarField", + "name": "emailDomain", "storageKey": null }, - (v15/*: any*/), - (v16/*: any*/) + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "enforcementPolicy", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "domainVerified", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "domainVerificationToken", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "domainVerifiedAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "spEntityId", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "spAcsUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "spMetadataUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "testLoginUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpEntityId", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpSsoUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpCertificate", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpMetadataUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeEmail", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeFirstname", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeLastname", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeRole", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "defaultRole", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "autoSignupEnabled", + "storageKey": null + } ], - "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\",\"EXPIRED\"]},first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" - }, - { - "alias": null, - "args": (v17/*: any*/), - "filters": [ - "orderBy", - "filter" - ], - "handle": "connection", - "key": "SettingsPageInvitations_invitations", - "kind": "LinkedHandle", - "name": "invitations" + "storageKey": null } ], "type": "Organization", @@ -517,16 +648,16 @@ return { ] }, "params": { - "cacheID": "3d38000d9d2105ef8ae3d56edf31b372", + "cacheID": "782dc2328cea4f0c350427bda92a5615", "id": null, "metadata": {}, "name": "OrganizationGraph_ViewQuery", "operationKind": "query", - "text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n ...SettingsPageMembershipsFragment\n ...SettingsPageInvitationsFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n createdAt\n updatedAt\n}\n\nfragment SettingsPageInvitationsFragment on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}, filter: {statuses: [PENDING, EXPIRED]}) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n status\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SettingsPageMembershipsFragment on Organization {\n memberships(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + "text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n }\n id\n }\n}\n\nfragment DomainSettingsTabFragment on Organization {\n id\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n}\n\nfragment GeneralSettingsTabFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n createdAt\n updatedAt\n}\n\nfragment MembersSettingsTabInvitationsFragment on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n email\n role\n status\n createdAt\n expiresAt\n acceptedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MembersSettingsTabMembershipsFragment on Organization {\n memberships(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n authMethod\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SAMLSettingsTabFragment on Organization {\n id\n name\n samlConfigurations {\n id\n enabled\n emailDomain\n enforcementPolicy\n domainVerified\n domainVerificationToken\n domainVerifiedAt\n spEntityId\n spAcsUrl\n spMetadataUrl\n testLoginUrl\n idpEntityId\n idpSsoUrl\n idpCertificate\n idpMetadataUrl\n attributeEmail\n attributeFirstname\n attributeLastname\n attributeRole\n defaultRole\n autoSignupEnabled\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n ...GeneralSettingsTabFragment\n ...MembersSettingsTabMembershipsFragment\n ...MembersSettingsTabInvitationsFragment\n ...DomainSettingsTabFragment\n ...SAMLSettingsTabFragment\n}\n" } }; })(); -(node as any).hash = "fda1489f2b80fd3d0b3962574bd7dfe3"; +(node as any).hash = "196e8c1fc9c2e0b3c76c8b338ed5c7f7"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphCreateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphCreateMutation.graphql.ts new file mode 100644 index 000000000..89a1ee911 --- /dev/null +++ b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphCreateMutation.graphql.ts @@ -0,0 +1,273 @@ +/** + * @generated SignedSource<<9232c5fa755e56b6a1ecdcdd51ad83db>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED"; +export type CreateSAMLConfigurationInput = { + attributeEmail?: string | null | undefined; + attributeFirstname?: string | null | undefined; + attributeLastname?: string | null | undefined; + attributeRole?: string | null | undefined; + autoSignupEnabled?: boolean | null | undefined; + defaultRole?: string | null | undefined; + emailDomain: string; + enforcementPolicy: SAMLEnforcementPolicy; + idpCertificate?: string | null | undefined; + idpEntityId?: string | null | undefined; + idpMetadataUrl?: string | null | undefined; + idpMetadataXml?: string | null | undefined; + idpSsoUrl?: string | null | undefined; + organizationId: string; + spCertificate?: string | null | undefined; + spPrivateKey?: string | null | undefined; +}; +export type SAMLConfigurationGraphCreateMutation$variables = { + input: CreateSAMLConfigurationInput; +}; +export type SAMLConfigurationGraphCreateMutation$data = { + readonly createSAMLConfiguration: { + readonly samlConfiguration: { + readonly attributeEmail: string; + readonly attributeFirstname: string; + readonly attributeLastname: string; + readonly attributeRole: string; + readonly autoSignupEnabled: boolean; + readonly createdAt: any; + readonly defaultRole: string; + readonly emailDomain: string; + readonly enabled: boolean; + readonly enforcementPolicy: SAMLEnforcementPolicy; + readonly id: string; + readonly idpCertificate: string; + readonly idpEntityId: string; + readonly idpMetadataUrl: string | null | undefined; + readonly idpSsoUrl: string; + readonly spAcsUrl: string; + readonly spEntityId: string; + readonly spMetadataUrl: string; + readonly testLoginUrl: string; + readonly updatedAt: any; + }; + }; +}; +export type SAMLConfigurationGraphCreateMutation = { + response: SAMLConfigurationGraphCreateMutation$data; + variables: SAMLConfigurationGraphCreateMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "CreateSAMLConfigurationPayload", + "kind": "LinkedField", + "name": "createSAMLConfiguration", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SAMLConfiguration", + "kind": "LinkedField", + "name": "samlConfiguration", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "enabled", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "emailDomain", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "enforcementPolicy", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "spEntityId", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "spAcsUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "spMetadataUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "testLoginUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpEntityId", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpSsoUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpCertificate", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpMetadataUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeEmail", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeFirstname", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeLastname", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeRole", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "defaultRole", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "autoSignupEnabled", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "SAMLConfigurationGraphCreateMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "SAMLConfigurationGraphCreateMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "71d5f80565cc8bc4cef0382c315b8cb1", + "id": null, + "metadata": {}, + "name": "SAMLConfigurationGraphCreateMutation", + "operationKind": "mutation", + "text": "mutation SAMLConfigurationGraphCreateMutation(\n $input: CreateSAMLConfigurationInput!\n) {\n createSAMLConfiguration(input: $input) {\n samlConfiguration {\n id\n enabled\n emailDomain\n enforcementPolicy\n spEntityId\n spAcsUrl\n spMetadataUrl\n testLoginUrl\n idpEntityId\n idpSsoUrl\n idpCertificate\n idpMetadataUrl\n attributeEmail\n attributeFirstname\n attributeLastname\n attributeRole\n defaultRole\n autoSignupEnabled\n createdAt\n updatedAt\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "04237f5ad3588264149310798d6137f2"; + +export default node; diff --git a/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphDeleteMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphDeleteMutation.graphql.ts new file mode 100644 index 000000000..df9fb29df --- /dev/null +++ b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphDeleteMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @generated SignedSource<<55ce4f4205b3eb01954423a75e6e9369>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteSAMLConfigurationInput = { + id: string; +}; +export type SAMLConfigurationGraphDeleteMutation$variables = { + input: DeleteSAMLConfigurationInput; +}; +export type SAMLConfigurationGraphDeleteMutation$data = { + readonly deleteSAMLConfiguration: { + readonly deletedSAMLConfigurationId: string; + }; +}; +export type SAMLConfigurationGraphDeleteMutation = { + response: SAMLConfigurationGraphDeleteMutation$data; + variables: SAMLConfigurationGraphDeleteMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "DeleteSAMLConfigurationPayload", + "kind": "LinkedField", + "name": "deleteSAMLConfiguration", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedSAMLConfigurationId", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "SAMLConfigurationGraphDeleteMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "SAMLConfigurationGraphDeleteMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "be1192099e1b3d0765a358de10685870", + "id": null, + "metadata": {}, + "name": "SAMLConfigurationGraphDeleteMutation", + "operationKind": "mutation", + "text": "mutation SAMLConfigurationGraphDeleteMutation(\n $input: DeleteSAMLConfigurationInput!\n) {\n deleteSAMLConfiguration(input: $input) {\n deletedSAMLConfigurationId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "869072f879524c5c2acbc684f536bfe5"; + +export default node; diff --git a/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphDisableMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphDisableMutation.graphql.ts new file mode 100644 index 000000000..8eab4d060 --- /dev/null +++ b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphDisableMutation.graphql.ts @@ -0,0 +1,113 @@ +/** + * @generated SignedSource<<2101e8d4c307eb6c37e83c625ca7801f>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DisableSAMLInput = { + id: string; +}; +export type SAMLConfigurationGraphDisableMutation$variables = { + input: DisableSAMLInput; +}; +export type SAMLConfigurationGraphDisableMutation$data = { + readonly disableSAML: { + readonly samlConfiguration: { + readonly enabled: boolean; + readonly id: string; + }; + }; +}; +export type SAMLConfigurationGraphDisableMutation = { + response: SAMLConfigurationGraphDisableMutation$data; + variables: SAMLConfigurationGraphDisableMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "DisableSAMLPayload", + "kind": "LinkedField", + "name": "disableSAML", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SAMLConfiguration", + "kind": "LinkedField", + "name": "samlConfiguration", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "enabled", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "SAMLConfigurationGraphDisableMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "SAMLConfigurationGraphDisableMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "dbed05d465035d865dfd1f6473f88401", + "id": null, + "metadata": {}, + "name": "SAMLConfigurationGraphDisableMutation", + "operationKind": "mutation", + "text": "mutation SAMLConfigurationGraphDisableMutation(\n $input: DisableSAMLInput!\n) {\n disableSAML(input: $input) {\n samlConfiguration {\n id\n enabled\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "6492fd7729c72ca805ef8f99d1399081"; + +export default node; diff --git a/apps/console/src/pages/__generated__/OrganizationsPage_AcceptInvitationMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphEnableMutation.graphql.ts similarity index 52% rename from apps/console/src/pages/__generated__/OrganizationsPage_AcceptInvitationMutation.graphql.ts rename to apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphEnableMutation.graphql.ts index 60158b26c..a9a2dc755 100644 --- a/apps/console/src/pages/__generated__/OrganizationsPage_AcceptInvitationMutation.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphEnableMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<8bcd8aad334a74db810b3d4c17fbb6ac>> * @lightSyntaxTransform * @nogrep */ @@ -9,22 +9,23 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type AcceptInvitationInput = { - invitationId: string; +export type EnableSAMLInput = { + id: string; }; -export type OrganizationsPage_AcceptInvitationMutation$variables = { - input: AcceptInvitationInput; +export type SAMLConfigurationGraphEnableMutation$variables = { + input: EnableSAMLInput; }; -export type OrganizationsPage_AcceptInvitationMutation$data = { - readonly acceptInvitation: { - readonly invitation: { +export type SAMLConfigurationGraphEnableMutation$data = { + readonly enableSAML: { + readonly samlConfiguration: { + readonly enabled: boolean; readonly id: string; }; }; }; -export type OrganizationsPage_AcceptInvitationMutation = { - response: OrganizationsPage_AcceptInvitationMutation$data; - variables: OrganizationsPage_AcceptInvitationMutation$variables; +export type SAMLConfigurationGraphEnableMutation = { + response: SAMLConfigurationGraphEnableMutation$data; + variables: SAMLConfigurationGraphEnableMutation$variables; }; const node: ConcreteRequest = (function(){ @@ -45,17 +46,17 @@ v1 = [ "variableName": "input" } ], - "concreteType": "AcceptInvitationPayload", + "concreteType": "EnableSAMLPayload", "kind": "LinkedField", - "name": "acceptInvitation", + "name": "enableSAML", "plural": false, "selections": [ { "alias": null, "args": null, - "concreteType": "Invitation", + "concreteType": "SAMLConfiguration", "kind": "LinkedField", - "name": "invitation", + "name": "samlConfiguration", "plural": false, "selections": [ { @@ -64,6 +65,13 @@ v1 = [ "kind": "ScalarField", "name": "id", "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "enabled", + "storageKey": null } ], "storageKey": null @@ -77,7 +85,7 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, - "name": "OrganizationsPage_AcceptInvitationMutation", + "name": "SAMLConfigurationGraphEnableMutation", "selections": (v1/*: any*/), "type": "Mutation", "abstractKey": null @@ -86,20 +94,20 @@ return { "operation": { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", - "name": "OrganizationsPage_AcceptInvitationMutation", + "name": "SAMLConfigurationGraphEnableMutation", "selections": (v1/*: any*/) }, "params": { - "cacheID": "cc4a442037edaa624948b5be0c009823", + "cacheID": "2bd2356d146a9fa5f5a5f5480d310bc9", "id": null, "metadata": {}, - "name": "OrganizationsPage_AcceptInvitationMutation", + "name": "SAMLConfigurationGraphEnableMutation", "operationKind": "mutation", - "text": "mutation OrganizationsPage_AcceptInvitationMutation(\n $input: AcceptInvitationInput!\n) {\n acceptInvitation(input: $input) {\n invitation {\n id\n }\n }\n}\n" + "text": "mutation SAMLConfigurationGraphEnableMutation(\n $input: EnableSAMLInput!\n) {\n enableSAML(input: $input) {\n samlConfiguration {\n id\n enabled\n }\n }\n}\n" } }; })(); -(node as any).hash = "190213ab6fdc068343a270b4fa94e160"; +(node as any).hash = "1627602d91776f718f1913011bdce786"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphInitiateDomainVerificationMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphInitiateDomainVerificationMutation.graphql.ts new file mode 100644 index 000000000..3bcea2f3b --- /dev/null +++ b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphInitiateDomainVerificationMutation.graphql.ts @@ -0,0 +1,146 @@ +/** + * @generated SignedSource<<55d58c6ee7e4dcd7ffe7e172aea932d5>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type InitiateDomainVerificationInput = { + emailDomain: string; + organizationId: string; +}; +export type SAMLConfigurationGraphInitiateDomainVerificationMutation$variables = { + input: InitiateDomainVerificationInput; +}; +export type SAMLConfigurationGraphInitiateDomainVerificationMutation$data = { + readonly initiateDomainVerification: { + readonly dnsRecord: string; + readonly samlConfiguration: { + readonly domainVerificationToken: string | null | undefined; + readonly domainVerified: boolean; + readonly domainVerifiedAt: any | null | undefined; + readonly emailDomain: string; + readonly id: string; + }; + }; +}; +export type SAMLConfigurationGraphInitiateDomainVerificationMutation = { + response: SAMLConfigurationGraphInitiateDomainVerificationMutation$data; + variables: SAMLConfigurationGraphInitiateDomainVerificationMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "InitiateDomainVerificationPayload", + "kind": "LinkedField", + "name": "initiateDomainVerification", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SAMLConfiguration", + "kind": "LinkedField", + "name": "samlConfiguration", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "emailDomain", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "domainVerified", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "domainVerificationToken", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "domainVerifiedAt", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "dnsRecord", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "SAMLConfigurationGraphInitiateDomainVerificationMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "SAMLConfigurationGraphInitiateDomainVerificationMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "61453ca7fb2c7ff2fe8b657f6cfb7497", + "id": null, + "metadata": {}, + "name": "SAMLConfigurationGraphInitiateDomainVerificationMutation", + "operationKind": "mutation", + "text": "mutation SAMLConfigurationGraphInitiateDomainVerificationMutation(\n $input: InitiateDomainVerificationInput!\n) {\n initiateDomainVerification(input: $input) {\n samlConfiguration {\n id\n emailDomain\n domainVerified\n domainVerificationToken\n domainVerifiedAt\n }\n dnsRecord\n }\n}\n" + } +}; +})(); + +(node as any).hash = "5424f4fd94618327ae64292d31349e73"; + +export default node; diff --git a/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphUpdateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphUpdateMutation.graphql.ts new file mode 100644 index 000000000..0067b1aa7 --- /dev/null +++ b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphUpdateMutation.graphql.ts @@ -0,0 +1,272 @@ +/** + * @generated SignedSource<<694befcb248cbb23e66b9555626f223b>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED"; +export type UpdateSAMLConfigurationInput = { + attributeEmail?: string | null | undefined; + attributeFirstname?: string | null | undefined; + attributeLastname?: string | null | undefined; + attributeRole?: string | null | undefined; + autoSignupEnabled?: boolean | null | undefined; + defaultRole?: string | null | undefined; + enabled?: boolean | null | undefined; + enforcementPolicy?: SAMLEnforcementPolicy | null | undefined; + id: string; + idpCertificate?: string | null | undefined; + idpEntityId?: string | null | undefined; + idpMetadataUrl?: string | null | undefined; + idpSsoUrl?: string | null | undefined; + spCertificate?: string | null | undefined; + spPrivateKey?: string | null | undefined; +}; +export type SAMLConfigurationGraphUpdateMutation$variables = { + input: UpdateSAMLConfigurationInput; +}; +export type SAMLConfigurationGraphUpdateMutation$data = { + readonly updateSAMLConfiguration: { + readonly samlConfiguration: { + readonly attributeEmail: string; + readonly attributeFirstname: string; + readonly attributeLastname: string; + readonly attributeRole: string; + readonly autoSignupEnabled: boolean; + readonly createdAt: any; + readonly defaultRole: string; + readonly emailDomain: string; + readonly enabled: boolean; + readonly enforcementPolicy: SAMLEnforcementPolicy; + readonly id: string; + readonly idpCertificate: string; + readonly idpEntityId: string; + readonly idpMetadataUrl: string | null | undefined; + readonly idpSsoUrl: string; + readonly spAcsUrl: string; + readonly spEntityId: string; + readonly spMetadataUrl: string; + readonly testLoginUrl: string; + readonly updatedAt: any; + }; + }; +}; +export type SAMLConfigurationGraphUpdateMutation = { + response: SAMLConfigurationGraphUpdateMutation$data; + variables: SAMLConfigurationGraphUpdateMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateSAMLConfigurationPayload", + "kind": "LinkedField", + "name": "updateSAMLConfiguration", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SAMLConfiguration", + "kind": "LinkedField", + "name": "samlConfiguration", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "enabled", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "emailDomain", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "enforcementPolicy", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "spEntityId", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "spAcsUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "spMetadataUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "testLoginUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpEntityId", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpSsoUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpCertificate", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "idpMetadataUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeEmail", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeFirstname", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeLastname", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "attributeRole", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "defaultRole", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "autoSignupEnabled", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "SAMLConfigurationGraphUpdateMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "SAMLConfigurationGraphUpdateMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "c9e4055888055b109aa71b665c9544ca", + "id": null, + "metadata": {}, + "name": "SAMLConfigurationGraphUpdateMutation", + "operationKind": "mutation", + "text": "mutation SAMLConfigurationGraphUpdateMutation(\n $input: UpdateSAMLConfigurationInput!\n) {\n updateSAMLConfiguration(input: $input) {\n samlConfiguration {\n id\n enabled\n emailDomain\n enforcementPolicy\n spEntityId\n spAcsUrl\n spMetadataUrl\n testLoginUrl\n idpEntityId\n idpSsoUrl\n idpCertificate\n idpMetadataUrl\n attributeEmail\n attributeFirstname\n attributeLastname\n attributeRole\n defaultRole\n autoSignupEnabled\n createdAt\n updatedAt\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "d572af05811d6a05cdd4446fe175894d"; + +export default node; diff --git a/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphVerifyDomainMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphVerifyDomainMutation.graphql.ts new file mode 100644 index 000000000..b3517a4e0 --- /dev/null +++ b/apps/console/src/hooks/graph/__generated__/SAMLConfigurationGraphVerifyDomainMutation.graphql.ts @@ -0,0 +1,129 @@ +/** + * @generated SignedSource<<8baf2363740e7eb47e7654f2f55c09df>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type VerifyDomainInput = { + id: string; +}; +export type SAMLConfigurationGraphVerifyDomainMutation$variables = { + input: VerifyDomainInput; +}; +export type SAMLConfigurationGraphVerifyDomainMutation$data = { + readonly verifyDomain: { + readonly samlConfiguration: { + readonly domainVerified: boolean; + readonly domainVerifiedAt: any | null | undefined; + readonly id: string; + }; + readonly verified: boolean; + }; +}; +export type SAMLConfigurationGraphVerifyDomainMutation = { + response: SAMLConfigurationGraphVerifyDomainMutation$data; + variables: SAMLConfigurationGraphVerifyDomainMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "VerifyDomainPayload", + "kind": "LinkedField", + "name": "verifyDomain", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SAMLConfiguration", + "kind": "LinkedField", + "name": "samlConfiguration", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "domainVerified", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "domainVerifiedAt", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "verified", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "SAMLConfigurationGraphVerifyDomainMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "SAMLConfigurationGraphVerifyDomainMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "1f241529f518d2f0701d9c4f7a731d6b", + "id": null, + "metadata": {}, + "name": "SAMLConfigurationGraphVerifyDomainMutation", + "operationKind": "mutation", + "text": "mutation SAMLConfigurationGraphVerifyDomainMutation(\n $input: VerifyDomainInput!\n) {\n verifyDomain(input: $input) {\n samlConfiguration {\n id\n domainVerified\n domainVerifiedAt\n }\n verified\n }\n}\n" + } +}; +})(); + +(node as any).hash = "ea3f1fe691b0c36ff7e54663479f7e7c"; + +export default node; diff --git a/apps/console/src/layouts/MainLayout.tsx b/apps/console/src/layouts/MainLayout.tsx index 8afa8ed16..3aed59a27 100644 --- a/apps/console/src/layouts/MainLayout.tsx +++ b/apps/console/src/layouts/MainLayout.tsx @@ -30,17 +30,16 @@ import { DropdownItem, IconChevronGrabberVertical, IconPlusLarge, - IconChevronDown, Avatar, IconPeopleAdd, Badge, + IconLock, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { graphql } from "relay-runtime"; -import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; +import { useLazyLoadQuery } from "react-relay"; import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql"; -import type { MainLayout_OrganizationSelector_viewer$key } from "./__generated__/MainLayout_OrganizationSelector_viewer.graphql"; -import { Suspense, useState } from "react"; +import { Suspense, useState, useEffect } from "react"; import { useToast } from "@probo/ui"; import { ErrorBoundary } from "react-error-boundary"; import { PageError } from "/components/PageError"; @@ -54,7 +53,9 @@ const MainLayoutQuery = graphql` fullName email } - ...MainLayout_OrganizationSelector_viewer + invitations(first: 1, filter: {statuses: [PENDING]}) { + totalCount + } } organization: node(id: $organizationId) { ... on Organization { @@ -66,33 +67,6 @@ const MainLayoutQuery = graphql` } `; -const OrganizationSelectorFragment = graphql` - fragment MainLayout_OrganizationSelector_viewer on Viewer - @refetchable(queryName: "MainLayoutOrganizationSelectorPaginationQuery") - @argumentDefinitions( - first: { type: "Int", defaultValue: 25 } - after: { type: "CursorKey" } - ) { - organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) - @connection(key: "MainLayout_OrganizationSelector_organizations") { - edges { - node { - id - name - logoUrl - } - } - pageInfo { - hasNextPage - endCursor - } - } - invitations(first: 1, filter: {statuses: [PENDING]}) { - totalCount - } - } -`; - /** * Site layout with a header and a sidebar */ @@ -228,7 +202,7 @@ function UserDropdown({ organizationId }: { organizationId: string }) { ) => { e.preventDefault(); - fetch(buildEndpoint("/api/console/v1/auth/logout"), { + fetch(buildEndpoint("/auth/logout"), { method: "DELETE", headers: { "Content-Type": "application/json", @@ -271,6 +245,19 @@ function UserDropdown({ organizationId }: { organizationId: string }) { ); } +interface Organization { + id: string; + name: string; + logoUrl?: string | null; + authenticationMethod: string; + authStatus: "authenticated" | "unauthenticated" | "expired"; + loginUrl: string; +} + +interface OrganizationsResponse { + organizations: Organization[]; +} + function OrganizationSelectorWrapper({ organizationId }: { organizationId: string }) { const data = useLazyLoadQuery(MainLayoutQuery, { organizationId }); return ; @@ -280,31 +267,55 @@ function OrganizationSelector({ viewer, currentOrganization }: { - viewer: MainLayout_OrganizationSelector_viewer$key; + viewer: MainLayoutQueryType["response"]["viewer"]; currentOrganization: MainLayoutQueryType["response"]["organization"]; }) { - const [isLoadingMore, setIsLoadingMore] = useState(false); + const [organizations, setOrganizations] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); const { __ } = useTranslate(); - const { data, loadNext, hasNext } = usePaginationFragment( - OrganizationSelectorFragment, - viewer - ); + const pendingInvitationsCount = viewer.invitations.totalCount; - const organizations = data.organizations.edges.map((edge) => edge.node); - const pendingInvitationsCount = data.invitations.totalCount; + useEffect(() => { + const fetchOrganizations = async () => { + try { + setIsLoading(true); + const response = await fetch('/auth/organizations', { + credentials: 'include', + }); - const handleLoadMore = (e?: React.MouseEvent) => { - e?.preventDefault(); - e?.stopPropagation(); + if (!response.ok) { + throw new Error('Failed to fetch organizations'); + } - if (hasNext && !isLoadingMore) { - setIsLoadingMore(true); - loadNext(25, { - onComplete: () => setIsLoadingMore(false), - }); - } - }; + const data: OrganizationsResponse = await response.json(); + setOrganizations(data.organizations); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + console.error('Failed to fetch organizations:', err); + } finally { + setIsLoading(false); + } + }; + + fetchOrganizations(); + }, []); + + if (error) { + return ( +
+ +
+ ); + } return (
@@ -314,39 +325,70 @@ function OrganizationSelector({ className="-ml-3" variant="tertiary" iconAfter={IconChevronGrabberVertical} + disabled={isLoading} > - {currentOrganization?.name || ""} + {isLoading ? __("Loading...") : (currentOrganization?.name || "")} } >
- {organizations.map((organization) => ( - - - - {organization.name} - - - ))} - {hasNext && ( -
- + {isLoading ? ( +
+ {__("Loading organizations...")}
+ ) : organizations.length === 0 ? ( +
+ {__("No organizations found")} +
+ ) : ( + organizations.map((organization) => { + const isAuthenticated = organization.authStatus === "authenticated"; + const isExpired = organization.authStatus === "expired"; + const needsAuth = organization.authStatus === "unauthenticated"; + + const targetUrl = isAuthenticated + ? `/organizations/${organization.id}` + : organization.loginUrl; + + const isSAMLUrl = targetUrl.includes('/auth/saml/'); + + return ( + + {isSAMLUrl ? ( + + + {organization.name} + {isAuthenticated && ( + + )} + {isExpired && ( + + )} + {needsAuth && ( + + )} + + ) : ( + + + {organization.name} + {isAuthenticated && ( + + )} + {isExpired && ( + + )} + {needsAuth && ( + + )} + + )} + + ); + }) )}
diff --git a/apps/console/src/layouts/__generated__/MainLayoutOrganizationSelectorPaginationQuery.graphql.ts b/apps/console/src/layouts/__generated__/MainLayoutOrganizationSelectorPaginationQuery.graphql.ts deleted file mode 100644 index bec342b62..000000000 --- a/apps/console/src/layouts/__generated__/MainLayoutOrganizationSelectorPaginationQuery.graphql.ts +++ /dev/null @@ -1,263 +0,0 @@ -/** - * @generated SignedSource<<3a99643b92330af6920aac4c2286376f>> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -import { FragmentRefs } from "relay-runtime"; -export type MainLayoutOrganizationSelectorPaginationQuery$variables = { - after?: any | null | undefined; - first?: number | null | undefined; -}; -export type MainLayoutOrganizationSelectorPaginationQuery$data = { - readonly viewer: { - readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">; - }; -}; -export type MainLayoutOrganizationSelectorPaginationQuery = { - response: MainLayoutOrganizationSelectorPaginationQuery$data; - variables: MainLayoutOrganizationSelectorPaginationQuery$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "after" - }, - { - "defaultValue": 25, - "kind": "LocalArgument", - "name": "first" - } -], -v1 = { - "kind": "Variable", - "name": "after", - "variableName": "after" -}, -v2 = { - "kind": "Variable", - "name": "first", - "variableName": "first" -}, -v3 = [ - (v1/*: any*/), - (v2/*: any*/), - { - "kind": "Literal", - "name": "orderBy", - "value": { - "direction": "ASC", - "field": "NAME" - } - } -], -v4 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null -}; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "MainLayoutOrganizationSelectorPaginationQuery", - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Viewer", - "kind": "LinkedField", - "name": "viewer", - "plural": false, - "selections": [ - { - "args": [ - (v1/*: any*/), - (v2/*: any*/) - ], - "kind": "FragmentSpread", - "name": "MainLayout_OrganizationSelector_viewer" - } - ], - "storageKey": null - } - ], - "type": "Query", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "MainLayoutOrganizationSelectorPaginationQuery", - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Viewer", - "kind": "LinkedField", - "name": "viewer", - "plural": false, - "selections": [ - { - "alias": null, - "args": (v3/*: any*/), - "concreteType": "OrganizationConnection", - "kind": "LinkedField", - "name": "organizations", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "OrganizationEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v4/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "logoUrl", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasNextPage", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "endCursor", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": (v3/*: any*/), - "filters": [ - "orderBy" - ], - "handle": "connection", - "key": "MainLayout_OrganizationSelector_organizations", - "kind": "LinkedHandle", - "name": "organizations" - }, - { - "alias": null, - "args": [ - { - "kind": "Literal", - "name": "filter", - "value": { - "statuses": [ - "PENDING" - ] - } - }, - { - "kind": "Literal", - "name": "first", - "value": 1 - } - ], - "concreteType": "InvitationConnection", - "kind": "LinkedField", - "name": "invitations", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "totalCount", - "storageKey": null - } - ], - "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)" - }, - (v4/*: any*/) - ], - "storageKey": null - } - ] - }, - "params": { - "cacheID": "d51b49a50afe664ee9903c1bb265ab79", - "id": null, - "metadata": {}, - "name": "MainLayoutOrganizationSelectorPaginationQuery", - "operationKind": "query", - "text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n}\n" - } -}; -})(); - -(node as any).hash = "3e00f1a6f8089fc59144807a07fd1bdf"; - -export default node; diff --git a/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts b/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts index b2a9c6600..d23b0081f 100644 --- a/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts +++ b/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<1a7aa899a9b6251477122c87ae1c6431>> + * @generated SignedSource<<26b3620e6aed7f97ffb1710be1eb267a>> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,6 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -import { FragmentRefs } from "relay-runtime"; export type MainLayoutQuery$variables = { organizationId: string; }; @@ -21,11 +20,13 @@ export type MainLayoutQuery$data = { }; readonly viewer: { readonly id: string; + readonly invitations: { + readonly totalCount: number; + }; readonly user: { readonly email: string; readonly fullName: string; }; - readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">; }; }; export type MainLayoutQuery = { @@ -62,48 +63,59 @@ v3 = { "name": "email", "storageKey": null }, -v4 = [ +v4 = { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "filter", + "value": { + "statuses": [ + "PENDING" + ] + } + }, + { + "kind": "Literal", + "name": "first", + "value": 1 + } + ], + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "invitations", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + } + ], + "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)" +}, +v5 = [ { "kind": "Variable", "name": "id", "variableName": "organizationId" } ], -v5 = { +v6 = { "alias": null, "args": null, "kind": "ScalarField", "name": "name", "storageKey": null }, -v6 = { +v7 = { "alias": null, "args": null, "kind": "ScalarField", "name": "logoUrl", "storageKey": null -}, -v7 = [ - { - "kind": "Literal", - "name": "first", - "value": 25 - }, - { - "kind": "Literal", - "name": "orderBy", - "value": { - "direction": "ASC", - "field": "NAME" - } - } -], -v8 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null }; return { "fragment": { @@ -134,17 +146,13 @@ return { ], "storageKey": null }, - { - "args": null, - "kind": "FragmentSpread", - "name": "MainLayout_OrganizationSelector_viewer" - } + (v4/*: any*/) ], "storageKey": null }, { "alias": "organization", - "args": (v4/*: any*/), + "args": (v5/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", @@ -154,8 +162,8 @@ return { "kind": "InlineFragment", "selections": [ (v1/*: any*/), - (v5/*: any*/), - (v6/*: any*/) + (v6/*: any*/), + (v7/*: any*/) ], "type": "Organization", "abstractKey": null @@ -196,137 +204,31 @@ return { ], "storageKey": null }, - { - "alias": null, - "args": (v7/*: any*/), - "concreteType": "OrganizationConnection", - "kind": "LinkedField", - "name": "organizations", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "OrganizationEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v1/*: any*/), - (v5/*: any*/), - (v6/*: any*/), - (v8/*: any*/) - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasNextPage", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "endCursor", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "organizations(first:25,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})" - }, - { - "alias": null, - "args": (v7/*: any*/), - "filters": [ - "orderBy" - ], - "handle": "connection", - "key": "MainLayout_OrganizationSelector_organizations", - "kind": "LinkedHandle", - "name": "organizations" - }, - { - "alias": null, - "args": [ - { - "kind": "Literal", - "name": "filter", - "value": { - "statuses": [ - "PENDING" - ] - } - }, - { - "kind": "Literal", - "name": "first", - "value": 1 - } - ], - "concreteType": "InvitationConnection", - "kind": "LinkedField", - "name": "invitations", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "totalCount", - "storageKey": null - } - ], - "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)" - } + (v4/*: any*/) ], "storageKey": null }, { "alias": "organization", - "args": (v4/*: any*/), + "args": (v5/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", "plural": false, "selections": [ - (v8/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, (v1/*: any*/), { "kind": "InlineFragment", "selections": [ - (v5/*: any*/), - (v6/*: any*/) + (v6/*: any*/), + (v7/*: any*/) ], "type": "Organization", "abstractKey": null @@ -337,16 +239,16 @@ return { ] }, "params": { - "cacheID": "6de25e54419d82cb6465c903a0afdd78", + "cacheID": "a8f9f58d27677c55b5a217617db83e27", "id": null, "metadata": {}, "name": "MainLayoutQuery", "operationKind": "query", - "text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n}\n" + "text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n" } }; })(); -(node as any).hash = "aaaca58a896839aa85ce7c2fcf75de39"; +(node as any).hash = "17986fcea321c4567d86584d1a9f89c1"; export default node; diff --git a/apps/console/src/layouts/__generated__/MainLayout_OrganizationSelector_viewer.graphql.ts b/apps/console/src/layouts/__generated__/MainLayout_OrganizationSelector_viewer.graphql.ts deleted file mode 100644 index 0b30c37a4..000000000 --- a/apps/console/src/layouts/__generated__/MainLayout_OrganizationSelector_viewer.graphql.ts +++ /dev/null @@ -1,226 +0,0 @@ -/** - * @generated SignedSource<> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ReaderFragment } from 'relay-runtime'; -import { FragmentRefs } from "relay-runtime"; -export type MainLayout_OrganizationSelector_viewer$data = { - readonly invitations: { - readonly totalCount: number; - }; - readonly organizations: { - readonly edges: ReadonlyArray<{ - readonly node: { - readonly id: string; - readonly logoUrl: string | null | undefined; - readonly name: string; - }; - }>; - readonly pageInfo: { - readonly endCursor: any | null | undefined; - readonly hasNextPage: boolean; - }; - }; - readonly " $fragmentType": "MainLayout_OrganizationSelector_viewer"; -}; -export type MainLayout_OrganizationSelector_viewer$key = { - readonly " $data"?: MainLayout_OrganizationSelector_viewer$data; - readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">; -}; - -import MainLayoutOrganizationSelectorPaginationQuery_graphql from './MainLayoutOrganizationSelectorPaginationQuery.graphql'; - -const node: ReaderFragment = (function(){ -var v0 = [ - "organizations" -]; -return { - "argumentDefinitions": [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "after" - }, - { - "defaultValue": 25, - "kind": "LocalArgument", - "name": "first" - } - ], - "kind": "Fragment", - "metadata": { - "connection": [ - { - "count": "first", - "cursor": "after", - "direction": "forward", - "path": (v0/*: any*/) - } - ], - "refetch": { - "connection": { - "forward": { - "count": "first", - "cursor": "after" - }, - "backward": null, - "path": (v0/*: any*/) - }, - "fragmentPathInResult": [ - "viewer" - ], - "operation": MainLayoutOrganizationSelectorPaginationQuery_graphql - } - }, - "name": "MainLayout_OrganizationSelector_viewer", - "selections": [ - { - "alias": "organizations", - "args": [ - { - "kind": "Literal", - "name": "orderBy", - "value": { - "direction": "ASC", - "field": "NAME" - } - } - ], - "concreteType": "OrganizationConnection", - "kind": "LinkedField", - "name": "__MainLayout_OrganizationSelector_organizations_connection", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "OrganizationEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "logoUrl", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasNextPage", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "endCursor", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "__MainLayout_OrganizationSelector_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})" - }, - { - "alias": null, - "args": [ - { - "kind": "Literal", - "name": "filter", - "value": { - "statuses": [ - "PENDING" - ] - } - }, - { - "kind": "Literal", - "name": "first", - "value": 1 - } - ], - "concreteType": "InvitationConnection", - "kind": "LinkedField", - "name": "invitations", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "totalCount", - "storageKey": null - } - ], - "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)" - } - ], - "type": "Viewer", - "abstractKey": null -}; -})(); - -(node as any).hash = "3e00f1a6f8089fc59144807a07fd1bdf"; - -export default node; diff --git a/apps/console/src/pages/OrganizationsPage.tsx b/apps/console/src/pages/OrganizationsPage.tsx index e91987512..42183ae7b 100644 --- a/apps/console/src/pages/OrganizationsPage.tsx +++ b/apps/console/src/pages/OrganizationsPage.tsx @@ -1,110 +1,138 @@ import { useTranslate } from "@probo/i18n"; -import { useLazyLoadQuery } from "react-relay"; -import { graphql } from "relay-runtime"; -import type { OrganizationsPageQuery as OrganizationsPageQueryType } from "./__generated__/OrganizationsPageQuery.graphql"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Link, useNavigate } from "react-router"; import { Avatar, Button, Card, IconPlusLarge, + IconCheckmark1, + IconLock, + IconClock, + Badge, } from "@probo/ui"; import { usePageTitle } from "@probo/hooks"; -import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { formatDate } from "@probo/helpers"; -const OrganizationsPageQuery = graphql` - query OrganizationsPageQuery { - viewer { - organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) @connection(key: "OrganizationsPage_organizations") { - __id - edges { - node { - id - name - logoUrl - } - } - } - invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {statuses: [PENDING]}) @connection(key: "OrganizationsPage_invitations") { - __id - edges { - node { - id - email - fullName - role - expiresAt - acceptedAt - createdAt - organization { - id - name - } - } - } - } - } - } -`; +interface Organization { + id: string; + name: string; + logoUrl?: string | null; + authenticationMethod: string; + authStatus: "authenticated" | "unauthenticated" | "expired"; + loginUrl: string; +} -const acceptInvitationMutation = graphql` - mutation OrganizationsPage_AcceptInvitationMutation($input: AcceptInvitationInput!) { - acceptInvitation(input: $input) { - invitation { - id - } - } - } -`; +interface Invitation { + id: string; + email: string; + fullName: string; + role: string; + expiresAt: string; + acceptedAt?: string | null; + createdAt: string; + organization: { + id: string; + name: string; + }; +} export default function OrganizationsPage() { const { __ } = useTranslate(); const navigate = useNavigate(); - const data = useLazyLoadQuery( - OrganizationsPageQuery, - {} - ); - const organizations = data.viewer.organizations.edges.map( - (edge) => edge.node - ); + const [organizations, setOrganizations] = useState([]); + const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(true); + const [invitations, setInvitations] = useState([]); + const [isLoadingInvitations, setIsLoadingInvitations] = useState(true); + const [isAccepting, setIsAccepting] = useState(false); - const pendingInvitations = data.viewer.invitations.edges.map( - (edge) => edge.node - ); + // Fetch organizations from REST endpoint + useEffect(() => { + const fetchOrganizations = async () => { + try { + const response = await fetch('/auth/organizations', { + credentials: 'include', + }); - const [acceptInvitation, isAccepting] = useMutationWithToasts( - acceptInvitationMutation, - { - successMessage: __("Invitation accepted successfully"), - errorMessage: __("Failed to accept invitation"), - } - ); + if (!response.ok) { + throw new Error('Failed to fetch organizations'); + } - const handleAcceptInvitation = (invitationId: string, organizationId: string) => { - acceptInvitation({ - variables: { - input: { - invitationId, + const data: { organizations: Organization[] } = await response.json(); + setOrganizations(data.organizations); + } catch (err) { + console.error('Failed to fetch organizations:', err); + } finally { + setIsLoadingOrganizations(false); + } + }; + + fetchOrganizations(); + }, []); + + // Fetch pending invitations from REST endpoint + useEffect(() => { + const fetchInvitations = async () => { + try { + const response = await fetch('/auth/invitations', { + credentials: 'include', + }); + + if (!response.ok) { + throw new Error('Failed to fetch invitations'); + } + + const data: { invitations: Invitation[] } = await response.json(); + setInvitations(data.invitations); + } catch (err) { + console.error('Failed to fetch invitations:', err); + } finally { + setIsLoadingInvitations(false); + } + }; + + fetchInvitations(); + }, []); + + const handleAcceptInvitation = async (invitationId: string, organizationId: string) => { + setIsAccepting(true); + try { + const response = await fetch('/auth/invitations/accept', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, - }, - onSuccess: () => { - navigate(`/organizations/${organizationId}`); - }, - }); + credentials: 'include', + body: JSON.stringify({ invitationId }), + }); + + if (!response.ok) { + throw new Error('Failed to accept invitation'); + } + + // Navigate to the organization after successful acceptance + navigate(`/organizations/${organizationId}`); + } catch (err) { + console.error('Failed to accept invitation:', err); + alert(__('Failed to accept invitation')); + } finally { + setIsAccepting(false); + } }; usePageTitle(__("Select an organization")); useEffect(() => { - if (organizations.length === 1 && pendingInvitations.length === 0) { - navigate(`/organizations/${organizations[0].id}`); - } else if (organizations.length === 0 && pendingInvitations.length === 0) { - navigate("/organizations/new"); + // Only auto-navigate once both organizations and invitations are loaded + if (!isLoadingOrganizations && !isLoadingInvitations) { + if (organizations.length === 1 && invitations.length === 0) { + navigate(`/organizations/${organizations[0].id}`); + } else if (organizations.length === 0 && invitations.length === 0) { + navigate("/organizations/new"); + } } - }, [organizations, pendingInvitations]); + }, [organizations, invitations, isLoadingOrganizations, isLoadingInvitations, navigate]); return ( <> @@ -113,12 +141,12 @@ export default function OrganizationsPage() { {__("Select an organization")}
- {pendingInvitations.length > 0 && ( + {invitations.length > 0 && (

{__("Pending invitations")}

- {pendingInvitations.map((invitation) => ( + {invitations.map((invitation) => ( 0 && (
- {pendingInvitations.length > 0 && ( + {invitations.length > 0 && (

{__("Your organizations")}

@@ -166,18 +194,7 @@ export default function OrganizationsPage() { } type InvitationCardProps = { - invitation: { - id: string; - email: string; - fullName: string; - role: string; - expiresAt: string; - createdAt: string; - organization: { - id: string; - name: string; - }; - }; + invitation: Invitation; onAccept: (invitationId: string, organizationId: string) => void; isAccepting: boolean; }; @@ -211,35 +228,106 @@ function InvitationCard({ invitation, onAccept, isAccepting }: InvitationCardPro } type OrganizationCardProps = { - organization: { - id: string; - name: string; - logoUrl: string | null | undefined; - }; + organization: Organization; }; function OrganizationCard({ organization }: OrganizationCardProps) { const { __ } = useTranslate(); + const isAuthenticated = organization.authStatus === "authenticated"; + const isExpired = organization.authStatus === "expired"; + const needsAuth = organization.authStatus === "unauthenticated"; + + // Determine target URL and button text based on auth status + const targetUrl = isAuthenticated + ? `/organizations/${organization.id}` + : organization.loginUrl; + + const getAuthBadge = () => { + if (isAuthenticated) { + return ( + + + {__("Authenticated")} + + ); + } + + if (isExpired) { + return ( + + + {__("Session expired")} + + ); + } + + if (needsAuth) { + return ( + + + {__("Authentication required")} + + ); + } + + return null; + }; + + const getButtonText = () => { + if (isAuthenticated) return __("Select"); + if (organization.authenticationMethod === "saml") return __("Login with SAML"); + return __("Login"); + }; + + // Check if the URL is a backend SAML endpoint + const isSAMLUrl = targetUrl.includes('/auth/saml/'); + return (
- - -

{organization.name}

- + {isSAMLUrl ? ( + + +
+

{organization.name}

+ {getAuthBadge()} +
+
+ ) : ( + + +
+

{organization.name}

+ {getAuthBadge()} +
+ + )}
diff --git a/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts b/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts deleted file mode 100644 index f73796a4d..000000000 --- a/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts +++ /dev/null @@ -1,420 +0,0 @@ -/** - * @generated SignedSource<<580387b50cff64395ba9afd4c7454d20>> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -export type OrganizationsPageQuery$variables = Record; -export type OrganizationsPageQuery$data = { - readonly viewer: { - readonly invitations: { - readonly __id: string; - readonly edges: ReadonlyArray<{ - readonly node: { - readonly acceptedAt: any | null | undefined; - readonly createdAt: any; - readonly email: string; - readonly expiresAt: any; - readonly fullName: string; - readonly id: string; - readonly organization: { - readonly id: string; - readonly name: string; - }; - readonly role: string; - }; - }>; - }; - readonly organizations: { - readonly __id: string; - readonly edges: ReadonlyArray<{ - readonly node: { - readonly id: string; - readonly logoUrl: string | null | undefined; - readonly name: string; - }; - }>; - }; - }; -}; -export type OrganizationsPageQuery = { - response: OrganizationsPageQuery$data; - variables: OrganizationsPageQuery$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = { - "kind": "Literal", - "name": "orderBy", - "value": { - "direction": "ASC", - "field": "NAME" - } -}, -v1 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null -}, -v2 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null -}, -v3 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null -}, -v4 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null -}, -v5 = { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "endCursor", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasNextPage", - "storageKey": null - } - ], - "storageKey": null -}, -v6 = { - "kind": "ClientExtension", - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__id", - "storageKey": null - } - ] -}, -v7 = [ - { - "alias": null, - "args": null, - "concreteType": "OrganizationEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v1/*: any*/), - (v2/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "logoUrl", - "storageKey": null - }, - (v3/*: any*/) - ], - "storageKey": null - }, - (v4/*: any*/) - ], - "storageKey": null - }, - (v5/*: any*/), - (v6/*: any*/) -], -v8 = { - "kind": "Literal", - "name": "filter", - "value": { - "statuses": [ - "PENDING" - ] - } -}, -v9 = { - "kind": "Literal", - "name": "orderBy", - "value": { - "direction": "DESC", - "field": "CREATED_AT" - } -}, -v10 = [ - { - "alias": null, - "args": null, - "concreteType": "InvitationEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Invitation", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v1/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "email", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "fullName", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "role", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "expiresAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "acceptedAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "organization", - "plural": false, - "selections": [ - (v1/*: any*/), - (v2/*: any*/) - ], - "storageKey": null - }, - (v3/*: any*/) - ], - "storageKey": null - }, - (v4/*: any*/) - ], - "storageKey": null - }, - (v5/*: any*/), - (v6/*: any*/) -], -v11 = { - "kind": "Literal", - "name": "first", - "value": 1000 -}, -v12 = [ - (v11/*: any*/), - (v0/*: any*/) -], -v13 = [ - (v8/*: any*/), - (v11/*: any*/), - (v9/*: any*/) -]; -return { - "fragment": { - "argumentDefinitions": [], - "kind": "Fragment", - "metadata": null, - "name": "OrganizationsPageQuery", - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Viewer", - "kind": "LinkedField", - "name": "viewer", - "plural": false, - "selections": [ - { - "alias": "organizations", - "args": [ - (v0/*: any*/) - ], - "concreteType": "OrganizationConnection", - "kind": "LinkedField", - "name": "__OrganizationsPage_organizations_connection", - "plural": false, - "selections": (v7/*: any*/), - "storageKey": "__OrganizationsPage_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})" - }, - { - "alias": "invitations", - "args": [ - (v8/*: any*/), - (v9/*: any*/) - ], - "concreteType": "InvitationConnection", - "kind": "LinkedField", - "name": "__OrganizationsPage_invitations_connection", - "plural": false, - "selections": (v10/*: any*/), - "storageKey": "__OrganizationsPage_invitations_connection(filter:{\"statuses\":[\"PENDING\"]},orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" - } - ], - "storageKey": null - } - ], - "type": "Query", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": [], - "kind": "Operation", - "name": "OrganizationsPageQuery", - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Viewer", - "kind": "LinkedField", - "name": "viewer", - "plural": false, - "selections": [ - { - "alias": null, - "args": (v12/*: any*/), - "concreteType": "OrganizationConnection", - "kind": "LinkedField", - "name": "organizations", - "plural": false, - "selections": (v7/*: any*/), - "storageKey": "organizations(first:1000,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})" - }, - { - "alias": null, - "args": (v12/*: any*/), - "filters": [ - "orderBy" - ], - "handle": "connection", - "key": "OrganizationsPage_organizations", - "kind": "LinkedHandle", - "name": "organizations" - }, - { - "alias": null, - "args": (v13/*: any*/), - "concreteType": "InvitationConnection", - "kind": "LinkedField", - "name": "invitations", - "plural": false, - "selections": (v10/*: any*/), - "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" - }, - { - "alias": null, - "args": (v13/*: any*/), - "filters": [ - "orderBy", - "filter" - ], - "handle": "connection", - "key": "OrganizationsPage_invitations", - "kind": "LinkedHandle", - "name": "invitations" - }, - (v1/*: any*/) - ], - "storageKey": null - } - ] - }, - "params": { - "cacheID": "1b2e528133e56da643b9f8214a2f7192", - "id": null, - "metadata": { - "connection": [ - { - "count": null, - "cursor": null, - "direction": "forward", - "path": [ - "viewer", - "organizations" - ] - }, - { - "count": null, - "cursor": null, - "direction": "forward", - "path": [ - "viewer", - "invitations" - ] - } - ] - }, - "name": "OrganizationsPageQuery", - "operationKind": "query", - "text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {statuses: [PENDING]}) {\n edges {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n organization {\n id\n name\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n" - } -}; -})(); - -(node as any).hash = "ecd2534f36aa95ff7ff864ff22f09189"; - -export default node; diff --git a/apps/console/src/pages/auth/ConfirmEmailPage.tsx b/apps/console/src/pages/auth/ConfirmEmailPage.tsx index f788e79a5..abaa29787 100644 --- a/apps/console/src/pages/auth/ConfirmEmailPage.tsx +++ b/apps/console/src/pages/auth/ConfirmEmailPage.tsx @@ -111,7 +111,7 @@ export default function ConfirmEmailPage() {

{__("Your email has been confirmed successfully!")}

-
@@ -141,7 +141,7 @@ export default function ConfirmEmailPage() { {!isConfirmed && (

{__("Back to Login")} diff --git a/apps/console/src/pages/auth/ForgotPasswordPage.tsx b/apps/console/src/pages/auth/ForgotPasswordPage.tsx index 891bac129..085e6165e 100644 --- a/apps/console/src/pages/auth/ForgotPasswordPage.tsx +++ b/apps/console/src/pages/auth/ForgotPasswordPage.tsx @@ -23,7 +23,7 @@ export default function ForgotPasswordPage() { const onSubmit = handleSubmit(async (data) => { const response = await fetch( - buildEndpoint("/api/console/v1/auth/forget-password"), + buildEndpoint("/auth/forget-password"), { method: "POST", headers: { @@ -81,7 +81,7 @@ export default function ForgotPasswordPage() {

{__("Remember your password?")}{" "} {__("Back to login")} @@ -124,7 +124,7 @@ export default function ForgotPasswordPage() {

{__("Remember your password?")}{" "} {__("Back to login")} diff --git a/apps/console/src/pages/auth/LoginPage.tsx b/apps/console/src/pages/auth/LoginPage.tsx index e9c0965fc..f9d7e138c 100644 --- a/apps/console/src/pages/auth/LoginPage.tsx +++ b/apps/console/src/pages/auth/LoginPage.tsx @@ -1,81 +1,261 @@ import { useTranslate } from "@probo/i18n"; -import { Button, Field, useToast } from "@probo/ui"; +import { Button, Field, IconChevronLeft, useToast } from "@probo/ui"; import type { FormEventHandler } from "react"; -import { Link } from "react-router"; +import { useState } from "react"; +import { Link, useSearchParams } from "react-router"; import { buildEndpoint } from "/providers/RelayProviders"; export default function LoginPage() { const { __ } = useTranslate(); const { toast } = useToast(); + const [searchParams] = useSearchParams(); - const handleSubmit: FormEventHandler = async (e) => { + const authMethod = searchParams.get("method"); + const initialMode = authMethod === "password" ? "password" : authMethod === "sso" ? "sso" : "default"; + + const [mode, setMode] = useState<"default" | "password" | "sso">(initialMode); + const [isLoading, setIsLoading] = useState(false); + const [isChecking, setIsChecking] = useState(false); + + const handlePasswordLogin: FormEventHandler = async (e) => { e.preventDefault(); const formData = new FormData(e.currentTarget); - const email = formData.get("email")?.toString(); - const password = formData.get("password")?.toString(); + const emailValue = formData.get("email")?.toString(); + const passwordValue = formData.get("password")?.toString(); - fetch(buildEndpoint("/api/console/v1/auth/login"), { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email, password }), - }) - .then(async (res) => { - if (!res.ok) { - const error = await res.json(); - throw new Error(error.message || __("Failed to login")); - } - window.location.href = "/"; - }) - .catch((e) => { - toast({ - title: __("Error"), - description: e.message as string, - variant: "error", - }); + if (!emailValue || !passwordValue) return; + + setIsLoading(true); + + try { + const res = await fetch(buildEndpoint("/auth/login"), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email: emailValue, password: passwordValue }), }); + + if (!res.ok) { + const error = await res.json(); + throw new Error(error.message || __("Failed to login")); + } + + window.location.href = "/"; + } catch (e: any) { + toast({ + title: __("Error"), + description: e.message as string, + variant: "error", + }); + } finally { + setIsLoading(false); + } }; + const handleSSOLogin: FormEventHandler = async (e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const emailValue = formData.get("email")?.toString(); + + if (!emailValue) return; + + setIsChecking(true); + + try { + const res = await fetch(buildEndpoint("/auth/check-sso"), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email: emailValue }), + }); + + if (!res.ok) { + const error = await res.json(); + throw new Error(error.message || __("SSO not available for this email domain")); + } + + const data = await res.json(); + + if (data.ssoAvailable && data.samlConfigId) { + window.location.href = buildEndpoint( + `/auth/saml/login/${data.samlConfigId}` + ); + } else { + throw new Error(__("SSO not available for this email domain")); + } + } catch (e: any) { + toast({ + title: __("Error"), + description: e.message as string, + variant: "error", + }); + } finally { + setIsChecking(false); + } + }; + + const handleBack = () => { + setMode("default"); + }; + + if (mode === "default") { + return ( +

+

+ {__("Login to your account")} +

+

+ {__("Choose your login method")} +

+ + + +
+
+
+
+
+ + {__("Or")} + +
+
+ + + +
+ {__("Don't have an account ?")}{" "} + + {__("Register")} + +
+ +
+ {__("Forgot password?")}{" "} + + {__("Reset password")} + +
+
+ ); + } + + if (mode === "password") { + return ( +
+ + +

+ {__("Login with Email")} +

+

+ {__("Enter your email and password")} +

+ + + + + + + +
+ {__("Don't have an account ?")}{" "} + + {__("Register")} + +
+ +
+ {__("Forgot password?")}{" "} + + {__("Reset password")} + +
+ + ); + } + return ( -
+ + +

- {__("Login to your account")} + {__("Login with SSO")}

- {__("Enter your email below to login to your account")} + {__("Enter your work email to continue with SSO")}

+ - - + +
{__("Don't have an account ?")}{" "} - + {__("Register")}
-
- {__("Forgot password?")}{" "} - - {__("Reset password")} - -
); } diff --git a/apps/console/src/pages/auth/RegisterPage.tsx b/apps/console/src/pages/auth/RegisterPage.tsx index 091e252ef..5b9d8d521 100644 --- a/apps/console/src/pages/auth/RegisterPage.tsx +++ b/apps/console/src/pages/auth/RegisterPage.tsx @@ -26,7 +26,7 @@ export default function RegisterPage() { const onSubmit = handleSubmit(async (data) => { const response = await fetch( - buildEndpoint("/api/console/v1/auth/register"), + buildEndpoint("/auth/register"), { method: "POST", headers: { @@ -106,7 +106,7 @@ export default function RegisterPage() {

{__("Already have an account?")}{" "} {__("Log in here")} diff --git a/apps/console/src/pages/auth/ResetPasswordPage.tsx b/apps/console/src/pages/auth/ResetPasswordPage.tsx index 3afa7f0d2..966b2d05a 100644 --- a/apps/console/src/pages/auth/ResetPasswordPage.tsx +++ b/apps/console/src/pages/auth/ResetPasswordPage.tsx @@ -44,7 +44,7 @@ export default function ResetPasswordPage() { } const response = await fetch( - buildEndpoint("/api/console/v1/auth/reset-password"), + buildEndpoint("/auth/reset-password"), { method: "POST", headers: { @@ -74,7 +74,7 @@ export default function ResetPasswordPage() { description: __("Password reset successfully"), variant: "success", }); - navigate("/auth/login", { replace: true }); + navigate("/authentication/login", { replace: true }); }); usePageTitle(__("Reset password")); @@ -118,7 +118,7 @@ export default function ResetPasswordPage() {

{__("Remember your password?")}{" "} {__("Log in here")} diff --git a/apps/console/src/pages/auth/SignupFromInvitationPage.tsx b/apps/console/src/pages/auth/SignupFromInvitationPage.tsx index d03eeeca7..9a2804f06 100644 --- a/apps/console/src/pages/auth/SignupFromInvitationPage.tsx +++ b/apps/console/src/pages/auth/SignupFromInvitationPage.tsx @@ -51,7 +51,7 @@ export default function SignupFromInvitationPage() { } const response = await fetch( - buildEndpoint("/api/console/v1/auth/signup-from-invitation"), + buildEndpoint("/auth/signup-from-invitation"), { method: "POST", headers: { @@ -125,7 +125,7 @@ export default function SignupFromInvitationPage() {

{__("Already have an account?")}{" "} {__("Log in here")} diff --git a/apps/console/src/pages/organizations/SettingsPage.tsx b/apps/console/src/pages/organizations/SettingsPage.tsx index 3e4468ad7..103c2e970 100644 --- a/apps/console/src/pages/organizations/SettingsPage.tsx +++ b/apps/console/src/pages/organizations/SettingsPage.tsx @@ -1,213 +1,41 @@ -import { - Avatar, - Badge, - Button, - Card, - Dialog, - DialogContent, - DialogFooter, - Field, - FileButton, - IconTrashCan, - Label, - PageHeader, - Spinner, - TabBadge, - TabItem, - Tabs, - Tbody, - Td, - Textarea, - Th, - Thead, - Tr, - useConfirm, - useDialogRef, -} from "@probo/ui"; +import { Outlet } from "react-router"; import { useTranslate } from "@probo/i18n"; import type { PreloadedQuery } from "react-relay"; import type { OrganizationGraph_ViewQuery } from "/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql"; -import { useFragment, usePreloadedQuery, usePaginationFragment } from "react-relay"; +import { usePreloadedQuery } from "react-relay"; import { organizationViewQuery } from "/hooks/graph/OrganizationGraph"; -import { graphql } from "relay-runtime"; -import { SortableTable, SortableTh } from "/components/SortableTable"; -import clsx from "clsx"; -import type { SettingsPageFragment$key } from "./__generated__/SettingsPageFragment.graphql"; -import type { - SettingsPageMembershipsFragment$data, - SettingsPageMembershipsFragment$key -} from "./__generated__/SettingsPageMembershipsFragment.graphql"; -import type { - SettingsPageInvitationsFragment$data, - SettingsPageInvitationsFragment$key -} from "./__generated__/SettingsPageInvitationsFragment.graphql"; -import { useState, type ChangeEventHandler, useEffect, useRef } from "react"; -import { sprintf } from "@probo/helpers"; -import { useFormWithSchema } from "/hooks/useFormWithSchema"; -import { z } from "zod"; -import type { NodeOf } from "/types"; +import { + IconSettingsGear2, + IconPeopleAdd, + IconStore, + IconLock, + PageHeader, + TabLink, + Tabs, +} from "@probo/ui"; import { useOrganizationId } from "/hooks/useOrganizationId"; -import { InviteUserDialog } from "/components/organizations/InviteUserDialog"; -import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph"; -import { useNavigate } from "react-router"; -import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog"; -import { CustomDomainManager } from "/components/customDomains/CustomDomainManager"; -import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; - -const organizationSchema = z.object({ - name: z.string().min(1, "Organization name is required"), - description: z.string().optional(), - websiteUrl: z.string().optional(), - email: z.string().optional(), - headquarterAddress: z.string().optional(), -}); - -type OrganizationFormData = z.infer; - -type Props = { - queryRef: PreloadedQuery; -}; +import { graphql } from "relay-runtime"; +import type { SettingsPageFragment$key } from "./__generated__/SettingsPageFragment.graphql"; +import { useFragment } from "react-relay"; const organizationFragment = graphql` fragment SettingsPageFragment on Organization { id name - logoUrl - horizontalLogoUrl - description - websiteUrl - email - headquarterAddress - customDomain { - id - domain - sslStatus - dnsRecords { - type - name - value - ttl - purpose - } - createdAt - updatedAt - sslExpiresAt - } - createdAt - updatedAt + ...GeneralSettingsTabFragment + ...MembersSettingsTabMembershipsFragment + ...MembersSettingsTabInvitationsFragment + ...DomainSettingsTabFragment + ...SAMLSettingsTabFragment } `; -const paginatedMembershipsFragment = graphql` - fragment SettingsPageMembershipsFragment on Organization - @refetchable(queryName: "SettingsMembershipsRefetchQuery") - @argumentDefinitions( - first: { type: "Int", defaultValue: 20 } - order: { type: "MembershipOrder", defaultValue: { direction: ASC, field: CREATED_AT } } - after: { type: "CursorKey", defaultValue: null } - before: { type: "CursorKey", defaultValue: null } - last: { type: "Int", defaultValue: null } - ) { - memberships( - first: $first - after: $after - last: $last - before: $before - orderBy: $order - ) @connection(key: "SettingsPageMemberships_memberships") { - __id - totalCount - edges { - node { - id - fullName - emailAddress - role - createdAt - } - } - } - } -`; - -const paginatedInvitationsFragment = graphql` - fragment SettingsPageInvitationsFragment on Organization - @refetchable(queryName: "SettingsInvitationsRefetchQuery") - @argumentDefinitions( - first: { type: "Int", defaultValue: 20 } - order: { type: "InvitationOrder", defaultValue: { direction: ASC, field: CREATED_AT } } - after: { type: "CursorKey", defaultValue: null } - before: { type: "CursorKey", defaultValue: null } - last: { type: "Int", defaultValue: null } - ) { - invitations( - first: $first - after: $after - last: $last - before: $before - orderBy: $order - filter: {statuses: [PENDING, EXPIRED]} - ) @connection(key: "SettingsPageInvitations_invitations") { - __id - totalCount - edges { - node { - id - email - fullName - role - status - expiresAt - acceptedAt - createdAt - } - } - } - } -`; - -const deleteInvitationMutation = graphql` - mutation SettingsPage_DeleteInvitationMutation( - $input: DeleteInvitationInput! - $connections: [ID!]! - ) { - deleteInvitation(input: $input) { - deletedInvitationId @deleteEdge(connections: $connections) - } - } -`; - -const updateOrganizationMutation = graphql` - mutation SettingsPage_UpdateMutation($input: UpdateOrganizationInput!) { - updateOrganization(input: $input) { - organization { - id - name - logoUrl - horizontalLogoUrl - description - websiteUrl - email - headquarterAddress - } - } - } -`; - -const deleteHorizontalLogoMutation = graphql` - mutation SettingsPage_DeleteHorizontalLogoMutation($input: DeleteOrganizationHorizontalLogoInput!) { - deleteOrganizationHorizontalLogo(input: $input) { - organization { - id - horizontalLogoUrl - } - } - } -`; +type Props = { + queryRef: PreloadedQuery; +}; export default function SettingsPage({ queryRef }: Props) { const { __ } = useTranslate(); - const navigate = useNavigate(); const organizationId = useOrganizationId(); const organizationKey = usePreloadedQuery( organizationViewQuery, @@ -218,683 +46,30 @@ export default function SettingsPage({ queryRef }: Props) { organizationKey ); - const membershipsPagination = usePaginationFragment( - paginatedMembershipsFragment, - organizationKey as SettingsPageMembershipsFragment$key - ); - - const invitationsPagination = usePaginationFragment( - paginatedInvitationsFragment, - organizationKey as SettingsPageInvitationsFragment$key - ); - - const refetchMemberships = () => { - membershipsPagination.refetch({}, { fetchPolicy: 'network-only' }); - }; - - const refetchInvitations = () => { - invitationsPagination.refetch({}, { fetchPolicy: 'network-only' }); - }; - - const [updateOrganization, isUpdatingOrganization] = useMutationWithToasts( - updateOrganizationMutation, - { - successMessage: __("Organization updated successfully"), - errorMessage: __("Failed to update organization"), - } - ); - const [deleteHorizontalLogo, isDeletingHorizontalLogo] = useMutationWithToasts( - deleteHorizontalLogoMutation, - { - successMessage: __("Horizontal logo deleted successfully"), - errorMessage: __("Failed to delete horizontal logo"), - } - ); - const [deleteOrganization, isDeleting] = useDeleteOrganizationMutation(); - const memberships = membershipsPagination.data.memberships?.edges.map((edge) => edge.node) || []; - const invitations = invitationsPagination.data.invitations?.edges.map((edge) => edge.node) || []; - const [activeTab, setActiveTab] = useState<"memberships" | "invitations">("memberships"); - const [logoPreview, setLogoPreview] = useState(null); - const [horizontalLogoPreview, setHorizontalLogoPreview] = useState(null); - - const { formState, handleSubmit, register, reset } = useFormWithSchema( - organizationSchema, - { - defaultValues: { - name: organization.name || "", - description: organization.description || "", - websiteUrl: organization.websiteUrl || "", - email: organization.email || "", - headquarterAddress: organization.headquarterAddress || "", - }, - } - ); - - const prevOrgDataRef = useRef({ - name: organization.name, - description: organization.description, - websiteUrl: organization.websiteUrl, - email: organization.email, - headquarterAddress: organization.headquarterAddress, - }); - - useEffect(() => { - const prev = prevOrgDataRef.current; - const hasFormFieldChanges = - prev.name !== organization.name || - prev.description !== organization.description || - prev.websiteUrl !== organization.websiteUrl || - prev.email !== organization.email || - prev.headquarterAddress !== organization.headquarterAddress; - - if (hasFormFieldChanges) { - reset({ - name: organization.name || "", - description: organization.description || "", - websiteUrl: organization.websiteUrl || "", - email: organization.email || "", - headquarterAddress: organization.headquarterAddress || "", - }); - setLogoPreview(null); - setHorizontalLogoPreview(null); - - prevOrgDataRef.current = { - name: organization.name, - description: organization.description, - websiteUrl: organization.websiteUrl, - email: organization.email, - headquarterAddress: organization.headquarterAddress, - }; - } - }, [organization, reset]); - - const onSubmit = handleSubmit((data: OrganizationFormData) => { - updateOrganization({ - variables: { - input: { - organizationId: organization.id, - name: data.name, - description: data.description || undefined, - websiteUrl: data.websiteUrl || undefined, - email: data.email || undefined, - headquarterAddress: data.headquarterAddress || undefined, - }, - }, - }); - }); - - const handleLogoChange: ChangeEventHandler = (e) => { - const file = e.target.files?.[0]; - if (!file) { - return; - } - - const reader = new FileReader(); - reader.onloadend = () => { - setLogoPreview(reader.result as string); - }; - reader.readAsDataURL(file); - - updateOrganization({ - variables: { - input: { - organizationId: organization.id, - logoFile: null, - }, - }, - uploadables: { - "input.logoFile": file, - }, - onSuccess: () => { - setLogoPreview(null); - }, - }); - }; - - const handleHorizontalLogoChange: ChangeEventHandler = (e) => { - const file = e.target.files?.[0]; - if (!file) { - return; - } - - const reader = new FileReader(); - reader.onloadend = () => { - setHorizontalLogoPreview(reader.result as string); - }; - reader.readAsDataURL(file); - - updateOrganization({ - variables: { - input: { - organizationId: organization.id, - horizontalLogoFile: null, - }, - }, - uploadables: { - "input.horizontalLogoFile": file, - }, - onSuccess: () => { - setHorizontalLogoPreview(null); - }, - }); - }; - - const deleteDialogRef = useDialogRef(); - - const handleDeleteHorizontalLogo = () => { - deleteHorizontalLogo({ - variables: { - input: { - organizationId: organization.id, - }, - }, - onSuccess: () => { - deleteDialogRef.current?.close(); - }, - }); - }; - - const handleDeleteOrganization = () => { - return deleteOrganization({ - variables: { - input: { - organizationId: organization.id, - }, - connections: [], - }, - onSuccess: () => { - navigate("/", { replace: true }); - }, - }); - }; - return (

- {/* Organization settings */} -
-
-
-

- {__("Organization details")} -

- {formState.isSubmitting && } -
- -
- -
- - - {isUpdatingOrganization ? __("Uploading...") : __("Change logo")} - -
-
-
- -

- {__("Upload a horizontal version of your logo for use in documents")} -

-
- {(horizontalLogoPreview || organization.horizontalLogoUrl) && ( -
- {__("Horizontal -
- )} - - {isUpdatingOrganization - ? __("Uploading...") - : (horizontalLogoPreview || organization.horizontalLogoUrl) - ? __("Change horizontal logo") - : __("Upload horizontal logo")} - - {organization.horizontalLogoUrl && ( - - } - title={__("Delete Horizontal Logo")} - className="max-w-md" - > - -

- {__("Are you sure you want to delete the horizontal logo?")} -

-

- {__("This action cannot be undone.")} -

-
+ + + + {__("General")} + + + + {__("Members")} + + + + {__("Domain")} + + + + {__("SAML SSO")} + + - - - -
- )} -
-
- -
- -