From 3a459f1992d97a9a50b6acdb76e09c4b4523ce64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 6 Feb 2026 19:50:42 +0400 Subject: [PATCH] Rework console pages and implement profile update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../PeopleGraphPaginatedFragment.graphql.ts | 6 +- ...MemberListFragment_RefetchQuery.graphql.ts | 22 +- .../iam/MemberListItemFragment.graphql.ts | 42 +- .../iam/MembersPageQuery.graphql.ts | 22 +- .../iam/SidebarFragment.graphql.ts | 18 +- .../iam/UserFormFragment.graphql.ts | 105 +++ .../iam/UserFormMutation.graphql.ts | 112 +++ .../iam/UserPageQuery.graphql.ts | 263 ++++++ .../iam/UserPage_removeMutation.graphql.ts | 93 ++ .../ViewerMembershipLayoutQuery.graphql.ts | 19 +- .../iam/organizations/_components/Sidebar.tsx | 9 + .../organizations/settings/SettingsLayout.tsx | 5 - .../{settings => users}/MembersPage.tsx | 28 +- .../{settings => users}/MembersPageLoader.tsx | 0 .../iam/organizations/users/UserPage.tsx | 125 +++ .../organizations/users/UserPageLoader.tsx | 35 + .../_components/InvitationList.tsx | 0 .../_components/InvitationListItem.tsx | 0 .../_components/InviteUserDialog.tsx | 0 .../_components/MemberList.tsx | 2 + .../_components/MemberListItem.tsx | 8 +- .../users/_components/UserForm.tsx | 144 +++ .../pages/iam/organizations/users/routes.ts | 21 + apps/console/src/routes.tsx | 9 +- pkg/coredata/membership_profile.go | 8 +- pkg/iam/iam_actions.go | 5 +- pkg/iam/iam_policies.go | 2 + pkg/iam/organization_service.go | 79 ++ pkg/server/api/connect/v1/schema.graphql | 37 + pkg/server/api/connect/v1/schema/schema.go | 877 +++++++++++++++++- .../api/connect/v1/types/identity_profile.go | 20 +- pkg/server/api/connect/v1/types/types.go | 32 +- pkg/server/api/connect/v1/v1_resolver.go | 85 ++ pkg/server/api/console/v1/schema.graphql | 4 +- pkg/server/api/console/v1/schema/schema.go | 68 +- 35 files changed, 2197 insertions(+), 108 deletions(-) create mode 100644 apps/console/src/__generated__/iam/UserFormFragment.graphql.ts create mode 100644 apps/console/src/__generated__/iam/UserFormMutation.graphql.ts create mode 100644 apps/console/src/__generated__/iam/UserPageQuery.graphql.ts create mode 100644 apps/console/src/__generated__/iam/UserPage_removeMutation.graphql.ts rename apps/console/src/pages/iam/organizations/{settings => users}/MembersPage.tsx (84%) rename apps/console/src/pages/iam/organizations/{settings => users}/MembersPageLoader.tsx (100%) create mode 100644 apps/console/src/pages/iam/organizations/users/UserPage.tsx create mode 100644 apps/console/src/pages/iam/organizations/users/UserPageLoader.tsx rename apps/console/src/pages/iam/organizations/{settings => users}/_components/InvitationList.tsx (100%) rename apps/console/src/pages/iam/organizations/{settings => users}/_components/InvitationListItem.tsx (100%) rename apps/console/src/pages/iam/organizations/{settings => users}/_components/InviteUserDialog.tsx (100%) rename apps/console/src/pages/iam/organizations/{settings => users}/_components/MemberList.tsx (97%) rename apps/console/src/pages/iam/organizations/{settings => users}/_components/MemberListItem.tsx (93%) create mode 100644 apps/console/src/pages/iam/organizations/users/_components/UserForm.tsx create mode 100644 apps/console/src/pages/iam/organizations/users/routes.ts diff --git a/apps/console/src/__generated__/core/PeopleGraphPaginatedFragment.graphql.ts b/apps/console/src/__generated__/core/PeopleGraphPaginatedFragment.graphql.ts index fcc6330ff..710c3ba4a 100644 --- a/apps/console/src/__generated__/core/PeopleGraphPaginatedFragment.graphql.ts +++ b/apps/console/src/__generated__/core/PeopleGraphPaginatedFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<10bf686b430d7a736370638d91ba0b58>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,7 @@ // @ts-nocheck import { ReaderFragment } from 'relay-runtime'; -export type MembershipProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; +export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; import { FragmentRefs } from "relay-runtime"; export type PeopleGraphPaginatedFragment$data = { readonly canCreatePeople: boolean; @@ -26,7 +26,7 @@ export type PeopleGraphPaginatedFragment$data = { readonly emailAddress: string; readonly fullName: string; readonly id: string; - readonly kind: MembershipProfileKind; + readonly kind: ProfileKind; readonly position: string | null | undefined; }; }>; diff --git a/apps/console/src/__generated__/iam/MemberListFragment_RefetchQuery.graphql.ts b/apps/console/src/__generated__/iam/MemberListFragment_RefetchQuery.graphql.ts index ab4adb030..837ba1489 100644 --- a/apps/console/src/__generated__/iam/MemberListFragment_RefetchQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/MemberListFragment_RefetchQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<6205161c89403b10363ea54bbb10ccea>> + * @generated SignedSource<<07e8369de4150113311dccdc51e4d1da>> * @lightSyntaxTransform * @nogrep */ @@ -251,6 +251,7 @@ return { "name": "profile", "plural": false, "selections": [ + (v12/*: any*/), { "alias": null, "args": null, @@ -258,7 +259,20 @@ return { "name": "fullName", "storageKey": null }, - (v12/*: any*/) + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "position", + "storageKey": null + } ], "storageKey": null }, @@ -403,12 +417,12 @@ return { ] }, "params": { - "cacheID": "8089993871afc0b3d2c0defd9182c31a", + "cacheID": "8a5e322c63c4dfa1c5528d50638ec6cc", "id": null, "metadata": {}, "name": "MemberListFragment_RefetchQuery", "operationKind": "query", - "text": "query MemberListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MembershipOrder = {direction: ASC, field: FULL_NAME}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MemberListFragment_16fISc\n id\n }\n}\n\nfragment MemberListFragment_16fISc on Organization {\n members(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n source\n state\n profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership:delete\")\n}\n" + "text": "query MemberListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MembershipOrder = {direction: ASC, field: FULL_NAME}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MemberListFragment_16fISc\n id\n }\n}\n\nfragment MemberListFragment_16fISc on Organization {\n members(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n source\n state\n profile {\n id\n fullName\n kind\n position\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership:delete\")\n}\n" } }; })(); diff --git a/apps/console/src/__generated__/iam/MemberListItemFragment.graphql.ts b/apps/console/src/__generated__/iam/MemberListItemFragment.graphql.ts index 8842022f6..838849b25 100644 --- a/apps/console/src/__generated__/iam/MemberListItemFragment.graphql.ts +++ b/apps/console/src/__generated__/iam/MemberListItemFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<81a5acfba89528014adf0731f16b9a90>> + * @generated SignedSource<<770fb773083cc4f819594e268d9eb0cc>> * @lightSyntaxTransform * @nogrep */ @@ -12,6 +12,7 @@ import { ReaderFragment } from 'relay-runtime'; export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; export type MembershipSource = "MANUAL" | "SAML" | "SCIM"; export type MembershipState = "ACTIVE" | "INACTIVE"; +export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; import { FragmentRefs } from "relay-runtime"; export type MemberListItemFragment$data = { readonly canDelete: boolean; @@ -23,6 +24,9 @@ export type MemberListItemFragment$data = { }; readonly profile: { readonly fullName: string; + readonly id: string; + readonly kind: ProfileKind; + readonly position: string | null | undefined; }; readonly role: MembershipRole; readonly source: MembershipSource; @@ -34,19 +38,21 @@ export type MemberListItemFragment$key = { readonly " $fragmentSpreads": FragmentRefs<"MemberListItemFragment">; }; -const node: ReaderFragment = { +const node: ReaderFragment = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, "name": "MemberListItemFragment", "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, + (v0/*: any*/), { "alias": null, "args": null, @@ -78,12 +84,27 @@ const node: ReaderFragment = { "name": "profile", "plural": false, "selections": [ + (v0/*: any*/), { "alias": null, "args": null, "kind": "ScalarField", "name": "fullName", "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "position", + "storageKey": null } ], "storageKey": null @@ -149,7 +170,8 @@ const node: ReaderFragment = { "type": "Membership", "abstractKey": null }; +})(); -(node as any).hash = "ffa3431077faeeb5e5779975d68c82bb"; +(node as any).hash = "378a4cdc212c16aecae368d41d80739e"; export default node; diff --git a/apps/console/src/__generated__/iam/MembersPageQuery.graphql.ts b/apps/console/src/__generated__/iam/MembersPageQuery.graphql.ts index ce6126c7d..fc8d4720b 100644 --- a/apps/console/src/__generated__/iam/MembersPageQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/MembersPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<11a6b18e62a7a93fa6c2365a7e3a86f6>> + * @generated SignedSource<<9a67aa27afef79bc9334f341f01d986d>> * @lightSyntaxTransform * @nogrep */ @@ -368,8 +368,22 @@ return { "name": "profile", "plural": false, "selections": [ + (v10/*: any*/), (v12/*: any*/), - (v10/*: any*/) + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "position", + "storageKey": null + } ], "storageKey": null }, @@ -532,12 +546,12 @@ return { ] }, "params": { - "cacheID": "438ac1a112e204b2a9ff30e3b36ec3ae", + "cacheID": "761152b197433a0f5f9e0734f98723d8", "id": null, "metadata": {}, "name": "MembersPageQuery", "operationKind": "query", - "text": "query MembersPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canInviteUser: permission(action: \"iam:invitation:create\")\n ...MemberListFragment_8lnpd\n members(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n }\n ...InvitationListFragment_1PypFi\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n totalCount\n __typename\n }\n }\n id\n }\n}\n\nfragment InvitationListFragment_1PypFi on Organization {\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment InvitationListItemFragment on Invitation {\n id\n fullName\n email\n role\n status\n createdAt\n expiresAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment MemberListFragment_8lnpd on Organization {\n members(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n source\n state\n profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership:delete\")\n}\n" + "text": "query MembersPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canInviteUser: permission(action: \"iam:invitation:create\")\n ...MemberListFragment_8lnpd\n members(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n }\n ...InvitationListFragment_1PypFi\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n totalCount\n __typename\n }\n }\n id\n }\n}\n\nfragment InvitationListFragment_1PypFi on Organization {\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment InvitationListItemFragment on Invitation {\n id\n fullName\n email\n role\n status\n createdAt\n expiresAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment MemberListFragment_8lnpd on Organization {\n members(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n source\n state\n profile {\n id\n fullName\n kind\n position\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership:delete\")\n}\n" } }; })(); diff --git a/apps/console/src/__generated__/iam/SidebarFragment.graphql.ts b/apps/console/src/__generated__/iam/SidebarFragment.graphql.ts index b23ada91d..4b4030d26 100644 --- a/apps/console/src/__generated__/iam/SidebarFragment.graphql.ts +++ b/apps/console/src/__generated__/iam/SidebarFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -20,6 +20,7 @@ export type SidebarFragment$data = { readonly canListFrameworks: boolean; readonly canListMeasures: boolean; readonly canListMeetings: boolean; + readonly canListMembers: boolean; readonly canListNonconformities: boolean; readonly canListObligations: boolean; readonly canListProcessingActivities: boolean; @@ -108,6 +109,19 @@ const node: ReaderFragment = { "name": "permission", "storageKey": "permission(action:\"core:framework:list\")" }, + { + "alias": "canListMembers", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:membership:list" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:membership:list\")" + }, { "alias": "canListVendors", "args": [ @@ -295,6 +309,6 @@ const node: ReaderFragment = { "abstractKey": null }; -(node as any).hash = "b5d26b301116eb710832a55c425ed168"; +(node as any).hash = "73a632deb40d322a670cd72fd855723f"; export default node; diff --git a/apps/console/src/__generated__/iam/UserFormFragment.graphql.ts b/apps/console/src/__generated__/iam/UserFormFragment.graphql.ts new file mode 100644 index 000000000..96e595d4b --- /dev/null +++ b/apps/console/src/__generated__/iam/UserFormFragment.graphql.ts @@ -0,0 +1,105 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; +import { FragmentRefs } from "relay-runtime"; +export type UserFormFragment$data = { + readonly additionalEmailAddresses: ReadonlyArray; + readonly canUpdate: boolean; + readonly contractEndDate: string | null | undefined; + readonly contractStartDate: string | null | undefined; + readonly fullName: string; + readonly id: string; + readonly kind: ProfileKind; + readonly position: string | null | undefined; + readonly " $fragmentType": "UserFormFragment"; +}; +export type UserFormFragment$key = { + readonly " $data"?: UserFormFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"UserFormFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "UserFormFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "position", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "additionalEmailAddresses", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "contractStartDate", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "contractEndDate", + "storageKey": null + }, + { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:membership-profile:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:membership-profile:update\")" + } + ], + "type": "MembershipProfile", + "abstractKey": null +}; + +(node as any).hash = "b7c1eb9f3703338d5d0fb4c5411bf2eb"; + +export default node; diff --git a/apps/console/src/__generated__/iam/UserFormMutation.graphql.ts b/apps/console/src/__generated__/iam/UserFormMutation.graphql.ts new file mode 100644 index 000000000..c076dd2a9 --- /dev/null +++ b/apps/console/src/__generated__/iam/UserFormMutation.graphql.ts @@ -0,0 +1,112 @@ +/** + * @generated SignedSource<<7a6d038560759279a60a293bc4be5bb8>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; +export type UpdateProfileInput = { + additionalEmailAddresses?: ReadonlyArray | null | undefined; + contractEndDate?: string | null | undefined; + contractStartDate?: string | null | undefined; + fullName: string; + id: string; + kind: ProfileKind; + position?: string | null | undefined; +}; +export type UserFormMutation$variables = { + input: UpdateProfileInput; +}; +export type UserFormMutation$data = { + readonly updateProfile: { + readonly profile: { + readonly id: string; + }; + }; +}; +export type UserFormMutation = { + response: UserFormMutation$data; + variables: UserFormMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateProfilePayload", + "kind": "LinkedField", + "name": "updateProfile", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MembershipProfile", + "kind": "LinkedField", + "name": "profile", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "UserFormMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "UserFormMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "edb3d0ac8f6a87857c470114ff6ad7f4", + "id": null, + "metadata": {}, + "name": "UserFormMutation", + "operationKind": "mutation", + "text": "mutation UserFormMutation(\n $input: UpdateProfileInput!\n) {\n updateProfile(input: $input) {\n profile {\n id\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "6ae1b8fd927611dd3b3b530ccd1de7d1"; + +export default node; diff --git a/apps/console/src/__generated__/iam/UserPageQuery.graphql.ts b/apps/console/src/__generated__/iam/UserPageQuery.graphql.ts new file mode 100644 index 000000000..aad37e8c1 --- /dev/null +++ b/apps/console/src/__generated__/iam/UserPageQuery.graphql.ts @@ -0,0 +1,263 @@ +/** + * @generated SignedSource<<10ec7c14d725880998dcd5ba2b99004e>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type UserPageQuery$variables = { + userId: string; +}; +export type UserPageQuery$data = { + readonly user: { + readonly __typename: "MembershipProfile"; + readonly canDelete: boolean; + readonly fullName: string; + readonly identity: { + readonly email: string; + }; + readonly membershipId: string; + readonly " $fragmentSpreads": FragmentRefs<"UserFormFragment">; + } | { + // This will never be '%other', but we need some + // value in case none of the concrete values match. + readonly __typename: "%other"; + }; +}; +export type UserPageQuery = { + response: UserPageQuery$data; + variables: UserPageQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "userId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "userId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "membershipId", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null +}, +v6 = { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:membership-profile:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:membership-profile:delete\")" +}, +v7 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "UserPageQuery", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": "user", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "identity", + "plural": false, + "selections": [ + (v5/*: any*/) + ], + "storageKey": null + }, + "action": "THROW" + }, + (v6/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "UserFormFragment" + } + ], + "type": "MembershipProfile", + "abstractKey": null + } + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "UserPageQuery", + "selections": [ + { + "alias": "user", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v7/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "identity", + "plural": false, + "selections": [ + (v5/*: any*/), + (v7/*: any*/) + ], + "storageKey": null + }, + (v6/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "position", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "additionalEmailAddresses", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "contractStartDate", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "contractEndDate", + "storageKey": null + }, + { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:membership-profile:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:membership-profile:update\")" + } + ], + "type": "MembershipProfile", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "9d437c761ed1795dde80f30a63e400fc", + "id": null, + "metadata": {}, + "name": "UserPageQuery", + "operationKind": "query", + "text": "query UserPageQuery(\n $userId: ID!\n) {\n user: node(id: $userId) {\n __typename\n ... on MembershipProfile {\n fullName\n membershipId\n identity {\n email\n id\n }\n canDelete: permission(action: \"iam:membership-profile:delete\")\n ...UserFormFragment\n }\n id\n }\n}\n\nfragment UserFormFragment on MembershipProfile {\n id\n fullName\n kind\n position\n additionalEmailAddresses\n contractStartDate\n contractEndDate\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n" + } +}; +})(); + +(node as any).hash = "7711b4e7a7463f0134a47017f3e65aca"; + +export default node; diff --git a/apps/console/src/__generated__/iam/UserPage_removeMutation.graphql.ts b/apps/console/src/__generated__/iam/UserPage_removeMutation.graphql.ts new file mode 100644 index 000000000..ad29a2169 --- /dev/null +++ b/apps/console/src/__generated__/iam/UserPage_removeMutation.graphql.ts @@ -0,0 +1,93 @@ +/** + * @generated SignedSource<<0cb21a9122ca2763aaa5dbe4d33e2b4a>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type RemoveMemberInput = { + membershipId: string; + organizationId: string; +}; +export type UserPage_removeMutation$variables = { + input: RemoveMemberInput; +}; +export type UserPage_removeMutation$data = { + readonly removeMember: { + readonly deletedMembershipId: string; + } | null | undefined; +}; +export type UserPage_removeMutation = { + response: UserPage_removeMutation$data; + variables: UserPage_removeMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "RemoveMemberPayload", + "kind": "LinkedField", + "name": "removeMember", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedMembershipId", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "UserPage_removeMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "UserPage_removeMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "7aa8db55205ca16403a5919a5151d69f", + "id": null, + "metadata": {}, + "name": "UserPage_removeMutation", + "operationKind": "mutation", + "text": "mutation UserPage_removeMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n deletedMembershipId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "3c9804054834be923048845b796a2dfd"; + +export default node; diff --git a/apps/console/src/__generated__/iam/ViewerMembershipLayoutQuery.graphql.ts b/apps/console/src/__generated__/iam/ViewerMembershipLayoutQuery.graphql.ts index 33bdc29ee..e15363d0c 100644 --- a/apps/console/src/__generated__/iam/ViewerMembershipLayoutQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/ViewerMembershipLayoutQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<6a26345e6949e02e37f5ab52c4d6fe05>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -385,6 +385,19 @@ return { "name": "permission", "storageKey": "permission(action:\"core:framework:list\")" }, + { + "alias": "canListMembers", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:membership:list" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:membership:list\")" + }, { "alias": "canListVendors", "args": [ @@ -605,12 +618,12 @@ return { ] }, "params": { - "cacheID": "6f7fa430d00e6734258bfc03bebb1b29", + "cacheID": "a553750ba3bad35fb5b3ed7c1bebcbb4", "id": null, "metadata": {}, "name": "ViewerMembershipLayoutQuery", "operationKind": "query", - "text": "query ViewerMembershipLayoutQuery(\n $organizationId: ID!\n $hideSidebar: Boolean!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...MembershipsDropdown_organizationFragment\n ...ViewerMembershipDropdownFragment\n ...SidebarFragment @skip(if: $hideSidebar)\n viewerMembership {\n role\n profile {\n fullName\n id\n }\n id\n }\n }\n id\n }\n viewer {\n email\n ...MembershipsDropdown_viewerFragment\n pendingInvitations {\n totalCount\n }\n id\n }\n}\n\nfragment MembershipsDropdown_organizationFragment on Organization {\n name\n}\n\nfragment MembershipsDropdown_viewerFragment on Identity {\n pendingInvitations {\n totalCount\n }\n}\n\nfragment SidebarFragment on Organization {\n canListMeetings: permission(action: \"core:meeting:list\")\n canListTasks: permission(action: \"core:task:list\")\n canListMeasures: permission(action: \"core:measure:list\")\n canListRisks: permission(action: \"core:risk:list\")\n canListFrameworks: permission(action: \"core:framework:list\")\n canListVendors: permission(action: \"core:vendor:list\")\n canListDocuments: permission(action: \"core:document:list\")\n canListAssets: permission(action: \"core:asset:list\")\n canListData: permission(action: \"core:datum:list\")\n canListAudits: permission(action: \"core:audit:list\")\n canListNonconformities: permission(action: \"core:nonconformity:list\")\n canListObligations: permission(action: \"core:obligation:list\")\n canListContinualImprovements: permission(action: \"core:continual-improvement:list\")\n canListProcessingActivities: permission(action: \"core:processing-activity:list\")\n canListRightsRequests: permission(action: \"core:rights-request:list\")\n canListSnapshots: permission(action: \"core:snapshot:list\")\n canGetTrustCenter: permission(action: \"core:trust-center:get\")\n canUpdateOrganization: permission(action: \"iam:organization:update\")\n canListStatesOfApplicability: permission(action: \"core:state-of-applicability:list\")\n}\n\nfragment ViewerMembershipDropdownFragment on Organization {\n viewerMembership {\n identity {\n email\n canListAPIKeys: permission(action: \"iam:personal-api-key:list\")\n id\n }\n profile {\n fullName\n id\n }\n id\n }\n}\n" + "text": "query ViewerMembershipLayoutQuery(\n $organizationId: ID!\n $hideSidebar: Boolean!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...MembershipsDropdown_organizationFragment\n ...ViewerMembershipDropdownFragment\n ...SidebarFragment @skip(if: $hideSidebar)\n viewerMembership {\n role\n profile {\n fullName\n id\n }\n id\n }\n }\n id\n }\n viewer {\n email\n ...MembershipsDropdown_viewerFragment\n pendingInvitations {\n totalCount\n }\n id\n }\n}\n\nfragment MembershipsDropdown_organizationFragment on Organization {\n name\n}\n\nfragment MembershipsDropdown_viewerFragment on Identity {\n pendingInvitations {\n totalCount\n }\n}\n\nfragment SidebarFragment on Organization {\n canListMeetings: permission(action: \"core:meeting:list\")\n canListTasks: permission(action: \"core:task:list\")\n canListMeasures: permission(action: \"core:measure:list\")\n canListRisks: permission(action: \"core:risk:list\")\n canListFrameworks: permission(action: \"core:framework:list\")\n canListMembers: permission(action: \"iam:membership:list\")\n canListVendors: permission(action: \"core:vendor:list\")\n canListDocuments: permission(action: \"core:document:list\")\n canListAssets: permission(action: \"core:asset:list\")\n canListData: permission(action: \"core:datum:list\")\n canListAudits: permission(action: \"core:audit:list\")\n canListNonconformities: permission(action: \"core:nonconformity:list\")\n canListObligations: permission(action: \"core:obligation:list\")\n canListContinualImprovements: permission(action: \"core:continual-improvement:list\")\n canListProcessingActivities: permission(action: \"core:processing-activity:list\")\n canListRightsRequests: permission(action: \"core:rights-request:list\")\n canListSnapshots: permission(action: \"core:snapshot:list\")\n canGetTrustCenter: permission(action: \"core:trust-center:get\")\n canUpdateOrganization: permission(action: \"iam:organization:update\")\n canListStatesOfApplicability: permission(action: \"core:state-of-applicability:list\")\n}\n\nfragment ViewerMembershipDropdownFragment on Organization {\n viewerMembership {\n identity {\n email\n canListAPIKeys: permission(action: \"iam:personal-api-key:list\")\n id\n }\n profile {\n fullName\n id\n }\n id\n }\n}\n" } }; })(); diff --git a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx index 1a5c978f3..1513ac8ae 100644 --- a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx +++ b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx @@ -8,6 +8,7 @@ import { IconClock, IconCrossLargeX, IconFire3, + IconGroup1, IconInboxEmpty, IconListStack, IconLock, @@ -34,6 +35,7 @@ const fragment = graphql` canListMeasures: permission(action: "core:measure:list") canListRisks: permission(action: "core:risk:list") canListFrameworks: permission(action: "core:framework:list") + canListMembers: permission(action: "iam:membership:list") canListVendors: permission(action: "core:vendor:list") canListDocuments: permission(action: "core:document:list") canListAssets: permission(action: "core:asset:list") @@ -104,6 +106,13 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) { to={`${prefix}/frameworks`} /> )} + {organization.canListMembers && ( + + )} {organization.canListVendors && ( {__("General")} - - - {__("Members")} - {__("SAML SSO")} diff --git a/apps/console/src/pages/iam/organizations/settings/MembersPage.tsx b/apps/console/src/pages/iam/organizations/users/MembersPage.tsx similarity index 84% rename from apps/console/src/pages/iam/organizations/settings/MembersPage.tsx rename to apps/console/src/pages/iam/organizations/users/MembersPage.tsx index 15bd0e9d3..631b64496 100644 --- a/apps/console/src/pages/iam/organizations/settings/MembersPage.tsx +++ b/apps/console/src/pages/iam/organizations/users/MembersPage.tsx @@ -1,5 +1,5 @@ import { useTranslate } from "@probo/i18n"; -import { Button, Card, TabBadge, TabItem, Tabs } from "@probo/ui"; +import { Button, TabBadge, TabItem, Tabs } from "@probo/ui"; import { useState } from "react"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { ConnectionHandler, graphql } from "relay-runtime"; @@ -73,7 +73,7 @@ export function MembersPage(props: { return (
-

