From ddf585fc18a83d27eba3ad39511406b107b0638d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Thu, 18 Dec 2025 20:28:03 +0100 Subject: [PATCH] Replug organizations page without assumable sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../iam/organizations/OrganizationsPage.tsx | 116 +++++ .../iam/organizations/OrganizationsQuery.tsx | 60 +++ .../OrganizationsQuery.graphql.ts | 340 +++++++++++++++ .../_components/InvitationCard.tsx | 56 +++ .../_components/OrganizationCard.tsx | 126 ++++++ .../InvitationCardFragment.graphql.ts | 85 ++++ .../OrganizationCardFragment.graphql.ts | 58 +++ apps/console/src/routes.tsx | 25 +- pkg/iam/organization_service.go | 39 ++ pkg/server/api/connect/v1/schema.graphql | 51 ++- pkg/server/api/connect/v1/schema/schema.go | 395 ++++++++++++++++-- pkg/server/api/connect/v1/types/invitation.go | 1 + pkg/server/api/connect/v1/types/types.go | 37 +- pkg/server/api/connect/v1/v1_resolver.go | 22 +- 14 files changed, 1342 insertions(+), 69 deletions(-) create mode 100644 apps/console/src/pages/iam/organizations/OrganizationsPage.tsx create mode 100644 apps/console/src/pages/iam/organizations/OrganizationsQuery.tsx create mode 100644 apps/console/src/pages/iam/organizations/__generated__/OrganizationsQuery.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/_components/InvitationCard.tsx create mode 100644 apps/console/src/pages/iam/organizations/_components/OrganizationCard.tsx create mode 100644 apps/console/src/pages/iam/organizations/_components/__generated__/InvitationCardFragment.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationCardFragment.graphql.ts diff --git a/apps/console/src/pages/iam/organizations/OrganizationsPage.tsx b/apps/console/src/pages/iam/organizations/OrganizationsPage.tsx new file mode 100644 index 000000000..5f503d983 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/OrganizationsPage.tsx @@ -0,0 +1,116 @@ +import { usePreloadedQuery, type PreloadedQuery } from "react-relay"; +import { useTranslate } from "@probo/i18n"; +import { + Button, + Card, + IconMagnifyingGlass, + IconPlusLarge, + Input, +} from "@probo/ui"; +import { useMemo, useState } from "react"; +import { OrganizationCard } from "./_components/OrganizationCard"; +import { InvitationCard } from "./_components/InvitationCard"; +import type { + OrganizationsQuery, + OrganizationsQuery$data, +} from "./__generated__/OrganizationsQuery.graphql"; +import { organizationsQuery } from "./OrganizationsQuery"; + +interface PageProps { + queryRef: PreloadedQuery; +} + +export default function Page(props: PageProps) { + const { __ } = useTranslate(); + const [search, setSearch] = useState(""); + + const { queryRef } = props; + const { + viewer: { + memberships: { edges: initialMemberships }, + pendingInvitations: { edges: invitations }, + }, + } = usePreloadedQuery(organizationsQuery, queryRef); + + const memberships = useMemo< + OrganizationsQuery$data["viewer"]["memberships"]["edges"] + >(() => { + if (!search.trim()) { + return initialMemberships; + } + return initialMemberships.filter(({ node }) => + node.organization.name.toLowerCase().includes(search.toLowerCase()), + ); + }, [initialMemberships, search]); + + return ( + <> +
+

+ {__("Select an organization")} +

+
+ {invitations.length > 0 && ( +
+

+ {__("Pending invitations")} +

+ {invitations.map(({ node }) => ( + + ))} +
+ )} + {memberships.length > 0 && ( +
+ {invitations.length > 0 && ( +

+ {__("Your organizations")} +

+ )} + {memberships.length > 3 && ( +
+ +
+ )} + {memberships.length === 0 ? ( +
+ {__("No organizations found")} +
+ ) : ( + memberships.map(({ node: { id, organization } }) => ( + + )) + )} +
+ )} + +

+ {__("Create an organization")} +

+

+ {__("Add a new organization to your account")} +

+ +
+
+
+ + ); +} diff --git a/apps/console/src/pages/iam/organizations/OrganizationsQuery.tsx b/apps/console/src/pages/iam/organizations/OrganizationsQuery.tsx new file mode 100644 index 000000000..9a169b6de --- /dev/null +++ b/apps/console/src/pages/iam/organizations/OrganizationsQuery.tsx @@ -0,0 +1,60 @@ +import { lazy } from "@probo/react-lazy"; +import { useQueryLoader } from "react-relay"; +import { graphql } from "relay-runtime"; +import type { OrganizationsQuery } from "./__generated__/OrganizationsQuery.graphql"; +import { Suspense, useEffect } from "react"; +import { CenteredLayoutSkeleton } from "@probo/ui"; + +const Page = lazy(() => import("./OrganizationsPage")); + +export const organizationsQuery = graphql` + query OrganizationsQuery { + viewer @required(action: THROW) { + memberships( + first: 1000 + orderBy: { direction: DESC, field: CREATED_AT } + ) { + edges { + node { + id + createdAt + organization { + name + ...OrganizationCardFragment + } + } + } + } + pendingInvitations( + first: 1000 + orderBy: { direction: DESC, field: CREATED_AT } + ) { + edges { + node { + id + ...InvitationCardFragment + } + } + } + } + } +`; + +export function OrganizationsQuery() { + const [queryRef, loadQuery] = + useQueryLoader(organizationsQuery); + + useEffect(() => { + loadQuery({}); + }, [loadQuery]); + + if (!queryRef) { + return ; + } + + return ( + }> + + + ); +} diff --git a/apps/console/src/pages/iam/organizations/__generated__/OrganizationsQuery.graphql.ts b/apps/console/src/pages/iam/organizations/__generated__/OrganizationsQuery.graphql.ts new file mode 100644 index 000000000..077e84e14 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/__generated__/OrganizationsQuery.graphql.ts @@ -0,0 +1,340 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type OrganizationsQuery$variables = Record; +export type OrganizationsQuery$data = { + readonly viewer: { + readonly memberships: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly createdAt: any; + readonly id: string; + readonly organization: { + readonly name: string; + readonly " $fragmentSpreads": FragmentRefs<"OrganizationCardFragment">; + }; + }; + }>; + }; + readonly pendingInvitations: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"InvitationCardFragment">; + }; + }>; + }; + }; +}; +export type OrganizationsQuery = { + response: OrganizationsQuery$data; + variables: OrganizationsQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "first", + "value": 1000 + }, + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "DESC", + "field": "CREATED_AT" + } + } +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "OrganizationsQuery", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": "MembershipConnection", + "kind": "LinkedField", + "name": "memberships", + "plural": false, + "selections": [ + { + "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": [ + (v1/*: any*/), + (v2/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "OrganizationCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + }, + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "pendingInvitations", + "plural": false, + "selections": [ + { + "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*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "InvitationCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "pendingInvitations(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + } + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "OrganizationsQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": "MembershipConnection", + "kind": "LinkedField", + "name": "memberships", + "plural": false, + "selections": [ + { + "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": [ + (v1/*: any*/), + (v2/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v3/*: any*/), + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "logoUrl", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + }, + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "pendingInvitations", + "plural": false, + "selections": [ + { + "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": "role", + "storageKey": null + }, + (v2/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v1/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "pendingInvitations(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + }, + (v1/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "145dddcd59c5c55a1c61913d9f43b38b", + "id": null, + "metadata": {}, + "name": "OrganizationsQuery", + "operationKind": "query", + "text": "query OrganizationsQuery {\n viewer {\n memberships(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n createdAt\n organization {\n name\n ...OrganizationCardFragment\n id\n }\n }\n }\n }\n pendingInvitations(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationCardFragment\n }\n }\n }\n id\n }\n}\n\nfragment InvitationCardFragment on Invitation {\n id\n role\n createdAt\n organization {\n id\n name\n }\n}\n\nfragment OrganizationCardFragment on Organization {\n id\n name\n logoUrl\n}\n" + } +}; +})(); + +(node as any).hash = "f85219c0fcd69be1b5cfb0a5b9c9b8db"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/_components/InvitationCard.tsx b/apps/console/src/pages/iam/organizations/_components/InvitationCard.tsx new file mode 100644 index 000000000..b4c0440a9 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/InvitationCard.tsx @@ -0,0 +1,56 @@ +import { formatDate } from "@probo/helpers"; +import { useTranslate } from "@probo/i18n"; +import { Card } from "@probo/ui"; +import { graphql } from "relay-runtime"; +import { useFragment } from "react-relay"; +import type { InvitationCardFragment$key } from "./__generated__/InvitationCardFragment.graphql"; + +const fragment = graphql` + fragment InvitationCardFragment on Invitation { + id + role + createdAt + organization { + id + name + } + } +`; + +interface InvitationCardProps { + fKey: InvitationCardFragment$key; + // onAccept: (invitationId: string, organizationId: string) => void; + // isAccepting: boolean; +} + +export function InvitationCard(props: InvitationCardProps) { + const { fKey } = props; + + const { __ } = useTranslate(); + + const invitation = useFragment(fragment, fKey); + + return ( + +
+
+

+ {invitation.organization.name} +

+

+ {__("Role")}: {invitation.role} +

+

+ {__("Invited on")} {formatDate(invitation.createdAt)} +

+
+ {/* */} +
+
+ ); +} diff --git a/apps/console/src/pages/iam/organizations/_components/OrganizationCard.tsx b/apps/console/src/pages/iam/organizations/_components/OrganizationCard.tsx new file mode 100644 index 000000000..347b03a41 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/OrganizationCard.tsx @@ -0,0 +1,126 @@ +import { useTranslate } from "@probo/i18n"; +import { Avatar, Badge, Button, Card, IconLock } from "@probo/ui"; +import { Link } from "react-router"; +import { graphql } from "relay-runtime"; +import { useFragment } from "react-relay"; +import type { OrganizationCardFragment$key } from "./__generated__/OrganizationCardFragment.graphql"; + +const fragment = graphql` + fragment OrganizationCardFragment on Organization { + id + name + logoUrl + } +`; + +interface OrganizationCardProps { + fKey: OrganizationCardFragment$key; +} + +export function OrganizationCard(props: OrganizationCardProps) { + const { fKey } = props; + const { __ } = useTranslate(); + + const organization = useFragment( + fragment, + fKey, + ); + // 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 targetUrl = `/organizations/${organization.id}`; + + 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("/connect/saml/"); + + return ( + +
+ {isSAMLUrl ? ( + + +
+

{organization.name}

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

{organization.name}

+ {getAuthBadge()} +
+ + )} +
+ +
+
+
+ ); +} diff --git a/apps/console/src/pages/iam/organizations/_components/__generated__/InvitationCardFragment.graphql.ts b/apps/console/src/pages/iam/organizations/_components/__generated__/InvitationCardFragment.graphql.ts new file mode 100644 index 000000000..c583b27cd --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/__generated__/InvitationCardFragment.graphql.ts @@ -0,0 +1,85 @@ +/** + * @generated SignedSource<<5193da6ee4b5da0bed510c255c3407b1>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; +import { FragmentRefs } from "relay-runtime"; +export type InvitationCardFragment$data = { + readonly createdAt: any; + readonly id: string; + readonly organization: { + readonly id: string; + readonly name: string; + }; + readonly role: MembershipRole; + readonly " $fragmentType": "InvitationCardFragment"; +}; +export type InvitationCardFragment$key = { + readonly " $data"?: InvitationCardFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"InvitationCardFragment">; +}; + +const node: ReaderFragment = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "InvitationCardFragment", + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "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": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Invitation", + "abstractKey": null +}; +})(); + +(node as any).hash = "1f61b5f07abc69ad33c880bc39cd0e96"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationCardFragment.graphql.ts b/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationCardFragment.graphql.ts new file mode 100644 index 000000000..270f86f2b --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationCardFragment.graphql.ts @@ -0,0 +1,58 @@ +/** + * @generated SignedSource<<06aa55abfa21c09c85bb308f813565d8>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type OrganizationCardFragment$data = { + readonly id: string; + readonly logoUrl: string | null | undefined; + readonly name: string; + readonly " $fragmentType": "OrganizationCardFragment"; +}; +export type OrganizationCardFragment$key = { + readonly " $data"?: OrganizationCardFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"OrganizationCardFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "OrganizationCardFragment", + "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 + } + ], + "type": "Organization", + "abstractKey": null +}; + +(node as any).hash = "a8b9f4a515650db79f41507fb52f7b96"; + +export default node; diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx index 90854ca13..f76a7e3f1 100644 --- a/apps/console/src/routes.tsx +++ b/apps/console/src/routes.tsx @@ -49,7 +49,7 @@ import { UnAuthenticatedError, UnauthorizedError, } from "@probo/relay"; -import { OrganizationListLoading } from "./pages/iam/organization/list/loading.tsx"; +import { OrganizationsQuery } from "./pages/iam/organizations/OrganizationsQuery.tsx"; /** * Top level error boundary @@ -121,7 +121,7 @@ const routes = [ index: true, Component: () => ( - + ), }, @@ -134,7 +134,7 @@ const routes = [ { path: "documents/signing-requests", Component: lazy( - () => import("./pages/DocumentSigningRequestsPage.tsx") + () => import("./pages/DocumentSigningRequestsPage.tsx"), ), }, { @@ -154,12 +154,13 @@ const routes = [ loader: loaderFromQueryLoader(({ organizationId }) => loadQuery(consoleEnvironment, employeeDocumentsQuery, { organizationId: organizationId!, - }) + }), ), Component: withQueryRef( lazy( - () => import("./pages/organizations/employee/EmployeeDocumentsPage") - ) + () => + import("./pages/organizations/employee/EmployeeDocumentsPage"), + ), ), }, { @@ -169,13 +170,13 @@ const routes = [ loader: loaderFromQueryLoader(({ documentId }) => loadQuery(consoleEnvironment, employeeDocumentSignatureQuery, { documentId: documentId!, - }) + }), ), Component: withQueryRef( lazy( () => - import("./pages/organizations/employee/EmployeeDocumentSignaturePage") - ) + import("./pages/organizations/employee/EmployeeDocumentSignaturePage"), + ), ), }, ], @@ -209,10 +210,10 @@ const routes = [ loader: loaderFromQueryLoader(({ organizationId }) => loadQuery(consoleEnvironment, organizationViewQuery, { organizationId: organizationId!, - }) + }), ), Component: withQueryRef( - lazy(() => import("./pages/organizations/SettingsPage")) + lazy(() => import("./pages/organizations/SettingsPage")), ), children: [ { @@ -236,7 +237,7 @@ const routes = [ { path: "domain", Component: lazy( - () => import("./pages/organizations/settings/DomainSettingsTab") + () => import("./pages/organizations/settings/DomainSettingsTab"), ), }, // { diff --git a/pkg/iam/organization_service.go b/pkg/iam/organization_service.go index 5204eb76c..1465a779e 100644 --- a/pkg/iam/organization_service.go +++ b/pkg/iam/organization_service.go @@ -753,6 +753,45 @@ func (s *OrganizationService) GetOrganizationForMembership(ctx context.Context, return organization, nil } +func (s *OrganizationService) GetOrganizationForInvitation(ctx context.Context, invitationID gid.GID) (*coredata.Organization, error) { + var ( + scope = coredata.NewScopeFromObjectID(invitationID) + organization = &coredata.Organization{} + ) + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + invitation := &coredata.Invitation{} + err := invitation.LoadByID(ctx, conn, scope, invitationID) + if err != nil { + if err == coredata.ErrResourceNotFound { + return NewInvitationNotFoundError(invitationID) + } + + return fmt.Errorf("cannot load invitation: %w", err) + } + + err = organization.LoadByID(ctx, conn, scope, invitation.OrganizationID) + if err != nil { + if err == coredata.ErrResourceNotFound { + return NewOrganizationNotFoundError(invitation.OrganizationID) + } + + return fmt.Errorf("cannot load organization: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return organization, nil +} + func (s OrganizationService) GenerateLogoURL( ctx context.Context, organizationID gid.GID, diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index 4b23be4c8..a65921dab 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -145,6 +145,7 @@ type Identity implements Node { after: CursorKey last: Int before: CursorKey + orderBy: InvitationOrder ): InvitationConnection! @goField(forceResolver: true) @isViewer sessions( @@ -224,6 +225,17 @@ type Organization implements Node { availableApplications: [Application!]! } +enum MembershipRole + @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") { + OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner") + ADMIN @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAdmin") + EMPLOYEE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee") + VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer") + AUDITOR + @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor") +} + type Membership implements Node { id: ID! identityId: ID! @@ -231,6 +243,7 @@ type Membership implements Node { profile: IdentityProfile! identity: Identity! @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true) + role: MembershipRole! permissions: [Permission!]! provisionedBy: ProvisioningSource! active: Boolean! @@ -242,10 +255,12 @@ type Membership implements Node { type Invitation implements Node { id: ID! email: EmailAddr! + role: MembershipRole! expiresAt: Datetime! acceptedAt: Datetime createdAt: Datetime! status: InvitationStatus! + organization: Organization! @goField(forceResolver: true) } type Session implements Node { @@ -404,14 +419,6 @@ enum ProvisioningSource { enum MembershipOrderField @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipOrderField") { - FULL_NAME - @goEnum( - value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldFullName" - ) - EMAIL_ADDRESS - @goEnum( - value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldEmailAddress" - ) ROLE @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldRole") CREATED_AT @@ -442,6 +449,34 @@ type MembershipEdge { cursor: CursorKey! } +enum InvitationOrderField + @goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationOrderField") { + EMAIL + @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldEmail") + ROLE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldRole") + CREATED_AT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldCreatedAt" + ) + EXPIRES_AT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldExpiresAt" + ) + ACCEPTED_AT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldAcceptedAt" + ) +} + +input InvitationOrder + @goModel( + model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.InvitationOrderBy" + ) { + direction: OrderDirection! + field: InvitationOrderField! +} + type InvitationConnection @goModel( model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.InvitationConnection" diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index 8a48cd9e0..eee0c3572 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -47,6 +47,7 @@ type Config struct { type ResolverRoot interface { Identity() IdentityResolver + Invitation() InvitationResolver InvitationConnection() InvitationConnectionResolver Membership() MembershipResolver MembershipConnection() MembershipConnectionResolver @@ -132,7 +133,7 @@ type ComplexityRoot struct { EmailVerified func(childComplexity int) int ID func(childComplexity int) int Memberships func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) int - PendingInvitations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int + PendingInvitations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrderBy) int PersonalAPIKeys func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int ProfileFor func(childComplexity int, organizationID gid.GID) int Sessions func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SessionOrder) int @@ -161,12 +162,14 @@ type ComplexityRoot struct { } Invitation struct { - AcceptedAt func(childComplexity int) int - CreatedAt func(childComplexity int) int - Email func(childComplexity int) int - ExpiresAt func(childComplexity int) int - ID func(childComplexity int) int - Status func(childComplexity int) int + AcceptedAt func(childComplexity int) int + CreatedAt func(childComplexity int) int + Email func(childComplexity int) int + ExpiresAt func(childComplexity int) int + ID func(childComplexity int) int + Organization func(childComplexity int) int + Role func(childComplexity int) int + Status func(childComplexity int) int } InvitationConnection struct { @@ -196,6 +199,7 @@ type ComplexityRoot struct { Permissions func(childComplexity int) int Profile func(childComplexity int) int ProvisionedBy func(childComplexity int) int + Role func(childComplexity int) int } MembershipConnection struct { @@ -429,10 +433,13 @@ type ComplexityRoot struct { type IdentityResolver interface { Memberships(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) - PendingInvitations(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.InvitationConnection, error) + PendingInvitations(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrderBy) (*types.InvitationConnection, error) Sessions(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SessionOrder) (*types.SessionConnection, error) PersonalAPIKeys(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PersonalAPIKeyConnection, error) } +type InvitationResolver interface { + Organization(ctx context.Context, obj *types.Invitation) (*types.Organization, error) +} type InvitationConnectionResolver interface { TotalCount(ctx context.Context, obj *types.InvitationConnection) (int, error) } @@ -693,7 +700,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Identity.PendingInvitations(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true + return e.complexity.Identity.PendingInvitations(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.InvitationOrderBy)), true case "Identity.personalAPIKeys": if e.complexity.Identity.PersonalAPIKeys == nil { break @@ -873,6 +880,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Invitation.ID(childComplexity), true + case "Invitation.organization": + if e.complexity.Invitation.Organization == nil { + break + } + + return e.complexity.Invitation.Organization(childComplexity), true + case "Invitation.role": + if e.complexity.Invitation.Role == nil { + break + } + + return e.complexity.Invitation.Role(childComplexity), true case "Invitation.status": if e.complexity.Invitation.Status == nil { break @@ -985,6 +1004,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Membership.ProvisionedBy(childComplexity), true + case "Membership.role": + if e.complexity.Membership.Role == nil { + break + } + + return e.complexity.Membership.Role(childComplexity), true case "MembershipConnection.edges": if e.complexity.MembershipConnection.Edges == nil { @@ -1937,6 +1962,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputDeleteOrganizationInput, ec.unmarshalInputDeleteSAMLConfigurationInput, ec.unmarshalInputForgotPasswordInput, + ec.unmarshalInputInvitationOrder, ec.unmarshalInputInviteMemberInput, ec.unmarshalInputMembershipOrder, ec.unmarshalInputRemoveIPAllowlistEntryInput, @@ -2199,6 +2225,7 @@ type Identity implements Node { after: CursorKey last: Int before: CursorKey + orderBy: InvitationOrder ): InvitationConnection! @goField(forceResolver: true) @isViewer sessions( @@ -2278,6 +2305,17 @@ type Organization implements Node { availableApplications: [Application!]! } +enum MembershipRole + @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") { + OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner") + ADMIN @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAdmin") + EMPLOYEE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee") + VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer") + AUDITOR + @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor") +} + type Membership implements Node { id: ID! identityId: ID! @@ -2285,6 +2323,7 @@ type Membership implements Node { profile: IdentityProfile! identity: Identity! @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true) + role: MembershipRole! permissions: [Permission!]! provisionedBy: ProvisioningSource! active: Boolean! @@ -2296,10 +2335,12 @@ type Membership implements Node { type Invitation implements Node { id: ID! email: EmailAddr! + role: MembershipRole! expiresAt: Datetime! acceptedAt: Datetime createdAt: Datetime! status: InvitationStatus! + organization: Organization! @goField(forceResolver: true) } type Session implements Node { @@ -2458,14 +2499,6 @@ enum ProvisioningSource { enum MembershipOrderField @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipOrderField") { - FULL_NAME - @goEnum( - value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldFullName" - ) - EMAIL_ADDRESS - @goEnum( - value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldEmailAddress" - ) ROLE @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldRole") CREATED_AT @@ -2496,6 +2529,34 @@ type MembershipEdge { cursor: CursorKey! } +enum InvitationOrderField + @goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationOrderField") { + EMAIL + @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldEmail") + ROLE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldRole") + CREATED_AT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldCreatedAt" + ) + EXPIRES_AT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldExpiresAt" + ) + ACCEPTED_AT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldAcceptedAt" + ) +} + +input InvitationOrder + @goModel( + model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.InvitationOrderBy" + ) { + direction: OrderDirection! + field: InvitationOrderField! +} + type InvitationConnection @goModel( model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.InvitationConnection" @@ -2908,6 +2969,11 @@ func (ec *executionContext) field_Identity_pendingInvitations_args(ctx context.C return nil, err } args["before"] = arg3 + arg4, err := graphql.ProcessArgField(ctx, rawArgs, "orderBy", ec.unmarshalOInvitationOrder2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐInvitationOrderBy) + if err != nil { + return nil, err + } + args["orderBy"] = arg4 return args, nil } @@ -4241,7 +4307,7 @@ func (ec *executionContext) _Identity_pendingInvitations(ctx context.Context, fi ec.fieldContext_Identity_pendingInvitations, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Identity().PendingInvitations(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey)) + return ec.resolvers.Identity().PendingInvitations(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.InvitationOrderBy)) }, func(ctx context.Context, next graphql.Resolver) graphql.Resolver { directive0 := next @@ -5178,6 +5244,35 @@ func (ec *executionContext) fieldContext_Invitation_email(_ context.Context, fie return fc, nil } +func (ec *executionContext) _Invitation_role(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Invitation_role, + func(ctx context.Context) (any, error) { + return obj.Role, nil + }, + nil, + ec.marshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Invitation_role(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Invitation", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type MembershipRole does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Invitation_expiresAt(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -5294,6 +5389,57 @@ func (ec *executionContext) fieldContext_Invitation_status(_ context.Context, fi return fc, nil } +func (ec *executionContext) _Invitation_organization(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Invitation_organization, + func(ctx context.Context) (any, error) { + return ec.resolvers.Invitation().Organization(ctx, obj) + }, + nil, + ec.marshalNOrganization2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐOrganization, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Invitation_organization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Invitation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Organization_id(ctx, field) + case "name": + return ec.fieldContext_Organization_name(ctx, field) + case "logoUrl": + return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) + case "createdAt": + return ec.fieldContext_Organization_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_Organization_updatedAt(ctx, field) + case "members": + return ec.fieldContext_Organization_members(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) + case "samlConfigurations": + return ec.fieldContext_Organization_samlConfigurations(ctx, field) + case "availableApplications": + return ec.fieldContext_Organization_availableApplications(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _InvitationConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.InvitationConnection) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -5425,6 +5571,8 @@ func (ec *executionContext) fieldContext_InvitationEdge_node(_ context.Context, return ec.fieldContext_Invitation_id(ctx, field) case "email": return ec.fieldContext_Invitation_email(ctx, field) + case "role": + return ec.fieldContext_Invitation_role(ctx, field) case "expiresAt": return ec.fieldContext_Invitation_expiresAt(ctx, field) case "acceptedAt": @@ -5433,6 +5581,8 @@ func (ec *executionContext) fieldContext_InvitationEdge_node(_ context.Context, return ec.fieldContext_Invitation_createdAt(ctx, field) case "status": return ec.fieldContext_Invitation_status(ctx, field) + case "organization": + return ec.fieldContext_Invitation_organization(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Invitation", field.Name) }, @@ -5760,6 +5910,35 @@ func (ec *executionContext) fieldContext_Membership_organization(_ context.Conte return fc, nil } +func (ec *executionContext) _Membership_role(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Membership_role, + func(ctx context.Context) (any, error) { + return obj.Role, nil + }, + nil, + ec.marshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Membership_role(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Membership", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type MembershipRole does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Membership_permissions(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -6087,6 +6266,8 @@ func (ec *executionContext) fieldContext_MembershipEdge_node(_ context.Context, return ec.fieldContext_Membership_identity(ctx, field) case "organization": return ec.fieldContext_Membership_organization(ctx, field) + case "role": + return ec.fieldContext_Membership_role(ctx, field) case "permissions": return ec.fieldContext_Membership_permissions(ctx, field) case "provisionedBy": @@ -13046,6 +13227,40 @@ func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Contex return it, nil } +func (ec *executionContext) unmarshalInputInvitationOrder(ctx context.Context, obj any) (types.InvitationOrderBy, error) { + var it types.InvitationOrderBy + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"direction", "field"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "direction": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("direction")) + data, err := ec.unmarshalNOrderDirection2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐOrderDirection(ctx, v) + if err != nil { + return it, err + } + it.Direction = data + case "field": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("field")) + data, err := ec.unmarshalNInvitationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐInvitationOrderField(ctx, v) + if err != nil { + return it, err + } + it.Field = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputInviteMemberInput(ctx context.Context, obj any) (types.InviteMemberInput, error) { var it types.InviteMemberInput asMap := map[string]any{} @@ -14750,30 +14965,71 @@ func (ec *executionContext) _Invitation(ctx context.Context, sel ast.SelectionSe case "id": out.Values[i] = ec._Invitation_id(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "email": out.Values[i] = ec._Invitation_email(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) + } + case "role": + out.Values[i] = ec._Invitation_role(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) } case "expiresAt": out.Values[i] = ec._Invitation_expiresAt(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "acceptedAt": out.Values[i] = ec._Invitation_acceptedAt(ctx, field, obj) case "createdAt": out.Values[i] = ec._Invitation_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "status": out.Values[i] = ec._Invitation_status(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } + case "organization": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Invitation_organization(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -15063,6 +15319,11 @@ func (ec *executionContext) _Membership(ctx context.Context, sel ast.SelectionSe } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "role": + out.Values[i] = ec._Membership_role(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "permissions": out.Values[i] = ec._Membership_permissions(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -18209,6 +18470,40 @@ func (ec *executionContext) marshalNInvitationEdge2ᚖgoᚗproboᚗincᚋprobo return ec._InvitationEdge(ctx, sel, v) } +func (ec *executionContext) unmarshalNInvitationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐInvitationOrderField(ctx context.Context, v any) (coredata.InvitationOrderField, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNInvitationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐInvitationOrderField[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInvitationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐInvitationOrderField(ctx context.Context, sel ast.SelectionSet, v coredata.InvitationOrderField) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNInvitationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐInvitationOrderField[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNInvitationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐInvitationOrderField = map[string]coredata.InvitationOrderField{ + "EMAIL": coredata.InvitationOrderFieldEmail, + "ROLE": coredata.InvitationOrderFieldRole, + "CREATED_AT": coredata.InvitationOrderFieldCreatedAt, + "EXPIRES_AT": coredata.InvitationOrderFieldExpiresAt, + "ACCEPTED_AT": coredata.InvitationOrderFieldAcceptedAt, + } + marshalNInvitationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐInvitationOrderField = map[coredata.InvitationOrderField]string{ + coredata.InvitationOrderFieldEmail: "EMAIL", + coredata.InvitationOrderFieldRole: "ROLE", + coredata.InvitationOrderFieldCreatedAt: "CREATED_AT", + coredata.InvitationOrderFieldExpiresAt: "EXPIRES_AT", + coredata.InvitationOrderFieldAcceptedAt: "ACCEPTED_AT", + } +) + func (ec *executionContext) unmarshalNInvitationStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐInvitationStatus(ctx context.Context, v any) (coredata.InvitationStatus, error) { tmp, err := graphql.UnmarshalString(v) res := unmarshalNInvitationStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐInvitationStatus[tmp] @@ -18355,16 +18650,46 @@ func (ec *executionContext) marshalNMembershipOrderField2goᚗproboᚗincᚋprob var ( unmarshalNMembershipOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipOrderField = map[string]coredata.MembershipOrderField{ - "FULL_NAME": coredata.MembershipOrderFieldFullName, - "EMAIL_ADDRESS": coredata.MembershipOrderFieldEmailAddress, - "ROLE": coredata.MembershipOrderFieldRole, - "CREATED_AT": coredata.MembershipOrderFieldCreatedAt, + "ROLE": coredata.MembershipOrderFieldRole, + "CREATED_AT": coredata.MembershipOrderFieldCreatedAt, } marshalNMembershipOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipOrderField = map[coredata.MembershipOrderField]string{ - coredata.MembershipOrderFieldFullName: "FULL_NAME", - coredata.MembershipOrderFieldEmailAddress: "EMAIL_ADDRESS", - coredata.MembershipOrderFieldRole: "ROLE", - coredata.MembershipOrderFieldCreatedAt: "CREATED_AT", + coredata.MembershipOrderFieldRole: "ROLE", + coredata.MembershipOrderFieldCreatedAt: "CREATED_AT", + } +) + +func (ec *executionContext) unmarshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole(ctx context.Context, v any) (coredata.MembershipRole, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole(ctx context.Context, sel ast.SelectionSet, v coredata.MembershipRole) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole = map[string]coredata.MembershipRole{ + "OWNER": coredata.MembershipRoleOwner, + "ADMIN": coredata.MembershipRoleAdmin, + "EMPLOYEE": coredata.MembershipRoleEmployee, + "VIEWER": coredata.MembershipRoleViewer, + "AUDITOR": coredata.MembershipRoleAuditor, + } + marshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole = map[coredata.MembershipRole]string{ + coredata.MembershipRoleOwner: "OWNER", + coredata.MembershipRoleAdmin: "ADMIN", + coredata.MembershipRoleEmployee: "EMPLOYEE", + coredata.MembershipRoleViewer: "VIEWER", + coredata.MembershipRoleAuditor: "AUDITOR", } ) @@ -19624,6 +19949,14 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele return res } +func (ec *executionContext) unmarshalOInvitationOrder2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐInvitationOrderBy(ctx context.Context, v any) (*types.InvitationOrderBy, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputInvitationOrder(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalOInvitationStatus2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐInvitationStatus(ctx context.Context, v any) (*coredata.InvitationStatus, error) { if v == nil { return nil, nil diff --git a/pkg/server/api/connect/v1/types/invitation.go b/pkg/server/api/connect/v1/types/invitation.go index 58c8ad542..e1751eb42 100644 --- a/pkg/server/api/connect/v1/types/invitation.go +++ b/pkg/server/api/connect/v1/types/invitation.go @@ -66,6 +66,7 @@ func NewInvitation(invitation *coredata.Invitation) *Invitation { return &Invitation{ ID: invitation.ID, Email: invitation.Email, + Role: invitation.Role, ExpiresAt: invitation.ExpiresAt, AcceptedAt: invitation.AcceptedAt, CreatedAt: invitation.CreatedAt, diff --git a/pkg/server/api/connect/v1/types/types.go b/pkg/server/api/connect/v1/types/types.go index efbabe323..1e744e2ac 100644 --- a/pkg/server/api/connect/v1/types/types.go +++ b/pkg/server/api/connect/v1/types/types.go @@ -193,12 +193,14 @@ func (IdentityProfile) IsNode() {} func (this IdentityProfile) GetID() gid.GID { return this.ID } type Invitation struct { - ID gid.GID `json:"id"` - Email mail.Addr `json:"email"` - ExpiresAt time.Time `json:"expiresAt"` - AcceptedAt *time.Time `json:"acceptedAt,omitempty"` - CreatedAt time.Time `json:"createdAt"` - Status coredata.InvitationStatus `json:"status"` + ID gid.GID `json:"id"` + Email mail.Addr `json:"email"` + Role coredata.MembershipRole `json:"role"` + ExpiresAt time.Time `json:"expiresAt"` + AcceptedAt *time.Time `json:"acceptedAt,omitempty"` + CreatedAt time.Time `json:"createdAt"` + Status coredata.InvitationStatus `json:"status"` + Organization *Organization `json:"organization"` } func (Invitation) IsNode() {} @@ -220,17 +222,18 @@ type InviteMemberPayload struct { } type Membership struct { - ID gid.GID `json:"id"` - IdentityID gid.GID `json:"identityId"` - CreatedAt time.Time `json:"createdAt"` - Profile *IdentityProfile `json:"profile"` - Identity *Identity `json:"identity"` - Organization *Organization `json:"organization"` - Permissions []*Permission `json:"permissions"` - ProvisionedBy ProvisioningSource `json:"provisionedBy"` - Active bool `json:"active"` - LastSyncedAt *time.Time `json:"lastSyncedAt,omitempty"` - ActiveSession *Session `json:"activeSession,omitempty"` + ID gid.GID `json:"id"` + IdentityID gid.GID `json:"identityId"` + CreatedAt time.Time `json:"createdAt"` + Profile *IdentityProfile `json:"profile"` + Identity *Identity `json:"identity"` + Organization *Organization `json:"organization"` + Role coredata.MembershipRole `json:"role"` + Permissions []*Permission `json:"permissions"` + ProvisionedBy ProvisioningSource `json:"provisionedBy"` + Active bool `json:"active"` + LastSyncedAt *time.Time `json:"lastSyncedAt,omitempty"` + ActiveSession *Session `json:"activeSession,omitempty"` } func (Membership) IsNode() {} diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index 0ceca4aec..fb6dc20d9 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -29,6 +29,12 @@ func (r *identityResolver) Memberships(ctx context.Context, obj *types.Identity, Field: coredata.MembershipOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } + if orderBy != nil { + pageOrderBy = page.OrderBy[coredata.MembershipOrderField]{ + Field: orderBy.Field, + Direction: orderBy.Direction, + } + } cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) @@ -41,7 +47,7 @@ func (r *identityResolver) Memberships(ctx context.Context, obj *types.Identity, } // PendingInvitations is the resolver for the pendingInvitations field. -func (r *identityResolver) PendingInvitations(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.InvitationConnection, error) { +func (r *identityResolver) PendingInvitations(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrderBy) (*types.InvitationConnection, error) { pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{ Field: coredata.InvitationOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, @@ -97,6 +103,16 @@ func (r *identityResolver) PersonalAPIKeys(ctx context.Context, obj *types.Ident return types.NewPersonalAPIKeyConnection(page, r, obj.ID), nil } +// Organization is the resolver for the organization field. +func (r *invitationResolver) Organization(ctx context.Context, obj *types.Invitation) (*types.Organization, error) { + organization, err := r.iam.OrganizationService.GetOrganizationForInvitation(ctx, obj.ID) + if err != nil { + panic(fmt.Errorf("cannot get organization for invitation: %w", err)) + } + + return types.NewOrganization(organization), nil +} + // TotalCount is the resolver for the totalCount field. func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *types.InvitationConnection) (int, error) { switch obj.Resolver.(type) { @@ -994,6 +1010,9 @@ func (r *sessionConnectionResolver) TotalCount(ctx context.Context, obj *types.S // Identity returns schema.IdentityResolver implementation. func (r *Resolver) Identity() schema.IdentityResolver { return &identityResolver{r} } +// Invitation returns schema.InvitationResolver implementation. +func (r *Resolver) Invitation() schema.InvitationResolver { return &invitationResolver{r} } + // InvitationConnection returns schema.InvitationConnectionResolver implementation. func (r *Resolver) InvitationConnection() schema.InvitationConnectionResolver { return &invitationConnectionResolver{r} @@ -1032,6 +1051,7 @@ func (r *Resolver) SessionConnection() schema.SessionConnectionResolver { } type identityResolver struct{ *Resolver } +type invitationResolver struct{ *Resolver } type invitationConnectionResolver struct{ *Resolver } type membershipResolver struct{ *Resolver } type membershipConnectionResolver struct{ *Resolver }