{__("Workspace members")}

+

{__("People")}

{organization.canInviteUser && ( - -
- {activeTab === "memberships" && } +
+ {activeTab === "memberships" && } - {activeTab === "invitations" && ( - - )} -
- + {activeTab === "invitations" && ( + + )} +
); } diff --git a/apps/console/src/pages/iam/organizations/settings/MembersPageLoader.tsx b/apps/console/src/pages/iam/organizations/users/MembersPageLoader.tsx similarity index 100% rename from apps/console/src/pages/iam/organizations/settings/MembersPageLoader.tsx rename to apps/console/src/pages/iam/organizations/users/MembersPageLoader.tsx diff --git a/apps/console/src/pages/iam/organizations/users/UserPage.tsx b/apps/console/src/pages/iam/organizations/users/UserPage.tsx new file mode 100644 index 000000000..3accdfeea --- /dev/null +++ b/apps/console/src/pages/iam/organizations/users/UserPage.tsx @@ -0,0 +1,125 @@ +import { sprintf } from "@probo/helpers"; +import { useTranslate } from "@probo/i18n"; +import { ActionDropdown, Avatar, Breadcrumb, DropdownItem, IconTrashCan, useConfirm } from "@probo/ui"; +import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; +import { useNavigate } from "react-router"; +import { graphql } from "relay-runtime"; + +import type { UserPageQuery } from "#/__generated__/iam/UserPageQuery.graphql"; +import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; + +import { UserForm } from "./_components/UserForm"; + +export const userPageQuery = graphql` + query UserPageQuery($userId: ID!) { + user: node(id: $userId) @required(action: THROW) { + __typename + ... on MembershipProfile { + fullName + membershipId + identity @required(action: THROW) { + email + } + canDelete: permission(action: "iam:membership-profile:delete") + ...UserFormFragment + } + } + } +`; + +const removeMemberMutation = graphql` + mutation UserPage_removeMutation( + $input: RemoveMemberInput! + ) { + removeMember(input: $input) { + deletedMembershipId + } + } +`; + +export function UserPage(props: { queryRef: PreloadedQuery }) { + const { queryRef } = props; + + const organizationId = useOrganizationId(); + const { __ } = useTranslate(); + const confirm = useConfirm(); + const navigate = useNavigate(); + + const { user } = usePreloadedQuery(userPageQuery, queryRef); + if (user.__typename !== "MembershipProfile") { + throw new Error("invalid type for node"); + } + + const [removeMembership, isRemoving] = useMutationWithToasts( + removeMemberMutation, + { + successMessage: __("Member removed successfully"), + errorMessage: __("Failed to remove member"), + }, + ); + + const handleRemove = () => { + confirm( + () => { + return removeMembership({ + variables: { + input: { + membershipId: user.membershipId, + organizationId: organizationId, + }, + }, + onCompleted: () => { + void navigate(`/organizations/${organizationId}/users`); + }, + }); + }, + { + message: sprintf( + __("Are you sure you want to remove %s?"), + user.fullName, + ), + }, + ); + }; + + return ( +
+ +
+
+ +
+ +
{user.fullName}
+
{user.identity.email}
+
+
+ {user.canDelete && ( + + + {__("Delete")} + + + )} +
+ + +
+ ); +}; diff --git a/apps/console/src/pages/iam/organizations/users/UserPageLoader.tsx b/apps/console/src/pages/iam/organizations/users/UserPageLoader.tsx new file mode 100644 index 000000000..296e2c6de --- /dev/null +++ b/apps/console/src/pages/iam/organizations/users/UserPageLoader.tsx @@ -0,0 +1,35 @@ +import { useEffect } from "react"; +import { useQueryLoader } from "react-relay"; +import { useParams } from "react-router"; + +import type { UserPageQuery } from "#/__generated__/iam/UserPageQuery.graphql"; +import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton"; +import { IAMRelayProvider } from "#/providers/IAMRelayProvider"; + +import { UserPage, userPageQuery } from "./UserPage"; + +function UserPageQueryLoader() { + const { userId } = useParams(); + if (!userId) { + throw new Error(":userId missing in route params"); + } + const [queryRef, loadQuery] = useQueryLoader(userPageQuery); + + useEffect(() => { + if (!queryRef) { + loadQuery({ userId }); + } + }, [loadQuery, queryRef, userId]); + + if (!queryRef) return ; + + return ; +} + +export default function UserPageLoader() { + return ( + + + + ); +} diff --git a/apps/console/src/pages/iam/organizations/settings/_components/InvitationList.tsx b/apps/console/src/pages/iam/organizations/users/_components/InvitationList.tsx similarity index 100% rename from apps/console/src/pages/iam/organizations/settings/_components/InvitationList.tsx rename to apps/console/src/pages/iam/organizations/users/_components/InvitationList.tsx diff --git a/apps/console/src/pages/iam/organizations/settings/_components/InvitationListItem.tsx b/apps/console/src/pages/iam/organizations/users/_components/InvitationListItem.tsx similarity index 100% rename from apps/console/src/pages/iam/organizations/settings/_components/InvitationListItem.tsx rename to apps/console/src/pages/iam/organizations/users/_components/InvitationListItem.tsx diff --git a/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx b/apps/console/src/pages/iam/organizations/users/_components/InviteUserDialog.tsx similarity index 100% rename from apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx rename to apps/console/src/pages/iam/organizations/users/_components/InviteUserDialog.tsx diff --git a/apps/console/src/pages/iam/organizations/settings/_components/MemberList.tsx b/apps/console/src/pages/iam/organizations/users/_components/MemberList.tsx similarity index 97% rename from apps/console/src/pages/iam/organizations/settings/_components/MemberList.tsx rename to apps/console/src/pages/iam/organizations/users/_components/MemberList.tsx index a53654619..d2d093cf8 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/MemberList.tsx +++ b/apps/console/src/pages/iam/organizations/users/_components/MemberList.tsx @@ -69,8 +69,10 @@ export function MemberList(props: { fKey: MemberListFragment$key }) { {__("Name")} {__("Email")} + {__("Type")} {__("Role")} {__("Joined")} + {__("Position")} diff --git a/apps/console/src/pages/iam/organizations/settings/_components/MemberListItem.tsx b/apps/console/src/pages/iam/organizations/users/_components/MemberListItem.tsx similarity index 93% rename from apps/console/src/pages/iam/organizations/settings/_components/MemberListItem.tsx rename to apps/console/src/pages/iam/organizations/users/_components/MemberListItem.tsx index 1743ef1d4..9fdddc65e 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/MemberListItem.tsx +++ b/apps/console/src/pages/iam/organizations/users/_components/MemberListItem.tsx @@ -19,7 +19,7 @@ import type { MemberListItemFragment$key } from "#/__generated__/iam/MemberListI import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useOrganizationId } from "#/hooks/useOrganizationId"; -import { EditMemberDialog } from "./EditMemberDialog"; +import { EditMemberDialog } from "../../settings/_components/EditMemberDialog"; const fragment = graphql` fragment MemberListItemFragment on Membership { @@ -28,7 +28,10 @@ const fragment = graphql` source state profile @required(action: THROW) { + id fullName + kind + position } identity @required(action: THROW) { email @@ -103,6 +106,7 @@ export function MemberListItem(props: { isRemoving && "opacity-60 pointer-events-none", isInactive && "opacity-50", )} + to={`/organizations/${organizationId}/users/${membership.profile.id}`} >
@@ -116,10 +120,12 @@ export function MemberListItem(props: { {membership.source}
+ {membership.profile.kind} {membership.role} {new Date(membership.createdAt).toLocaleDateString()} + {membership.profile.position} {!isInactive && (
(v as string[]).filter(v => !!v), + z.array(z.string().email()), + ), + kind: z.enum(peopleRoles), + contractStartDate: z.string().optional().nullable(), + contractEndDate: z.string().optional().nullable(), +}); + +export function UserForm(props: { fragmentRef: UserFormFragment$key }) { + const { fragmentRef } = props; + + const { __ } = useTranslate(); + + const user = useFragment(fragment, fragmentRef); + + const { control, formState, handleSubmit, register, reset } + = useFormWithSchema(schema, { + defaultValues: { + kind: user.kind, + fullName: user.fullName, + position: user.position, + additionalEmailAddresses: [...user.additionalEmailAddresses], + contractStartDate: user.contractStartDate?.split("T")[0] || "", + contractEndDate: user.contractEndDate?.split("T")[0] || "", + }, + }); + const [mutate, isMutating] = useMutationWithToasts( + updateUserMutation, + { + successMessage: __("Member updated successfully."), + errorMessage: __("Failed to update member"), + }, + ); + const onSubmit = handleSubmit(async (data: z.infer) => { + const input = { + id: user.id, + fullName: data.fullName, + additionalEmailAddresses: data.additionalEmailAddresses, + kind: data.kind, + position: data.position, + contractStartDate: formatDatetime(data.contractStartDate) ?? null, + contractEndDate: formatDatetime(data.contractEndDate) ?? null, + }; + + await mutate({ + variables: { input }, + onCompleted: () => { + reset(data); + }, + }); + }); + + return ( +
void onSubmit(e)} className="space-y-4"> + + + + {getRoles(__).map(role => ( + + ))} + + + + + + + + + + +
+ {formState.isDirty && user.canUpdate && ( + + )} +
+
+ ); +} diff --git a/apps/console/src/pages/iam/organizations/users/routes.ts b/apps/console/src/pages/iam/organizations/users/routes.ts new file mode 100644 index 000000000..353072a63 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/users/routes.ts @@ -0,0 +1,21 @@ +import { lazy } from "@probo/react-lazy"; + +import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton"; + +export const userRoutes = [ + { + path: "users", + children: [ + { + index: true, + Component: lazy(() => import("#/pages/iam/organizations/users/MembersPageLoader")), + Fallback: LinkCardSkeleton, + }, + { + path: ":userId", + Component: lazy(() => import("#/pages/iam/organizations/users/UserPageLoader")), + Fallback: LinkCardSkeleton, + }, + ], + }, +]; diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx index 8a6abab40..0071c3154 100644 --- a/apps/console/src/routes.tsx +++ b/apps/console/src/routes.tsx @@ -14,6 +14,7 @@ import { PageError } from "./components/PageError"; import { RootErrorBoundary } from "./components/RootErrorBoundary"; import { PageSkeleton } from "./components/skeletons/PageSkeleton"; import { ViewerLayoutLoading } from "./pages/iam/memberships/ViewerLayoutLoading"; +import { userRoutes } from "./pages/iam/organizations/users/routes"; import { compliancePageRoutes } from "./pages/organizations/compliance-page/routes"; import { CurrentUser } from "./providers/CurrentUser"; import { assetRoutes } from "./routes/assetRoutes"; @@ -190,13 +191,6 @@ const routes = [ import("./pages/iam/organizations/settings/GeneralSettingsPageLoader"), ), }, - { - path: "members", - Component: lazy( - () => - import("./pages/iam/organizations/settings/MembersPageLoader"), - ), - }, { path: "saml-sso", Component: lazy( @@ -213,6 +207,7 @@ const routes = [ }, ], }, + ...userRoutes, ...riskRoutes, ...measureRoutes, ...documentsRoutes, diff --git a/pkg/coredata/membership_profile.go b/pkg/coredata/membership_profile.go index ef4907a4e..99c0dd79e 100644 --- a/pkg/coredata/membership_profile.go +++ b/pkg/coredata/membership_profile.go @@ -65,14 +65,18 @@ func (p *MembershipProfile) AuthorizationAttributes(ctx context.Context, conn pg q := `SELECT m.organization_id, mp.identity_id FROM iam_membership_profiles mp JOIN iam_memberships m ON mp.membership_id = m.id WHERE mp.id = $1 LIMIT 1;` var organizationID gid.GID - if err := conn.QueryRow(ctx, q, p.ID).Scan(&organizationID); err != nil { + var identityID gid.GID + if err := conn.QueryRow(ctx, q, p.ID).Scan(&organizationID, &identityID); err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrResourceNotFound } return nil, fmt.Errorf("cannot query membership profile authorization attributes: %w", err) } - return map[string]string{"organization_id": organizationID.String()}, nil + return map[string]string{ + "organization_id": organizationID.String(), + "identity_id": identityID.String(), + }, nil } func (p *MembershipProfile) LoadByMembershipID( diff --git a/pkg/iam/iam_actions.go b/pkg/iam/iam_actions.go index 04d79a72c..dca2a0a0a 100644 --- a/pkg/iam/iam_actions.go +++ b/pkg/iam/iam_actions.go @@ -52,8 +52,9 @@ const ( ActionMembershipRoleSetOwner = "iam:membership-role:set-owner" // Membership Profile actions - ActionMembershipProfileGet = "iam:membership-profile:get" - ActionMembershipProfileList = "iam:membership-profile:list" + ActionMembershipProfileGet = "iam:membership-profile:get" + ActionMembershipProfileList = "iam:membership-profile:list" + ActionMembershipProfileUpdate = "iam:membership-profile:update" // Personal API Key actions ActionPersonalAPIKeyCreate = "iam:personal-api-key:create" diff --git a/pkg/iam/iam_policies.go b/pkg/iam/iam_policies.go index 0dc188295..adb096ef9 100644 --- a/pkg/iam/iam_policies.go +++ b/pkg/iam/iam_policies.go @@ -161,6 +161,7 @@ var IAMOwnerPolicy = policy.NewPolicy( policy.Allow( ActionMembershipProfileGet, ActionMembershipProfileList, + ActionMembershipProfileUpdate, ). WithSID("full-membership-profile-access"). When(policy.Equals("principal.organization_id", "resource.organization_id")), @@ -239,6 +240,7 @@ var IAMAdminPolicy = policy.NewPolicy( policy.Allow( ActionMembershipProfileGet, ActionMembershipProfileList, + ActionMembershipProfileUpdate, ). WithSID("membership-profile-admin-access"). When(policy.Equals("principal.organization_id", "resource.organization_id")), diff --git a/pkg/iam/organization_service.go b/pkg/iam/organization_service.go index b3d55c3d8..69366be56 100644 --- a/pkg/iam/organization_service.go +++ b/pkg/iam/organization_service.go @@ -97,6 +97,16 @@ type ( FullName string Role coredata.MembershipRole } + + UpdateProfileRequest struct { + ID gid.GID + FullName string + AdditionalEmailAddresses mail.Addrs + Kind coredata.MembershipProfileKind + Position **string + ContractStartDate **time.Time + ContractEndDate **time.Time + } ) var ( @@ -124,6 +134,8 @@ var ( const ( TokenTypeAPIKey = "api_key" + NameMaxLength = 100 + TitleMaxLength = 1000 ContentMaxLength = 5000 DefaultAttributeEmail = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" @@ -180,6 +192,22 @@ func (req UpdateOrganizationRequest) Validate() error { return v.Error() } +func (upr *UpdateProfileRequest) Validate() error { + v := validator.New() + + v.Check(upr.ID, "id", validator.Required(), validator.GID(coredata.MembershipProfileEntityType)) + v.Check(upr.Kind, "kind", validator.OneOfSlice(coredata.PeopleKinds())) + v.Check(upr.FullName, "full_name", validator.SafeTextNoNewLine(NameMaxLength)) + v.CheckEach(upr.AdditionalEmailAddresses, "additional_email_addresses", func(index int, item any) { + v.Check(item, fmt.Sprintf("additional_email_addresses[%d]", index), validator.Required(), validator.NotEmpty()) + }) + v.Check(upr.Position, "position", validator.SafeText(TitleMaxLength)) + v.Check(upr.ContractStartDate, "contract_start_date", validator.Before(upr.ContractEndDate)) + v.Check(upr.ContractEndDate, "contract_end_date", validator.After(upr.ContractStartDate)) + + return v.Error() +} + func NewOrganizationService(svc *Service) *OrganizationService { return &OrganizationService{Service: svc} } @@ -946,6 +974,57 @@ func (s *OrganizationService) ListMembers( return page.NewPage(memberships, cursor), nil } +func (s *OrganizationService) UpdateProfile(ctx context.Context, req *UpdateProfileRequest) (*coredata.MembershipProfile, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + var ( + scope = coredata.NewScopeFromObjectID(req.ID) + profile = &coredata.MembershipProfile{} + ) + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := profile.LoadByID(ctx, conn, scope, req.ID); err != nil { + return fmt.Errorf("cannot load profile: %w", err) + } + + profile.FullName = req.FullName + profile.Kind = req.Kind + + profile.AdditionalEmailAddresses = req.AdditionalEmailAddresses + + profile.Position = *req.Position + + if req.ContractStartDate != nil { + profile.ContractStartDate = *req.ContractStartDate + } + + if req.ContractEndDate != nil { + profile.ContractEndDate = *req.ContractEndDate + } + + if profile.ContractStartDate != nil && profile.ContractEndDate != nil { + if profile.ContractEndDate.Before(*profile.ContractStartDate) { + return fmt.Errorf("contract end date must be after or equal to start date") + } + } + + profile.UpdatedAt = time.Now() + + return profile.Update(ctx, conn, scope) + }, + ) + + if err != nil { + return nil, err + } + + return profile, nil +} + func (s *OrganizationService) GetProfile(ctx context.Context, profileID gid.GID) (*coredata.MembershipProfile, error) { profile := &coredata.MembershipProfile{} diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index c6bbe80ee..31b6719cb 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -98,6 +98,7 @@ type Mutation { @session(required: PRESENT) deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload @session(required: PRESENT) + updateProfile(input: UpdateProfileInput!): UpdateProfilePayload! updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload! removeMember(input: RemoveMemberInput!): RemoveMemberPayload @session(required: PRESENT) @@ -176,9 +177,20 @@ type Identity implements Node { type MembershipProfile implements Node { id: ID! fullName: String! + additionalEmailAddresses: [EmailAddr!]! + kind: ProfileKind! + position: String + contractStartDate: Datetime + contractEndDate: Datetime createdAt: Datetime! updatedAt: Datetime! + identity: Identity @goField(forceResolver: true) + organization: Organization @goField(forceResolver: true) + + # TODO: remove when memberships are under profile + membershipId: ID! + permission(action: String!): Boolean! @goField(forceResolver: true) @session(required: PRESENT) @@ -229,6 +241,17 @@ type Organization implements Node { @session(required: PRESENT) } +enum ProfileKind @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileKind") { + EMPLOYEE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindEmployee") + CONTRACTOR + @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindContractor") + SERVICE_ACCOUNT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindServiceAccount" + ) +} + enum MembershipRole @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") { OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner") @@ -717,6 +740,16 @@ input InviteMemberInput { role: MembershipRole! } +input UpdateProfileInput { + id: ID! + fullName: String! + additionalEmailAddresses: [EmailAddr!] + kind: ProfileKind! + position: String @goField(omittable: true) + contractStartDate: Datetime @goField(omittable: true) + contractEndDate: Datetime @goField(omittable: true) +} + input UpdateMembershipInput { organizationId: ID! membershipId: ID! @@ -868,6 +901,10 @@ type InviteMemberPayload { invitationEdge: InvitationEdge! } +type UpdateProfilePayload { + profile: MembershipProfile! +} + type UpdateMembershipPayload { membership: Membership! } diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index 03b34798c..125f15bb5 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -210,11 +210,19 @@ type ComplexityRoot struct { } MembershipProfile struct { - CreatedAt func(childComplexity int) int - FullName func(childComplexity int) int - ID func(childComplexity int) int - Permission func(childComplexity int, action string) int - UpdatedAt func(childComplexity int) int + AdditionalEmailAddresses func(childComplexity int) int + ContractEndDate func(childComplexity int) int + ContractStartDate func(childComplexity int) int + CreatedAt func(childComplexity int) int + FullName func(childComplexity int) int + ID func(childComplexity int) int + Identity func(childComplexity int) int + Kind func(childComplexity int) int + MembershipID func(childComplexity int) int + Organization func(childComplexity int) int + Permission func(childComplexity int, action string) int + Position func(childComplexity int) int + UpdatedAt func(childComplexity int) int } Mutation struct { @@ -245,6 +253,7 @@ type ComplexityRoot struct { SignUpFromInvitation func(childComplexity int, input types.SignUpFromInvitationInput) int UpdateMembership func(childComplexity int, input types.UpdateMembershipInput) int UpdateOrganization func(childComplexity int, input types.UpdateOrganizationInput) int + UpdateProfile func(childComplexity int, input types.UpdateProfileInput) int UpdateSAMLConfiguration func(childComplexity int, input types.UpdateSAMLConfigurationInput) int UpdateSCIMBridge func(childComplexity int, input types.UpdateSCIMBridgeInput) int VerifyEmail func(childComplexity int, input types.VerifyEmailInput) int @@ -472,6 +481,10 @@ type ComplexityRoot struct { Organization func(childComplexity int) int } + UpdateProfilePayload struct { + Profile func(childComplexity int) int + } + UpdateSAMLConfigurationPayload struct { SamlConfiguration func(childComplexity int) int } @@ -514,6 +527,9 @@ type MembershipConnectionResolver interface { TotalCount(ctx context.Context, obj *types.MembershipConnection) (*int, error) } type MembershipProfileResolver interface { + Identity(ctx context.Context, obj *types.MembershipProfile) (*types.Identity, error) + Organization(ctx context.Context, obj *types.MembershipProfile) (*types.Organization, error) + Permission(ctx context.Context, obj *types.MembershipProfile, action string) (bool, error) } type MutationResolver interface { @@ -537,6 +553,7 @@ type MutationResolver interface { DeleteOrganizationHorizontalLogo(ctx context.Context, input types.DeleteOrganizationHorizontalLogoInput) (*types.DeleteOrganizationHorizontalLogoPayload, error) InviteMember(ctx context.Context, input types.InviteMemberInput) (*types.InviteMemberPayload, error) DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error) + UpdateProfile(ctx context.Context, input types.UpdateProfileInput) (*types.UpdateProfilePayload, error) UpdateMembership(ctx context.Context, input types.UpdateMembershipInput) (*types.UpdateMembershipPayload, error) RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error) AcceptInvitation(ctx context.Context, input types.AcceptInvitationInput) (*types.AcceptInvitationPayload, error) @@ -1088,6 +1105,24 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.MembershipEdge.Node(childComplexity), true + case "MembershipProfile.additionalEmailAddresses": + if e.complexity.MembershipProfile.AdditionalEmailAddresses == nil { + break + } + + return e.complexity.MembershipProfile.AdditionalEmailAddresses(childComplexity), true + case "MembershipProfile.contractEndDate": + if e.complexity.MembershipProfile.ContractEndDate == nil { + break + } + + return e.complexity.MembershipProfile.ContractEndDate(childComplexity), true + case "MembershipProfile.contractStartDate": + if e.complexity.MembershipProfile.ContractStartDate == nil { + break + } + + return e.complexity.MembershipProfile.ContractStartDate(childComplexity), true case "MembershipProfile.createdAt": if e.complexity.MembershipProfile.CreatedAt == nil { break @@ -1106,6 +1141,30 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.MembershipProfile.ID(childComplexity), true + case "MembershipProfile.identity": + if e.complexity.MembershipProfile.Identity == nil { + break + } + + return e.complexity.MembershipProfile.Identity(childComplexity), true + case "MembershipProfile.kind": + if e.complexity.MembershipProfile.Kind == nil { + break + } + + return e.complexity.MembershipProfile.Kind(childComplexity), true + case "MembershipProfile.membershipId": + if e.complexity.MembershipProfile.MembershipID == nil { + break + } + + return e.complexity.MembershipProfile.MembershipID(childComplexity), true + case "MembershipProfile.organization": + if e.complexity.MembershipProfile.Organization == nil { + break + } + + return e.complexity.MembershipProfile.Organization(childComplexity), true case "MembershipProfile.permission": if e.complexity.MembershipProfile.Permission == nil { break @@ -1117,6 +1176,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.MembershipProfile.Permission(childComplexity, args["action"].(string)), true + case "MembershipProfile.position": + if e.complexity.MembershipProfile.Position == nil { + break + } + + return e.complexity.MembershipProfile.Position(childComplexity), true case "MembershipProfile.updatedAt": if e.complexity.MembershipProfile.UpdatedAt == nil { break @@ -1411,6 +1476,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.UpdateOrganization(childComplexity, args["input"].(types.UpdateOrganizationInput)), true + case "Mutation.updateProfile": + if e.complexity.Mutation.UpdateProfile == nil { + break + } + + args, err := ec.field_Mutation_updateProfile_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateProfile(childComplexity, args["input"].(types.UpdateProfileInput)), true case "Mutation.updateSAMLConfiguration": if e.complexity.Mutation.UpdateSAMLConfiguration == nil { break @@ -2281,6 +2357,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.UpdateOrganizationPayload.Organization(childComplexity), true + case "UpdateProfilePayload.profile": + if e.complexity.UpdateProfilePayload.Profile == nil { + break + } + + return e.complexity.UpdateProfilePayload.Profile(childComplexity), true + case "UpdateSAMLConfigurationPayload.samlConfiguration": if e.complexity.UpdateSAMLConfigurationPayload.SamlConfiguration == nil { break @@ -2340,6 +2423,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputSignUpInput, ec.unmarshalInputUpdateMembershipInput, ec.unmarshalInputUpdateOrganizationInput, + ec.unmarshalInputUpdateProfileInput, ec.unmarshalInputUpdateSAMLConfigurationInput, ec.unmarshalInputUpdateSCIMBridgeInput, ec.unmarshalInputVerifyEmailInput, @@ -2540,6 +2624,7 @@ type Mutation { @session(required: PRESENT) deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload @session(required: PRESENT) + updateProfile(input: UpdateProfileInput!): UpdateProfilePayload! updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload! removeMember(input: RemoveMemberInput!): RemoveMemberPayload @session(required: PRESENT) @@ -2618,9 +2703,20 @@ type Identity implements Node { type MembershipProfile implements Node { id: ID! fullName: String! + additionalEmailAddresses: [EmailAddr!]! + kind: ProfileKind! + position: String + contractStartDate: Datetime + contractEndDate: Datetime createdAt: Datetime! updatedAt: Datetime! + identity: Identity @goField(forceResolver: true) + organization: Organization @goField(forceResolver: true) + + # TODO: remove when memberships are under profile + membershipId: ID! + permission(action: String!): Boolean! @goField(forceResolver: true) @session(required: PRESENT) @@ -2671,6 +2767,17 @@ type Organization implements Node { @session(required: PRESENT) } +enum ProfileKind @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileKind") { + EMPLOYEE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindEmployee") + CONTRACTOR + @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindContractor") + SERVICE_ACCOUNT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindServiceAccount" + ) +} + enum MembershipRole @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") { OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner") @@ -3159,6 +3266,16 @@ input InviteMemberInput { role: MembershipRole! } +input UpdateProfileInput { + id: ID! + fullName: String! + additionalEmailAddresses: [EmailAddr!] + kind: ProfileKind! + position: String @goField(omittable: true) + contractStartDate: Datetime @goField(omittable: true) + contractEndDate: Datetime @goField(omittable: true) +} + input UpdateMembershipInput { organizationId: ID! membershipId: ID! @@ -3310,6 +3427,10 @@ type InviteMemberPayload { invitationEdge: InvitationEdge! } +type UpdateProfilePayload { + profile: MembershipProfile! +} + type UpdateMembershipPayload { membership: Membership! } @@ -3885,6 +4006,17 @@ func (ec *executionContext) field_Mutation_updateOrganization_args(ctx context.C return args, nil } +func (ec *executionContext) field_Mutation_updateProfile_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNUpdateProfileInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateProfileInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_updateSAMLConfiguration_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -6222,10 +6354,26 @@ func (ec *executionContext) fieldContext_Membership_profile(_ context.Context, f return ec.fieldContext_MembershipProfile_id(ctx, field) case "fullName": return ec.fieldContext_MembershipProfile_fullName(ctx, field) + case "additionalEmailAddresses": + return ec.fieldContext_MembershipProfile_additionalEmailAddresses(ctx, field) + case "kind": + return ec.fieldContext_MembershipProfile_kind(ctx, field) + case "position": + return ec.fieldContext_MembershipProfile_position(ctx, field) + case "contractStartDate": + return ec.fieldContext_MembershipProfile_contractStartDate(ctx, field) + case "contractEndDate": + return ec.fieldContext_MembershipProfile_contractEndDate(ctx, field) case "createdAt": return ec.fieldContext_MembershipProfile_createdAt(ctx, field) case "updatedAt": return ec.fieldContext_MembershipProfile_updatedAt(ctx, field) + case "identity": + return ec.fieldContext_MembershipProfile_identity(ctx, field) + case "organization": + return ec.fieldContext_MembershipProfile_organization(ctx, field) + case "membershipId": + return ec.fieldContext_MembershipProfile_membershipId(ctx, field) case "permission": return ec.fieldContext_MembershipProfile_permission(ctx, field) } @@ -6732,6 +6880,151 @@ func (ec *executionContext) fieldContext_MembershipProfile_fullName(_ context.Co return fc, nil } +func (ec *executionContext) _MembershipProfile_additionalEmailAddresses(ctx context.Context, field graphql.CollectedField, obj *types.MembershipProfile) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_MembershipProfile_additionalEmailAddresses, + func(ctx context.Context) (any, error) { + return obj.AdditionalEmailAddresses, nil + }, + nil, + ec.marshalNEmailAddr2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddrᚄ, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_MembershipProfile_additionalEmailAddresses(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipProfile", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type EmailAddr does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipProfile_kind(ctx context.Context, field graphql.CollectedField, obj *types.MembershipProfile) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_MembershipProfile_kind, + func(ctx context.Context) (any, error) { + return obj.Kind, nil + }, + nil, + ec.marshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_MembershipProfile_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipProfile", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ProfileKind does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipProfile_position(ctx context.Context, field graphql.CollectedField, obj *types.MembershipProfile) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_MembershipProfile_position, + func(ctx context.Context) (any, error) { + return obj.Position, nil + }, + nil, + ec.marshalOString2ᚖstring, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_MembershipProfile_position(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipProfile", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipProfile_contractStartDate(ctx context.Context, field graphql.CollectedField, obj *types.MembershipProfile) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_MembershipProfile_contractStartDate, + func(ctx context.Context) (any, error) { + return obj.ContractStartDate, nil + }, + nil, + ec.marshalODatetime2ᚖtimeᚐTime, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_MembershipProfile_contractStartDate(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipProfile", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipProfile_contractEndDate(ctx context.Context, field graphql.CollectedField, obj *types.MembershipProfile) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_MembershipProfile_contractEndDate, + func(ctx context.Context) (any, error) { + return obj.ContractEndDate, nil + }, + nil, + ec.marshalODatetime2ᚖtimeᚐTime, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_MembershipProfile_contractEndDate(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipProfile", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _MembershipProfile_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.MembershipProfile) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -6790,6 +7083,151 @@ func (ec *executionContext) fieldContext_MembershipProfile_updatedAt(_ context.C return fc, nil } +func (ec *executionContext) _MembershipProfile_identity(ctx context.Context, field graphql.CollectedField, obj *types.MembershipProfile) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_MembershipProfile_identity, + func(ctx context.Context) (any, error) { + return ec.resolvers.MembershipProfile().Identity(ctx, obj) + }, + nil, + ec.marshalOIdentity2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐIdentity, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_MembershipProfile_identity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipProfile", + 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_Identity_id(ctx, field) + case "email": + return ec.fieldContext_Identity_email(ctx, field) + case "fullName": + return ec.fieldContext_Identity_fullName(ctx, field) + case "emailVerified": + return ec.fieldContext_Identity_emailVerified(ctx, field) + case "createdAt": + return ec.fieldContext_Identity_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_Identity_updatedAt(ctx, field) + case "memberships": + return ec.fieldContext_Identity_memberships(ctx, field) + case "pendingInvitations": + return ec.fieldContext_Identity_pendingInvitations(ctx, field) + case "sessions": + return ec.fieldContext_Identity_sessions(ctx, field) + case "personalAPIKeys": + return ec.fieldContext_Identity_personalAPIKeys(ctx, field) + case "permission": + return ec.fieldContext_Identity_permission(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Identity", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipProfile_organization(ctx context.Context, field graphql.CollectedField, obj *types.MembershipProfile) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_MembershipProfile_organization, + func(ctx context.Context) (any, error) { + return ec.resolvers.MembershipProfile().Organization(ctx, obj) + }, + nil, + ec.marshalOOrganization2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐOrganization, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_MembershipProfile_organization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipProfile", + 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 "email": + return ec.fieldContext_Organization_email(ctx, field) + case "description": + return ec.fieldContext_Organization_description(ctx, field) + case "websiteUrl": + return ec.fieldContext_Organization_websiteUrl(ctx, field) + case "headquarterAddress": + return ec.fieldContext_Organization_headquarterAddress(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 "scimConfiguration": + return ec.fieldContext_Organization_scimConfiguration(ctx, field) + case "viewerMembership": + return ec.fieldContext_Organization_viewerMembership(ctx, field) + case "permission": + return ec.fieldContext_Organization_permission(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipProfile_membershipId(ctx context.Context, field graphql.CollectedField, obj *types.MembershipProfile) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_MembershipProfile_membershipId, + func(ctx context.Context) (any, error) { + return obj.MembershipID, nil + }, + nil, + ec.marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_MembershipProfile_membershipId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipProfile", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _MembershipProfile_permission(ctx context.Context, field graphql.CollectedField, obj *types.MembershipProfile) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -8091,6 +8529,51 @@ func (ec *executionContext) fieldContext_Mutation_deleteInvitation(ctx context.C return fc, nil } +func (ec *executionContext) _Mutation_updateProfile(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_updateProfile, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().UpdateProfile(ctx, fc.Args["input"].(types.UpdateProfileInput)) + }, + nil, + ec.marshalNUpdateProfilePayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateProfilePayload, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Mutation_updateProfile(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "profile": + return ec.fieldContext_UpdateProfilePayload_profile(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type UpdateProfilePayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateProfile_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_updateMembership(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -13314,6 +13797,63 @@ func (ec *executionContext) fieldContext_UpdateOrganizationPayload_organization( return fc, nil } +func (ec *executionContext) _UpdateProfilePayload_profile(ctx context.Context, field graphql.CollectedField, obj *types.UpdateProfilePayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_UpdateProfilePayload_profile, + func(ctx context.Context) (any, error) { + return obj.Profile, nil + }, + nil, + ec.marshalNMembershipProfile2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐMembershipProfile, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_UpdateProfilePayload_profile(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "UpdateProfilePayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_MembershipProfile_id(ctx, field) + case "fullName": + return ec.fieldContext_MembershipProfile_fullName(ctx, field) + case "additionalEmailAddresses": + return ec.fieldContext_MembershipProfile_additionalEmailAddresses(ctx, field) + case "kind": + return ec.fieldContext_MembershipProfile_kind(ctx, field) + case "position": + return ec.fieldContext_MembershipProfile_position(ctx, field) + case "contractStartDate": + return ec.fieldContext_MembershipProfile_contractStartDate(ctx, field) + case "contractEndDate": + return ec.fieldContext_MembershipProfile_contractEndDate(ctx, field) + case "createdAt": + return ec.fieldContext_MembershipProfile_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_MembershipProfile_updatedAt(ctx, field) + case "identity": + return ec.fieldContext_MembershipProfile_identity(ctx, field) + case "organization": + return ec.fieldContext_MembershipProfile_organization(ctx, field) + case "membershipId": + return ec.fieldContext_MembershipProfile_membershipId(ctx, field) + case "permission": + return ec.fieldContext_MembershipProfile_permission(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type MembershipProfile", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _UpdateSAMLConfigurationPayload_samlConfiguration(ctx context.Context, field graphql.CollectedField, obj *types.UpdateSAMLConfigurationPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -16015,6 +16555,75 @@ func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Co return it, nil } +func (ec *executionContext) unmarshalInputUpdateProfileInput(ctx context.Context, obj any) (types.UpdateProfileInput, error) { + var it types.UpdateProfileInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"id", "fullName", "additionalEmailAddresses", "kind", "position", "contractStartDate", "contractEndDate"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "id": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.ID = data + case "fullName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.FullName = data + case "additionalEmailAddresses": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("additionalEmailAddresses")) + data, err := ec.unmarshalOEmailAddr2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddrᚄ(ctx, v) + if err != nil { + return it, err + } + it.AdditionalEmailAddresses = data + case "kind": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("kind")) + data, err := ec.unmarshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind(ctx, v) + if err != nil { + return it, err + } + it.Kind = data + case "position": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("position")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Position = graphql.OmittableOf(data) + case "contractStartDate": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("contractStartDate")) + data, err := ec.unmarshalODatetime2ᚖtimeᚐTime(ctx, v) + if err != nil { + return it, err + } + it.ContractStartDate = graphql.OmittableOf(data) + case "contractEndDate": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("contractEndDate")) + data, err := ec.unmarshalODatetime2ᚖtimeᚐTime(ctx, v) + if err != nil { + return it, err + } + it.ContractEndDate = graphql.OmittableOf(data) + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputUpdateSAMLConfigurationInput(ctx context.Context, obj any) (types.UpdateSAMLConfigurationInput, error) { var it types.UpdateSAMLConfigurationInput asMap := map[string]any{} @@ -17854,6 +18463,22 @@ func (ec *executionContext) _MembershipProfile(ctx context.Context, sel ast.Sele if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "additionalEmailAddresses": + out.Values[i] = ec._MembershipProfile_additionalEmailAddresses(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "kind": + out.Values[i] = ec._MembershipProfile_kind(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "position": + out.Values[i] = ec._MembershipProfile_position(ctx, field, obj) + case "contractStartDate": + out.Values[i] = ec._MembershipProfile_contractStartDate(ctx, field, obj) + case "contractEndDate": + out.Values[i] = ec._MembershipProfile_contractEndDate(ctx, field, obj) case "createdAt": out.Values[i] = ec._MembershipProfile_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -17864,6 +18489,77 @@ func (ec *executionContext) _MembershipProfile(ctx context.Context, sel ast.Sele if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "identity": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._MembershipProfile_identity(ctx, field, obj) + 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) }) + case "organization": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._MembershipProfile_organization(ctx, field, obj) + 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) }) + case "membershipId": + out.Values[i] = ec._MembershipProfile_membershipId(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "permission": field := field @@ -18025,6 +18721,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_deleteInvitation(ctx, field) }) + case "updateProfile": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateProfile(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "updateMembership": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_updateMembership(ctx, field) @@ -20646,6 +21349,45 @@ func (ec *executionContext) _UpdateOrganizationPayload(ctx context.Context, sel return out } +var updateProfilePayloadImplementors = []string{"UpdateProfilePayload"} + +func (ec *executionContext) _UpdateProfilePayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateProfilePayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, updateProfilePayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("UpdateProfilePayload") + case "profile": + out.Values[i] = ec._UpdateProfilePayload_profile(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var updateSAMLConfigurationPayloadImplementors = []string{"UpdateSAMLConfigurationPayload"} func (ec *executionContext) _UpdateSAMLConfigurationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateSAMLConfigurationPayload) graphql.Marshaler { @@ -21262,6 +22004,36 @@ func (ec *executionContext) marshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋm return res } +func (ec *executionContext) unmarshalNEmailAddr2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddrᚄ(ctx context.Context, v any) ([]mail.Addr, error) { + var vSlice []any + vSlice = graphql.CoerceList(v) + var err error + res := make([]mail.Addr, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalNEmailAddr2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddrᚄ(ctx context.Context, sel ast.SelectionSet, v []mail.Addr) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, sel, v[i]) + } + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) unmarshalNForgotPasswordInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐForgotPasswordInput(ctx context.Context, v any) (types.ForgotPasswordInput, error) { res, err := ec.unmarshalInputForgotPasswordInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -21532,6 +22304,16 @@ var ( } ) +func (ec *executionContext) marshalNMembershipProfile2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐMembershipProfile(ctx context.Context, sel ast.SelectionSet, v *types.MembershipProfile) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._MembershipProfile(ctx, sel, v) +} + 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] @@ -21730,6 +22512,36 @@ func (ec *executionContext) marshalNPersonalAPIKeyEdge2ᚖgoᚗproboᚗincᚋpro return ec._PersonalAPIKeyEdge(ctx, sel, v) } +func (ec *executionContext) unmarshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind(ctx context.Context, v any) (coredata.MembershipProfileKind, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind(ctx context.Context, sel ast.SelectionSet, v coredata.MembershipProfileKind) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind[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 ( + unmarshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind = map[string]coredata.MembershipProfileKind{ + "EMPLOYEE": coredata.MembershipProfileKindEmployee, + "CONTRACTOR": coredata.MembershipProfileKindContractor, + "SERVICE_ACCOUNT": coredata.MembershipProfileKindServiceAccount, + } + marshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind = map[coredata.MembershipProfileKind]string{ + coredata.MembershipProfileKindEmployee: "EMPLOYEE", + coredata.MembershipProfileKindContractor: "CONTRACTOR", + coredata.MembershipProfileKindServiceAccount: "SERVICE_ACCOUNT", + } +) + func (ec *executionContext) unmarshalNReauthenticationReason2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐReauthenticationReason(ctx context.Context, v any) (types.ReauthenticationReason, error) { var res types.ReauthenticationReason err := res.UnmarshalGQL(v) @@ -22258,6 +23070,25 @@ func (ec *executionContext) unmarshalNUpdateOrganizationInput2goᚗproboᚗinc return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNUpdateProfileInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateProfileInput(ctx context.Context, v any) (types.UpdateProfileInput, error) { + res, err := ec.unmarshalInputUpdateProfileInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNUpdateProfilePayload2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateProfilePayload(ctx context.Context, sel ast.SelectionSet, v types.UpdateProfilePayload) graphql.Marshaler { + return ec._UpdateProfilePayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNUpdateProfilePayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateProfilePayload(ctx context.Context, sel ast.SelectionSet, v *types.UpdateProfilePayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._UpdateProfilePayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNUpdateSAMLConfigurationInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateSAMLConfigurationInput(ctx context.Context, v any) (types.UpdateSAMLConfigurationInput, error) { res, err := ec.unmarshalInputUpdateSAMLConfigurationInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -22690,6 +23521,42 @@ func (ec *executionContext) marshalODeleteSCIMConfigurationPayload2ᚖgoᚗprobo return ec._DeleteSCIMConfigurationPayload(ctx, sel, v) } +func (ec *executionContext) unmarshalOEmailAddr2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddrᚄ(ctx context.Context, v any) ([]mail.Addr, error) { + if v == nil { + return nil, nil + } + var vSlice []any + vSlice = graphql.CoerceList(v) + var err error + res := make([]mail.Addr, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalOEmailAddr2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddrᚄ(ctx context.Context, sel ast.SelectionSet, v []mail.Addr) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, sel, v[i]) + } + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalOForgotPasswordPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐForgotPasswordPayload(ctx context.Context, sel ast.SelectionSet, v *types.ForgotPasswordPayload) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/server/api/connect/v1/types/identity_profile.go b/pkg/server/api/connect/v1/types/identity_profile.go index 1097c396e..b861ee842 100644 --- a/pkg/server/api/connect/v1/types/identity_profile.go +++ b/pkg/server/api/connect/v1/types/identity_profile.go @@ -18,9 +18,21 @@ import "go.probo.inc/probo/pkg/coredata" func NewMembershipProfile(profile *coredata.MembershipProfile) *MembershipProfile { return &MembershipProfile{ - ID: profile.ID, - FullName: profile.FullName, - CreatedAt: profile.CreatedAt, - UpdatedAt: profile.UpdatedAt, + ID: profile.ID, + FullName: profile.FullName, + AdditionalEmailAddresses: profile.AdditionalEmailAddresses, + Kind: profile.Kind, + Position: profile.Position, + ContractStartDate: profile.ContractStartDate, + ContractEndDate: profile.ContractEndDate, + CreatedAt: profile.CreatedAt, + UpdatedAt: profile.UpdatedAt, + MembershipID: profile.MembershipID, + Identity: &Identity{ + ID: profile.IdentityID, + }, + Organization: &Organization{ + ID: profile.OrganizationID, + }, } } diff --git a/pkg/server/api/connect/v1/types/types.go b/pkg/server/api/connect/v1/types/types.go index a32c3806e..642581acf 100644 --- a/pkg/server/api/connect/v1/types/types.go +++ b/pkg/server/api/connect/v1/types/types.go @@ -240,11 +240,19 @@ type MembershipEdge struct { } type MembershipProfile struct { - ID gid.GID `json:"id"` - FullName string `json:"fullName"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - Permission bool `json:"permission"` + ID gid.GID `json:"id"` + FullName string `json:"fullName"` + AdditionalEmailAddresses []mail.Addr `json:"additionalEmailAddresses"` + Kind coredata.MembershipProfileKind `json:"kind"` + Position *string `json:"position,omitempty"` + ContractStartDate *time.Time `json:"contractStartDate,omitempty"` + ContractEndDate *time.Time `json:"contractEndDate,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Identity *Identity `json:"identity,omitempty"` + Organization *Organization `json:"organization,omitempty"` + MembershipID gid.GID `json:"membershipId"` + Permission bool `json:"permission"` } func (MembershipProfile) IsNode() {} @@ -545,6 +553,20 @@ type UpdateOrganizationPayload struct { Organization *Organization `json:"organization,omitempty"` } +type UpdateProfileInput struct { + ID gid.GID `json:"id"` + FullName string `json:"fullName"` + AdditionalEmailAddresses []mail.Addr `json:"additionalEmailAddresses,omitempty"` + Kind coredata.MembershipProfileKind `json:"kind"` + Position graphql.Omittable[*string] `json:"position,omitempty"` + ContractStartDate graphql.Omittable[*time.Time] `json:"contractStartDate,omitempty"` + ContractEndDate graphql.Omittable[*time.Time] `json:"contractEndDate,omitempty"` +} + +type UpdateProfilePayload struct { + Profile *MembershipProfile `json:"profile"` +} + type UpdateSAMLConfigurationInput struct { OrganizationID gid.GID `json:"organizationId"` SamlConfigurationID gid.GID `json:"samlConfigurationId"` diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index 458d7a3ea..6e08ea286 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -349,6 +349,53 @@ func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *type return nil, gqlutils.Internal(ctx) } +// Identity is the resolver for the identity field. +func (r *membershipProfileResolver) Identity(ctx context.Context, obj *types.MembershipProfile) (*types.Identity, error) { + if err := r.authorize( + ctx, + obj.Identity.ID, + iam.ActionIdentityGet, + authz.WithAttr("organization_id", obj.Organization.ID.String()), + ); err != nil { + return nil, err + } + + if gqlutils.OnlyIDSelected(ctx) { + return &types.Identity{ + ID: obj.Identity.ID, + }, nil + } + + identity, err := r.iam.AccountService.GetIdentity(ctx, obj.Identity.ID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get identity for membership", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewIdentity(identity), nil +} + +// Organization is the resolver for the organization field. +func (r *membershipProfileResolver) Organization(ctx context.Context, obj *types.MembershipProfile) (*types.Organization, error) { + if err := r.authorize(ctx, obj.Organization.ID, iam.ActionOrganizationGet, authz.WithSkipAssumptionCheck()); err != nil { + return nil, err + } + + if gqlutils.OnlyIDSelected(ctx) { + return &types.Organization{ + ID: obj.Organization.ID, + }, nil + } + + organization, err := r.iam.OrganizationService.GetOrganizationForMembership(ctx, obj.ID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get organization for membership", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewOrganization(organization), nil +} + // Permission is the resolver for the permission field. func (r *membershipProfileResolver) Permission(ctx context.Context, obj *types.MembershipProfile, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) @@ -991,6 +1038,34 @@ func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.Del return &types.DeleteInvitationPayload{DeletedInvitationID: input.InvitationID}, nil } +// UpdateProfile is the resolver for the updateProfile field. +func (r *mutationResolver) UpdateProfile(ctx context.Context, input types.UpdateProfileInput) (*types.UpdateProfilePayload, error) { + if err := r.authorize(ctx, input.ID, iam.ActionMembershipProfileUpdate); err != nil { + return nil, err + } + + profile, err := r.iam.OrganizationService.UpdateProfile( + ctx, + &iam.UpdateProfileRequest{ + ID: input.ID, + FullName: input.FullName, + AdditionalEmailAddresses: input.AdditionalEmailAddresses, + Kind: input.Kind, + Position: gqlutils.UnwrapOmittable(input.Position), + ContractStartDate: gqlutils.UnwrapOmittable(input.ContractStartDate), + ContractEndDate: gqlutils.UnwrapOmittable(input.ContractEndDate), + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot update profile", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return &types.UpdateProfilePayload{ + Profile: types.NewMembershipProfile(profile), + }, nil +} + // UpdateMembership is the resolver for the updateMembership field. func (r *mutationResolver) UpdateMembership(ctx context.Context, input types.UpdateMembershipInput) (*types.UpdateMembershipPayload, error) { if err := r.authorize(ctx, input.MembershipID, iam.ActionMembershipUpdate); err != nil { @@ -1494,6 +1569,16 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error return types.NewSession(session), nil } + case coredata.MembershipProfileEntityType: + action = iam.ActionMembershipGet + loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { + profile, err := r.iam.OrganizationService.GetProfile(ctx, id) + if err != nil { + return nil, err + } + + return types.NewMembershipProfile(profile), nil + } case coredata.MembershipEntityType: action = iam.ActionMembershipGet loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 2e0768017..205d1e678 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -69,7 +69,7 @@ enum EvidenceState @goEnum(value: "go.probo.inc/probo/pkg/coredata.EvidenceStateRequested") } -enum MembershipProfileKind @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileKind") { +enum ProfileKind @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileKind") { EMPLOYEE @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindEmployee") CONTRACTOR @@ -1846,7 +1846,7 @@ type Profile implements Node { fullName: String! emailAddress: EmailAddr! additionalEmailAddresses: [EmailAddr!]! - kind: MembershipProfileKind! + kind: ProfileKind! position: String contractStartDate: Datetime contractEndDate: Datetime diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 0ce8af0a4..bde68d222 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -10347,7 +10347,7 @@ enum EvidenceState @goEnum(value: "go.probo.inc/probo/pkg/coredata.EvidenceStateRequested") } -enum MembershipProfileKind @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileKind") { +enum ProfileKind @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileKind") { EMPLOYEE @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipProfileKindEmployee") CONTRACTOR @@ -12124,7 +12124,7 @@ type Profile implements Node { fullName: String! emailAddress: EmailAddr! additionalEmailAddresses: [EmailAddr!]! - kind: MembershipProfileKind! + kind: ProfileKind! position: String contractStartDate: Datetime contractEndDate: Datetime @@ -44174,7 +44174,7 @@ func (ec *executionContext) _Profile_kind(ctx context.Context, field graphql.Col return obj.Kind, nil }, nil, - ec.marshalNMembershipProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind, + ec.marshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind, true, true, ) @@ -44187,7 +44187,7 @@ func (ec *executionContext) fieldContext_Profile_kind(_ context.Context, field g IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type MembershipProfileKind does not have child fields") + return nil, errors.New("field of type ProfileKind does not have child fields") }, } return fc, nil @@ -94083,36 +94083,6 @@ var ( } ) -func (ec *executionContext) unmarshalNMembershipProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind(ctx context.Context, v any) (coredata.MembershipProfileKind, error) { - tmp, err := graphql.UnmarshalString(v) - res := unmarshalNMembershipProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind[tmp] - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalNMembershipProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind(ctx context.Context, sel ast.SelectionSet, v coredata.MembershipProfileKind) graphql.Marshaler { - _ = sel - res := graphql.MarshalString(marshalNMembershipProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind[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 ( - unmarshalNMembershipProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind = map[string]coredata.MembershipProfileKind{ - "EMPLOYEE": coredata.MembershipProfileKindEmployee, - "CONTRACTOR": coredata.MembershipProfileKindContractor, - "SERVICE_ACCOUNT": coredata.MembershipProfileKindServiceAccount, - } - marshalNMembershipProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind = map[coredata.MembershipProfileKind]string{ - coredata.MembershipProfileKindEmployee: "EMPLOYEE", - coredata.MembershipProfileKindContractor: "CONTRACTOR", - coredata.MembershipProfileKindServiceAccount: "SERVICE_ACCOUNT", - } -) - func (ec *executionContext) marshalNNode2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐNode(ctx context.Context, sel ast.SelectionSet, v types.Node) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -94885,6 +94855,36 @@ func (ec *executionContext) marshalNProfileEdge2ᚖgoᚗproboᚗincᚋproboᚋpk return ec._ProfileEdge(ctx, sel, v) } +func (ec *executionContext) unmarshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind(ctx context.Context, v any) (coredata.MembershipProfileKind, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind(ctx context.Context, sel ast.SelectionSet, v coredata.MembershipProfileKind) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind[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 ( + unmarshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind = map[string]coredata.MembershipProfileKind{ + "EMPLOYEE": coredata.MembershipProfileKindEmployee, + "CONTRACTOR": coredata.MembershipProfileKindContractor, + "SERVICE_ACCOUNT": coredata.MembershipProfileKindServiceAccount, + } + marshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind = map[coredata.MembershipProfileKind]string{ + coredata.MembershipProfileKindEmployee: "EMPLOYEE", + coredata.MembershipProfileKindContractor: "CONTRACTOR", + coredata.MembershipProfileKindServiceAccount: "SERVICE_ACCOUNT", + } +) + func (ec *executionContext) unmarshalNProfileOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileOrderField(ctx context.Context, v any) (coredata.MembershipProfileOrderField, error) { tmp, err := graphql.UnmarshalString(v) res := unmarshalNProfileOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileOrderField[tmp]