From 0f96b8518f980a8d806b13f03befb912b592dbdb Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Mon, 8 Sep 2025 10:02:52 +0200 Subject: [PATCH] Refactoring of authentification Signed-off-by: Bryan Frimin --- .../organizations/InviteUserDialog.tsx | 24 +- .../InviteUserDialogMutation.graphql.ts | 192 +- .../src/hooks/graph/OrganizationGraph.ts | 2 + .../OrganizationGraph_ViewQuery.graphql.ts | 329 +- .../src/pages/organizations/SettingsPage.tsx | 405 +- ...SettingsInvitationsRefetchQuery.graphql.ts | 368 ++ ...SettingsMembershipsRefetchQuery.graphql.ts | 354 ++ .../SettingsPageFragment.graphql.ts | 100 +- ...SettingsPageInvitationsFragment.graphql.ts | 278 ++ ...SettingsPageMembershipsFragment.graphql.ts | 262 ++ ...gsPage_DeleteInvitationMutation.graphql.ts | 132 + ...tingsPage_RemoveMemberMutation.graphql.ts} | 36 +- packages/ui/src/Atoms/Tabs/Tabs.tsx | 2 +- pkg/auth/emails/password_reset.txt.tmpl | 12 + pkg/auth/emails/signup_confirmation.txt.tmpl | 12 + pkg/auth/service.go | 602 +++ pkg/authz/emails/invitation.txt.tmpl | 10 + pkg/authz/service.go | 559 +++ pkg/coredata/entity_type_reg.go | 2 + pkg/coredata/invitation.go | 280 ++ pkg/coredata/invitation_order_field.go | 73 + pkg/coredata/membership.go | 356 ++ pkg/coredata/membership_order_field.go | 53 + pkg/coredata/migrations/20251006T220024Z.sql | 41 + pkg/coredata/organization.go | 57 +- pkg/coredata/session.go | 1 + pkg/coredata/trust_center.go | 1 + pkg/coredata/user.go | 33 +- pkg/coredata/user_organization.go | 1 + pkg/probo/service.go | 14 +- pkg/probo/trust_center_access_service.go | 4 +- pkg/probod/probod.go | 28 +- pkg/server/api/api.go | 43 +- .../api/console/v1/forget_password_handler.go | 6 +- .../v1/invitation_confirmation_handler.go | 36 +- .../api/console/v1/reset_password_handler.go | 10 +- pkg/server/api/console/v1/resolver.go | 40 +- pkg/server/api/console/v1/schema.graphql | 138 +- pkg/server/api/console/v1/schema/schema.go | 3301 +++++++++++++++-- pkg/server/api/console/v1/sign_in_handler.go | 8 +- pkg/server/api/console/v1/sign_out_handler.go | 6 +- pkg/server/api/console/v1/sign_up_handler.go | 11 +- pkg/server/api/console/v1/types/invitation.go | 52 + pkg/server/api/console/v1/types/membership.go | 57 + pkg/server/api/console/v1/types/types.go | 78 +- pkg/server/api/console/v1/v1_resolver.go | 172 +- pkg/server/api/trust/v1/resolver.go | 44 +- .../api/trust/v1/{auth => trustauth}/auth.go | 2 +- pkg/server/server.go | 13 +- pkg/server/session/session.go | 17 +- pkg/trust/service.go | 14 +- pkg/trust/trust_center_access_service.go | 4 +- pkg/usrmgr/usrmgr.go | 953 ----- 53 files changed, 7997 insertions(+), 1631 deletions(-) create mode 100644 apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts create mode 100644 apps/console/src/pages/organizations/__generated__/SettingsMembershipsRefetchQuery.graphql.ts create mode 100644 apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts create mode 100644 apps/console/src/pages/organizations/__generated__/SettingsPageMembershipsFragment.graphql.ts create mode 100644 apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteInvitationMutation.graphql.ts rename apps/console/src/pages/organizations/__generated__/{SettingsPage_RemoveUserMutation.graphql.ts => SettingsPage_RemoveMemberMutation.graphql.ts} (57%) create mode 100644 pkg/auth/emails/password_reset.txt.tmpl create mode 100644 pkg/auth/emails/signup_confirmation.txt.tmpl create mode 100644 pkg/auth/service.go create mode 100644 pkg/authz/emails/invitation.txt.tmpl create mode 100644 pkg/authz/service.go create mode 100644 pkg/coredata/invitation.go create mode 100644 pkg/coredata/invitation_order_field.go create mode 100644 pkg/coredata/membership.go create mode 100644 pkg/coredata/membership_order_field.go create mode 100644 pkg/coredata/migrations/20251006T220024Z.sql create mode 100644 pkg/server/api/console/v1/types/invitation.go create mode 100644 pkg/server/api/console/v1/types/membership.go rename pkg/server/api/trust/v1/{auth => trustauth}/auth.go (99%) delete mode 100644 pkg/usrmgr/usrmgr.go diff --git a/apps/console/src/components/organizations/InviteUserDialog.tsx b/apps/console/src/components/organizations/InviteUserDialog.tsx index 97139d991..146cfce6c 100644 --- a/apps/console/src/components/organizations/InviteUserDialog.tsx +++ b/apps/console/src/components/organizations/InviteUserDialog.tsx @@ -17,9 +17,22 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { Controller } from "react-hook-form"; const inviteMutation = graphql` - mutation InviteUserDialogMutation($input: InviteUserInput!) { + mutation InviteUserDialogMutation( + $input: InviteUserInput! + $connections: [ID!]! + ) { inviteUser(input: $input) { - success + invitationEdge @appendEdge(connections: $connections) { + node { + id + email + fullName + role + expiresAt + acceptedAt + createdAt + } + } } } `; @@ -30,7 +43,11 @@ const schema = z.object({ createPeople: z.boolean().default(false), }); -export function InviteUserDialog({ children }: PropsWithChildren) { +type Props = PropsWithChildren & { + connectionId?: string; +}; + +export function InviteUserDialog({ children, connectionId }: Props) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, { @@ -53,6 +70,7 @@ export function InviteUserDialog({ children }: PropsWithChildren) { fullName: data.fullName, createPeople: data.createPeople, }, + connections: connectionId ? [connectionId] : ["SettingsPageInvitations_invitations"], }, onSuccess: () => { reset(); diff --git a/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts b/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts index e1bc3cb71..c95bc2594 100644 --- a/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts +++ b/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<9ed3f33dde5f6a00c742b8db245ba03b>> + * @generated SignedSource<<8efbb638045cb24913b04b4de2033357>> * @lightSyntaxTransform * @nogrep */ @@ -16,11 +16,22 @@ export type InviteUserInput = { organizationId: string; }; export type InviteUserDialogMutation$variables = { + connections: ReadonlyArray; input: InviteUserInput; }; export type InviteUserDialogMutation$data = { readonly inviteUser: { - readonly success: boolean; + readonly invitationEdge: { + readonly node: { + readonly acceptedAt: any | null | undefined; + readonly createdAt: any; + readonly email: string; + readonly expiresAt: any; + readonly fullName: string; + readonly id: string; + readonly role: string; + }; + }; }; }; export type InviteUserDialogMutation = { @@ -29,67 +40,170 @@ export type InviteUserDialogMutation = { }; const node: ConcreteRequest = (function(){ -var v0 = [ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" + "kind": "Variable", + "name": "input", + "variableName": "input" } ], -v1 = [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "InviteUserPayload", - "kind": "LinkedField", - "name": "inviteUser", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "success", - "storageKey": null - } - ], - "storageKey": null - } -]; +v3 = { + "alias": null, + "args": null, + "concreteType": "InvitationEdge", + "kind": "LinkedField", + "name": "invitationEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "acceptedAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null +}; return { "fragment": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], "kind": "Fragment", "metadata": null, "name": "InviteUserDialogMutation", - "selections": (v1/*: any*/), + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "InviteUserPayload", + "kind": "LinkedField", + "name": "inviteUser", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], "type": "Mutation", "abstractKey": null }, "kind": "Request", "operation": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], "kind": "Operation", "name": "InviteUserDialogMutation", - "selections": (v1/*: any*/) + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "InviteUserPayload", + "kind": "LinkedField", + "name": "inviteUser", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "appendEdge", + "key": "", + "kind": "LinkedHandle", + "name": "invitationEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] }, "params": { - "cacheID": "aa667927b5e2cd0019a8457edc286181", + "cacheID": "2f6a83e238f7749e18757ec74e86b64b", "id": null, "metadata": {}, "name": "InviteUserDialogMutation", "operationKind": "mutation", - "text": "mutation InviteUserDialogMutation(\n $input: InviteUserInput!\n) {\n inviteUser(input: $input) {\n success\n }\n}\n" + "text": "mutation InviteUserDialogMutation(\n $input: InviteUserInput!\n) {\n inviteUser(input: $input) {\n invitationEdge {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "de4c5b5208a2ff0953e9d15b844df842"; +(node as any).hash = "3981061f31a11e83ad32bed9fabddf64"; export default node; diff --git a/apps/console/src/hooks/graph/OrganizationGraph.ts b/apps/console/src/hooks/graph/OrganizationGraph.ts index 2d4330cb5..843af3484 100644 --- a/apps/console/src/hooks/graph/OrganizationGraph.ts +++ b/apps/console/src/hooks/graph/OrganizationGraph.ts @@ -10,6 +10,8 @@ export const organizationViewQuery = graphql` id name ...SettingsPageFragment + ...SettingsPageMembershipsFragment + ...SettingsPageInvitationsFragment } } } diff --git a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts index 96f92c960..da3ae2a5a 100644 --- a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<5a41cd3709282172e4d5a3ca65d07b96>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -17,7 +17,7 @@ export type OrganizationGraph_ViewQuery$data = { readonly node: { readonly id?: string; readonly name?: string; - readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment">; + readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment" | "SettingsPageInvitationsFragment" | "SettingsPageMembershipsFragment">; }; }; export type OrganizationGraph_ViewQuery = { @@ -58,29 +58,133 @@ v4 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "email", + "name": "__typename", "storageKey": null }, v5 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "type", + "name": "email", "storageKey": null }, v6 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "type", + "storageKey": null +}, +v7 = { "alias": null, "args": null, "kind": "ScalarField", "name": "createdAt", "storageKey": null }, -v7 = [ +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null +}, +v9 = [ { "kind": "Literal", "name": "first", - "value": 100 + "value": 20 + }, + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "CREATED_AT" + } } +], +v10 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null +}, +v13 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v14 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null +}, +v15 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}, +v16 = [ + "orderBy" ]; return { "fragment": { @@ -106,6 +210,16 @@ return { "args": null, "kind": "FragmentSpread", "name": "SettingsPageFragment" + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "SettingsPageMembershipsFragment" + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "SettingsPageInvitationsFragment" } ], "type": "Organization", @@ -132,13 +246,7 @@ return { "name": "node", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - }, + (v4/*: any*/), (v2/*: any*/), { "kind": "InlineFragment", @@ -172,7 +280,7 @@ return { "name": "websiteUrl", "storageKey": null }, - (v4/*: any*/), + (v5/*: any*/), { "alias": null, "args": null, @@ -211,7 +319,7 @@ return { "name": "dnsRecords", "plural": true, "selections": [ - (v5/*: any*/), + (v6/*: any*/), (v3/*: any*/), { "alias": null, @@ -237,14 +345,8 @@ return { ], "storageKey": null }, - (v6/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "updatedAt", - "storageKey": null - }, + (v7/*: any*/), + (v8/*: any*/), { "alias": null, "args": null, @@ -255,52 +357,17 @@ return { ], "storageKey": null }, + (v7/*: any*/), + (v8/*: any*/), { "alias": null, - "args": (v7/*: any*/), - "concreteType": "UserConnection", - "kind": "LinkedField", - "name": "users", - "plural": false, - "selections": [ + "args": [ { - "alias": null, - "args": null, - "concreteType": "UserEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "User", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "fullName", - "storageKey": null - }, - (v4/*: any*/), - (v6/*: any*/) - ], - "storageKey": null - } - ], - "storageKey": null + "kind": "Literal", + "name": "first", + "value": 100 } ], - "storageKey": "users(first:100)" - }, - { - "alias": null, - "args": (v7/*: any*/), "concreteType": "ConnectorConnection", "kind": "LinkedField", "name": "connectors", @@ -324,8 +391,8 @@ return { "selections": [ (v2/*: any*/), (v3/*: any*/), - (v5/*: any*/), - (v6/*: any*/) + (v6/*: any*/), + (v7/*: any*/) ], "storageKey": null } @@ -334,6 +401,130 @@ return { } ], "storageKey": "connectors(first:100)" + }, + { + "alias": null, + "args": (v9/*: any*/), + "concreteType": "MembershipConnection", + "kind": "LinkedField", + "name": "memberships", + "plural": false, + "selections": [ + (v10/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "MembershipEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v11/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "emailAddress", + "storageKey": null + }, + (v12/*: any*/), + (v7/*: any*/), + (v4/*: any*/) + ], + "storageKey": null + }, + (v13/*: any*/) + ], + "storageKey": null + }, + (v14/*: any*/), + (v15/*: any*/) + ], + "storageKey": "memberships(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" + }, + { + "alias": null, + "args": (v9/*: any*/), + "filters": (v16/*: any*/), + "handle": "connection", + "key": "SettingsPageMemberships_memberships", + "kind": "LinkedHandle", + "name": "memberships" + }, + { + "alias": null, + "args": (v9/*: any*/), + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "invitations", + "plural": false, + "selections": [ + (v10/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "InvitationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v5/*: any*/), + (v11/*: any*/), + (v12/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "acceptedAt", + "storageKey": null + }, + (v7/*: any*/), + (v4/*: any*/) + ], + "storageKey": null + }, + (v13/*: any*/) + ], + "storageKey": null + }, + (v14/*: any*/), + (v15/*: any*/) + ], + "storageKey": "invitations(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" + }, + { + "alias": null, + "args": (v9/*: any*/), + "filters": (v16/*: any*/), + "handle": "connection", + "key": "SettingsPageInvitations_invitations", + "kind": "LinkedHandle", + "name": "invitations" } ], "type": "Organization", @@ -345,16 +536,16 @@ return { ] }, "params": { - "cacheID": "8b9e1f3b1e93e1823354e77f10764e54", + "cacheID": "eb300e10b8aec548831c1610ea7bc0ec", "id": null, "metadata": {}, "name": "OrganizationGraph_ViewQuery", "operationKind": "query", - "text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n users(first: 100) {\n edges {\n node {\n id\n fullName\n email\n createdAt\n }\n }\n }\n connectors(first: 100) {\n edges {\n node {\n id\n name\n type\n createdAt\n }\n }\n }\n}\n" + "text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n ...SettingsPageMembershipsFragment\n ...SettingsPageInvitationsFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n createdAt\n updatedAt\n connectors(first: 100) {\n edges {\n node {\n id\n name\n type\n createdAt\n }\n }\n }\n}\n\nfragment SettingsPageInvitationsFragment on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SettingsPageMembershipsFragment on Organization {\n memberships(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" } }; })(); -(node as any).hash = "196e8c1fc9c2e0b3c76c8b338ed5c7f7"; +(node as any).hash = "fda1489f2b80fd3d0b3962574bd7dfe3"; export default node; diff --git a/apps/console/src/pages/organizations/SettingsPage.tsx b/apps/console/src/pages/organizations/SettingsPage.tsx index 066ecf972..cfe518c6c 100644 --- a/apps/console/src/pages/organizations/SettingsPage.tsx +++ b/apps/console/src/pages/organizations/SettingsPage.tsx @@ -1,5 +1,4 @@ import { - ActionDropdown, Avatar, Badge, Button, @@ -7,14 +6,21 @@ import { Dialog, DialogContent, DialogFooter, - DropdownItem, Field, FileButton, IconTrashCan, Label, PageHeader, Spinner, + TabBadge, + TabItem, + Tabs, + Tbody, + Td, Textarea, + Th, + Thead, + Tr, useConfirm, useDialogRef, useToast, @@ -22,19 +28,28 @@ import { import { useTranslate } from "@probo/i18n"; import type { PreloadedQuery } from "react-relay"; import type { OrganizationGraph_ViewQuery } from "/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql"; -import { useFragment, useMutation, usePreloadedQuery } from "react-relay"; +import { useFragment, useMutation, usePreloadedQuery, usePaginationFragment } from "react-relay"; import { organizationViewQuery } from "/hooks/graph/OrganizationGraph"; import { graphql } from "relay-runtime"; +import { SortableTable, SortableTh } from "/components/SortableTable"; +import clsx from "clsx"; import type { SettingsPageFragment$data, SettingsPageFragment$key, } from "./__generated__/SettingsPageFragment.graphql"; +import type { + SettingsPageMembershipsFragment$data, + SettingsPageMembershipsFragment$key +} from "./__generated__/SettingsPageMembershipsFragment.graphql"; +import type { + SettingsPageInvitationsFragment$data, + SettingsPageInvitationsFragment$key +} from "./__generated__/SettingsPageInvitationsFragment.graphql"; import { useState, type ChangeEventHandler, useEffect } from "react"; import { sprintf } from "@probo/helpers"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { z } from "zod"; import type { NodeOf } from "/types"; -import clsx from "clsx"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { InviteUserDialog } from "/components/organizations/InviteUserDialog"; @@ -82,16 +97,8 @@ const organizationFragment = graphql` updatedAt sslExpiresAt } - users(first: 100) { - edges { - node { - id - fullName - email - createdAt - } - } - } + createdAt + updatedAt connectors(first: 100) { edges { node { @@ -105,6 +112,83 @@ const organizationFragment = graphql` } `; +const paginatedMembershipsFragment = graphql` + fragment SettingsPageMembershipsFragment on Organization + @refetchable(queryName: "SettingsMembershipsRefetchQuery") + @argumentDefinitions( + first: { type: "Int", defaultValue: 20 } + order: { type: "MembershipOrder", defaultValue: { direction: ASC, field: CREATED_AT } } + after: { type: "CursorKey", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + last: { type: "Int", defaultValue: null } + ) { + memberships( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + ) @connection(key: "SettingsPageMemberships_memberships") { + __id + totalCount + edges { + node { + id + fullName + emailAddress + role + createdAt + } + } + } + } +`; + +const paginatedInvitationsFragment = graphql` + fragment SettingsPageInvitationsFragment on Organization + @refetchable(queryName: "SettingsInvitationsRefetchQuery") + @argumentDefinitions( + first: { type: "Int", defaultValue: 20 } + order: { type: "InvitationOrder", defaultValue: { direction: ASC, field: CREATED_AT } } + after: { type: "CursorKey", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + last: { type: "Int", defaultValue: null } + ) { + invitations( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + ) @connection(key: "SettingsPageInvitations_invitations") { + __id + totalCount + edges { + node { + id + email + fullName + role + expiresAt + acceptedAt + createdAt + } + } + } + } +`; + +const deleteInvitationMutation = graphql` + mutation SettingsPage_DeleteInvitationMutation( + $input: DeleteInvitationInput! + $connections: [ID!]! + ) { + deleteInvitation(input: $input) { + deletedInvitationId @deleteEdge(connections: $connections) + } + } +`; + const updateOrganizationMutation = graphql` mutation SettingsPage_UpdateMutation($input: UpdateOrganizationInput!) { updateOrganization(input: $input) { @@ -145,6 +229,17 @@ export default function SettingsPage({ queryRef }: Props) { organizationFragment, organizationKey ); + + const membershipsPagination = usePaginationFragment( + paginatedMembershipsFragment, + organizationKey as SettingsPageMembershipsFragment$key + ); + + const invitationsPagination = usePaginationFragment( + paginatedInvitationsFragment, + organizationKey as SettingsPageInvitationsFragment$key + ); + const [updateOrganization] = useMutation(updateOrganizationMutation); const [deleteHorizontalLogo, isDeletingHorizontalLogo] = useMutationWithToasts( deleteHorizontalLogoMutation, @@ -154,7 +249,27 @@ export default function SettingsPage({ queryRef }: Props) { } ); const [deleteOrganization, isDeleting] = useDeleteOrganizationMutation(); - const users = organization.users.edges.map((edge) => edge.node); + const memberships = membershipsPagination.data.memberships?.edges.map((edge) => edge.node) || []; + const invitations = invitationsPagination.data.invitations?.edges.map((edge) => edge.node) || []; + const [activeTab, setActiveTab] = useState<"memberships" | "invitations">("memberships"); + + const refetchMemberships = ({ order }: { order: { direction: string; field: string } }) => { + membershipsPagination.refetch({ + order: { + direction: order.direction as "ASC" | "DESC", + field: order.field as "CREATED_AT" | "FULL_NAME" | "EMAIL_ADDRESS" | "ROLE" + } + }); + }; + + const refetchInvitations = ({ order }: { order: { direction: string; field: string } }) => { + invitationsPagination.refetch({ + order: { + direction: order.direction as "ASC" | "DESC", + field: order.field as "CREATED_AT" | "EXPIRES_AT" | "FULL_NAME" | "EMAIL" | "ROLE" | "STATUS" | "ACCEPTED_AT" + } + }); + }; const { formState, handleSubmit, register, reset } = useFormWithSchema( organizationSchema, @@ -454,19 +569,103 @@ export default function SettingsPage({ queryRef }: Props) { - - {/* Integrations */} -
+

{__("Workspace members")}

- +
- - {users.map((user) => ( - - ))} + + + setActiveTab("memberships")} + > + {__("Members")} + {(membershipsPagination.data.memberships?.totalCount || 0) > 0 && ( + {membershipsPagination.data.memberships?.totalCount} + )} + + setActiveTab("invitations")} + > + {__("Invitations")} + {(invitationsPagination.data.invitations?.totalCount || 0) > 0 && ( + {invitationsPagination.data.invitations?.totalCount} + )} + + + + +
+ {activeTab === "memberships" && ( + + + + {__("Name")} + {__("Email")} + {__("Role")} + {__("Joined")} + + + + + {memberships.length === 0 ? ( + + + {__("No members")} + + + ) : ( + memberships.map((membership) => ( + + )) + )} + + + )} + + {activeTab === "invitations" && ( + + + + {__("Name")} + {__("Email")} + {__("Role")} + {__("Invited")} + {__("Status")} + {__("Accepted at")} + + + + + {invitations.length === 0 ? ( + + + {__("No invitations")} + + + ) : ( + invitations.map((invitation) => ( + + )) + )} + + + )} +
@@ -594,23 +793,107 @@ function Connectors(props: { ); } -const removeUserMutation = graphql` - mutation SettingsPage_RemoveUserMutation($input: RemoveUserInput!) { - removeUser(input: $input) { +const removeMemberMutation = graphql` + mutation SettingsPage_RemoveMemberMutation($input: RemoveMemberInput!) { + removeMember(input: $input) { success } } `; -function UserRow(props: { user: NodeOf }) { +function InvitationRow(props: { + invitation: NodeOf; + connectionId?: string; +}) { + const { __ } = useTranslate(); + const confirm = useConfirm(); + const [deleteInvitation, isDeleting] = useMutationWithToasts( + deleteInvitationMutation, + { + successMessage: sprintf( + __("Invitation for %s deleted successfully"), + props.invitation.fullName + ), + } + ); + + const isExpired = new Date() > new Date(props.invitation.expiresAt); + const isAccepted = !!props.invitation.acceptedAt; + + const onDelete = () => { + confirm( + () => { + return deleteInvitation({ + variables: { + input: { + invitationId: props.invitation.id, + }, + connections: props.connectionId ? [props.connectionId] : [], + }, + }); + }, + { + message: sprintf( + __("Are you sure you want to delete the invitation for %s?"), + props.invitation.fullName + ), + } + ); + }; + + return ( + + +
{props.invitation.fullName}
+ + {props.invitation.email} + + {props.invitation.role} + + {new Date(props.invitation.createdAt).toLocaleDateString()} + + {isAccepted ? ( + {__("Accepted")} + ) : isExpired ? ( + {__("Expired")} + ) : ( + {__("Pending")} + )} + + + {props.invitation.acceptedAt ? new Date(props.invitation.acceptedAt).toLocaleDateString() : "-"} + + +
e.stopPropagation()} + > + {isDeleting ? ( + + ) : ( +
+ + + ); +} + +function MembershipRow(props: { membership: NodeOf }) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); - const [removeUser, isRemoving] = useMutationWithToasts(removeUserMutation, { + const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, { successMessage: sprintf( - __("User %s removed successfully"), - props.user.fullName + __("Member %s removed successfully"), + props.membership.fullName ), - errorMessage: sprintf(__("Failed to remove user %s"), props.user.fullName), + errorMessage: sprintf(__("Failed to remove member %s"), props.membership.fullName), }); const confirm = useConfirm(); const [isRemoved, setIsRemoved] = useState(false); @@ -622,10 +905,10 @@ function UserRow(props: { user: NodeOf }) { const onRemove = async () => { confirm( () => { - return removeUser({ + return removeMember({ variables: { input: { - userId: props.user.id, + memberId: props.membership.id, organizationId: organizationId, }, }, @@ -637,42 +920,40 @@ function UserRow(props: { user: NodeOf }) { { message: sprintf( __("Are you sure you want to remove %s?"), - props.user.fullName + props.membership.fullName ), } ); }; return ( -
-
- -
-

{props.user.fullName}

-

{props.user.email}

-
-
-
- {__("Owner")} - {isRemoving ? ( - - ) : ( - - + +
{props.membership.fullName}
+ + {props.membership.emailAddress} + + {props.membership.role} + + {new Date(props.membership.createdAt).toLocaleDateString()} + +
e.stopPropagation()} + > + {isRemoving ? ( + + ) : ( +
-
+ disabled={isRemoving} + icon={IconTrashCan} + aria-label={__("Remove member")} + /> + )} +
+ + ); } diff --git a/apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts new file mode 100644 index 000000000..5fcde040e --- /dev/null +++ b/apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts @@ -0,0 +1,368 @@ +/** + * @generated SignedSource<<9b6812ed468b85a1d3a8684ed1c3ad6f>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type InvitationOrderField = "ACCEPTED_AT" | "CREATED_AT" | "EMAIL" | "EXPIRES_AT" | "FULL_NAME" | "ROLE"; +export type OrderDirection = "ASC" | "DESC"; +export type InvitationOrder = { + direction: OrderDirection; + field: InvitationOrderField; +}; +export type SettingsInvitationsRefetchQuery$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + first?: number | null | undefined; + id: string; + last?: number | null | undefined; + order?: InvitationOrder | null | undefined; +}; +export type SettingsInvitationsRefetchQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"SettingsPageInvitationsFragment">; + }; +}; +export type SettingsInvitationsRefetchQuery = { + response: SettingsInvitationsRefetchQuery$data; + variables: SettingsInvitationsRefetchQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v5 = { + "defaultValue": { + "direction": "ASC", + "field": "CREATED_AT" + }, + "kind": "LocalArgument", + "name": "order" +}, +v6 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v7 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v8 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v9 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v10 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v13 = [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "SettingsInvitationsRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "order", + "variableName": "order" + } + ], + "kind": "FragmentSpread", + "name": "SettingsPageInvitationsFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v3/*: any*/) + ], + "kind": "Operation", + "name": "SettingsInvitationsRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v13/*: any*/), + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "invitations", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "InvitationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v12/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "acceptedAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + (v11/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v13/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "SettingsPageInvitations_invitations", + "kind": "LinkedHandle", + "name": "invitations" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "d1ad09b7e16875a4fc09c5e8dc7e3b7e", + "id": null, + "metadata": {}, + "name": "SettingsInvitationsRefetchQuery", + "operationKind": "query", + "text": "query SettingsInvitationsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: InvitationOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...SettingsPageInvitationsFragment_16fISc\n id\n }\n}\n\nfragment SettingsPageInvitationsFragment_16fISc on Organization {\n invitations(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "d971d93653991efde284ed2b87068698"; + +export default node; diff --git a/apps/console/src/pages/organizations/__generated__/SettingsMembershipsRefetchQuery.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsMembershipsRefetchQuery.graphql.ts new file mode 100644 index 000000000..1cb1e791a --- /dev/null +++ b/apps/console/src/pages/organizations/__generated__/SettingsMembershipsRefetchQuery.graphql.ts @@ -0,0 +1,354 @@ +/** + * @generated SignedSource<<49986e7498d757f2074e93e6cf4747a5>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type MembershipOrderField = "CREATED_AT" | "EMAIL_ADDRESS" | "FULL_NAME" | "ROLE"; +export type OrderDirection = "ASC" | "DESC"; +export type MembershipOrder = { + direction: OrderDirection; + field: MembershipOrderField; +}; +export type SettingsMembershipsRefetchQuery$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + first?: number | null | undefined; + id: string; + last?: number | null | undefined; + order?: MembershipOrder | null | undefined; +}; +export type SettingsMembershipsRefetchQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"SettingsPageMembershipsFragment">; + }; +}; +export type SettingsMembershipsRefetchQuery = { + response: SettingsMembershipsRefetchQuery$data; + variables: SettingsMembershipsRefetchQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v5 = { + "defaultValue": { + "direction": "ASC", + "field": "CREATED_AT" + }, + "kind": "LocalArgument", + "name": "order" +}, +v6 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v7 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v8 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v9 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v10 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v13 = [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "SettingsMembershipsRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "order", + "variableName": "order" + } + ], + "kind": "FragmentSpread", + "name": "SettingsPageMembershipsFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v3/*: any*/) + ], + "kind": "Operation", + "name": "SettingsMembershipsRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v13/*: any*/), + "concreteType": "MembershipConnection", + "kind": "LinkedField", + "name": "memberships", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "MembershipEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v12/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "emailAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + (v11/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v13/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "SettingsPageMemberships_memberships", + "kind": "LinkedHandle", + "name": "memberships" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "57c4ca08006166b58c1fb2407f091704", + "id": null, + "metadata": {}, + "name": "SettingsMembershipsRefetchQuery", + "operationKind": "query", + "text": "query SettingsMembershipsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MembershipOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...SettingsPageMembershipsFragment_16fISc\n id\n }\n}\n\nfragment SettingsPageMembershipsFragment_16fISc on Organization {\n memberships(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "d28514cdb5181fc1023dc4ea5bddb4f2"; + +export default node; diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts index 4605cc817..95346b07a 100644 --- a/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts +++ b/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<122f3da12674107565d07159872492db>> + * @generated SignedSource<<8477f42a02a102a4ce8540b794ac4289>> * @lightSyntaxTransform * @nogrep */ @@ -22,6 +22,7 @@ export type SettingsPageFragment$data = { }; }>; }; + readonly createdAt: any; readonly customDomain: { readonly createdAt: any; readonly dnsRecords: ReadonlyArray<{ @@ -44,16 +45,7 @@ export type SettingsPageFragment$data = { readonly id: string; readonly logoUrl: string | null | undefined; readonly name: string; - readonly users: { - readonly edges: ReadonlyArray<{ - readonly node: { - readonly createdAt: any; - readonly email: string; - readonly fullName: string; - readonly id: string; - }; - }>; - }; + readonly updatedAt: any; readonly websiteUrl: string | null | undefined; readonly " $fragmentType": "SettingsPageFragment"; }; @@ -81,30 +73,23 @@ v2 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "email", + "name": "type", "storageKey": null }, v3 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "type", + "name": "createdAt", "storageKey": null }, v4 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "createdAt", + "name": "updatedAt", "storageKey": null -}, -v5 = [ - { - "kind": "Literal", - "name": "first", - "value": 100 - } -]; +}; return { "argumentDefinitions": [], "kind": "Fragment", @@ -141,7 +126,13 @@ return { "name": "websiteUrl", "storageKey": null }, - (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, { "alias": null, "args": null, @@ -180,7 +171,7 @@ return { "name": "dnsRecords", "plural": true, "selections": [ - (v3/*: any*/), + (v2/*: any*/), (v1/*: any*/), { "alias": null, @@ -206,14 +197,8 @@ return { ], "storageKey": null }, + (v3/*: any*/), (v4/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "updatedAt", - "storageKey": null - }, { "alias": null, "args": null, @@ -224,52 +209,17 @@ return { ], "storageKey": null }, + (v3/*: any*/), + (v4/*: any*/), { "alias": null, - "args": (v5/*: any*/), - "concreteType": "UserConnection", - "kind": "LinkedField", - "name": "users", - "plural": false, - "selections": [ + "args": [ { - "alias": null, - "args": null, - "concreteType": "UserEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "User", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v0/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "fullName", - "storageKey": null - }, - (v2/*: any*/), - (v4/*: any*/) - ], - "storageKey": null - } - ], - "storageKey": null + "kind": "Literal", + "name": "first", + "value": 100 } ], - "storageKey": "users(first:100)" - }, - { - "alias": null, - "args": (v5/*: any*/), "concreteType": "ConnectorConnection", "kind": "LinkedField", "name": "connectors", @@ -293,8 +243,8 @@ return { "selections": [ (v0/*: any*/), (v1/*: any*/), - (v3/*: any*/), - (v4/*: any*/) + (v2/*: any*/), + (v3/*: any*/) ], "storageKey": null } @@ -310,6 +260,6 @@ return { }; })(); -(node as any).hash = "6cea6f88fb0d7b2ae7ef9053b9979898"; +(node as any).hash = "b3b152b507befd6b6918a12972e05f14"; export default node; diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts new file mode 100644 index 000000000..da2a45b73 --- /dev/null +++ b/apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts @@ -0,0 +1,278 @@ +/** + * @generated SignedSource<<4f099d1ee6b4635ca8129eca77d3f7e8>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type SettingsPageInvitationsFragment$data = { + readonly id: string; + readonly invitations: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly acceptedAt: any | null | undefined; + readonly createdAt: any; + readonly email: string; + readonly expiresAt: any; + readonly fullName: string; + readonly id: string; + readonly role: string; + }; + }>; + readonly totalCount: number; + }; + readonly " $fragmentType": "SettingsPageInvitationsFragment"; +}; +export type SettingsPageInvitationsFragment$key = { + readonly " $data"?: SettingsPageInvitationsFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"SettingsPageInvitationsFragment">; +}; + +import SettingsInvitationsRefetchQuery_graphql from './SettingsInvitationsRefetchQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "invitations" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" + }, + { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" + }, + { + "defaultValue": { + "direction": "ASC", + "field": "CREATED_AT" + }, + "kind": "LocalArgument", + "name": "order" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "bidirectional", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": { + "count": "last", + "cursor": "before" + }, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": SettingsInvitationsRefetchQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "SettingsPageInvitationsFragment", + "selections": [ + { + "alias": "invitations", + "args": [ + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } + ], + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "__SettingsPageInvitations_invitations_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "InvitationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "acceptedAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + (v1/*: any*/) + ], + "type": "Organization", + "abstractKey": null +}; +})(); + +(node as any).hash = "d971d93653991efde284ed2b87068698"; + +export default node; diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPageMembershipsFragment.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsPageMembershipsFragment.graphql.ts new file mode 100644 index 000000000..902b3977a --- /dev/null +++ b/apps/console/src/pages/organizations/__generated__/SettingsPageMembershipsFragment.graphql.ts @@ -0,0 +1,262 @@ +/** + * @generated SignedSource<<778a6585d3f3d9b346273c20319ce96d>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type SettingsPageMembershipsFragment$data = { + readonly id: string; + readonly memberships: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly createdAt: any; + readonly emailAddress: string; + readonly fullName: string; + readonly id: string; + readonly role: string; + }; + }>; + readonly totalCount: number; + }; + readonly " $fragmentType": "SettingsPageMembershipsFragment"; +}; +export type SettingsPageMembershipsFragment$key = { + readonly " $data"?: SettingsPageMembershipsFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"SettingsPageMembershipsFragment">; +}; + +import SettingsMembershipsRefetchQuery_graphql from './SettingsMembershipsRefetchQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "memberships" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" + }, + { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" + }, + { + "defaultValue": { + "direction": "ASC", + "field": "CREATED_AT" + }, + "kind": "LocalArgument", + "name": "order" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "bidirectional", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": { + "count": "last", + "cursor": "before" + }, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": SettingsMembershipsRefetchQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "SettingsPageMembershipsFragment", + "selections": [ + { + "alias": "memberships", + "args": [ + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } + ], + "concreteType": "MembershipConnection", + "kind": "LinkedField", + "name": "__SettingsPageMemberships_memberships_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "MembershipEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "emailAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + (v1/*: any*/) + ], + "type": "Organization", + "abstractKey": null +}; +})(); + +(node as any).hash = "d28514cdb5181fc1023dc4ea5bddb4f2"; + +export default node; diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteInvitationMutation.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteInvitationMutation.graphql.ts new file mode 100644 index 000000000..d608f79ec --- /dev/null +++ b/apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteInvitationMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteInvitationInput = { + invitationId: string; +}; +export type SettingsPage_DeleteInvitationMutation$variables = { + connections: ReadonlyArray; + input: DeleteInvitationInput; +}; +export type SettingsPage_DeleteInvitationMutation$data = { + readonly deleteInvitation: { + readonly deletedInvitationId: string; + }; +}; +export type SettingsPage_DeleteInvitationMutation = { + response: SettingsPage_DeleteInvitationMutation$data; + variables: SettingsPage_DeleteInvitationMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedInvitationId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "SettingsPage_DeleteInvitationMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteInvitationPayload", + "kind": "LinkedField", + "name": "deleteInvitation", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "SettingsPage_DeleteInvitationMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteInvitationPayload", + "kind": "LinkedField", + "name": "deleteInvitation", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedInvitationId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "1c362c5db7a985d7548166b2b1eb42c9", + "id": null, + "metadata": {}, + "name": "SettingsPage_DeleteInvitationMutation", + "operationKind": "mutation", + "text": "mutation SettingsPage_DeleteInvitationMutation(\n $input: DeleteInvitationInput!\n) {\n deleteInvitation(input: $input) {\n deletedInvitationId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "3c484508ba04b5a75eca62fa6afeb16d"; + +export default node; diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPage_RemoveUserMutation.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsPage_RemoveMemberMutation.graphql.ts similarity index 57% rename from apps/console/src/pages/organizations/__generated__/SettingsPage_RemoveUserMutation.graphql.ts rename to apps/console/src/pages/organizations/__generated__/SettingsPage_RemoveMemberMutation.graphql.ts index eea38e52d..326c0fe77 100644 --- a/apps/console/src/pages/organizations/__generated__/SettingsPage_RemoveUserMutation.graphql.ts +++ b/apps/console/src/pages/organizations/__generated__/SettingsPage_RemoveMemberMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<994eb713978ecef3329fd9873e4c744c>> * @lightSyntaxTransform * @nogrep */ @@ -9,21 +9,21 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type RemoveUserInput = { +export type RemoveMemberInput = { + memberId: string; organizationId: string; - userId: string; }; -export type SettingsPage_RemoveUserMutation$variables = { - input: RemoveUserInput; +export type SettingsPage_RemoveMemberMutation$variables = { + input: RemoveMemberInput; }; -export type SettingsPage_RemoveUserMutation$data = { - readonly removeUser: { +export type SettingsPage_RemoveMemberMutation$data = { + readonly removeMember: { readonly success: boolean; }; }; -export type SettingsPage_RemoveUserMutation = { - response: SettingsPage_RemoveUserMutation$data; - variables: SettingsPage_RemoveUserMutation$variables; +export type SettingsPage_RemoveMemberMutation = { + response: SettingsPage_RemoveMemberMutation$data; + variables: SettingsPage_RemoveMemberMutation$variables; }; const node: ConcreteRequest = (function(){ @@ -44,9 +44,9 @@ v1 = [ "variableName": "input" } ], - "concreteType": "RemoveUserPayload", + "concreteType": "RemoveMemberPayload", "kind": "LinkedField", - "name": "removeUser", + "name": "removeMember", "plural": false, "selections": [ { @@ -65,7 +65,7 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, - "name": "SettingsPage_RemoveUserMutation", + "name": "SettingsPage_RemoveMemberMutation", "selections": (v1/*: any*/), "type": "Mutation", "abstractKey": null @@ -74,20 +74,20 @@ return { "operation": { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", - "name": "SettingsPage_RemoveUserMutation", + "name": "SettingsPage_RemoveMemberMutation", "selections": (v1/*: any*/) }, "params": { - "cacheID": "a397335d917a93baf77ceba83c735326", + "cacheID": "97e29046871ce8aab01abf98a62236fc", "id": null, "metadata": {}, - "name": "SettingsPage_RemoveUserMutation", + "name": "SettingsPage_RemoveMemberMutation", "operationKind": "mutation", - "text": "mutation SettingsPage_RemoveUserMutation(\n $input: RemoveUserInput!\n) {\n removeUser(input: $input) {\n success\n }\n}\n" + "text": "mutation SettingsPage_RemoveMemberMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n success\n }\n}\n" } }; })(); -(node as any).hash = "ba5bf0e182d15f4805080f63d1604d91"; +(node as any).hash = "f61071a0fb6f6554e56e79b6b04bc135"; export default node; diff --git a/packages/ui/src/Atoms/Tabs/Tabs.tsx b/packages/ui/src/Atoms/Tabs/Tabs.tsx index 0534054d1..880529214 100644 --- a/packages/ui/src/Atoms/Tabs/Tabs.tsx +++ b/packages/ui/src/Atoms/Tabs/Tabs.tsx @@ -8,7 +8,7 @@ const cls = tv({ slots: { wrapper: "border-b border-border-low flex gap-6 text-sm font-medium text-txt-secondary", - item: "py-4 hover:text-txt-primary border-b-2 active:border-border-active -mb-[1px] active:text-txt-primary flex items-center gap-2", + item: "py-4 hover:text-txt-primary border-b-2 active:border-border-active -mb-[1px] active:text-txt-primary flex items-center gap-2 cursor-pointer", badge: "py-1 px-2 text-txt-secondary text-xs font-semibold rounded-lg bg-highlight", }, variants: { diff --git a/pkg/auth/emails/password_reset.txt.tmpl b/pkg/auth/emails/password_reset.txt.tmpl new file mode 100644 index 000000000..aa995cdfa --- /dev/null +++ b/pkg/auth/emails/password_reset.txt.tmpl @@ -0,0 +1,12 @@ +Hi {{.FullName}}, + +You have requested a password reset for your Probo account. + +Please click the link below to reset your password: + +{{.ResetURL}} + +If you did not request this password reset, please ignore this email. + +Thanks, +Probo Team diff --git a/pkg/auth/emails/signup_confirmation.txt.tmpl b/pkg/auth/emails/signup_confirmation.txt.tmpl new file mode 100644 index 000000000..513e0f5ce --- /dev/null +++ b/pkg/auth/emails/signup_confirmation.txt.tmpl @@ -0,0 +1,12 @@ +Hi {{.FullName}}, + +Thanks for joining Probo! + +Please confirm your email address by clicking the link below: + +{{.ConfirmationURL}} + +If you did not sign up for Probo, please ignore this email. + +Thanks, +Probo Team diff --git a/pkg/auth/service.go b/pkg/auth/service.go new file mode 100644 index 000000000..8f76c1a46 --- /dev/null +++ b/pkg/auth/service.go @@ -0,0 +1,602 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package auth + +import ( + "bytes" + "context" + _ "embed" + "errors" + "fmt" + "html/template" + "net/mail" + "net/url" + "time" + + "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/crypto/passwdhash" + "github.com/getprobo/probo/pkg/gid" + "github.com/getprobo/probo/pkg/statelesstoken" + "go.gearno.de/kit/pg" +) + +type ( + // Service handles ONLY user authentication and management + // No organization-related logic - that belongs to authz service + Service struct { + pg *pg.Client + hp *passwdhash.Profile + hostname string + tokenSecret string + disableSignup bool + invitationTokenValidity time.Duration + } + + ErrInvalidCredentials struct { + message string + } + + ErrInvalidEmail struct { + email string + } + + ErrInvalidPassword struct { + minLength int + maxLength int + } + + ErrInvalidFullName struct { + fullName string + } + + ErrUserAlreadyExists struct { + message string + } + + ErrSessionNotFound struct { + message string + } + + ErrSessionExpired struct { + message string + } + + ErrInvalidTokenType struct { + message string + } + + ErrSignupDisabled struct{} + + EmailConfirmationData struct { + UserID gid.GID `json:"uid"` + Email string `json:"email"` + } + + InvitationData struct { + OrganizationID gid.GID `json:"organization_id"` + Email string `json:"email"` + FullName string `json:"full_name"` + CreatePeople bool `json:"create_people"` + } + PasswordResetData struct { + Email string `json:"email"` + } +) + +const ( + TokenTypeEmailConfirmation = "email_confirmation" + TokenTypePasswordReset = "password_reset" +) + +var ( + //go:embed emails/signup_confirmation.txt.tmpl + signupEmailTemplateData string + signupEmailTemplate = template.Must(template.New("signup").Parse(signupEmailTemplateData)) + signupEmailSubject = "Confirm your email address" + + //go:embed emails/password_reset.txt.tmpl + passwordResetEmailTemplateData string + passwordResetEmailTemplate = template.Must(template.New("password_reset").Parse(passwordResetEmailTemplateData)) + passwordResetEmailSubject = "Reset your password" +) + +func (e ErrInvalidCredentials) Error() string { + return e.message +} + +func (e ErrUserAlreadyExists) Error() string { + return e.message +} + +func (e ErrSessionNotFound) Error() string { + return e.message +} + +func (e ErrSessionExpired) Error() string { + return e.message +} + +func (e ErrInvalidEmail) Error() string { + return fmt.Sprintf("invalid email: %s", e.email) +} + +func (e ErrInvalidPassword) Error() string { + return fmt.Sprintf("invalid password: the length must be between %d and %d characters", e.minLength, e.maxLength) +} + +func (e ErrInvalidFullName) Error() string { + return fmt.Sprintf("invalid full name: %s", e.fullName) +} + +func (e ErrInvalidTokenType) Error() string { + return e.message +} + +func (e ErrSignupDisabled) Error() string { + return "signup is disabled, contact the owner of the Probo instance" +} + +func NewService( + ctx context.Context, + pgClient *pg.Client, + hp *passwdhash.Profile, + tokenSecret string, + hostname string, + disableSignup bool, + invitationTokenValidity time.Duration, +) (*Service, error) { + return &Service{ + pg: pgClient, + hp: hp, + hostname: hostname, + tokenSecret: tokenSecret, + disableSignup: disableSignup, + invitationTokenValidity: invitationTokenValidity, + }, nil +} + +func (s Service) ForgetPassword( + ctx context.Context, + email string, +) error { + // Always generate a new token to avoid timing attacks and leaking information + // about existing emails + passwordResetToken, err := statelesstoken.NewToken( + s.tokenSecret, + TokenTypePasswordReset, + 1*time.Hour, + PasswordResetData{Email: email}, + ) + if err != nil { + return fmt.Errorf("cannot generate password reset token: %w", err) + } + + resetPasswordUrl := url.URL{ + Scheme: "https", + Host: s.hostname, + Path: "/auth/reset-password", + RawQuery: url.Values{ + "token": []string{passwordResetToken}, + }.Encode(), + } + + return s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + user := &coredata.User{} + if err := user.LoadByEmail(ctx, conn, email); err != nil { + var errUserNotFound *coredata.ErrUserNotFound + if errors.As(err, &errUserNotFound) { + return nil // Don't leak information about non-existent users + } + return fmt.Errorf("cannot load user: %w", err) + } + + body := bytes.NewBuffer(nil) + err = passwordResetEmailTemplate.Execute( + body, + map[string]string{ + "FullName": user.FullName, + "ResetURL": resetPasswordUrl.String(), + }, + ) + if err != nil { + return fmt.Errorf("cannot execute password reset template: %w", err) + } + + passwordResetEmail := coredata.NewEmail( + user.FullName, + email, + passwordResetEmailSubject, + body.String(), + ) + if err := passwordResetEmail.Insert(ctx, conn); err != nil { + return fmt.Errorf("cannot insert email: %w", err) + } + + return nil + }, + ) +} + +func (s Service) SignUp( + ctx context.Context, + emailAddress string, + password string, + fullName string, +) (*coredata.User, *coredata.Session, error) { + if s.disableSignup { + return nil, nil, &ErrSignupDisabled{} + } + + if _, err := mail.ParseAddress(emailAddress); err != nil { + return nil, nil, &ErrInvalidEmail{emailAddress} + } + + if len(password) < 8 || len(password) > 128 { + return nil, nil, &ErrInvalidPassword{minLength: 8, maxLength: 128} + } + + if fullName == "" { + return nil, nil, &ErrInvalidFullName{fullName} + } + + hashedPassword, err := s.hp.HashPassword([]byte(password)) + if err != nil { + return nil, nil, fmt.Errorf("cannot hash password: %w", err) + } + + now := time.Now() + user := &coredata.User{ + ID: gid.New(gid.NilTenant, coredata.UserEntityType), + EmailAddress: emailAddress, + HashedPassword: hashedPassword, + EmailAddressVerified: false, + FullName: fullName, + CreatedAt: now, + UpdatedAt: now, + } + + session := &coredata.Session{ + ID: gid.New(gid.NilTenant, coredata.SessionEntityType), + UserID: user.ID, + Data: coredata.SessionData{}, + ExpiredAt: now.Add(24 * time.Hour * 7), // 7 days, + CreatedAt: now, + UpdatedAt: now, + } + + err = s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + if err := user.Insert(ctx, tx); err != nil { + var errUserAlreadyExists *coredata.ErrUserAlreadyExists + if errors.As(err, &errUserAlreadyExists) { + return &ErrUserAlreadyExists{errUserAlreadyExists.Error()} + } + return fmt.Errorf("cannot insert user: %w", err) + } + + confirmationToken, err := statelesstoken.NewToken( + s.tokenSecret, + TokenTypeEmailConfirmation, + 24*time.Hour, + EmailConfirmationData{UserID: user.ID, Email: user.EmailAddress}, + ) + if err != nil { + return fmt.Errorf("cannot generate confirmation token: %w", err) + } + + confirmationUrl := url.URL{ + Scheme: "https", + Host: s.hostname, + Path: "/auth/confirm-email", + RawQuery: url.Values{ + "token": []string{confirmationToken}, + }.Encode(), + } + + body := bytes.NewBuffer(nil) + err = signupEmailTemplate.Execute( + body, + map[string]string{ + "FullName": user.FullName, + "ConfirmationURL": confirmationUrl.String(), + }, + ) + if err != nil { + return fmt.Errorf("cannot execute signup template: %w", err) + } + + confirmationEmail := coredata.NewEmail( + user.FullName, + user.EmailAddress, + signupEmailSubject, + body.String(), + ) + + if err := confirmationEmail.Insert(ctx, tx); err != nil { + return fmt.Errorf("cannot insert email: %w", err) + } + + if err := session.Insert(ctx, tx); err != nil { + return fmt.Errorf("cannot insert session: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, nil, err + } + + return user, session, nil +} + +func (s Service) SignIn( + ctx context.Context, + emailAddress string, + password string, +) (*coredata.Session, *coredata.User, error) { + if _, err := mail.ParseAddress(emailAddress); err != nil { + return nil, nil, &ErrInvalidCredentials{"invalid email or password"} + } + + user := &coredata.User{} + session := &coredata.Session{} + + err := s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + if err := user.LoadByEmail(ctx, tx, emailAddress); err != nil { + var errUserNotFound *coredata.ErrUserNotFound + if errors.As(err, &errUserNotFound) { + return &ErrInvalidCredentials{"invalid email or password"} + } + return fmt.Errorf("cannot load user by email: %w", err) + } + + match, err := s.hp.ComparePasswordAndHash([]byte(password), user.HashedPassword) + if err != nil { + return fmt.Errorf("cannot verify password: %w", err) + } + if !match { + return &ErrInvalidCredentials{"invalid email or password"} + } + + now := time.Now() + session = &coredata.Session{ + ID: gid.New(gid.NilTenant, coredata.SessionEntityType), + UserID: user.ID, + Data: coredata.SessionData{}, + ExpiredAt: now.Add(24 * time.Hour * 7), // 7 days + CreatedAt: now, + UpdatedAt: now, + } + + if err := session.Insert(ctx, tx); err != nil { + return fmt.Errorf("cannot insert session: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, nil, err + } + + return session, user, nil +} + +func (s Service) SignOut(ctx context.Context, sessionID gid.GID) error { + return s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + session := &coredata.Session{} + if err := session.LoadByID(ctx, conn, sessionID); err != nil { + return &ErrSessionNotFound{"session not found"} + } + + if err := coredata.DeleteSession(ctx, conn, sessionID); err != nil { + return fmt.Errorf("cannot delete session: %w", err) + } + + return nil + }, + ) +} + +func (s Service) GetSession(ctx context.Context, sessionID gid.GID) (*coredata.Session, error) { + session := &coredata.Session{} + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := session.LoadByID(ctx, conn, sessionID); err != nil { + return &ErrSessionNotFound{"session not found"} + } + + if time.Now().After(session.ExpiredAt) { + // Clean up expired session + _ = coredata.DeleteSession(ctx, conn, sessionID) + return &ErrSessionExpired{"session expired"} + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return session, nil +} + +func (s Service) GetUserByID(ctx context.Context, userID gid.GID) (*coredata.User, error) { + user := &coredata.User{} + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := user.LoadByID(ctx, conn, userID); err != nil { + return fmt.Errorf("cannot load user by ID: %w", err) + } + return nil + }, + ) + + if err != nil { + return nil, err + } + + return user, nil +} + +func (s Service) GetUserByEmail(ctx context.Context, email string) (*coredata.User, error) { + user := &coredata.User{} + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := user.LoadByEmail(ctx, conn, email); err != nil { + return fmt.Errorf("cannot load user by email: %w", err) + } + return nil + }, + ) + + if err != nil { + return nil, err + } + + return user, nil +} + +func (s Service) GetUserBySession(ctx context.Context, sessionID gid.GID) (*coredata.User, error) { + session, err := s.GetSession(ctx, sessionID) + if err != nil { + return nil, err + } + + return s.GetUserByID(ctx, session.UserID) +} + +func (s Service) UpdateSession(ctx context.Context, sessionID gid.GID) (*coredata.Session, error) { + session := &coredata.Session{} + + err := s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + if err := session.LoadByID(ctx, tx, sessionID); err != nil { + return &ErrSessionNotFound{"session not found"} + } + + if time.Now().After(session.ExpiredAt) { + return &ErrSessionExpired{"session expired"} + } + + now := time.Now() + session.ExpiredAt = now.Add(24 * time.Hour * 7) // Extend by 7 days + session.UpdatedAt = now + if err := session.Update(ctx, tx); err != nil { + return fmt.Errorf("cannot update session: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return session, nil +} + +func (s Service) ConfirmEmail(ctx context.Context, tokenString string) error { + payload, err := statelesstoken.ValidateToken[EmailConfirmationData]( + s.tokenSecret, + TokenTypeEmailConfirmation, + tokenString, + ) + if err != nil { + return &ErrInvalidTokenType{"invalid confirmation token"} + } + emailConfirmationData := payload.Data + + return s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + user := &coredata.User{} + if err := user.LoadByID(ctx, tx, emailConfirmationData.UserID); err != nil { + return fmt.Errorf("cannot load user: %w", err) + } + + if user.EmailAddressVerified { + return nil + } + + if err := user.UpdateEmailVerification(ctx, tx, true); err != nil { + return fmt.Errorf("cannot update user email verification: %w", err) + } + + return nil + }, + ) +} + +func (s Service) ResetPassword(ctx context.Context, tokenString string, newPassword string) error { + payload, err := statelesstoken.ValidateToken[PasswordResetData]( + s.tokenSecret, + TokenTypePasswordReset, + tokenString, + ) + if err != nil { + return &ErrInvalidTokenType{"invalid reset token"} + } + passwordResetData := payload.Data + + if len(newPassword) < 8 || len(newPassword) > 128 { + return &ErrInvalidPassword{minLength: 8, maxLength: 128} + } + + hashedPassword, err := s.hp.HashPassword([]byte(newPassword)) + if err != nil { + return fmt.Errorf("cannot hash password: %w", err) + } + + return s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + user := &coredata.User{} + if err := user.LoadByEmail(ctx, tx, passwordResetData.Email); err != nil { + var errUserNotFound *coredata.ErrUserNotFound + if errors.As(err, &errUserNotFound) { + return nil // Don't leak information about non-existent users + } + return fmt.Errorf("cannot load user: %w", err) + } + + if err := user.UpdatePassword(ctx, tx, hashedPassword); err != nil { + return fmt.Errorf("cannot update password: %w", err) + } + + return nil + }, + ) +} diff --git a/pkg/authz/emails/invitation.txt.tmpl b/pkg/authz/emails/invitation.txt.tmpl new file mode 100644 index 000000000..414c7d358 --- /dev/null +++ b/pkg/authz/emails/invitation.txt.tmpl @@ -0,0 +1,10 @@ +Hi {{.FullName}}, + +You have been invited to join organization {{.OrganizationName}}. Please click the link below to accept the invitation: + +{{.InvitationURL}} + +If you don't want to accept the invitation, you can ignore this email. + +Thanks, +Probo Team diff --git a/pkg/authz/service.go b/pkg/authz/service.go new file mode 100644 index 000000000..153d23a43 --- /dev/null +++ b/pkg/authz/service.go @@ -0,0 +1,559 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package authz + +import ( + "bytes" + "context" + _ "embed" + "errors" + "fmt" + "html/template" + "time" + + "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/gid" + "github.com/getprobo/probo/pkg/page" + "github.com/getprobo/probo/pkg/statelesstoken" + "go.gearno.de/kit/pg" +) + +type ( + // Service handles all authorization logic including organization + // membership and permissions. This service is completely independent + // of authentication methods. + Service struct { + pg *pg.Client + hostname string + tokenSecret string + invitationTokenValidity time.Duration + } + + Role string +) + +const ( + RoleOwner Role = "OWNER" + RoleAdmin Role = "ADMIN" + RoleMember Role = "MEMBER" + RoleViewer Role = "VIEWER" +) + +const ( + TokenTypeOrganizationInvitation = "organization_invitation" +) + +var ( + //go:embed emails/invitation.txt.tmpl + invitationEmailBodyData string + + invitationEmailBodyTemplate = template.Must(template.New("invitation").Parse(invitationEmailBodyData)) + invitationEmailSubject = "Invitation to join organization" +) + +func NewService( + ctx context.Context, + pgClient *pg.Client, + hostname string, + tokenSecret string, + invitationTokenValidity time.Duration, +) (*Service, error) { + return &Service{ + pg: pgClient, + hostname: hostname, + tokenSecret: tokenSecret, + invitationTokenValidity: invitationTokenValidity, + }, nil +} + +func (s *Service) GetAllUserOrganizations( + ctx context.Context, + userID gid.GID, +) ([]*coredata.Organization, error) { + var organizations []*coredata.Organization + + err := s.pg.WithConn(ctx, func(conn pg.Conn) error { + var organizationList coredata.Organizations + if err := organizationList.LoadAllByUserID(ctx, conn, userID); err != nil { + return fmt.Errorf("failed to load user organizations: %w", err) + } + + organizations = organizationList + + return nil + }) + + return organizations, err +} + +func (s *Service) GetUserOrganizations( + ctx context.Context, + userID gid.GID, + cursor *page.Cursor[coredata.OrganizationOrderField], +) ([]*coredata.Organization, error) { + var organizations coredata.Organizations + + err := s.pg.WithConn(ctx, func(conn pg.Conn) error { + if err := organizations.LoadByUserID(ctx, conn, userID, cursor); err != nil { + return fmt.Errorf("failed to load user organizations: %w", err) + } + return nil + }) + + return organizations, err +} + +func (s *Service) GetAllOrganizationInvitations( + ctx context.Context, + orgID gid.GID, + cursor *page.Cursor[coredata.InvitationOrderField], +) (*page.Page[*coredata.Invitation, coredata.InvitationOrderField], error) { + var invitations coredata.Invitations + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := invitations.LoadByOrganizationID(ctx, conn, orgID, cursor); err != nil { + return fmt.Errorf("failed to load organization invitations: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return page.NewPage(invitations, cursor), nil +} + +func (s *Service) CountOrganizationInvitations( + ctx context.Context, + orgID gid.GID, +) (int, error) { + var count int + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + var invitations coredata.Invitations + var err error + count, err = invitations.CountByOrganizationID(ctx, conn, orgID) + return err + }, + ) + if err != nil { + return 0, fmt.Errorf("failed to count invitations: %w", err) + } + + return count, nil +} + +func (s *Service) DeleteInvitation( + ctx context.Context, + invitationID gid.GID, +) error { + return s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + invitation := &coredata.Invitation{} + if err := invitation.LoadByID(ctx, conn, invitationID); err != nil { + return fmt.Errorf("failed to load invitation: %w", err) + } + + if err := invitation.Delete(ctx, conn); err != nil { + return fmt.Errorf("failed to delete invitation: %w", err) + } + + return nil + }, + ) +} + +func (s *Service) GetAllOrganizationMemberships( + ctx context.Context, + orgID gid.GID, + cursor *page.Cursor[coredata.MembershipOrderField], +) (*page.Page[*coredata.Membership, coredata.MembershipOrderField], error) { + var memberships coredata.Memberships + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := memberships.LoadByOrganizationID(ctx, conn, orgID, cursor); err != nil { + return fmt.Errorf("failed to load organization memberships: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return page.NewPage(memberships, cursor), nil +} + +func (s *Service) CountOrganizationMemberships( + ctx context.Context, + orgID gid.GID, +) (int, error) { + var count int + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + var memberships coredata.Memberships + var err error + count, err = memberships.CountByOrganizationID(ctx, conn, orgID) + return err + }, + ) + if err != nil { + return 0, fmt.Errorf("failed to count memberships: %w", err) + } + + return count, nil +} + +func (s *Service) CanUserAccessOrganization( + ctx context.Context, + userID gid.GID, + orgID gid.GID, +) (bool, error) { + membership := &coredata.Membership{} + + haveAccess := false + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := membership.LoadByUserAndOrg(ctx, conn, userID, orgID); err != nil { + if _, ok := err.(coredata.ErrMembershipNotFound); ok { + return nil // Not an error, just no access + } + return fmt.Errorf("failed to check organization access: %w", err) + } + haveAccess = true + return nil + }, + ) + + if err != nil { + return false, err + } + + return haveAccess, nil +} + +func (s *Service) GetUserRoleInOrganization( + ctx context.Context, + userID gid.GID, + orgID gid.GID, +) (string, error) { + membership := &coredata.Membership{} + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := membership.LoadByUserAndOrg(ctx, conn, userID, orgID); err != nil { + return fmt.Errorf("failed to get user role: %w", err) + } + return nil + }, + ) + + if err != nil { + return "", err + } + + return membership.Role, nil +} + +func (s *Service) RemoveMemberFromOrganization( + ctx context.Context, + orgID gid.GID, + memberID gid.GID, +) error { + membership := &coredata.Membership{} + + return s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + if err := membership.LoadByID(ctx, tx, memberID); err != nil { + return fmt.Errorf("failed to load membership: %w", err) + } + + if membership.OrganizationID != orgID { + return fmt.Errorf("membership does not belong to organization") + } + + if err := membership.Delete(ctx, tx); err != nil { + return fmt.Errorf("failed to delete membership: %w", err) + } + + return nil + }, + ) +} + +func (s *Service) AddUserToOrganization( + ctx context.Context, + userID gid.GID, + orgID gid.GID, + role string, +) error { + membership := &coredata.Membership{ + UserID: userID, + OrganizationID: orgID, + Role: role, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := membership.Create(ctx, conn); err != nil { + return fmt.Errorf("failed to add user to organization: %w", err) + } + return nil + }, + ) +} + +func (s *Service) UpdateUserRole( + ctx context.Context, + userID gid.GID, + orgID gid.GID, + newRole string, +) error { + return s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + membership := &coredata.Membership{} + if err := membership.LoadByUserAndOrg(ctx, tx, userID, orgID); err != nil { + return fmt.Errorf("failed to find membership: %w", err) + } + + membership.Role = newRole + membership.UpdatedAt = time.Now() + + if err := membership.Update(ctx, tx); err != nil { + return fmt.Errorf("failed to update user role: %w", err) + } + + return nil + }, + ) +} + +func (s *Service) InviteUserToOrganization( + ctx context.Context, + organizationID gid.GID, + emailAddress string, + fullName string, + role string, +) (*coredata.Invitation, error) { + var invitation *coredata.Invitation + + err := s.pg.WithTx(ctx, func(tx pg.Conn) error { + user := &coredata.User{} + userExists := true + if err := user.LoadByEmail(ctx, tx, emailAddress); err != nil { + var userNotFound *coredata.ErrUserNotFound + if errors.As(err, &userNotFound) { + userExists = false + } else { + return fmt.Errorf("failed to check if user exists: %w", err) + } + } + + organization := &coredata.Organization{} + scope := coredata.NewScope(organizationID.TenantID()) + if err := organization.LoadByID(ctx, tx, scope, organizationID); err != nil { + return fmt.Errorf("failed to load organization: %w", err) + } + + invitationID := gid.New(organizationID.TenantID(), coredata.InvitationEntityType) + now := time.Now() + invitation = &coredata.Invitation{ + ID: invitationID, + OrganizationID: organizationID, + Email: emailAddress, + FullName: fullName, + Role: role, + ExpiresAt: now.Add(s.invitationTokenValidity), + CreatedAt: now, + } + + if userExists { + membership := &coredata.Membership{ + UserID: user.ID, + OrganizationID: organizationID, + Role: role, + CreatedAt: now, + UpdatedAt: now, + } + if err := membership.Create(ctx, tx); err != nil { + return fmt.Errorf("failed to add user to organization: %w", err) + } + + invitation.AcceptedAt = &now + } else { + invitationData := coredata.InvitationData{ + InvitationID: invitationID, + OrganizationID: organizationID, + Email: emailAddress, + FullName: fullName, + Role: role, + } + + invitationToken, err := statelesstoken.NewToken( + s.tokenSecret, + TokenTypeOrganizationInvitation, + s.invitationTokenValidity, + invitationData, + ) + if err != nil { + return fmt.Errorf("failed to generate invitation token: %w", err) + } + + body := bytes.NewBuffer(nil) + err = invitationEmailBodyTemplate.Execute( + body, + map[string]string{ + "FullName": fullName, + "OrganizationName": organization.Name, + "InvitationURL": fmt.Sprintf("https://%s/auth/confirm-invitation?token=%s", s.hostname, invitationToken), + }, + ) + if err != nil { + return fmt.Errorf("failed to execute template: %w", err) + } + + email := coredata.NewEmail( + fullName, + emailAddress, + invitationEmailSubject, + body.String(), + ) + + if err := email.Insert(ctx, tx); err != nil { + return fmt.Errorf("cannot insert email: %w", err) + } + } + + if err := invitation.Create(ctx, tx); err != nil { + return fmt.Errorf("cannot create invitation: %w", err) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return invitation, nil +} + +func (s *Service) AcceptInvitation( + ctx context.Context, + token string, + userID gid.GID, +) error { + payload, err := statelesstoken.ValidateToken[coredata.InvitationData]( + s.tokenSecret, + TokenTypeOrganizationInvitation, + token, + ) + if err != nil { + return fmt.Errorf("invalid invitation token: %w", err) + } + invitationData := payload.Data + + return s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + invitation := &coredata.Invitation{} + if err := invitation.LoadByID(ctx, tx, invitationData.InvitationID); err != nil { + var errInvitationNotFound *coredata.ErrInvitationNotFound + if errors.As(err, &errInvitationNotFound) { + return fmt.Errorf("invitation was deleted or no longer exists") + } + return fmt.Errorf("cannot load invitation: %w", err) + } + + if invitation.AcceptedAt != nil { + return fmt.Errorf("invitation already accepted") + } + + if time.Now().After(invitation.ExpiresAt) { + return fmt.Errorf("invitation expired") + } + + membership := &coredata.Membership{ + UserID: userID, + OrganizationID: invitation.OrganizationID, + Role: invitation.Role, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := membership.Create(ctx, tx); err != nil { + return fmt.Errorf("failed to add user to organization: %w", err) + } + + now := time.Now() + invitation.AcceptedAt = &now + if err := invitation.Update(ctx, tx); err != nil { + return fmt.Errorf("failed to mark invitation as accepted: %w", err) + } + + return nil + }, + ) +} + +// This is a placeholder for future permission system +func (s *Service) HasPermission( + ctx context.Context, + userID gid.GID, + orgID gid.GID, + resource string, + action string, +) (bool, error) { + // For now, just check if user is a member + // In the future, this will check specific permissions based on role + return s.CanUserAccessOrganization(ctx, userID, orgID) +} + +func (s *Service) ListUserInvitations( + ctx context.Context, + email string, +) ([]*coredata.Invitation, error) { + var invitations coredata.Invitations + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := invitations.LoadByEmail(ctx, conn, email); err != nil { + return fmt.Errorf("failed to load invitations: %w", err) + } + return nil + }, + ) + + return invitations, err +} diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go index 749acdb0b..b543ae000 100644 --- a/pkg/coredata/entity_type_reg.go +++ b/pkg/coredata/entity_type_reg.go @@ -59,4 +59,6 @@ const ( TrustCenterReferenceEntityType TrustCenterDocumentAccessEntityType CustomDomainEntityType + InvitationEntityType + MembershipEntityType ) diff --git a/pkg/coredata/invitation.go b/pkg/coredata/invitation.go new file mode 100644 index 000000000..5a5be5010 --- /dev/null +++ b/pkg/coredata/invitation.go @@ -0,0 +1,280 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "errors" + "fmt" + "maps" + "time" + + "github.com/getprobo/probo/pkg/gid" + "github.com/getprobo/probo/pkg/page" + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" +) + +type ( + Invitation struct { + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + Email string `db:"email"` + FullName string `db:"full_name"` + Role string `db:"role"` + ExpiresAt time.Time `db:"expires_at"` + AcceptedAt *time.Time `db:"accepted_at"` + CreatedAt time.Time `db:"created_at"` + } + + Invitations []*Invitation + + InvitationData struct { + InvitationID gid.GID `json:"invitation_id"` + OrganizationID gid.GID `json:"organization_id"` + Email string `json:"email"` + FullName string `json:"full_name"` + Role string `json:"role"` + } + + ErrInvitationNotFound struct { + Token string + } +) + +func (e ErrInvitationNotFound) Error() string { + return fmt.Sprintf("invitation not found: %s", e.Token) +} + +func (i Invitation) CursorKey(orderBy InvitationOrderField) page.CursorKey { + switch orderBy { + case InvitationOrderFieldFullName: + return page.NewCursorKey(i.ID, i.FullName) + case InvitationOrderFieldEmail: + return page.NewCursorKey(i.ID, i.Email) + case InvitationOrderFieldRole: + return page.NewCursorKey(i.ID, i.Role) + case InvitationOrderFieldCreatedAt: + return page.NewCursorKey(i.ID, i.CreatedAt) + case InvitationOrderFieldExpiresAt: + return page.NewCursorKey(i.ID, i.ExpiresAt) + case InvitationOrderFieldAcceptedAt: + acceptedAt := time.Time{} + if i.AcceptedAt != nil { + acceptedAt = *i.AcceptedAt + } + return page.NewCursorKey(i.ID, acceptedAt) + } + + panic(fmt.Sprintf("unsupported order by: %s", orderBy)) +} + +// Tenant id scope is not applied because invitations are managed at the organization level and don't require tenant isolation. +func (i *Invitation) Create(ctx context.Context, conn pg.Conn) error { + query := ` + INSERT INTO authz_invitations ( + id, organization_id, email, full_name, role, expires_at, created_at + ) VALUES ( + @id, @organization_id, @email, @full_name, @role, @expires_at, @created_at + ) + ` + + args := pgx.StrictNamedArgs{ + "id": i.ID, + "organization_id": i.OrganizationID, + "email": i.Email, + "full_name": i.FullName, + "role": i.Role, + "expires_at": i.ExpiresAt, + "created_at": i.CreatedAt, + } + + _, err := conn.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("failed to create invitation: %w", err) + } + + return nil +} + +// Tenant id scope is not applied because we want to access invitations across all tenants for authentication purposes. +func (i *Invitation) LoadByID( + ctx context.Context, + conn pg.Conn, + id gid.GID, +) error { + query := ` + SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at + FROM authz_invitations + WHERE id = @id + ` + + args := pgx.StrictNamedArgs{ + "id": id, + } + + rows, err := conn.Query(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot query invitation: %w", err) + } + + invitation, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Invitation]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrInvitationNotFound{Token: id.String()} + } + return fmt.Errorf("cannot collect invitation: %w", err) + } + + *i = invitation + return nil +} + +// Tenant id scope is not applied because invitations are managed at the organization level and don't require tenant isolation. +func (i *Invitation) Update(ctx context.Context, conn pg.Conn) error { + query := ` + UPDATE authz_invitations + SET accepted_at = @accepted_at + WHERE id = @id + ` + + args := pgx.StrictNamedArgs{ + "id": i.ID, + "accepted_at": i.AcceptedAt, + } + + result, err := conn.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("failed to update invitation: %w", err) + } + + if result.RowsAffected() == 0 { + return ErrInvitationNotFound{Token: i.ID.String()} + } + + return nil +} + +// Tenant id scope is not applied because invitations are managed at the organization level and don't require tenant isolation. +func (i *Invitation) Delete(ctx context.Context, conn pg.Conn) error { + query := ` + DELETE FROM authz_invitations + WHERE id = @id + ` + + args := pgx.StrictNamedArgs{ + "id": i.ID, + } + + result, err := conn.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("failed to delete invitation: %w", err) + } + + if result.RowsAffected() == 0 { + return ErrInvitationNotFound{Token: i.ID.String()} + } + + return nil +} + +func (i *Invitations) LoadByEmail( + ctx context.Context, + conn pg.Conn, + email string, +) error { + query := ` + SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at + FROM authz_invitations + WHERE email = @email AND accepted_at IS NULL + ORDER BY created_at DESC + ` + + args := pgx.StrictNamedArgs{ + "email": email, + } + + rows, err := conn.Query(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot query invitations: %w", err) + } + + invitations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Invitation]) + if err != nil { + return fmt.Errorf("cannot collect invitations: %w", err) + } + + *i = invitations + return nil +} + +func (i *Invitations) LoadByOrganizationID( + ctx context.Context, + conn pg.Conn, + orgID gid.GID, + cursor *page.Cursor[InvitationOrderField], +) error { + query := ` + SELECT id, organization_id, email, full_name, role, expires_at, accepted_at, created_at + FROM authz_invitations + WHERE organization_id = @organization_id + AND %s + ` + + query = fmt.Sprintf(query, cursor.SQLFragment()) + + args := pgx.StrictNamedArgs{"organization_id": orgID} + maps.Copy(args, cursor.SQLArguments()) + + rows, err := conn.Query(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot query invitations: %w", err) + } + + invitations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Invitation]) + if err != nil { + return fmt.Errorf("cannot collect invitations: %w", err) + } + + *i = invitations + return nil +} + +func (i *Invitations) CountByOrganizationID( + ctx context.Context, + conn pg.Conn, + orgID gid.GID, +) (int, error) { + q := ` +SELECT + COUNT(*) +FROM + authz_invitations +WHERE + organization_id = @organization_id +` + + args := pgx.StrictNamedArgs{"organization_id": orgID} + + row := conn.QueryRow(ctx, q, args) + + var count int + err := row.Scan(&count) + if err != nil { + return 0, fmt.Errorf("cannot count invitations: %w", err) + } + + return count, nil +} diff --git a/pkg/coredata/invitation_order_field.go b/pkg/coredata/invitation_order_field.go new file mode 100644 index 000000000..3916634bb --- /dev/null +++ b/pkg/coredata/invitation_order_field.go @@ -0,0 +1,73 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import "fmt" + +// InvitationOrderField defines the fields that can be used to order invitations +type InvitationOrderField string + +// InvitationOrderField constants +const ( + InvitationOrderFieldFullName InvitationOrderField = "FULL_NAME" + InvitationOrderFieldEmail InvitationOrderField = "EMAIL" + InvitationOrderFieldRole InvitationOrderField = "ROLE" + InvitationOrderFieldCreatedAt InvitationOrderField = "CREATED_AT" + InvitationOrderFieldExpiresAt InvitationOrderField = "EXPIRES_AT" + InvitationOrderFieldAcceptedAt InvitationOrderField = "ACCEPTED_AT" +) + +func (p InvitationOrderField) Column() string { + switch p { + case InvitationOrderFieldFullName: + return "full_name" + case InvitationOrderFieldEmail: + return "email" + case InvitationOrderFieldRole: + return "role" + case InvitationOrderFieldCreatedAt: + return "created_at" + case InvitationOrderFieldExpiresAt: + return "expires_at" + case InvitationOrderFieldAcceptedAt: + return "accepted_at" + } + + panic(fmt.Sprintf("unsupported order by: %s", p)) +} + +func (e InvitationOrderField) IsValid() bool { + switch e { + case InvitationOrderFieldFullName, InvitationOrderFieldEmail, InvitationOrderFieldRole, InvitationOrderFieldCreatedAt, InvitationOrderFieldExpiresAt, InvitationOrderFieldAcceptedAt: + return true + } + return false +} + +func (e InvitationOrderField) String() string { + return string(e) +} + +func (e *InvitationOrderField) UnmarshalText(text []byte) error { + *e = InvitationOrderField(text) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid InvitationOrderField", string(text)) + } + return nil +} + +func (e InvitationOrderField) MarshalText() ([]byte, error) { + return []byte(e.String()), nil +} diff --git a/pkg/coredata/membership.go b/pkg/coredata/membership.go new file mode 100644 index 000000000..5b8140336 --- /dev/null +++ b/pkg/coredata/membership.go @@ -0,0 +1,356 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "errors" + "fmt" + "maps" + "time" + + "github.com/getprobo/probo/pkg/gid" + "github.com/getprobo/probo/pkg/page" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "go.gearno.de/kit/pg" +) + +type ( + Membership struct { + ID gid.GID `db:"id"` + UserID gid.GID `db:"user_id"` + OrganizationID gid.GID `db:"organization_id"` + Role string `db:"role"` + FullName string `db:"full_name"` + EmailAddress string `db:"email_address"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + Memberships []*Membership + + ErrMembershipNotFound struct { + UserID gid.GID + OrgID gid.GID + } + + ErrMembershipAlreadyExists struct { + UserID gid.GID + OrgID gid.GID + } +) + +func (e ErrMembershipNotFound) Error() string { + return fmt.Sprintf("membership not found for user %s in organization %s", e.UserID, e.OrgID) +} + +func (e ErrMembershipAlreadyExists) Error() string { + return fmt.Sprintf("membership already exists for user %s in organization %s", e.UserID, e.OrgID) +} + +func (m Membership) CursorKey(orderBy MembershipOrderField) page.CursorKey { + switch orderBy { + case MembershipOrderFieldFullName: + return page.NewCursorKey(m.ID, m.FullName) + case MembershipOrderFieldEmailAddress: + return page.NewCursorKey(m.ID, m.EmailAddress) + case MembershipOrderFieldRole: + return page.NewCursorKey(m.ID, m.Role) + case MembershipOrderFieldCreatedAt: + return page.NewCursorKey(m.ID, m.CreatedAt) + } + + panic(fmt.Sprintf("unsupported order by: %s", orderBy)) +} + +// Tenant id scope is not applied because memberships are managed at the organization level and don't require tenant isolation. +func (m *Membership) Create(ctx context.Context, conn pg.Conn) error { + query := ` + INSERT INTO authz_memberships (id, user_id, organization_id, role, created_at, updated_at) + SELECT + generate_gid(decode_base64_unpadded(o.tenant_id), @entity_type), + @user_id, + @organization_id, + @role, + @created_at, + @updated_at + FROM organizations o + WHERE o.id = @organization_id + ` + + args := pgx.StrictNamedArgs{ + "user_id": m.UserID, + "organization_id": m.OrganizationID, + "role": m.Role, + "created_at": m.CreatedAt, + "updated_at": m.UpdatedAt, + "entity_type": MembershipEntityType, + } + + result, err := conn.Exec(ctx, query, args) + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + return ErrMembershipAlreadyExists{UserID: m.UserID, OrgID: m.OrganizationID} + } + return fmt.Errorf("failed to create membership: %w", err) + } + + if result.RowsAffected() == 0 { + return fmt.Errorf("failed to create membership: organization %s not found", m.OrganizationID) + } + + return nil +} + +// Tenant id scope is not applied because we want to access memberships across all tenants for authentication purposes. +func (m *Membership) LoadByID( + ctx context.Context, + conn pg.Conn, + membershipID gid.GID, +) error { + query := ` + SELECT + m.id, + m.user_id, + m.organization_id, + m.role, + u.fullname as full_name, + u.email_address, + m.created_at, + m.updated_at + FROM authz_memberships m + JOIN users u ON m.user_id = u.id + WHERE m.id = @membership_id + ` + + args := pgx.StrictNamedArgs{ + "membership_id": membershipID, + } + + rows, err := conn.Query(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot query membership: %w", err) + } + + membership, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Membership]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrMembershipNotFound{UserID: gid.GID{}, OrgID: gid.GID{}} + } + return fmt.Errorf("cannot collect membership: %w", err) + } + + *m = membership + return nil +} + +// Tenant id scope is not applied because we want to access memberships across all tenants for authentication purposes. +func (m *Membership) LoadByUserAndOrg( + ctx context.Context, + conn pg.Conn, + userID gid.GID, + orgID gid.GID, +) error { + query := ` + SELECT + m.id, + m.user_id, + m.organization_id, + m.role, + u.fullname as full_name, + u.email_address, + m.created_at, + m.updated_at + FROM authz_memberships m + JOIN users u ON m.user_id = u.id + WHERE m.user_id = @user_id AND m.organization_id = @organization_id + ` + + args := pgx.StrictNamedArgs{ + "user_id": userID, + "organization_id": orgID, + } + + rows, err := conn.Query(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot query membership: %w", err) + } + + membership, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Membership]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrMembershipNotFound{UserID: userID, OrgID: orgID} + } + return fmt.Errorf("cannot collect membership: %w", err) + } + + *m = membership + return nil +} + +// Tenant id scope is not applied because memberships are managed at the organization level and don't require tenant isolation. +func (m *Membership) Update(ctx context.Context, conn pg.Conn) error { + query := ` + UPDATE authz_memberships + SET role = @role, updated_at = @updated_at + WHERE id = @id + ` + + args := pgx.StrictNamedArgs{ + "id": m.ID, + "role": m.Role, + "updated_at": m.UpdatedAt, + } + + result, err := conn.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("failed to update membership: %w", err) + } + + if result.RowsAffected() == 0 { + return ErrMembershipNotFound{UserID: m.UserID, OrgID: m.OrganizationID} + } + + return nil +} + +// Tenant id scope is not applied because memberships are managed at the organization level and don't require tenant isolation. +func (m *Membership) Delete(ctx context.Context, conn pg.Conn) error { + query := ` + DELETE FROM authz_memberships + WHERE id = @id + ` + + args := pgx.StrictNamedArgs{ + "id": m.ID, + } + + result, err := conn.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("failed to delete membership: %w", err) + } + + if result.RowsAffected() == 0 { + return ErrMembershipNotFound{UserID: m.UserID, OrgID: m.OrganizationID} + } + + return nil +} + +// Tenant id scope is not applied because we want to access all user's memberships across tenants for authentication purposes. +func (m *Memberships) LoadByUserID( + ctx context.Context, + conn pg.Conn, + userID gid.GID, +) error { + query := ` +SELECT + m.id, + m.user_id, + m.organization_id, + m.role, + u.fullname as full_name, + u.email_address, + m.created_at, + m.updated_at +FROM + authz_memberships m +JOIN users u ON m.user_id = u.id +WHERE + m.user_id = @user_id +ORDER BY + m.created_at DESC + ` + + args := pgx.StrictNamedArgs{"user_id": userID} + + rows, err := conn.Query(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot query memberships: %w", err) + } + + memberships, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Membership]) + if err != nil { + return fmt.Errorf("cannot collect memberships: %w", err) + } + + *m = memberships + return nil +} + +// Tenant id scope is not applied because we want to access memberships across all tenants for authentication purposes. +func (m *Memberships) LoadByOrganizationID( + ctx context.Context, + conn pg.Conn, + organizationID gid.GID, + cursor *page.Cursor[MembershipOrderField], +) error { + query := ` +SELECT + m.id, + m.user_id, + m.organization_id, + m.role, + u.fullname as full_name, + u.email_address, + m.created_at, + m.updated_at +FROM + authz_memberships m +JOIN users u ON m.user_id = u.id +WHERE + m.organization_id = @organization_id + AND %s +` + + query = fmt.Sprintf(query, cursor.SQLFragment()) + + args := pgx.StrictNamedArgs{"organization_id": organizationID} + maps.Copy(args, cursor.SQLArguments()) + + rows, err := conn.Query(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot query memberships: %w", err) + } + + memberships, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Membership]) + if err != nil { + return fmt.Errorf("cannot collect memberships: %w", err) + } + + *m = memberships + return nil +} + +func (m *Memberships) CountByOrganizationID( + ctx context.Context, + conn pg.Conn, + organizationID gid.GID, +) (int, error) { + query := ` + SELECT COUNT(*) + FROM authz_memberships + WHERE organization_id = @organization_id + ` + args := pgx.StrictNamedArgs{"organization_id": organizationID} + row := conn.QueryRow(ctx, query, args) + var count int + if err := row.Scan(&count); err != nil { + return 0, fmt.Errorf("cannot count memberships: %w", err) + } + return count, nil +} diff --git a/pkg/coredata/membership_order_field.go b/pkg/coredata/membership_order_field.go new file mode 100644 index 000000000..6c0b00e3b --- /dev/null +++ b/pkg/coredata/membership_order_field.go @@ -0,0 +1,53 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +type ( + MembershipOrderField string +) + +const ( + MembershipOrderFieldFullName MembershipOrderField = "FULL_NAME" + MembershipOrderFieldEmailAddress MembershipOrderField = "EMAIL_ADDRESS" + MembershipOrderFieldRole MembershipOrderField = "ROLE" + MembershipOrderFieldCreatedAt MembershipOrderField = "CREATED_AT" +) + +func (p MembershipOrderField) Column() string { + switch p { + case MembershipOrderFieldFullName: + return "u.fullname" + case MembershipOrderFieldEmailAddress: + return "u.email_address" + case MembershipOrderFieldRole: + return "m.role" + case MembershipOrderFieldCreatedAt: + return "m.created_at" + } + return string(p) +} + +func (p MembershipOrderField) String() string { + return string(p) +} + +func (p MembershipOrderField) MarshalText() ([]byte, error) { + return []byte(p.String()), nil +} + +func (p *MembershipOrderField) UnmarshalText(text []byte) error { + *p = MembershipOrderField(text) + return nil +} diff --git a/pkg/coredata/migrations/20251006T220024Z.sql b/pkg/coredata/migrations/20251006T220024Z.sql new file mode 100644 index 000000000..b520f1959 --- /dev/null +++ b/pkg/coredata/migrations/20251006T220024Z.sql @@ -0,0 +1,41 @@ +-- Create authorization tables for the new authz service +-- This migration creates the new authz tables while keeping the existing users_organizations table +-- for backward compatibility during the transition + +-- Create role enum +CREATE TYPE authz_role AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'VIEWER'); + +-- Create authz_memberships table with id as primary key +CREATE TABLE authz_memberships ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + organization_id TEXT NOT NULL, + role authz_role NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + UNIQUE (user_id, organization_id) +); + +-- Create authz_invitations table +CREATE TABLE authz_invitations ( + id TEXT PRIMARY KEY, + organization_id TEXT NOT NULL, + email TEXT NOT NULL, + full_name TEXT NOT NULL, + role authz_role NOT NULL, + expires_at TIMESTAMP NOT NULL, + accepted_at TIMESTAMP, + created_at TIMESTAMP NOT NULL +); + +-- Copy data from users_organizations to authz_memberships +INSERT INTO authz_memberships (id, user_id, organization_id, role, created_at, updated_at) +SELECT + generate_gid(decode_base64_unpadded(organizations.tenant_id), 38) as id, + users_organizations.user_id, + users_organizations.organization_id, + 'MEMBER'::authz_role as role, -- Default role for existing memberships + users_organizations.created_at, + users_organizations.created_at as updated_at +FROM users_organizations +JOIN organizations ON users_organizations.organization_id = organizations.id; diff --git a/pkg/coredata/organization.go b/pkg/coredata/organization.go index 6da6606f1..47f7d215a 100644 --- a/pkg/coredata/organization.go +++ b/pkg/coredata/organization.go @@ -107,7 +107,7 @@ LIMIT 1; } // Tenant id scope is not applied in this functions because we want to access all user's organizations. -func (o *Organizations) ListForUserID( +func (o *Organizations) LoadByUserID( ctx context.Context, conn pg.Conn, userID gid.GID, @@ -118,7 +118,7 @@ WITH user_org AS ( SELECT organization_id FROM - users_organizations + authz_memberships WHERE user_id = @user_id ) @@ -163,6 +163,59 @@ WHERE return nil } +// Tenant id scope is not applied in this function because we want to access all user's organizations. +func (o *Organizations) LoadAllByUserID( + ctx context.Context, + conn pg.Conn, + userID gid.GID, +) error { + q := ` +WITH user_org AS ( + SELECT + organization_id + FROM + authz_memberships + WHERE + user_id = @user_id +) +SELECT + tenant_id, + id, + name, + description, + website_url, + email, + headquarter_address, + custom_domain_id, + logo_file_id, + horizontal_logo_file_id, + created_at, + updated_at +FROM + organizations +INNER JOIN + user_org ON organizations.id = user_org.organization_id +ORDER BY + created_at DESC +` + + args := pgx.StrictNamedArgs{"user_id": userID} + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query organizations: %w", err) + } + + organizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Organization]) + if err != nil { + return fmt.Errorf("cannot collect organizations: %w", err) + } + + *o = organizations + + return nil +} + func (o *Organization) Insert( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/session.go b/pkg/coredata/session.go index 06aaf5612..c3d052f5c 100644 --- a/pkg/coredata/session.go +++ b/pkg/coredata/session.go @@ -47,6 +47,7 @@ func (s Session) CursorKey(orderBy SessionOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +// Tenant id scope is not applied because we want to access sessions across all tenants for authentication purposes. func (s *Session) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/trust_center.go b/pkg/coredata/trust_center.go index 66c9f8228..92c88fe3f 100644 --- a/pkg/coredata/trust_center.go +++ b/pkg/coredata/trust_center.go @@ -138,6 +138,7 @@ LIMIT 1; return nil } +// Tenant id scope is not applied because we want to access trust centers by slug across all tenants for public access. func (tc *TrustCenter) LoadBySlug( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/user.go b/pkg/coredata/user.go index 92b6b1831..0229f12ca 100644 --- a/pkg/coredata/user.go +++ b/pkg/coredata/user.go @@ -87,7 +87,7 @@ FROM users WHERE id IN ( - SELECT user_id FROM users_organizations WHERE organization_id = @organization_id + SELECT user_id FROM authz_memberships WHERE organization_id = @organization_id ) AND %s ` @@ -112,6 +112,36 @@ WHERE return nil } +func (u *Users) CountByOrganizationID( + ctx context.Context, + conn pg.Conn, + organizationID gid.GID, +) (int, error) { + q := ` +SELECT + COUNT(*) +FROM + users +WHERE + id IN ( + SELECT user_id FROM authz_memberships WHERE organization_id = @organization_id + ) +` + + args := pgx.StrictNamedArgs{"organization_id": organizationID} + + row := conn.QueryRow(ctx, q, args) + + var count int + err := row.Scan(&count) + if err != nil { + return 0, fmt.Errorf("cannot count users: %w", err) + } + + return count, nil +} + +// Tenant id scope is not applied because we want to access users across all tenants for authentication purposes. func (u *User) LoadByEmail( ctx context.Context, conn pg.Conn, @@ -154,6 +184,7 @@ LIMIT 1; return nil } +// Tenant id scope is not applied because we want to access users across all tenants for authentication purposes. func (u *User) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/user_organization.go b/pkg/coredata/user_organization.go index 5322b9ef5..0009d7be0 100644 --- a/pkg/coredata/user_organization.go +++ b/pkg/coredata/user_organization.go @@ -46,6 +46,7 @@ VALUES (@user_id, @organization_id, @created_at) return err } +// Tenant id scope is not applied because user organizations are managed at the organization level and don't require tenant isolation. func (uo UserOrganization) Delete(ctx context.Context, conn pg.Conn) error { q := ` DELETE FROM users_organizations WHERE user_id = @user_id AND organization_id = @organization_id diff --git a/pkg/probo/service.go b/pkg/probo/service.go index 4d0c32b2e..d67a93115 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -21,6 +21,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/getprobo/probo/pkg/agents" + "github.com/getprobo/probo/pkg/auth" + "github.com/getprobo/probo/pkg/authz" "github.com/getprobo/probo/pkg/certmanager" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/crypto/cipher" @@ -28,7 +30,6 @@ import ( "github.com/getprobo/probo/pkg/filevalidation" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/html2pdf" - "github.com/getprobo/probo/pkg/usrmgr" "go.gearno.de/kit/log" "go.gearno.de/kit/pg" "go.gearno.de/x/ref" @@ -56,9 +57,10 @@ type ( trustConfig TrustConfig agentConfig agents.Config html2pdfConverter *html2pdf.Converter - usrmgr *usrmgr.Service acmeService *certmanager.ACMEService fileManager *filemanager.Service + auth *auth.Service + authz *authz.Service logger *log.Logger } @@ -117,9 +119,10 @@ func NewService( trustConfig TrustConfig, agentConfig agents.Config, html2pdfConverter *html2pdf.Converter, - usrmgrService *usrmgr.Service, acmeService *certmanager.ACMEService, fileManagerService *filemanager.Service, + authService *auth.Service, + authzService *authz.Service, logger *log.Logger, ) (*Service, error) { if bucket == "" { @@ -136,9 +139,10 @@ func NewService( trustConfig: trustConfig, agentConfig: agentConfig, html2pdfConverter: html2pdfConverter, - usrmgr: usrmgrService, acmeService: acmeService, fileManager: fileManagerService, + auth: authService, + authz: authzService, logger: logger, } @@ -202,7 +206,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { tenantService.Audits = &AuditService{svc: tenantService} tenantService.Reports = &ReportService{svc: tenantService} tenantService.TrustCenters = &TrustCenterService{svc: tenantService} - tenantService.TrustCenterAccesses = &TrustCenterAccessService{svc: tenantService, usrmgr: s.usrmgr} + tenantService.TrustCenterAccesses = &TrustCenterAccessService{svc: tenantService} tenantService.TrustCenterReferences = &TrustCenterReferenceService{svc: tenantService} tenantService.Nonconformities = &NonconformityService{svc: tenantService} tenantService.Obligations = &ObligationService{svc: tenantService} diff --git a/pkg/probo/trust_center_access_service.go b/pkg/probo/trust_center_access_service.go index a1e705577..3668c4467 100644 --- a/pkg/probo/trust_center_access_service.go +++ b/pkg/probo/trust_center_access_service.go @@ -25,14 +25,12 @@ import ( "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/page" "github.com/getprobo/probo/pkg/statelesstoken" - "github.com/getprobo/probo/pkg/usrmgr" "go.gearno.de/kit/pg" ) type ( TrustCenterAccessService struct { - svc *TenantService - usrmgr *usrmgr.Service + svc *TenantService } CreateTrustCenterAccessRequest struct { diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index b8df209e1..8add8a665 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -28,6 +28,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/getprobo/probo/pkg/agents" + "github.com/getprobo/probo/pkg/auth" + "github.com/getprobo/probo/pkg/authz" "github.com/getprobo/probo/pkg/awsconfig" "github.com/getprobo/probo/pkg/certmanager" "github.com/getprobo/probo/pkg/connector" @@ -44,7 +46,6 @@ import ( "github.com/getprobo/probo/pkg/server" "github.com/getprobo/probo/pkg/server/api" "github.com/getprobo/probo/pkg/trust" - "github.com/getprobo/probo/pkg/usrmgr" "github.com/prometheus/client_golang/prometheus" "go.gearno.de/kit/httpclient" "go.gearno.de/kit/httpserver" @@ -257,7 +258,7 @@ func (impl *Implm) Run( agent := agents.NewAgent(l.Named("agent"), agentConfig) - usrmgrService, err := usrmgr.NewService( + authService, err := auth.NewService( ctx, pgClient, hp, @@ -267,7 +268,18 @@ func (impl *Implm) Run( time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second, ) if err != nil { - return fmt.Errorf("cannot create usrmgr service: %w", err) + return fmt.Errorf("cannot create auth service: %w", err) + } + + authzService, err := authz.NewService( + ctx, + pgClient, + impl.cfg.Hostname, + impl.cfg.Auth.Cookie.Secret, + time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second, + ) + if err != nil { + return fmt.Errorf("cannot create authz service: %w", err) } fileManagerService := filemanager.NewService(s3Client) @@ -312,9 +324,10 @@ func (impl *Implm) Run( trustConfig, agentConfig, html2pdfConverter, - usrmgrService, acmeService, fileManagerService, + authService, + authzService, l.Named("probo"), ) if err != nil { @@ -327,7 +340,7 @@ func (impl *Implm) Run( impl.cfg.AWS.Bucket, impl.cfg.EncryptionKey, impl.cfg.TrustAuth.TokenSecret, - usrmgrService, + authService, html2pdfConverter, fileManagerService, ) @@ -337,14 +350,15 @@ func (impl *Implm) Run( AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins, ExtraHeaderFields: impl.cfg.Api.ExtraHeaderFields, Probo: proboService, - Usrmgr: usrmgrService, + Auth: authService, + Authz: authzService, Trust: trustService, ConnectorRegistry: defaultConnectorRegistry, Agent: agent, SafeRedirect: &saferedirect.SafeRedirect{AllowedHost: impl.cfg.Hostname}, CustomDomainCname: impl.cfg.CustomDomains.CnameTarget, Logger: l.Named("http.server"), - Auth: api.ConsoleAuthConfig{ + ConsoleAuth: api.ConsoleAuthConfig{ CookieName: impl.cfg.Auth.Cookie.Name, CookieDomain: impl.cfg.Auth.Cookie.Domain, SessionDuration: time.Duration(impl.cfg.Auth.Cookie.Duration) * time.Hour, diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index 24e8e2326..633a7dd09 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -20,13 +20,14 @@ import ( "time" + "github.com/getprobo/probo/pkg/auth" + "github.com/getprobo/probo/pkg/authz" "github.com/getprobo/probo/pkg/connector" "github.com/getprobo/probo/pkg/probo" "github.com/getprobo/probo/pkg/saferedirect" console_v1 "github.com/getprobo/probo/pkg/server/api/console/v1" trust_v1 "github.com/getprobo/probo/pkg/server/api/trust/v1" "github.com/getprobo/probo/pkg/trust" - "github.com/getprobo/probo/pkg/usrmgr" "github.com/go-chi/chi/v5" "github.com/go-chi/cors" "go.gearno.de/kit/httpserver" @@ -55,9 +56,10 @@ type ( Config struct { AllowedOrigins []string Probo *probo.Service - Usrmgr *usrmgr.Service + Auth *auth.Service + Authz *authz.Service Trust *trust.Service - Auth ConsoleAuthConfig + ConsoleAuth ConsoleAuthConfig TrustAuth TrustAuthConfig ConnectorRegistry *connector.ConnectorRegistry SafeRedirect *saferedirect.SafeRedirect @@ -72,8 +74,9 @@ type ( ) var ( - ErrMissingProboService = errors.New("server configuration requires a valid probo.Service instance") - ErrMissingUsrmgrService = errors.New("server configuration requires a valid usrmgr.Service instance") + ErrMissingProboService = errors.New("server configuration requires a valid probo.Service instance") + ErrMissingAuthService = errors.New("server configuration requires a valid auth.Service instance") + ErrMissingAuthzService = errors.New("server configuration requires a valid authz.Service instance") ) func methodNotAllowed(w http.ResponseWriter, r *http.Request) { @@ -105,20 +108,25 @@ func NewServer(cfg Config) (*Server, error) { return nil, ErrMissingProboService } - if cfg.Usrmgr == nil { - return nil, ErrMissingUsrmgrService + if cfg.Auth == nil { + return nil, ErrMissingAuthService + } + + if cfg.Authz == nil { + return nil, ErrMissingAuthzService } // Create trust API handler once trustAPIHandler := trust_v1.NewMux( cfg.Logger.Named("trust.v1"), - cfg.Usrmgr, + cfg.Auth, + cfg.Authz, cfg.Trust, console_v1.AuthConfig{ - CookieName: cfg.Auth.CookieName, - CookieDomain: cfg.Auth.CookieDomain, - SessionDuration: cfg.Auth.SessionDuration, - CookieSecret: cfg.Auth.CookieSecret, + CookieName: cfg.ConsoleAuth.CookieName, + CookieDomain: cfg.ConsoleAuth.CookieDomain, + SessionDuration: cfg.ConsoleAuth.SessionDuration, + CookieSecret: cfg.ConsoleAuth.CookieSecret, }, trust_v1.TrustAuthConfig{ CookieName: cfg.TrustAuth.CookieName, @@ -175,12 +183,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { console_v1.NewMux( s.cfg.Logger.Named("console.v1"), s.cfg.Probo, - s.cfg.Usrmgr, + s.cfg.Auth, + s.cfg.Authz, console_v1.AuthConfig{ - CookieName: s.cfg.Auth.CookieName, - CookieDomain: s.cfg.Auth.CookieDomain, - SessionDuration: s.cfg.Auth.SessionDuration, - CookieSecret: s.cfg.Auth.CookieSecret, + CookieName: s.cfg.ConsoleAuth.CookieName, + CookieDomain: s.cfg.ConsoleAuth.CookieDomain, + SessionDuration: s.cfg.ConsoleAuth.SessionDuration, + CookieSecret: s.cfg.ConsoleAuth.CookieSecret, }, s.cfg.ConnectorRegistry, s.cfg.SafeRedirect, diff --git a/pkg/server/api/console/v1/forget_password_handler.go b/pkg/server/api/console/v1/forget_password_handler.go index 40642fd59..c227b7511 100644 --- a/pkg/server/api/console/v1/forget_password_handler.go +++ b/pkg/server/api/console/v1/forget_password_handler.go @@ -19,7 +19,7 @@ import ( "fmt" "net/http" - "github.com/getprobo/probo/pkg/usrmgr" + "github.com/getprobo/probo/pkg/auth" "go.gearno.de/kit/httpserver" ) @@ -33,7 +33,7 @@ type ( } ) -func ForgetPasswordHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { +func ForgetPasswordHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req ForgetPasswordRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -41,7 +41,7 @@ func ForgetPasswordHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.H return } - err := usrmgrSvc.ForgetPassword(r.Context(), req.Email) + err := authSvc.ForgetPassword(r.Context(), req.Email) if err != nil { // For security reasons, we don't expose whether an email exists or not httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("cannot process request: %w", err)) diff --git a/pkg/server/api/console/v1/invitation_confirmation_handler.go b/pkg/server/api/console/v1/invitation_confirmation_handler.go index e9ced33f6..1654abf43 100644 --- a/pkg/server/api/console/v1/invitation_confirmation_handler.go +++ b/pkg/server/api/console/v1/invitation_confirmation_handler.go @@ -16,11 +16,14 @@ package console_v1 import ( "encoding/json" + "errors" "fmt" "net/http" - "github.com/getprobo/probo/pkg/probo" - "github.com/getprobo/probo/pkg/usrmgr" + "github.com/getprobo/probo/pkg/auth" + "github.com/getprobo/probo/pkg/authz" + "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/statelesstoken" "go.gearno.de/kit/httpserver" ) @@ -34,7 +37,7 @@ type ( } ) -func InvitationConfirmationHandler(usrmgrSvc *usrmgr.Service, proboSvc *probo.Service, authCfg AuthConfig) http.HandlerFunc { +func InvitationConfirmationHandler(authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req InvitationConfirmationRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -42,7 +45,32 @@ func InvitationConfirmationHandler(usrmgrSvc *usrmgr.Service, proboSvc *probo.Se return } - _, err := usrmgrSvc.ConfirmInvitation(r.Context(), req.Token, req.Password) + payload, err := statelesstoken.ValidateToken[coredata.InvitationData]( + authCfg.CookieSecret, + authz.TokenTypeOrganizationInvitation, + req.Token, + ) + if err != nil { + httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("invalid invitation token: %w", err)) + return + } + + user, _, err := authSvc.SignUp(r.Context(), payload.Data.Email, req.Password, payload.Data.FullName) + if err != nil { + var errUserAlreadyExists *auth.ErrUserAlreadyExists + if errors.As(err, &errUserAlreadyExists) { + user, err = authSvc.GetUserByEmail(r.Context(), payload.Data.Email) + if err != nil { + httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("failed to load existing user: %w", err)) + return + } + } else { + httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("failed to create user: %w", err)) + return + } + } + + err = authzSvc.AcceptInvitation(r.Context(), req.Token, user.ID) if err != nil { httpserver.RenderError(w, http.StatusInternalServerError, err) return diff --git a/pkg/server/api/console/v1/reset_password_handler.go b/pkg/server/api/console/v1/reset_password_handler.go index 634eb4aca..4a85e7c42 100644 --- a/pkg/server/api/console/v1/reset_password_handler.go +++ b/pkg/server/api/console/v1/reset_password_handler.go @@ -21,7 +21,7 @@ import ( "errors" - "github.com/getprobo/probo/pkg/usrmgr" + "github.com/getprobo/probo/pkg/auth" "go.gearno.de/kit/httpserver" ) @@ -36,7 +36,7 @@ type ( } ) -func ResetPasswordHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { +func ResetPasswordHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req ResetPasswordRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -44,10 +44,10 @@ func ResetPasswordHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.Ha return } - err := usrmgrSvc.ResetPassword(r.Context(), req.Token, req.Password) + err := authSvc.ResetPassword(r.Context(), req.Token, req.Password) if err != nil { - var invalidPasswordErr *usrmgr.ErrInvalidPassword - var invalidTokenErr *usrmgr.ErrInvalidTokenType + var invalidPasswordErr *auth.ErrInvalidPassword + var invalidTokenErr *auth.ErrInvalidTokenType if errors.As(err, &invalidPasswordErr) { httpserver.RenderError(w, http.StatusBadRequest, err) diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index 7caf09728..4337d4172 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -29,6 +29,8 @@ import ( "github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/playground" + "github.com/getprobo/probo/pkg/auth" + "github.com/getprobo/probo/pkg/authz" "github.com/getprobo/probo/pkg/connector" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" @@ -38,7 +40,6 @@ import ( gqlutils "github.com/getprobo/probo/pkg/server/graphql" "github.com/getprobo/probo/pkg/server/session" "github.com/getprobo/probo/pkg/statelesstoken" - "github.com/getprobo/probo/pkg/usrmgr" "github.com/go-chi/chi/v5" "github.com/vektah/gqlparser/v2/gqlerror" "go.gearno.de/kit/log" @@ -54,7 +55,8 @@ type ( Resolver struct { proboSvc *probo.Service - usrmgrSvc *usrmgr.Service + authSvc *auth.Service + authzSvc *authz.Service authCfg AuthConfig customDomainCname string } @@ -81,7 +83,8 @@ func UserFromContext(ctx context.Context) *coredata.User { func NewMux( logger *log.Logger, proboSvc *probo.Service, - usrmgrSvc *usrmgr.Service, + authSvc *auth.Service, + authzSvc *authz.Service, authCfg AuthConfig, connectorRegistry *connector.ConnectorRegistry, safeRedirect *saferedirect.SafeRedirect, @@ -151,14 +154,14 @@ func NewMux( }, ) - r.Post("/auth/register", SignUpHandler(usrmgrSvc, authCfg)) - r.Post("/auth/login", SignInHandler(usrmgrSvc, authCfg)) - r.Delete("/auth/logout", SignOutHandler(usrmgrSvc, authCfg)) - r.Post("/auth/invitation", InvitationConfirmationHandler(usrmgrSvc, proboSvc, authCfg)) - r.Post("/auth/forget-password", ForgetPasswordHandler(usrmgrSvc, authCfg)) - r.Post("/auth/reset-password", ResetPasswordHandler(usrmgrSvc, authCfg)) + r.Post("/auth/register", SignUpHandler(authSvc, authCfg)) + r.Post("/auth/login", SignInHandler(authSvc, authCfg)) + r.Delete("/auth/logout", SignOutHandler(authSvc, authCfg)) + r.Post("/auth/invitation", InvitationConfirmationHandler(authSvc, authzSvc, authCfg)) + r.Post("/auth/forget-password", ForgetPasswordHandler(authSvc, authCfg)) + r.Post("/auth/reset-password", ResetPasswordHandler(authSvc, authCfg)) - r.Get("/connectors/initiate", WithSession(usrmgrSvc, authCfg, func(w http.ResponseWriter, r *http.Request) { + r.Get("/connectors/initiate", WithSession(authSvc, authzSvc, authCfg, func(w http.ResponseWriter, r *http.Request) { connectorID := r.URL.Query().Get("connector_id") organizationID, err := gid.ParseGID(r.URL.Query().Get("organization_id")) if err != nil { @@ -175,7 +178,7 @@ func NewMux( http.Redirect(w, r, redirectURL, http.StatusSeeOther) })) - r.Get("/connectors/complete", WithSession(usrmgrSvc, authCfg, func(w http.ResponseWriter, r *http.Request) { + r.Get("/connectors/complete", WithSession(authSvc, authzSvc, authCfg, func(w http.ResponseWriter, r *http.Request) { connectorID := r.URL.Query().Get("connector_id") organizationID, err := gid.ParseGID(r.URL.Query().Get("organization_id")) if err != nil { @@ -206,19 +209,20 @@ func NewMux( })) r.Get("/", playground.Handler("GraphQL", "/api/console/v1/query")) - r.Post("/query", graphqlHandler(logger, proboSvc, usrmgrSvc, authCfg, customDomainCname)) + r.Post("/query", graphqlHandler(logger, proboSvc, authSvc, authzSvc, authCfg, customDomainCname)) return r } -func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg AuthConfig, customDomainCname string) http.HandlerFunc { +func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthConfig, customDomainCname string) http.HandlerFunc { var mb int64 = 1 << 20 es := schema.NewExecutableSchema( schema.Config{ Resolvers: &Resolver{ proboSvc: proboSvc, - usrmgrSvc: usrmgrSvc, + authSvc: authSvc, + authzSvc: authzSvc, authCfg: authCfg, customDomainCname: customDomainCname, }, @@ -259,10 +263,10 @@ func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, usrmgrSvc *usrm }, ) - return WithSession(usrmgrSvc, authCfg, srv.ServeHTTP) + return WithSession(authSvc, authzSvc, authCfg, srv.ServeHTTP) } -func WithSession(usrmgrSvc *usrmgr.Service, authCfg AuthConfig, next http.HandlerFunc) http.HandlerFunc { +func WithSession(authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthConfig, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -289,7 +293,7 @@ func WithSession(usrmgrSvc *usrmgr.Service, authCfg AuthConfig, next http.Handle }, } - authResult := session.TryAuth(ctx, w, r, usrmgrSvc, sessionAuthCfg, errorHandler) + authResult := session.TryAuth(ctx, w, r, authSvc, authzSvc, sessionAuthCfg, errorHandler) if authResult == nil { next(w, r) return @@ -302,7 +306,7 @@ func WithSession(usrmgrSvc *usrmgr.Service, authCfg AuthConfig, next http.Handle next(w, r.WithContext(ctx)) // Update session after the handler completes - if err := usrmgrSvc.UpdateSession(ctx, authResult.Session); err != nil { + if _, err := authSvc.UpdateSession(ctx, authResult.Session.ID); err != nil { panic(fmt.Errorf("failed to update session: %w", err)) } } diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index f06eb6db2..d4afddacb 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1209,6 +1209,54 @@ enum SnapshotOrderField ) } +enum MembershipOrderField + @goModel(model: "github.com/getprobo/probo/pkg/coredata.MembershipOrderField") { + FULL_NAME + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.MembershipOrderFieldFullName" + ) + EMAIL_ADDRESS + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.MembershipOrderFieldEmailAddress" + ) + ROLE + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.MembershipOrderFieldRole" + ) + CREATED_AT + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.MembershipOrderFieldCreatedAt" + ) +} + +enum InvitationOrderField + @goModel(model: "github.com/getprobo/probo/pkg/coredata.InvitationOrderField") { + FULL_NAME + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldFullName" + ) + EMAIL + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldEmail" + ) + ROLE + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldRole" + ) + CREATED_AT + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldCreatedAt" + ) + EXPIRES_AT + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldExpiresAt" + ) + ACCEPTED_AT + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldAcceptedAt" + ) +} + # Input Types input UserOrder @goModel( @@ -1404,6 +1452,19 @@ input SnapshotOrder field: SnapshotOrderField! } +input MembershipOrder + @goModel( + model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.MembershipOrderBy" + ) { + direction: OrderDirection! + field: MembershipOrderField! +} + +input InvitationOrder { + direction: OrderDirection! + field: InvitationOrderField! +} + input DocumentVersionFilter { status: DocumentStatus } @@ -1497,13 +1558,21 @@ type Organization implements Node { email: String headquarterAddress: String - users( + memberships( first: Int after: CursorKey last: Int before: CursorKey - orderBy: UserOrder - ): UserConnection! @goField(forceResolver: true) + orderBy: MembershipOrder + ): MembershipConnection! @goField(forceResolver: true) + + invitations( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: InvitationOrder + ): InvitationConnection! @goField(forceResolver: true) connectors( first: Int @@ -1671,6 +1740,27 @@ type User implements Node { people(organizationId: ID!): People @goField(forceResolver: true) } +type Membership implements Node { + id: ID! + userID: ID! + organizationID: ID! + role: String! + fullName: String! + emailAddress: String! + createdAt: Datetime! + updatedAt: Datetime! +} + +type Invitation implements Node { + id: ID! + email: String! + fullName: String! + role: String! + expiresAt: Datetime! + acceptedAt: Datetime + createdAt: Datetime! +} + type Connector implements Node { id: ID! name: String! @@ -2305,10 +2395,22 @@ type TrustCenterReferenceEdge { } type UserConnection { + totalCount: Int! @goField(forceResolver: true) edges: [UserEdge!]! pageInfo: PageInfo! } +type MembershipConnection { + totalCount: Int! @goField(forceResolver: true) + edges: [MembershipEdge!]! + pageInfo: PageInfo! +} + +type MembershipEdge { + cursor: CursorKey! + node: Membership! +} + type UserEdge { cursor: CursorKey! node: User! @@ -2607,6 +2709,17 @@ type File { updatedAt: Datetime! } +type InvitationConnection { + totalCount: Int! @goField(forceResolver: true) + edges: [InvitationEdge!]! + pageInfo: PageInfo! +} + +type InvitationEdge { + cursor: CursorKey! + node: Invitation! +} + # Root Types type Query { node(id: ID!): Node! @@ -2667,7 +2780,8 @@ type Mutation { # User mutations confirmEmail(input: ConfirmEmailInput!): ConfirmEmailPayload! inviteUser(input: InviteUserInput!): InviteUserPayload! - removeUser(input: RemoveUserInput!): RemoveUserPayload! + deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload! + removeMember(input: RemoveMemberInput!): RemoveMemberPayload! # People mutations createPeople(input: CreatePeopleInput!): CreatePeoplePayload! @@ -3426,9 +3540,13 @@ input InviteUserInput { createPeople: Boolean! } -input RemoveUserInput { +input DeleteInvitationInput { + invitationId: ID! +} + +input RemoveMemberInput { organizationId: ID! - userId: ID! + memberId: ID! } input CreateControlInput { @@ -3945,10 +4063,14 @@ type ConfirmEmailPayload { } type InviteUserPayload { - success: Boolean! + invitationEdge: InvitationEdge! } -type RemoveUserPayload { +type DeleteInvitationPayload { + deletedInvitationId: ID! +} + +type RemoveMemberPayload { success: Boolean! } diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 083010cd7..d55c227f9 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -64,8 +64,10 @@ type ResolverRoot interface { File() FileResolver Framework() FrameworkResolver FrameworkConnection() FrameworkConnectionResolver + InvitationConnection() InvitationConnectionResolver Measure() MeasureResolver MeasureConnection() MeasureConnectionResolver + MembershipConnection() MembershipConnectionResolver Mutation() MutationResolver Nonconformity() NonconformityResolver NonconformityConnection() NonconformityConnectionResolver @@ -90,6 +92,7 @@ type ResolverRoot interface { TrustCenterReference() TrustCenterReferenceResolver TrustCenterReferenceConnection() TrustCenterReferenceConnectionResolver User() UserResolver + UserConnection() UserConnectionResolver Vendor() VendorResolver VendorBusinessAssociateAgreement() VendorBusinessAssociateAgreementResolver VendorComplianceReport() VendorComplianceReportResolver @@ -504,6 +507,10 @@ type ComplexityRoot struct { DeletedFrameworkID func(childComplexity int) int } + DeleteInvitationPayload struct { + DeletedInvitationID func(childComplexity int) int + } + DeleteMeasurePayload struct { DeletedMeasureID func(childComplexity int) int } @@ -751,8 +758,29 @@ type ComplexityRoot struct { MeasureEdges func(childComplexity int) int } + Invitation struct { + AcceptedAt func(childComplexity int) int + CreatedAt func(childComplexity int) int + Email func(childComplexity int) int + ExpiresAt func(childComplexity int) int + FullName func(childComplexity int) int + ID func(childComplexity int) int + Role func(childComplexity int) int + } + + InvitationConnection struct { + Edges func(childComplexity int) int + PageInfo func(childComplexity int) int + TotalCount func(childComplexity int) int + } + + InvitationEdge struct { + Cursor func(childComplexity int) int + Node func(childComplexity int) int + } + InviteUserPayload struct { - Success func(childComplexity int) int + InvitationEdge func(childComplexity int) int } Measure struct { @@ -780,6 +808,28 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + Membership struct { + CreatedAt func(childComplexity int) int + EmailAddress func(childComplexity int) int + FullName func(childComplexity int) int + ID func(childComplexity int) int + OrganizationID func(childComplexity int) int + Role func(childComplexity int) int + UpdatedAt func(childComplexity int) int + UserID func(childComplexity int) int + } + + MembershipConnection struct { + Edges func(childComplexity int) int + PageInfo func(childComplexity int) int + TotalCount func(childComplexity int) int + } + + MembershipEdge struct { + Cursor func(childComplexity int) int + Node func(childComplexity int) int + } + Mutation struct { AssessVendor func(childComplexity int, input types.AssessVendorInput) int AssignTask func(childComplexity int, input types.AssignTaskInput) int @@ -835,6 +885,7 @@ type ComplexityRoot struct { DeleteDraftDocumentVersion func(childComplexity int, input types.DeleteDraftDocumentVersionInput) int DeleteEvidence func(childComplexity int, input types.DeleteEvidenceInput) int DeleteFramework func(childComplexity int, input types.DeleteFrameworkInput) int + DeleteInvitation func(childComplexity int, input types.DeleteInvitationInput) int DeleteMeasure func(childComplexity int, input types.DeleteMeasureInput) int DeleteNonconformity func(childComplexity int, input types.DeleteNonconformityInput) int DeleteObligation func(childComplexity int, input types.DeleteObligationInput) int @@ -865,7 +916,7 @@ type ComplexityRoot struct { ImportMeasure func(childComplexity int, input types.ImportMeasureInput) int InviteUser func(childComplexity int, input types.InviteUserInput) int PublishDocumentVersion func(childComplexity int, input types.PublishDocumentVersionInput) int - RemoveUser func(childComplexity int, input types.RemoveUserInput) int + RemoveMember func(childComplexity int, input types.RemoveMemberInput) int RequestSignature func(childComplexity int, input types.RequestSignatureInput) int SendSigningNotifications func(childComplexity int, input types.SendSigningNotificationsInput) int UnassignTask func(childComplexity int, input types.UnassignTaskInput) int @@ -975,8 +1026,10 @@ type ComplexityRoot struct { HeadquarterAddress func(childComplexity int) int HorizontalLogoURL func(childComplexity int) int ID func(childComplexity int) int + Invitations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder) int LogoURL func(childComplexity int) int Measures func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MeasureOrderBy, filter *types.MeasureFilter) int + Memberships func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) int Name func(childComplexity int) int Nonconformities func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.NonconformityOrderBy, filter *types.NonconformityFilter) int Obligations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ObligationOrderBy, filter *types.ObligationFilter) int @@ -987,7 +1040,6 @@ type ComplexityRoot struct { Tasks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) int TrustCenter func(childComplexity int) int UpdatedAt func(childComplexity int) int - Users func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.UserOrderBy) int Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy, filter *types.VendorFilter) int WebsiteURL func(childComplexity int) int } @@ -1078,7 +1130,7 @@ type ComplexityRoot struct { Viewer func(childComplexity int) int } - RemoveUserPayload struct { + RemoveMemberPayload struct { Success func(childComplexity int) int } @@ -1412,8 +1464,9 @@ type ComplexityRoot struct { } UserConnection struct { - Edges func(childComplexity int) int - PageInfo func(childComplexity int) int + Edges func(childComplexity int) int + PageInfo func(childComplexity int) int + TotalCount func(childComplexity int) int } UserEdge struct { @@ -1666,6 +1719,9 @@ type FrameworkResolver interface { type FrameworkConnectionResolver interface { TotalCount(ctx context.Context, obj *types.FrameworkConnection) (int, error) } +type InvitationConnectionResolver interface { + TotalCount(ctx context.Context, obj *types.InvitationConnection) (int, error) +} type MeasureResolver interface { Evidences(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.EvidenceOrderBy) (*types.EvidenceConnection, error) Tasks(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) (*types.TaskConnection, error) @@ -1675,6 +1731,9 @@ type MeasureResolver interface { type MeasureConnectionResolver interface { TotalCount(ctx context.Context, obj *types.MeasureConnection) (int, error) } +type MembershipConnectionResolver interface { + TotalCount(ctx context.Context, obj *types.MembershipConnection) (int, error) +} type MutationResolver interface { CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) @@ -1691,7 +1750,8 @@ type MutationResolver interface { DeleteTrustCenterReference(ctx context.Context, input types.DeleteTrustCenterReferenceInput) (*types.DeleteTrustCenterReferencePayload, error) ConfirmEmail(ctx context.Context, input types.ConfirmEmailInput) (*types.ConfirmEmailPayload, error) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) - RemoveUser(ctx context.Context, input types.RemoveUserInput) (*types.RemoveUserPayload, error) + DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error) + RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error) CreatePeople(ctx context.Context, input types.CreatePeopleInput) (*types.CreatePeoplePayload, error) UpdatePeople(ctx context.Context, input types.UpdatePeopleInput) (*types.UpdatePeoplePayload, error) DeletePeople(ctx context.Context, input types.DeletePeopleInput) (*types.DeletePeoplePayload, error) @@ -1817,7 +1877,8 @@ type OrganizationResolver interface { LogoURL(ctx context.Context, obj *types.Organization) (*string, error) HorizontalLogoURL(ctx context.Context, obj *types.Organization) (*string, error) - Users(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.UserOrderBy) (*types.UserConnection, error) + Memberships(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) + Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder) (*types.InvitationConnection, error) Connectors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ConnectorOrder) (*types.ConnectorConnection, error) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.FrameworkOrderBy) (*types.FrameworkConnection, error) Controls(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) @@ -1911,6 +1972,9 @@ type TrustCenterReferenceConnectionResolver interface { type UserResolver interface { People(ctx context.Context, obj *types.User, organizationID gid.GID) (*types.People, error) } +type UserConnectionResolver interface { + TotalCount(ctx context.Context, obj *types.UserConnection) (int, error) +} type VendorResolver interface { Organization(ctx context.Context, obj *types.Vendor) (*types.Organization, error) ComplianceReports(ctx context.Context, obj *types.Vendor, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorComplianceReportOrderBy) (*types.VendorComplianceReportConnection, error) @@ -3225,6 +3289,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.DeleteFrameworkPayload.DeletedFrameworkID(childComplexity), true + case "DeleteInvitationPayload.deletedInvitationId": + if e.complexity.DeleteInvitationPayload.DeletedInvitationID == nil { + break + } + + return e.complexity.DeleteInvitationPayload.DeletedInvitationID(childComplexity), true + case "DeleteMeasurePayload.deletedMeasureId": if e.complexity.DeleteMeasurePayload.DeletedMeasureID == nil { break @@ -4029,12 +4100,96 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.ImportMeasurePayload.MeasureEdges(childComplexity), true - case "InviteUserPayload.success": - if e.complexity.InviteUserPayload.Success == nil { + case "Invitation.acceptedAt": + if e.complexity.Invitation.AcceptedAt == nil { break } - return e.complexity.InviteUserPayload.Success(childComplexity), true + return e.complexity.Invitation.AcceptedAt(childComplexity), true + + case "Invitation.createdAt": + if e.complexity.Invitation.CreatedAt == nil { + break + } + + return e.complexity.Invitation.CreatedAt(childComplexity), true + + case "Invitation.email": + if e.complexity.Invitation.Email == nil { + break + } + + return e.complexity.Invitation.Email(childComplexity), true + + case "Invitation.expiresAt": + if e.complexity.Invitation.ExpiresAt == nil { + break + } + + return e.complexity.Invitation.ExpiresAt(childComplexity), true + + case "Invitation.fullName": + if e.complexity.Invitation.FullName == nil { + break + } + + return e.complexity.Invitation.FullName(childComplexity), true + + case "Invitation.id": + if e.complexity.Invitation.ID == nil { + break + } + + return e.complexity.Invitation.ID(childComplexity), true + + case "Invitation.role": + if e.complexity.Invitation.Role == nil { + break + } + + return e.complexity.Invitation.Role(childComplexity), true + + case "InvitationConnection.edges": + if e.complexity.InvitationConnection.Edges == nil { + break + } + + return e.complexity.InvitationConnection.Edges(childComplexity), true + + case "InvitationConnection.pageInfo": + if e.complexity.InvitationConnection.PageInfo == nil { + break + } + + return e.complexity.InvitationConnection.PageInfo(childComplexity), true + + case "InvitationConnection.totalCount": + if e.complexity.InvitationConnection.TotalCount == nil { + break + } + + return e.complexity.InvitationConnection.TotalCount(childComplexity), true + + case "InvitationEdge.cursor": + if e.complexity.InvitationEdge.Cursor == nil { + break + } + + return e.complexity.InvitationEdge.Cursor(childComplexity), true + + case "InvitationEdge.node": + if e.complexity.InvitationEdge.Node == nil { + break + } + + return e.complexity.InvitationEdge.Node(childComplexity), true + + case "InviteUserPayload.invitationEdge": + if e.complexity.InviteUserPayload.InvitationEdge == nil { + break + } + + return e.complexity.InviteUserPayload.InvitationEdge(childComplexity), true case "Measure.category": if e.complexity.Measure.Category == nil { @@ -4168,6 +4323,97 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.MeasureEdge.Node(childComplexity), true + case "Membership.createdAt": + if e.complexity.Membership.CreatedAt == nil { + break + } + + return e.complexity.Membership.CreatedAt(childComplexity), true + + case "Membership.emailAddress": + if e.complexity.Membership.EmailAddress == nil { + break + } + + return e.complexity.Membership.EmailAddress(childComplexity), true + + case "Membership.fullName": + if e.complexity.Membership.FullName == nil { + break + } + + return e.complexity.Membership.FullName(childComplexity), true + + case "Membership.id": + if e.complexity.Membership.ID == nil { + break + } + + return e.complexity.Membership.ID(childComplexity), true + + case "Membership.organizationID": + if e.complexity.Membership.OrganizationID == nil { + break + } + + return e.complexity.Membership.OrganizationID(childComplexity), true + + case "Membership.role": + if e.complexity.Membership.Role == nil { + break + } + + return e.complexity.Membership.Role(childComplexity), true + + case "Membership.updatedAt": + if e.complexity.Membership.UpdatedAt == nil { + break + } + + return e.complexity.Membership.UpdatedAt(childComplexity), true + + case "Membership.userID": + if e.complexity.Membership.UserID == nil { + break + } + + return e.complexity.Membership.UserID(childComplexity), true + + case "MembershipConnection.edges": + if e.complexity.MembershipConnection.Edges == nil { + break + } + + return e.complexity.MembershipConnection.Edges(childComplexity), true + + case "MembershipConnection.pageInfo": + if e.complexity.MembershipConnection.PageInfo == nil { + break + } + + return e.complexity.MembershipConnection.PageInfo(childComplexity), true + + case "MembershipConnection.totalCount": + if e.complexity.MembershipConnection.TotalCount == nil { + break + } + + return e.complexity.MembershipConnection.TotalCount(childComplexity), true + + case "MembershipEdge.cursor": + if e.complexity.MembershipEdge.Cursor == nil { + break + } + + return e.complexity.MembershipEdge.Cursor(childComplexity), true + + case "MembershipEdge.node": + if e.complexity.MembershipEdge.Node == nil { + break + } + + return e.complexity.MembershipEdge.Node(childComplexity), true + case "Mutation.assessVendor": if e.complexity.Mutation.AssessVendor == nil { break @@ -4816,6 +5062,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Mutation.DeleteFramework(childComplexity, args["input"].(types.DeleteFrameworkInput)), true + case "Mutation.deleteInvitation": + if e.complexity.Mutation.DeleteInvitation == nil { + break + } + + args, err := ec.field_Mutation_deleteInvitation_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteInvitation(childComplexity, args["input"].(types.DeleteInvitationInput)), true + case "Mutation.deleteMeasure": if e.complexity.Mutation.DeleteMeasure == nil { break @@ -5176,17 +5434,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Mutation.PublishDocumentVersion(childComplexity, args["input"].(types.PublishDocumentVersionInput)), true - case "Mutation.removeUser": - if e.complexity.Mutation.RemoveUser == nil { + case "Mutation.removeMember": + if e.complexity.Mutation.RemoveMember == nil { break } - args, err := ec.field_Mutation_removeUser_args(ctx, rawArgs) + args, err := ec.field_Mutation_removeMember_args(ctx, rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.RemoveUser(childComplexity, args["input"].(types.RemoveUserInput)), true + return e.complexity.Mutation.RemoveMember(childComplexity, args["input"].(types.RemoveMemberInput)), true case "Mutation.requestSignature": if e.complexity.Mutation.RequestSignature == nil { @@ -6009,6 +6267,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Organization.ID(childComplexity), true + case "Organization.invitations": + if e.complexity.Organization.Invitations == nil { + break + } + + args, err := ec.field_Organization_invitations_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Organization.Invitations(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.InvitationOrder)), true + case "Organization.logoUrl": if e.complexity.Organization.LogoURL == nil { break @@ -6028,6 +6298,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Organization.Measures(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.MeasureOrderBy), args["filter"].(*types.MeasureFilter)), true + case "Organization.memberships": + if e.complexity.Organization.Memberships == nil { + break + } + + args, err := ec.field_Organization_memberships_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Organization.Memberships(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.MembershipOrderBy)), true + case "Organization.name": if e.complexity.Organization.Name == nil { break @@ -6133,18 +6415,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Organization.UpdatedAt(childComplexity), true - case "Organization.users": - if e.complexity.Organization.Users == nil { - break - } - - args, err := ec.field_Organization_users_args(ctx, rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Organization.Users(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.UserOrderBy)), true - case "Organization.vendors": if e.complexity.Organization.Vendors == nil { break @@ -6540,12 +6810,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Query.Viewer(childComplexity), true - case "RemoveUserPayload.success": - if e.complexity.RemoveUserPayload.Success == nil { + case "RemoveMemberPayload.success": + if e.complexity.RemoveMemberPayload.Success == nil { break } - return e.complexity.RemoveUserPayload.Success(childComplexity), true + return e.complexity.RemoveMemberPayload.Success(childComplexity), true case "Report.audit": if e.complexity.Report.Audit == nil { @@ -7696,6 +7966,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.UserConnection.PageInfo(childComplexity), true + case "UserConnection.totalCount": + if e.complexity.UserConnection.TotalCount == nil { + break + } + + return e.complexity.UserConnection.TotalCount(childComplexity), true + case "UserEdge.cursor": if e.complexity.UserEdge.Cursor == nil { break @@ -8529,6 +8806,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputDeleteDraftDocumentVersionInput, ec.unmarshalInputDeleteEvidenceInput, ec.unmarshalInputDeleteFrameworkInput, + ec.unmarshalInputDeleteInvitationInput, ec.unmarshalInputDeleteMeasureInput, ec.unmarshalInputDeleteNonconformityInput, ec.unmarshalInputDeleteObligationInput, @@ -8565,9 +8843,11 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputGenerateFrameworkStateOfApplicabilityInput, ec.unmarshalInputImportFrameworkInput, ec.unmarshalInputImportMeasureInput, + ec.unmarshalInputInvitationOrder, ec.unmarshalInputInviteUserInput, ec.unmarshalInputMeasureFilter, ec.unmarshalInputMeasureOrder, + ec.unmarshalInputMembershipOrder, ec.unmarshalInputNonconformityFilter, ec.unmarshalInputNonconformityOrder, ec.unmarshalInputObligationFilter, @@ -8578,7 +8858,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputProcessingActivityFilter, ec.unmarshalInputProcessingActivityOrder, ec.unmarshalInputPublishDocumentVersionInput, - ec.unmarshalInputRemoveUserInput, + ec.unmarshalInputRemoveMemberInput, ec.unmarshalInputRequestEvidenceInput, ec.unmarshalInputRequestSignatureInput, ec.unmarshalInputRiskFilter, @@ -9935,6 +10215,54 @@ enum SnapshotOrderField ) } +enum MembershipOrderField + @goModel(model: "github.com/getprobo/probo/pkg/coredata.MembershipOrderField") { + FULL_NAME + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.MembershipOrderFieldFullName" + ) + EMAIL_ADDRESS + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.MembershipOrderFieldEmailAddress" + ) + ROLE + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.MembershipOrderFieldRole" + ) + CREATED_AT + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.MembershipOrderFieldCreatedAt" + ) +} + +enum InvitationOrderField + @goModel(model: "github.com/getprobo/probo/pkg/coredata.InvitationOrderField") { + FULL_NAME + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldFullName" + ) + EMAIL + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldEmail" + ) + ROLE + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldRole" + ) + CREATED_AT + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldCreatedAt" + ) + EXPIRES_AT + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldExpiresAt" + ) + ACCEPTED_AT + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.InvitationOrderFieldAcceptedAt" + ) +} + # Input Types input UserOrder @goModel( @@ -10130,6 +10458,19 @@ input SnapshotOrder field: SnapshotOrderField! } +input MembershipOrder + @goModel( + model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.MembershipOrderBy" + ) { + direction: OrderDirection! + field: MembershipOrderField! +} + +input InvitationOrder { + direction: OrderDirection! + field: InvitationOrderField! +} + input DocumentVersionFilter { status: DocumentStatus } @@ -10223,13 +10564,21 @@ type Organization implements Node { email: String headquarterAddress: String - users( + memberships( first: Int after: CursorKey last: Int before: CursorKey - orderBy: UserOrder - ): UserConnection! @goField(forceResolver: true) + orderBy: MembershipOrder + ): MembershipConnection! @goField(forceResolver: true) + + invitations( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: InvitationOrder + ): InvitationConnection! @goField(forceResolver: true) connectors( first: Int @@ -10397,6 +10746,27 @@ type User implements Node { people(organizationId: ID!): People @goField(forceResolver: true) } +type Membership implements Node { + id: ID! + userID: ID! + organizationID: ID! + role: String! + fullName: String! + emailAddress: String! + createdAt: Datetime! + updatedAt: Datetime! +} + +type Invitation implements Node { + id: ID! + email: String! + fullName: String! + role: String! + expiresAt: Datetime! + acceptedAt: Datetime + createdAt: Datetime! +} + type Connector implements Node { id: ID! name: String! @@ -11031,10 +11401,22 @@ type TrustCenterReferenceEdge { } type UserConnection { + totalCount: Int! @goField(forceResolver: true) edges: [UserEdge!]! pageInfo: PageInfo! } +type MembershipConnection { + totalCount: Int! @goField(forceResolver: true) + edges: [MembershipEdge!]! + pageInfo: PageInfo! +} + +type MembershipEdge { + cursor: CursorKey! + node: Membership! +} + type UserEdge { cursor: CursorKey! node: User! @@ -11333,6 +11715,17 @@ type File { updatedAt: Datetime! } +type InvitationConnection { + totalCount: Int! @goField(forceResolver: true) + edges: [InvitationEdge!]! + pageInfo: PageInfo! +} + +type InvitationEdge { + cursor: CursorKey! + node: Invitation! +} + # Root Types type Query { node(id: ID!): Node! @@ -11393,7 +11786,8 @@ type Mutation { # User mutations confirmEmail(input: ConfirmEmailInput!): ConfirmEmailPayload! inviteUser(input: InviteUserInput!): InviteUserPayload! - removeUser(input: RemoveUserInput!): RemoveUserPayload! + deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload! + removeMember(input: RemoveMemberInput!): RemoveMemberPayload! # People mutations createPeople(input: CreatePeopleInput!): CreatePeoplePayload! @@ -12152,9 +12546,13 @@ input InviteUserInput { createPeople: Boolean! } -input RemoveUserInput { +input DeleteInvitationInput { + invitationId: ID! +} + +input RemoveMemberInput { organizationId: ID! - userId: ID! + memberId: ID! } input CreateControlInput { @@ -12671,10 +13069,14 @@ type ConfirmEmailPayload { } type InviteUserPayload { - success: Boolean! + invitationEdge: InvitationEdge! } -type RemoveUserPayload { +type DeleteInvitationPayload { + deletedInvitationId: ID! +} + +type RemoveMemberPayload { success: Boolean! } @@ -16026,6 +16428,29 @@ func (ec *executionContext) field_Mutation_deleteFramework_argsInput( return zeroVal, nil } +func (ec *executionContext) field_Mutation_deleteInvitation_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_deleteInvitation_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_deleteInvitation_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (types.DeleteInvitationInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNDeleteInvitationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteInvitationInput(ctx, tmp) + } + + var zeroVal types.DeleteInvitationInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_deleteMeasure_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -16716,26 +17141,26 @@ func (ec *executionContext) field_Mutation_publishDocumentVersion_argsInput( return zeroVal, nil } -func (ec *executionContext) field_Mutation_removeUser_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { +func (ec *executionContext) field_Mutation_removeMember_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := ec.field_Mutation_removeUser_argsInput(ctx, rawArgs) + arg0, err := ec.field_Mutation_removeMember_argsInput(ctx, rawArgs) if err != nil { return nil, err } args["input"] = arg0 return args, nil } -func (ec *executionContext) field_Mutation_removeUser_argsInput( +func (ec *executionContext) field_Mutation_removeMember_argsInput( ctx context.Context, rawArgs map[string]any, -) (types.RemoveUserInput, error) { +) (types.RemoveMemberInput, error) { ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) if tmp, ok := rawArgs["input"]; ok { - return ec.unmarshalNRemoveUserInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRemoveUserInput(ctx, tmp) + return ec.unmarshalNRemoveMemberInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRemoveMemberInput(ctx, tmp) } - var zeroVal types.RemoveUserInput + var zeroVal types.RemoveMemberInput return zeroVal, nil } @@ -18348,6 +18773,101 @@ func (ec *executionContext) field_Organization_frameworks_argsOrderBy( return zeroVal, nil } +func (ec *executionContext) field_Organization_invitations_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Organization_invitations_argsFirst(ctx, rawArgs) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := ec.field_Organization_invitations_argsAfter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := ec.field_Organization_invitations_argsLast(ctx, rawArgs) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := ec.field_Organization_invitations_argsBefore(ctx, rawArgs) + if err != nil { + return nil, err + } + args["before"] = arg3 + arg4, err := ec.field_Organization_invitations_argsOrderBy(ctx, rawArgs) + if err != nil { + return nil, err + } + args["orderBy"] = arg4 + return args, nil +} +func (ec *executionContext) field_Organization_invitations_argsFirst( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + if tmp, ok := rawArgs["first"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_invitations_argsAfter( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + if tmp, ok := rawArgs["after"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_invitations_argsLast( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last")) + if tmp, ok := rawArgs["last"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_invitations_argsBefore( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) + if tmp, ok := rawArgs["before"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_invitations_argsOrderBy( + ctx context.Context, + rawArgs map[string]any, +) (*types.InvitationOrder, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("orderBy")) + if tmp, ok := rawArgs["orderBy"]; ok { + return ec.unmarshalOInvitationOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitationOrder(ctx, tmp) + } + + var zeroVal *types.InvitationOrder + return zeroVal, nil +} + func (ec *executionContext) field_Organization_measures_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -18461,6 +18981,101 @@ func (ec *executionContext) field_Organization_measures_argsFilter( return zeroVal, nil } +func (ec *executionContext) field_Organization_memberships_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Organization_memberships_argsFirst(ctx, rawArgs) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := ec.field_Organization_memberships_argsAfter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := ec.field_Organization_memberships_argsLast(ctx, rawArgs) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := ec.field_Organization_memberships_argsBefore(ctx, rawArgs) + if err != nil { + return nil, err + } + args["before"] = arg3 + arg4, err := ec.field_Organization_memberships_argsOrderBy(ctx, rawArgs) + if err != nil { + return nil, err + } + args["orderBy"] = arg4 + return args, nil +} +func (ec *executionContext) field_Organization_memberships_argsFirst( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + if tmp, ok := rawArgs["first"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_memberships_argsAfter( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + if tmp, ok := rawArgs["after"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_memberships_argsLast( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last")) + if tmp, ok := rawArgs["last"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_memberships_argsBefore( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) + if tmp, ok := rawArgs["before"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_memberships_argsOrderBy( + ctx context.Context, + rawArgs map[string]any, +) (*types.MembershipOrderBy, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("orderBy")) + if tmp, ok := rawArgs["orderBy"]; ok { + return ec.unmarshalOMembershipOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembershipOrderBy(ctx, tmp) + } + + var zeroVal *types.MembershipOrderBy + return zeroVal, nil +} + func (ec *executionContext) field_Organization_nonconformities_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -19216,101 +19831,6 @@ func (ec *executionContext) field_Organization_tasks_argsOrderBy( return zeroVal, nil } -func (ec *executionContext) field_Organization_users_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field_Organization_users_argsFirst(ctx, rawArgs) - if err != nil { - return nil, err - } - args["first"] = arg0 - arg1, err := ec.field_Organization_users_argsAfter(ctx, rawArgs) - if err != nil { - return nil, err - } - args["after"] = arg1 - arg2, err := ec.field_Organization_users_argsLast(ctx, rawArgs) - if err != nil { - return nil, err - } - args["last"] = arg2 - arg3, err := ec.field_Organization_users_argsBefore(ctx, rawArgs) - if err != nil { - return nil, err - } - args["before"] = arg3 - arg4, err := ec.field_Organization_users_argsOrderBy(ctx, rawArgs) - if err != nil { - return nil, err - } - args["orderBy"] = arg4 - return args, nil -} -func (ec *executionContext) field_Organization_users_argsFirst( - ctx context.Context, - rawArgs map[string]any, -) (*int, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) - if tmp, ok := rawArgs["first"]; ok { - return ec.unmarshalOInt2ᚖint(ctx, tmp) - } - - var zeroVal *int - return zeroVal, nil -} - -func (ec *executionContext) field_Organization_users_argsAfter( - ctx context.Context, - rawArgs map[string]any, -) (*page.CursorKey, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - if tmp, ok := rawArgs["after"]; ok { - return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) - } - - var zeroVal *page.CursorKey - return zeroVal, nil -} - -func (ec *executionContext) field_Organization_users_argsLast( - ctx context.Context, - rawArgs map[string]any, -) (*int, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last")) - if tmp, ok := rawArgs["last"]; ok { - return ec.unmarshalOInt2ᚖint(ctx, tmp) - } - - var zeroVal *int - return zeroVal, nil -} - -func (ec *executionContext) field_Organization_users_argsBefore( - ctx context.Context, - rawArgs map[string]any, -) (*page.CursorKey, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - if tmp, ok := rawArgs["before"]; ok { - return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) - } - - var zeroVal *page.CursorKey - return zeroVal, nil -} - -func (ec *executionContext) field_Organization_users_argsOrderBy( - ctx context.Context, - rawArgs map[string]any, -) (*types.UserOrderBy, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("orderBy")) - if tmp, ok := rawArgs["orderBy"]; ok { - return ec.unmarshalOUserOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUserOrderBy(ctx, tmp) - } - - var zeroVal *types.UserOrderBy - return zeroVal, nil -} - func (ec *executionContext) field_Organization_vendors_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -21566,8 +22086,10 @@ func (ec *executionContext) fieldContext_Asset_organization(_ context.Context, f return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -22175,8 +22697,10 @@ func (ec *executionContext) fieldContext_Audit_organization(_ context.Context, f return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -23904,8 +24428,10 @@ func (ec *executionContext) fieldContext_ContinualImprovement_organization(_ con return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -27664,8 +28190,10 @@ func (ec *executionContext) fieldContext_CustomDomain_organization(_ context.Con return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -28565,8 +29093,10 @@ func (ec *executionContext) fieldContext_Datum_organization(_ context.Context, f return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -29824,6 +30354,50 @@ func (ec *executionContext) fieldContext_DeleteFrameworkPayload_deletedFramework return fc, nil } +func (ec *executionContext) _DeleteInvitationPayload_deletedInvitationId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteInvitationPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DeleteInvitationPayload_deletedInvitationId(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DeletedInvitationID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(gid.GID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DeleteInvitationPayload_deletedInvitationId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DeleteInvitationPayload", + 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) _DeleteMeasurePayload_deletedMeasureId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteMeasurePayload) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DeleteMeasurePayload_deletedMeasureId(ctx, field) if err != nil { @@ -30011,8 +30585,10 @@ func (ec *executionContext) fieldContext_DeleteOrganizationHorizontalLogoPayload return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -31388,8 +31964,10 @@ func (ec *executionContext) fieldContext_Document_organization(_ context.Context return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -34712,8 +35290,10 @@ func (ec *executionContext) fieldContext_Framework_organization(_ context.Contex return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -35404,8 +35984,8 @@ func (ec *executionContext) fieldContext_ImportMeasurePayload_measureEdges(_ con return fc, nil } -func (ec *executionContext) _InviteUserPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.InviteUserPayload) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_InviteUserPayload_success(ctx, field) +func (ec *executionContext) _Invitation_id(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Invitation_id(ctx, field) if err != nil { return graphql.Null } @@ -35418,7 +35998,7 @@ func (ec *executionContext) _InviteUserPayload_success(ctx context.Context, fiel }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Success, nil + return obj.ID, nil }) if err != nil { ec.Error(ctx, err) @@ -35430,19 +36010,582 @@ func (ec *executionContext) _InviteUserPayload_success(ctx context.Context, fiel } return graphql.Null } - res := resTmp.(bool) + res := resTmp.(gid.GID) fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_InviteUserPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Invitation_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Invitation", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Invitation_email(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Invitation_email(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Email, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Invitation_email(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Invitation", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Invitation_fullName(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Invitation_fullName(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.FullName, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Invitation_fullName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Invitation", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Invitation_role(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Invitation_role(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Role, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Invitation_role(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Invitation", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Invitation_expiresAt(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Invitation_expiresAt(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ExpiresAt, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Invitation_expiresAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Invitation", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Invitation_acceptedAt(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Invitation_acceptedAt(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.AcceptedAt, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*time.Time) + fc.Result = res + return ec.marshalODatetime2ᚖtimeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Invitation_acceptedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Invitation", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Invitation_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Invitation_createdAt(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.CreatedAt, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Invitation_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Invitation", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _InvitationConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *types.InvitationConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_InvitationConnection_totalCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.InvitationConnection().TotalCount(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_InvitationConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "InvitationConnection", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _InvitationConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.InvitationConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_InvitationConnection_edges(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Edges, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*types.InvitationEdge) + fc.Result = res + return ec.marshalNInvitationEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitationEdgeᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_InvitationConnection_edges(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "InvitationConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "cursor": + return ec.fieldContext_InvitationEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_InvitationEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type InvitationEdge", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _InvitationConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *types.InvitationConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_InvitationConnection_pageInfo(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.PageInfo, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.PageInfo) + fc.Result = res + return ec.marshalNPageInfo2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPageInfo(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_InvitationConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "InvitationConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "hasNextPage": + return ec.fieldContext_PageInfo_hasNextPage(ctx, field) + case "hasPreviousPage": + return ec.fieldContext_PageInfo_hasPreviousPage(ctx, field) + case "startCursor": + return ec.fieldContext_PageInfo_startCursor(ctx, field) + case "endCursor": + return ec.fieldContext_PageInfo_endCursor(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _InvitationEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *types.InvitationEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_InvitationEdge_cursor(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Cursor, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(page.CursorKey) + fc.Result = res + return ec.marshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_InvitationEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "InvitationEdge", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type CursorKey does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _InvitationEdge_node(ctx context.Context, field graphql.CollectedField, obj *types.InvitationEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_InvitationEdge_node(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Node, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.Invitation) + fc.Result = res + return ec.marshalNInvitation2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitation(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_InvitationEdge_node(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "InvitationEdge", + 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_Invitation_id(ctx, field) + case "email": + return ec.fieldContext_Invitation_email(ctx, field) + case "fullName": + return ec.fieldContext_Invitation_fullName(ctx, field) + case "role": + return ec.fieldContext_Invitation_role(ctx, field) + case "expiresAt": + return ec.fieldContext_Invitation_expiresAt(ctx, field) + case "acceptedAt": + return ec.fieldContext_Invitation_acceptedAt(ctx, field) + case "createdAt": + return ec.fieldContext_Invitation_createdAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Invitation", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _InviteUserPayload_invitationEdge(ctx context.Context, field graphql.CollectedField, obj *types.InviteUserPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_InviteUserPayload_invitationEdge(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.InvitationEdge, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.InvitationEdge) + fc.Result = res + return ec.marshalNInvitationEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitationEdge(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_InviteUserPayload_invitationEdge(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "InviteUserPayload", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + switch field.Name { + case "cursor": + return ec.fieldContext_InvitationEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_InvitationEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type InvitationEdge", field.Name) }, } return fc, nil @@ -36268,6 +37411,612 @@ func (ec *executionContext) fieldContext_MeasureEdge_node(_ context.Context, fie return fc, nil } +func (ec *executionContext) _Membership_id(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Membership_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(gid.GID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Membership_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Membership", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Membership_userID(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Membership_userID(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.UserID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(gid.GID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Membership_userID(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Membership", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Membership_organizationID(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Membership_organizationID(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.OrganizationID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(gid.GID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Membership_organizationID(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Membership", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Membership_role(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Membership_role(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Role, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Membership_role(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Membership", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Membership_fullName(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Membership_fullName(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.FullName, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Membership_fullName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Membership", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Membership_emailAddress(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Membership_emailAddress(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.EmailAddress, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Membership_emailAddress(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Membership", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Membership_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Membership_createdAt(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.CreatedAt, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Membership_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Membership", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Membership_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Membership_updatedAt(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.UpdatedAt, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Membership_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Membership", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *types.MembershipConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_MembershipConnection_totalCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.MembershipConnection().TotalCount(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_MembershipConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipConnection", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.MembershipConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_MembershipConnection_edges(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Edges, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*types.MembershipEdge) + fc.Result = res + return ec.marshalNMembershipEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembershipEdgeᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_MembershipConnection_edges(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "cursor": + return ec.fieldContext_MembershipEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_MembershipEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type MembershipEdge", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *types.MembershipConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_MembershipConnection_pageInfo(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.PageInfo, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.PageInfo) + fc.Result = res + return ec.marshalNPageInfo2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPageInfo(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_MembershipConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "hasNextPage": + return ec.fieldContext_PageInfo_hasNextPage(ctx, field) + case "hasPreviousPage": + return ec.fieldContext_PageInfo_hasPreviousPage(ctx, field) + case "startCursor": + return ec.fieldContext_PageInfo_startCursor(ctx, field) + case "endCursor": + return ec.fieldContext_PageInfo_endCursor(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *types.MembershipEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_MembershipEdge_cursor(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Cursor, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(page.CursorKey) + fc.Result = res + return ec.marshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_MembershipEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipEdge", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type CursorKey does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _MembershipEdge_node(ctx context.Context, field graphql.CollectedField, obj *types.MembershipEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_MembershipEdge_node(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Node, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.Membership) + fc.Result = res + return ec.marshalNMembership2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembership(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_MembershipEdge_node(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MembershipEdge", + 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_Membership_id(ctx, field) + case "userID": + return ec.fieldContext_Membership_userID(ctx, field) + case "organizationID": + return ec.fieldContext_Membership_organizationID(ctx, field) + case "role": + return ec.fieldContext_Membership_role(ctx, field) + case "fullName": + return ec.fieldContext_Membership_fullName(ctx, field) + case "emailAddress": + return ec.fieldContext_Membership_emailAddress(ctx, field) + case "createdAt": + return ec.fieldContext_Membership_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_Membership_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Membership", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Mutation_createOrganization(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createOrganization(ctx, field) if err != nil { @@ -37133,8 +38882,8 @@ func (ec *executionContext) fieldContext_Mutation_inviteUser(ctx context.Context IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "success": - return ec.fieldContext_InviteUserPayload_success(ctx, field) + case "invitationEdge": + return ec.fieldContext_InviteUserPayload_invitationEdge(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type InviteUserPayload", field.Name) }, @@ -37153,8 +38902,8 @@ func (ec *executionContext) fieldContext_Mutation_inviteUser(ctx context.Context return fc, nil } -func (ec *executionContext) _Mutation_removeUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_removeUser(ctx, field) +func (ec *executionContext) _Mutation_deleteInvitation(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_deleteInvitation(ctx, field) if err != nil { return graphql.Null } @@ -37167,7 +38916,7 @@ func (ec *executionContext) _Mutation_removeUser(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().RemoveUser(rctx, fc.Args["input"].(types.RemoveUserInput)) + return ec.resolvers.Mutation().DeleteInvitation(rctx, fc.Args["input"].(types.DeleteInvitationInput)) }) if err != nil { ec.Error(ctx, err) @@ -37179,12 +38928,12 @@ func (ec *executionContext) _Mutation_removeUser(ctx context.Context, field grap } return graphql.Null } - res := resTmp.(*types.RemoveUserPayload) + res := resTmp.(*types.DeleteInvitationPayload) fc.Result = res - return ec.marshalNRemoveUserPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRemoveUserPayload(ctx, field.Selections, res) + return ec.marshalNDeleteInvitationPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteInvitationPayload(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_removeUser(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_deleteInvitation(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -37192,10 +38941,10 @@ func (ec *executionContext) fieldContext_Mutation_removeUser(ctx context.Context IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "success": - return ec.fieldContext_RemoveUserPayload_success(ctx, field) + case "deletedInvitationId": + return ec.fieldContext_DeleteInvitationPayload_deletedInvitationId(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type RemoveUserPayload", field.Name) + return nil, fmt.Errorf("no field named %q was found under type DeleteInvitationPayload", field.Name) }, } defer func() { @@ -37205,7 +38954,66 @@ func (ec *executionContext) fieldContext_Mutation_removeUser(ctx context.Context } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_removeUser_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_deleteInvitation_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_removeMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_removeMember(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().RemoveMember(rctx, fc.Args["input"].(types.RemoveMemberInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.RemoveMemberPayload) + fc.Result = res + return ec.marshalNRemoveMemberPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRemoveMemberPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_removeMember(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 "success": + return ec.fieldContext_RemoveMemberPayload_success(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type RemoveMemberPayload", 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_removeMember_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -43404,8 +45212,10 @@ func (ec *executionContext) fieldContext_Nonconformity_organization(_ context.Co return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -44467,8 +46277,10 @@ func (ec *executionContext) fieldContext_Obligation_organization(_ context.Conte return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -45605,8 +47417,8 @@ func (ec *executionContext) fieldContext_Organization_headquarterAddress(_ conte return fc, nil } -func (ec *executionContext) _Organization_users(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Organization_users(ctx, field) +func (ec *executionContext) _Organization_memberships(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Organization_memberships(ctx, field) if err != nil { return graphql.Null } @@ -45619,7 +47431,7 @@ func (ec *executionContext) _Organization_users(ctx context.Context, field graph }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Organization().Users(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.UserOrderBy)) + return ec.resolvers.Organization().Memberships(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.MembershipOrderBy)) }) if err != nil { ec.Error(ctx, err) @@ -45631,12 +47443,12 @@ func (ec *executionContext) _Organization_users(ctx context.Context, field graph } return graphql.Null } - res := resTmp.(*types.UserConnection) + res := resTmp.(*types.MembershipConnection) fc.Result = res - return ec.marshalNUserConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUserConnection(ctx, field.Selections, res) + return ec.marshalNMembershipConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembershipConnection(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Organization_users(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Organization_memberships(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Organization", Field: field, @@ -45644,12 +47456,14 @@ func (ec *executionContext) fieldContext_Organization_users(ctx context.Context, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { + case "totalCount": + return ec.fieldContext_MembershipConnection_totalCount(ctx, field) case "edges": - return ec.fieldContext_UserConnection_edges(ctx, field) + return ec.fieldContext_MembershipConnection_edges(ctx, field) case "pageInfo": - return ec.fieldContext_UserConnection_pageInfo(ctx, field) + return ec.fieldContext_MembershipConnection_pageInfo(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type UserConnection", field.Name) + return nil, fmt.Errorf("no field named %q was found under type MembershipConnection", field.Name) }, } defer func() { @@ -45659,7 +47473,70 @@ func (ec *executionContext) fieldContext_Organization_users(ctx context.Context, } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Organization_users_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Organization_memberships_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Organization_invitations(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Organization_invitations(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Organization().Invitations(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.InvitationOrder)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.InvitationConnection) + fc.Result = res + return ec.marshalNInvitationConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitationConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Organization_invitations(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Organization", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "totalCount": + return ec.fieldContext_InvitationConnection_totalCount(ctx, field) + case "edges": + return ec.fieldContext_InvitationConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_InvitationConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type InvitationConnection", 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_Organization_invitations_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -47146,8 +49023,10 @@ func (ec *executionContext) fieldContext_OrganizationEdge_node(_ context.Context return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -48237,8 +50116,10 @@ func (ec *executionContext) fieldContext_ProcessingActivity_organization(_ conte return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -49667,8 +51548,8 @@ func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field return fc, nil } -func (ec *executionContext) _RemoveUserPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.RemoveUserPayload) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_RemoveUserPayload_success(ctx, field) +func (ec *executionContext) _RemoveMemberPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.RemoveMemberPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_RemoveMemberPayload_success(ctx, field) if err != nil { return graphql.Null } @@ -49698,9 +51579,9 @@ func (ec *executionContext) _RemoveUserPayload_success(ctx context.Context, fiel return ec.marshalNBoolean2bool(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_RemoveUserPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_RemoveMemberPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "RemoveUserPayload", + Object: "RemoveMemberPayload", Field: field, IsMethod: false, IsResolver: false, @@ -50916,8 +52797,10 @@ func (ec *executionContext) fieldContext_Risk_organization(_ context.Context, fi return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -51818,8 +53701,10 @@ func (ec *executionContext) fieldContext_Snapshot_organization(_ context.Context return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -52733,8 +54618,10 @@ func (ec *executionContext) fieldContext_Task_organization(_ context.Context, fi return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -53575,8 +55462,10 @@ func (ec *executionContext) fieldContext_TrustCenter_organization(_ context.Cont return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -56614,8 +58503,10 @@ func (ec *executionContext) fieldContext_UpdateOrganizationPayload_organization( return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -58177,6 +60068,50 @@ func (ec *executionContext) fieldContext_User_people(ctx context.Context, field return fc, nil } +func (ec *executionContext) _UserConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *types.UserConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_UserConnection_totalCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.UserConnection().TotalCount(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_UserConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "UserConnection", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _UserConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.UserConnection) (ret graphql.Marshaler) { fc, err := ec.fieldContext_UserConnection_edges(ctx, field) if err != nil { @@ -58652,8 +60587,10 @@ func (ec *executionContext) fieldContext_Vendor_organization(_ context.Context, return ec.fieldContext_Organization_email(ctx, field) case "headquarterAddress": return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "users": - return ec.fieldContext_Organization_users(ctx, field) + case "memberships": + return ec.fieldContext_Organization_memberships(ctx, field) + case "invitations": + return ec.fieldContext_Organization_invitations(ctx, field) case "connectors": return ec.fieldContext_Organization_connectors(ctx, field) case "frameworks": @@ -68550,6 +70487,33 @@ func (ec *executionContext) unmarshalInputDeleteFrameworkInput(ctx context.Conte return it, nil } +func (ec *executionContext) unmarshalInputDeleteInvitationInput(ctx context.Context, obj any) (types.DeleteInvitationInput, error) { + var it types.DeleteInvitationInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"invitationId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "invitationId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("invitationId")) + data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.InvitationID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDeleteMeasureInput(ctx context.Context, obj any) (types.DeleteMeasureInput, error) { var it types.DeleteMeasureInput asMap := map[string]any{} @@ -69634,6 +71598,40 @@ func (ec *executionContext) unmarshalInputImportMeasureInput(ctx context.Context return it, nil } +func (ec *executionContext) unmarshalInputInvitationOrder(ctx context.Context, obj any) (types.InvitationOrder, error) { + var it types.InvitationOrder + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"direction", "field"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "direction": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("direction")) + data, err := ec.unmarshalNOrderDirection2githubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐOrderDirection(ctx, v) + if err != nil { + return it, err + } + it.Direction = data + case "field": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("field")) + data, err := ec.unmarshalNInvitationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationOrderField(ctx, v) + if err != nil { + return it, err + } + it.Field = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputInviteUserInput(ctx context.Context, obj any) (types.InviteUserInput, error) { var it types.InviteUserInput asMap := map[string]any{} @@ -69750,6 +71748,40 @@ func (ec *executionContext) unmarshalInputMeasureOrder(ctx context.Context, obj return it, nil } +func (ec *executionContext) unmarshalInputMembershipOrder(ctx context.Context, obj any) (types.MembershipOrderBy, error) { + var it types.MembershipOrderBy + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"direction", "field"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "direction": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("direction")) + data, err := ec.unmarshalNOrderDirection2githubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐOrderDirection(ctx, v) + if err != nil { + return it, err + } + it.Direction = data + case "field": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("field")) + data, err := ec.unmarshalNMembershipOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMembershipOrderField(ctx, v) + if err != nil { + return it, err + } + it.Field = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputNonconformityFilter(ctx context.Context, obj any) (types.NonconformityFilter, error) { var it types.NonconformityFilter asMap := map[string]any{} @@ -70062,14 +72094,14 @@ func (ec *executionContext) unmarshalInputPublishDocumentVersionInput(ctx contex return it, nil } -func (ec *executionContext) unmarshalInputRemoveUserInput(ctx context.Context, obj any) (types.RemoveUserInput, error) { - var it types.RemoveUserInput +func (ec *executionContext) unmarshalInputRemoveMemberInput(ctx context.Context, obj any) (types.RemoveMemberInput, error) { + var it types.RemoveMemberInput asMap := map[string]any{} for k, v := range obj.(map[string]any) { asMap[k] = v } - fieldsInOrder := [...]string{"organizationId", "userId"} + fieldsInOrder := [...]string{"organizationId", "memberId"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -70083,13 +72115,13 @@ func (ec *executionContext) unmarshalInputRemoveUserInput(ctx context.Context, o return it, err } it.OrganizationID = data - case "userId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("userId")) + case "memberId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("memberId")) data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) if err != nil { return it, err } - it.UserID = data + it.MemberID = data } } @@ -72779,6 +74811,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj return graphql.Null } return ec._Nonconformity(ctx, sel, obj) + case types.Membership: + return ec._Membership(ctx, sel, &obj) + case *types.Membership: + if obj == nil { + return graphql.Null + } + return ec._Membership(ctx, sel, obj) case types.Measure: return ec._Measure(ctx, sel, &obj) case *types.Measure: @@ -72786,6 +74825,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj return graphql.Null } return ec._Measure(ctx, sel, obj) + case types.Invitation: + return ec._Invitation(ctx, sel, &obj) + case *types.Invitation: + if obj == nil { + return graphql.Null + } + return ec._Invitation(ctx, sel, obj) case types.Framework: return ec._Framework(ctx, sel, &obj) case *types.Framework: @@ -77001,6 +79047,45 @@ func (ec *executionContext) _DeleteFrameworkPayload(ctx context.Context, sel ast return out } +var deleteInvitationPayloadImplementors = []string{"DeleteInvitationPayload"} + +func (ec *executionContext) _DeleteInvitationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteInvitationPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, deleteInvitationPayloadImplementors) + + 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("DeleteInvitationPayload") + case "deletedInvitationId": + out.Values[i] = ec._DeleteInvitationPayload_deletedInvitationId(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 deleteMeasurePayloadImplementors = []string{"DeleteMeasurePayload"} func (ec *executionContext) _DeleteMeasurePayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteMeasurePayload) graphql.Marshaler { @@ -79631,6 +81716,196 @@ func (ec *executionContext) _ImportMeasurePayload(ctx context.Context, sel ast.S return out } +var invitationImplementors = []string{"Invitation", "Node"} + +func (ec *executionContext) _Invitation(ctx context.Context, sel ast.SelectionSet, obj *types.Invitation) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, invitationImplementors) + + 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("Invitation") + case "id": + out.Values[i] = ec._Invitation_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "email": + out.Values[i] = ec._Invitation_email(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fullName": + out.Values[i] = ec._Invitation_fullName(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "role": + out.Values[i] = ec._Invitation_role(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "expiresAt": + out.Values[i] = ec._Invitation_expiresAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "acceptedAt": + out.Values[i] = ec._Invitation_acceptedAt(ctx, field, obj) + case "createdAt": + out.Values[i] = ec._Invitation_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + 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 invitationConnectionImplementors = []string{"InvitationConnection"} + +func (ec *executionContext) _InvitationConnection(ctx context.Context, sel ast.SelectionSet, obj *types.InvitationConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, invitationConnectionImplementors) + + 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("InvitationConnection") + case "totalCount": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._InvitationConnection_totalCount(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "edges": + out.Values[i] = ec._InvitationConnection_edges(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "pageInfo": + out.Values[i] = ec._InvitationConnection_pageInfo(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + 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 invitationEdgeImplementors = []string{"InvitationEdge"} + +func (ec *executionContext) _InvitationEdge(ctx context.Context, sel ast.SelectionSet, obj *types.InvitationEdge) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, invitationEdgeImplementors) + + 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("InvitationEdge") + case "cursor": + out.Values[i] = ec._InvitationEdge_cursor(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "node": + out.Values[i] = ec._InvitationEdge_node(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 inviteUserPayloadImplementors = []string{"InviteUserPayload"} func (ec *executionContext) _InviteUserPayload(ctx context.Context, sel ast.SelectionSet, obj *types.InviteUserPayload) graphql.Marshaler { @@ -79642,8 +81917,8 @@ func (ec *executionContext) _InviteUserPayload(ctx context.Context, sel ast.Sele switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("InviteUserPayload") - case "success": - out.Values[i] = ec._InviteUserPayload_success(ctx, field, obj) + case "invitationEdge": + out.Values[i] = ec._InviteUserPayload_invitationEdge(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -80007,6 +82282,204 @@ func (ec *executionContext) _MeasureEdge(ctx context.Context, sel ast.SelectionS return out } +var membershipImplementors = []string{"Membership", "Node"} + +func (ec *executionContext) _Membership(ctx context.Context, sel ast.SelectionSet, obj *types.Membership) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, membershipImplementors) + + 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("Membership") + case "id": + out.Values[i] = ec._Membership_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "userID": + out.Values[i] = ec._Membership_userID(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "organizationID": + out.Values[i] = ec._Membership_organizationID(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "role": + out.Values[i] = ec._Membership_role(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fullName": + out.Values[i] = ec._Membership_fullName(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "emailAddress": + out.Values[i] = ec._Membership_emailAddress(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "createdAt": + out.Values[i] = ec._Membership_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updatedAt": + out.Values[i] = ec._Membership_updatedAt(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 membershipConnectionImplementors = []string{"MembershipConnection"} + +func (ec *executionContext) _MembershipConnection(ctx context.Context, sel ast.SelectionSet, obj *types.MembershipConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, membershipConnectionImplementors) + + 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("MembershipConnection") + case "totalCount": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._MembershipConnection_totalCount(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "edges": + out.Values[i] = ec._MembershipConnection_edges(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "pageInfo": + out.Values[i] = ec._MembershipConnection_pageInfo(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + 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 membershipEdgeImplementors = []string{"MembershipEdge"} + +func (ec *executionContext) _MembershipEdge(ctx context.Context, sel ast.SelectionSet, obj *types.MembershipEdge) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, membershipEdgeImplementors) + + 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("MembershipEdge") + case "cursor": + out.Values[i] = ec._MembershipEdge_cursor(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "node": + out.Values[i] = ec._MembershipEdge_node(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 mutationImplementors = []string{"Mutation"} func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -80131,9 +82604,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } - case "removeUser": + case "deleteInvitation": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_removeUser(ctx, field) + return ec._Mutation_deleteInvitation(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "removeMember": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_removeMember(ctx, field) }) if out.Values[i] == graphql.Null { out.Invalids++ @@ -81546,7 +84026,7 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection out.Values[i] = ec._Organization_email(ctx, field, obj) case "headquarterAddress": out.Values[i] = ec._Organization_headquarterAddress(ctx, field, obj) - case "users": + case "memberships": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -81555,7 +84035,43 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Organization_users(ctx, field, obj) + res = ec._Organization_memberships(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "invitations": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Organization_invitations(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -83027,19 +85543,19 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return out } -var removeUserPayloadImplementors = []string{"RemoveUserPayload"} +var removeMemberPayloadImplementors = []string{"RemoveMemberPayload"} -func (ec *executionContext) _RemoveUserPayload(ctx context.Context, sel ast.SelectionSet, obj *types.RemoveUserPayload) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, removeUserPayloadImplementors) +func (ec *executionContext) _RemoveMemberPayload(ctx context.Context, sel ast.SelectionSet, obj *types.RemoveMemberPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, removeMemberPayloadImplementors) 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("RemoveUserPayload") + out.Values[i] = graphql.MarshalString("RemoveMemberPayload") case "success": - out.Values[i] = ec._RemoveUserPayload_success(ctx, field, obj) + out.Values[i] = ec._RemoveMemberPayload_success(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -86681,15 +89197,51 @@ func (ec *executionContext) _UserConnection(ctx context.Context, sel ast.Selecti switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("UserConnection") + case "totalCount": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._UserConnection_totalCount(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "edges": out.Values[i] = ec._UserConnection_edges(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "pageInfo": out.Values[i] = ec._UserConnection_pageInfo(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } default: panic("unknown field " + strconv.Quote(field.Name)) @@ -91890,6 +94442,25 @@ func (ec *executionContext) marshalNDeleteFrameworkPayload2ᚖgithubᚗcomᚋget return ec._DeleteFrameworkPayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNDeleteInvitationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteInvitationInput(ctx context.Context, v any) (types.DeleteInvitationInput, error) { + res, err := ec.unmarshalInputDeleteInvitationInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDeleteInvitationPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteInvitationPayload(ctx context.Context, sel ast.SelectionSet, v types.DeleteInvitationPayload) graphql.Marshaler { + return ec._DeleteInvitationPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDeleteInvitationPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteInvitationPayload(ctx context.Context, sel ast.SelectionSet, v *types.DeleteInvitationPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DeleteInvitationPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNDeleteMeasureInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteMeasureInput(ctx context.Context, v any) (types.DeleteMeasureInput, error) { res, err := ec.unmarshalInputDeleteMeasureInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -93159,6 +95730,120 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } +func (ec *executionContext) marshalNInvitation2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitation(ctx context.Context, sel ast.SelectionSet, v *types.Invitation) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Invitation(ctx, sel, v) +} + +func (ec *executionContext) marshalNInvitationConnection2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitationConnection(ctx context.Context, sel ast.SelectionSet, v types.InvitationConnection) graphql.Marshaler { + return ec._InvitationConnection(ctx, sel, &v) +} + +func (ec *executionContext) marshalNInvitationConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitationConnection(ctx context.Context, sel ast.SelectionSet, v *types.InvitationConnection) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._InvitationConnection(ctx, sel, v) +} + +func (ec *executionContext) marshalNInvitationEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitationEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.InvitationEdge) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNInvitationEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitationEdge(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNInvitationEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitationEdge(ctx context.Context, sel ast.SelectionSet, v *types.InvitationEdge) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._InvitationEdge(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNInvitationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationOrderField(ctx context.Context, v any) (coredata.InvitationOrderField, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNInvitationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationOrderField[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInvitationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationOrderField(ctx context.Context, sel ast.SelectionSet, v coredata.InvitationOrderField) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNInvitationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationOrderField[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNInvitationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationOrderField = map[string]coredata.InvitationOrderField{ + "FULL_NAME": coredata.InvitationOrderFieldFullName, + "EMAIL": coredata.InvitationOrderFieldEmail, + "ROLE": coredata.InvitationOrderFieldRole, + "CREATED_AT": coredata.InvitationOrderFieldCreatedAt, + "EXPIRES_AT": coredata.InvitationOrderFieldExpiresAt, + "ACCEPTED_AT": coredata.InvitationOrderFieldAcceptedAt, + } + marshalNInvitationOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationOrderField = map[coredata.InvitationOrderField]string{ + coredata.InvitationOrderFieldFullName: "FULL_NAME", + coredata.InvitationOrderFieldEmail: "EMAIL", + coredata.InvitationOrderFieldRole: "ROLE", + coredata.InvitationOrderFieldCreatedAt: "CREATED_AT", + coredata.InvitationOrderFieldExpiresAt: "EXPIRES_AT", + coredata.InvitationOrderFieldAcceptedAt: "ACCEPTED_AT", + } +) + func (ec *executionContext) unmarshalNInviteUserInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInviteUserInput(ctx context.Context, v any) (types.InviteUserInput, error) { res, err := ec.unmarshalInputInviteUserInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -93320,6 +96005,116 @@ var ( } ) +func (ec *executionContext) marshalNMembership2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembership(ctx context.Context, sel ast.SelectionSet, v *types.Membership) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Membership(ctx, sel, v) +} + +func (ec *executionContext) marshalNMembershipConnection2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembershipConnection(ctx context.Context, sel ast.SelectionSet, v types.MembershipConnection) graphql.Marshaler { + return ec._MembershipConnection(ctx, sel, &v) +} + +func (ec *executionContext) marshalNMembershipConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembershipConnection(ctx context.Context, sel ast.SelectionSet, v *types.MembershipConnection) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._MembershipConnection(ctx, sel, v) +} + +func (ec *executionContext) marshalNMembershipEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembershipEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.MembershipEdge) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNMembershipEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembershipEdge(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNMembershipEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembershipEdge(ctx context.Context, sel ast.SelectionSet, v *types.MembershipEdge) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._MembershipEdge(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNMembershipOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMembershipOrderField(ctx context.Context, v any) (coredata.MembershipOrderField, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNMembershipOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMembershipOrderField[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNMembershipOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMembershipOrderField(ctx context.Context, sel ast.SelectionSet, v coredata.MembershipOrderField) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNMembershipOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMembershipOrderField[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNMembershipOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMembershipOrderField = map[string]coredata.MembershipOrderField{ + "FULL_NAME": coredata.MembershipOrderFieldFullName, + "EMAIL_ADDRESS": coredata.MembershipOrderFieldEmailAddress, + "ROLE": coredata.MembershipOrderFieldRole, + "CREATED_AT": coredata.MembershipOrderFieldCreatedAt, + } + marshalNMembershipOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐMembershipOrderField = map[coredata.MembershipOrderField]string{ + coredata.MembershipOrderFieldFullName: "FULL_NAME", + coredata.MembershipOrderFieldEmailAddress: "EMAIL_ADDRESS", + coredata.MembershipOrderFieldRole: "ROLE", + coredata.MembershipOrderFieldCreatedAt: "CREATED_AT", + } +) + func (ec *executionContext) marshalNNode2githubᚗcomᚋgetproboᚋ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)) { @@ -94155,23 +96950,23 @@ func (ec *executionContext) marshalNPublishDocumentVersionPayload2ᚖgithubᚗco return ec._PublishDocumentVersionPayload(ctx, sel, v) } -func (ec *executionContext) unmarshalNRemoveUserInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRemoveUserInput(ctx context.Context, v any) (types.RemoveUserInput, error) { - res, err := ec.unmarshalInputRemoveUserInput(ctx, v) +func (ec *executionContext) unmarshalNRemoveMemberInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRemoveMemberInput(ctx context.Context, v any) (types.RemoveMemberInput, error) { + res, err := ec.unmarshalInputRemoveMemberInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNRemoveUserPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRemoveUserPayload(ctx context.Context, sel ast.SelectionSet, v types.RemoveUserPayload) graphql.Marshaler { - return ec._RemoveUserPayload(ctx, sel, &v) +func (ec *executionContext) marshalNRemoveMemberPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRemoveMemberPayload(ctx context.Context, sel ast.SelectionSet, v types.RemoveMemberPayload) graphql.Marshaler { + return ec._RemoveMemberPayload(ctx, sel, &v) } -func (ec *executionContext) marshalNRemoveUserPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRemoveUserPayload(ctx context.Context, sel ast.SelectionSet, v *types.RemoveUserPayload) graphql.Marshaler { +func (ec *executionContext) marshalNRemoveMemberPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRemoveMemberPayload(ctx context.Context, sel ast.SelectionSet, v *types.RemoveMemberPayload) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") } return graphql.Null } - return ec._RemoveUserPayload(ctx, sel, v) + return ec._RemoveMemberPayload(ctx, sel, v) } func (ec *executionContext) unmarshalNRequestSignatureInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐRequestSignatureInput(ctx context.Context, v any) (types.RequestSignatureInput, error) { @@ -95744,20 +98539,6 @@ func (ec *executionContext) marshalNUser2ᚖgithubᚗcomᚋgetproboᚋproboᚋpk return ec._User(ctx, sel, v) } -func (ec *executionContext) marshalNUserConnection2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUserConnection(ctx context.Context, sel ast.SelectionSet, v types.UserConnection) graphql.Marshaler { - return ec._UserConnection(ctx, sel, &v) -} - -func (ec *executionContext) marshalNUserConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUserConnection(ctx context.Context, sel ast.SelectionSet, v *types.UserConnection) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._UserConnection(ctx, sel, v) -} - func (ec *executionContext) marshalNUserEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUserEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.UserEdge) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -97879,6 +100660,14 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele return res } +func (ec *executionContext) unmarshalOInvitationOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInvitationOrder(ctx context.Context, v any) (*types.InvitationOrder, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputInvitationOrder(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalOMeasure2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMeasure(ctx context.Context, sel ast.SelectionSet, v *types.Measure) graphql.Marshaler { if v == nil { return graphql.Null @@ -97936,6 +100725,14 @@ var ( } ) +func (ec *executionContext) unmarshalOMembershipOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐMembershipOrderBy(ctx context.Context, v any) (*types.MembershipOrderBy, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputMembershipOrder(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalONonconformityFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐNonconformityFilter(ctx context.Context, v any) (*types.NonconformityFilter, error) { if v == nil { return nil, nil @@ -98524,14 +101321,6 @@ func (ec *executionContext) marshalOUpload2ᚖgithubᚗcomᚋ99designsᚋgqlgen return res } -func (ec *executionContext) unmarshalOUserOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUserOrderBy(ctx context.Context, v any) (*types.UserOrderBy, error) { - if v == nil { - return nil, nil - } - res, err := ec.unmarshalInputUserOrder(ctx, v) - return &res, graphql.ErrorOnPath(ctx, err) -} - func (ec *executionContext) marshalOVendorBusinessAssociateAgreement2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVendorBusinessAssociateAgreement(ctx context.Context, sel ast.SelectionSet, v *types.VendorBusinessAssociateAgreement) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/server/api/console/v1/sign_in_handler.go b/pkg/server/api/console/v1/sign_in_handler.go index c8512b1de..f0e870a9d 100644 --- a/pkg/server/api/console/v1/sign_in_handler.go +++ b/pkg/server/api/console/v1/sign_in_handler.go @@ -23,7 +23,7 @@ import ( "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/securecookie" - "github.com/getprobo/probo/pkg/usrmgr" + "github.com/getprobo/probo/pkg/auth" "go.gearno.de/kit/httpserver" ) @@ -46,7 +46,7 @@ type ( } ) -func SignInHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { +func SignInHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req SignInRequest @@ -55,9 +55,9 @@ func SignInHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFu return } - user, session, err := usrmgrSvc.SignIn(r.Context(), req.Email, req.Password) + session, user, err := authSvc.SignIn(r.Context(), req.Email, req.Password) if err != nil { - var ErrInvalidCredentials *usrmgr.ErrInvalidCredentials + var ErrInvalidCredentials *auth.ErrInvalidCredentials if errors.As(err, &ErrInvalidCredentials) { httpserver.RenderError(w, http.StatusUnauthorized, err) return diff --git a/pkg/server/api/console/v1/sign_out_handler.go b/pkg/server/api/console/v1/sign_out_handler.go index 02365d6ea..5e9302f27 100644 --- a/pkg/server/api/console/v1/sign_out_handler.go +++ b/pkg/server/api/console/v1/sign_out_handler.go @@ -20,11 +20,11 @@ import ( "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/securecookie" - "github.com/getprobo/probo/pkg/usrmgr" + "github.com/getprobo/probo/pkg/auth" "go.gearno.de/kit/httpserver" ) -func SignOutHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { +func SignOutHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sessionID, err := securecookie.Get(r, securecookie.DefaultConfig( @@ -42,7 +42,7 @@ func SignOutHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerF return } - err = usrmgrSvc.SignOut(r.Context(), gid) + err = authSvc.SignOut(r.Context(), gid) if err != nil { panic(fmt.Errorf("cannot sign out: %w", err)) } diff --git a/pkg/server/api/console/v1/sign_up_handler.go b/pkg/server/api/console/v1/sign_up_handler.go index b7069bd6b..d70696082 100644 --- a/pkg/server/api/console/v1/sign_up_handler.go +++ b/pkg/server/api/console/v1/sign_up_handler.go @@ -20,9 +20,8 @@ import ( "fmt" "net/http" - "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/auth" "github.com/getprobo/probo/pkg/securecookie" - "github.com/getprobo/probo/pkg/usrmgr" "go.gearno.de/kit/httpserver" ) @@ -38,7 +37,7 @@ type ( } ) -func SignUpHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc { +func SignUpHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req SignUpRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -46,20 +45,20 @@ func SignUpHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFu return } - user, session, err := usrmgrSvc.SignUp( + user, session, err := authSvc.SignUp( r.Context(), req.Email, req.Password, req.FullName, ) if err != nil { - var errUserAlreadyExists *coredata.ErrUserAlreadyExists + var errUserAlreadyExists *auth.ErrUserAlreadyExists if errors.As(err, &errUserAlreadyExists) { httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot register user: %w", err)) return } - var errSignupDisabled *usrmgr.ErrSignupDisabled + var errSignupDisabled *auth.ErrSignupDisabled if errors.As(err, &errSignupDisabled) { httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot register user: %w", err)) return diff --git a/pkg/server/api/console/v1/types/invitation.go b/pkg/server/api/console/v1/types/invitation.go new file mode 100644 index 000000000..d2954c24f --- /dev/null +++ b/pkg/server/api/console/v1/types/invitation.go @@ -0,0 +1,52 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/page" +) + +func NewInvitationConnection(p *page.Page[*coredata.Invitation, coredata.InvitationOrderField]) *InvitationConnection { + var edges = make([]*InvitationEdge, len(p.Data)) + + for i := range edges { + edges[i] = NewInvitationEdge(p.Data[i], p.Cursor.OrderBy.Field) + } + + return &InvitationConnection{ + Edges: edges, + PageInfo: NewPageInfo(p), + } +} + +func NewInvitationEdge(invitation *coredata.Invitation, orderBy coredata.InvitationOrderField) *InvitationEdge { + return &InvitationEdge{ + Cursor: invitation.CursorKey(orderBy), + Node: NewInvitation(invitation), + } +} + +func NewInvitation(i *coredata.Invitation) *Invitation { + return &Invitation{ + ID: i.ID, + Email: i.Email, + FullName: i.FullName, + Role: i.Role, + ExpiresAt: i.ExpiresAt, + AcceptedAt: i.AcceptedAt, + CreatedAt: i.CreatedAt, + } +} diff --git a/pkg/server/api/console/v1/types/membership.go b/pkg/server/api/console/v1/types/membership.go new file mode 100644 index 000000000..9d512890e --- /dev/null +++ b/pkg/server/api/console/v1/types/membership.go @@ -0,0 +1,57 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/page" +) + +type ( + MembershipOrderBy OrderBy[coredata.MembershipOrderField] +) + +func NewMembershipConnection(p *page.Page[*coredata.Membership, coredata.MembershipOrderField]) *MembershipConnection { + var edges = make([]*MembershipEdge, len(p.Data)) + + for i := range edges { + edges[i] = NewMembershipEdge(p.Data[i], p.Cursor.OrderBy.Field) + } + + return &MembershipConnection{ + Edges: edges, + PageInfo: NewPageInfo(p), + } +} + +func NewMembershipEdge(membership *coredata.Membership, orderBy coredata.MembershipOrderField) *MembershipEdge { + return &MembershipEdge{ + Cursor: membership.CursorKey(orderBy), + Node: NewMembership(membership), + } +} + +func NewMembership(m *coredata.Membership) *Membership { + return &Membership{ + ID: m.ID, + UserID: m.UserID, + OrganizationID: m.OrganizationID, + Role: m.Role, + FullName: m.FullName, + EmailAddress: m.EmailAddress, + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt, + } +} diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index acfefc058..c4072dd2a 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -807,6 +807,14 @@ type DeleteFrameworkPayload struct { DeletedFrameworkID gid.GID `json:"deletedFrameworkId"` } +type DeleteInvitationInput struct { + InvitationID gid.GID `json:"invitationId"` +} + +type DeleteInvitationPayload struct { + DeletedInvitationID gid.GID `json:"deletedInvitationId"` +} + type DeleteMeasureInput struct { MeasureID gid.GID `json:"measureId"` } @@ -1191,6 +1199,35 @@ type ImportMeasurePayload struct { MeasureEdges []*MeasureEdge `json:"measureEdges"` } +type Invitation struct { + ID gid.GID `json:"id"` + Email string `json:"email"` + FullName string `json:"fullName"` + Role string `json:"role"` + ExpiresAt time.Time `json:"expiresAt"` + AcceptedAt *time.Time `json:"acceptedAt,omitempty"` + CreatedAt time.Time `json:"createdAt"` +} + +func (Invitation) IsNode() {} +func (this Invitation) GetID() gid.GID { return this.ID } + +type InvitationConnection struct { + TotalCount int `json:"totalCount"` + Edges []*InvitationEdge `json:"edges"` + PageInfo *PageInfo `json:"pageInfo"` +} + +type InvitationEdge struct { + Cursor page.CursorKey `json:"cursor"` + Node *Invitation `json:"node"` +} + +type InvitationOrder struct { + Direction page.OrderDirection `json:"direction"` + Field coredata.InvitationOrderField `json:"field"` +} + type InviteUserInput struct { OrganizationID gid.GID `json:"organizationId"` Email string `json:"email"` @@ -1199,7 +1236,7 @@ type InviteUserInput struct { } type InviteUserPayload struct { - Success bool `json:"success"` + InvitationEdge *InvitationEdge `json:"invitationEdge"` } type Measure struct { @@ -1229,6 +1266,31 @@ type MeasureFilter struct { State *coredata.MeasureState `json:"state,omitempty"` } +type Membership struct { + ID gid.GID `json:"id"` + UserID gid.GID `json:"userID"` + OrganizationID gid.GID `json:"organizationID"` + Role string `json:"role"` + FullName string `json:"fullName"` + EmailAddress string `json:"emailAddress"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (Membership) IsNode() {} +func (this Membership) GetID() gid.GID { return this.ID } + +type MembershipConnection struct { + TotalCount int `json:"totalCount"` + Edges []*MembershipEdge `json:"edges"` + PageInfo *PageInfo `json:"pageInfo"` +} + +type MembershipEdge struct { + Cursor page.CursorKey `json:"cursor"` + Node *Membership `json:"node"` +} + type Mutation struct { } @@ -1301,7 +1363,8 @@ type Organization struct { WebsiteURL *string `json:"websiteUrl,omitempty"` Email *string `json:"email,omitempty"` HeadquarterAddress *string `json:"headquarterAddress,omitempty"` - Users *UserConnection `json:"users"` + Memberships *MembershipConnection `json:"memberships"` + Invitations *InvitationConnection `json:"invitations"` Connectors *ConnectorConnection `json:"connectors"` Frameworks *FrameworkConnection `json:"frameworks"` Controls *ControlConnection `json:"controls"` @@ -1424,12 +1487,12 @@ type PublishDocumentVersionPayload struct { type Query struct { } -type RemoveUserInput struct { +type RemoveMemberInput struct { OrganizationID gid.GID `json:"organizationId"` - UserID gid.GID `json:"userId"` + MemberID gid.GID `json:"memberId"` } -type RemoveUserPayload struct { +type RemoveMemberPayload struct { Success bool `json:"success"` } @@ -2064,8 +2127,9 @@ func (User) IsNode() {} func (this User) GetID() gid.GID { return this.ID } type UserConnection struct { - Edges []*UserEdge `json:"edges"` - PageInfo *PageInfo `json:"pageInfo"` + TotalCount int `json:"totalCount"` + Edges []*UserEdge `json:"edges"` + PageInfo *PageInfo `json:"pageInfo"` } type UserEdge struct { diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 1455a8111..5be863675 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -12,6 +12,7 @@ import ( "fmt" "time" + "github.com/getprobo/probo/pkg/authz" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/page" @@ -890,6 +891,27 @@ func (r *frameworkConnectionResolver) TotalCount(ctx context.Context, obj *types panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver)) } +// TotalCount is the resolver for the totalCount field. +func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *types.InvitationConnection) (int, error) { + currentUser := UserFromContext(ctx) + if currentUser == nil { + return 0, fmt.Errorf("no authenticated user") + } + + memberships, err := r.authzSvc.GetAllUserOrganizations(ctx, currentUser.ID) + if err != nil || len(memberships) == 0 { + return 0, fmt.Errorf("user has no organization memberships") + } + + orgID := memberships[0].ID + count, err := r.authzSvc.CountOrganizationInvitations(ctx, orgID) + if err != nil { + return 0, fmt.Errorf("failed to count invitations: %w", err) + } + + return count, nil +} + // Evidences is the resolver for the evidences field. func (r *measureResolver) Evidences(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.EvidenceOrderBy) (*types.EvidenceConnection, error) { prb := r.ProboService(ctx, obj.ID.TenantID()) @@ -1028,6 +1050,27 @@ func (r *measureConnectionResolver) TotalCount(ctx context.Context, obj *types.M panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver)) } +// TotalCount is the resolver for the totalCount field. +func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *types.MembershipConnection) (int, error) { + currentUser := UserFromContext(ctx) + if currentUser == nil { + return 0, fmt.Errorf("no authenticated user") + } + + memberships, err := r.authzSvc.GetAllUserOrganizations(ctx, currentUser.ID) + if err != nil || len(memberships) == 0 { + return 0, fmt.Errorf("user has no organization memberships") + } + + orgID := memberships[0].ID + count, err := r.authzSvc.CountOrganizationMemberships(ctx, orgID) + if err != nil { + return 0, fmt.Errorf("failed to count memberships: %w", err) + } + + return count, nil +} + // CreateOrganization is the resolver for the createOrganization field. func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) { prb := r.proboSvc.WithTenant(gid.NewTenantID()) @@ -1042,10 +1085,11 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C return nil, fmt.Errorf("cannot create organization: %w", err) } - err = r.usrmgrSvc.EnrollUserInOrganization( + err = r.authzSvc.AddUserToOrganization( ctx, UserFromContext(ctx).ID, organization.ID, + string(authz.RoleMember), ) if err != nil { return nil, fmt.Errorf("cannot add user to organization: %w", err) @@ -1324,7 +1368,7 @@ func (r *mutationResolver) DeleteTrustCenterReference(ctx context.Context, input // ConfirmEmail is the resolver for the confirmEmail field. func (r *mutationResolver) ConfirmEmail(ctx context.Context, input types.ConfirmEmailInput) (*types.ConfirmEmailPayload, error) { - err := r.usrmgrSvc.ConfirmEmail(ctx, input.Token) + err := r.authSvc.ConfirmEmail(ctx, input.Token) if err != nil { return nil, err @@ -1337,44 +1381,70 @@ func (r *mutationResolver) ConfirmEmail(ctx context.Context, input types.Confirm func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) { user := UserFromContext(ctx) - organizations, err := r.usrmgrSvc.ListOrganizationsForUserID(ctx, user.ID) + organizations, err := r.authzSvc.GetAllUserOrganizations(ctx, user.ID) if err != nil { panic(fmt.Errorf("failed to list organizations for user: %w", err)) } for _, organization := range organizations { if organization.ID == input.OrganizationID { - createPeople := input.CreatePeople - - err := r.usrmgrSvc.InviteUser(ctx, input.OrganizationID, input.FullName, input.Email, createPeople) + invitation, err := r.authzSvc.InviteUserToOrganization(ctx, input.OrganizationID, input.Email, input.FullName, string(authz.RoleMember)) if err != nil { return nil, err } - return &types.InviteUserPayload{Success: true}, nil + if input.CreatePeople { + prb := r.ProboService(ctx, input.OrganizationID.TenantID()) + _, err := prb.Peoples.Create(ctx, probo.CreatePeopleRequest{ + OrganizationID: input.OrganizationID, + FullName: input.FullName, + PrimaryEmailAddress: input.Email, + AdditionalEmailAddresses: []string{}, + Kind: coredata.PeopleKindEmployee, + }) + if err != nil { + return nil, fmt.Errorf("failed to create people record: %w", err) + } + } + + return &types.InviteUserPayload{ + InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt), + }, nil } } return nil, fmt.Errorf("organization not found") } -// RemoveUser is the resolver for the removeUser field. -func (r *mutationResolver) RemoveUser(ctx context.Context, input types.RemoveUserInput) (*types.RemoveUserPayload, error) { +// DeleteInvitation is the resolver for the deleteInvitation field. +func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error) { + err := r.authzSvc.DeleteInvitation(ctx, input.InvitationID) + if err != nil { + return nil, err + } + + return &types.DeleteInvitationPayload{ + DeletedInvitationID: input.InvitationID, + }, nil +} + +// RemoveMember is the resolver for the removeMember field. +func (r *mutationResolver) RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error) { user := UserFromContext(ctx) - organizations, err := r.usrmgrSvc.ListOrganizationsForUserID(ctx, user.ID) + organizations, err := r.authzSvc.GetAllUserOrganizations(ctx, user.ID) if err != nil { panic(fmt.Errorf("failed to list organizations for user: %w", err)) } for _, organization := range organizations { if organization.ID == input.OrganizationID { - err := r.usrmgrSvc.RemoveUser(ctx, input.OrganizationID, input.UserID) + err := r.authzSvc.RemoveMemberFromOrganization(ctx, input.OrganizationID, input.MemberID) if err != nil { return nil, err } - return &types.RemoveUserPayload{Success: true}, nil + return &types.RemoveMemberPayload{Success: true}, nil } } @@ -3526,14 +3596,14 @@ func (r *organizationResolver) HorizontalLogoURL(ctx context.Context, obj *types return prb.Organizations.GenerateHorizontalLogoURL(ctx, obj.ID, 1*time.Hour) } -// Users is the resolver for the users field. -func (r *organizationResolver) Users(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.UserOrderBy) (*types.UserConnection, error) { - pageOrderBy := page.OrderBy[coredata.UserOrderField]{ - Field: coredata.UserOrderFieldCreatedAt, +// Memberships is the resolver for the memberships field. +func (r *organizationResolver) Memberships(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) { + pageOrderBy := page.OrderBy[coredata.MembershipOrderField]{ + Field: coredata.MembershipOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } if orderBy != nil { - pageOrderBy = page.OrderBy[coredata.UserOrderField]{ + pageOrderBy = page.OrderBy[coredata.MembershipOrderField]{ Field: orderBy.Field, Direction: orderBy.Direction, } @@ -3541,12 +3611,35 @@ func (r *organizationResolver) Users(ctx context.Context, obj *types.Organizatio cursor := types.NewCursor(first, after, last, before, pageOrderBy) - page, err := r.usrmgrSvc.ListUsersForTenant(ctx, obj.ID, cursor) + page, err := r.authzSvc.GetAllOrganizationMemberships(ctx, obj.ID, cursor) if err != nil { - panic(fmt.Errorf("cannot list users: %w", err)) + panic(fmt.Errorf("cannot list memberships: %w", err)) } - return types.NewUserConnection(page), nil + return types.NewMembershipConnection(page), nil +} + +// Invitations is the resolver for the invitations field. +func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder) (*types.InvitationConnection, error) { + pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{ + Field: coredata.InvitationOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if orderBy != nil { + pageOrderBy = page.OrderBy[coredata.InvitationOrderField]{ + Field: orderBy.Field, + Direction: orderBy.Direction, + } + } + + cursor := types.NewCursor(first, after, last, before, pageOrderBy) + + page, err := r.authzSvc.GetAllOrganizationInvitations(ctx, obj.ID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list invitations: %w", err)) + } + + return types.NewInvitationConnection(page), nil } // Connectors is the resolver for the connectors field. @@ -4866,6 +4959,27 @@ func (r *userResolver) People(ctx context.Context, obj *types.User, organization return types.NewPeople(people), nil } +// TotalCount is the resolver for the totalCount field. +func (r *userConnectionResolver) TotalCount(ctx context.Context, obj *types.UserConnection) (int, error) { + currentUser := UserFromContext(ctx) + if currentUser == nil { + return 0, fmt.Errorf("no authenticated user") + } + + memberships, err := r.authzSvc.GetAllUserOrganizations(ctx, currentUser.ID) + if err != nil || len(memberships) == 0 { + return 0, fmt.Errorf("user has no organization memberships") + } + + orgID := memberships[0].ID + count, err := r.authzSvc.CountOrganizationMemberships(ctx, orgID) + if err != nil { + return 0, fmt.Errorf("failed to count memberships: %w", err) + } + + return count, nil +} + // Organization is the resolver for the organization field. func (r *vendorResolver) Organization(ctx context.Context, obj *types.Vendor) (*types.Organization, error) { prb := r.ProboService(ctx, obj.ID.TenantID()) @@ -5229,7 +5343,7 @@ func (r *viewerResolver) Organizations(ctx context.Context, obj *types.Viewer, f } cursor := types.NewCursor(first, after, last, before, pageOrderBy) - organizations, err := r.usrmgrSvc.ListOrganizationsForUserIDPaginated(ctx, user.ID, cursor) + organizations, err := r.authzSvc.GetUserOrganizations(ctx, user.ID, cursor) if err != nil { panic(fmt.Errorf("failed to list organizations for user: %w", err)) } @@ -5318,6 +5432,11 @@ func (r *Resolver) FrameworkConnection() schema.FrameworkConnectionResolver { return &frameworkConnectionResolver{r} } +// InvitationConnection returns schema.InvitationConnectionResolver implementation. +func (r *Resolver) InvitationConnection() schema.InvitationConnectionResolver { + return &invitationConnectionResolver{r} +} + // Measure returns schema.MeasureResolver implementation. func (r *Resolver) Measure() schema.MeasureResolver { return &measureResolver{r} } @@ -5326,6 +5445,11 @@ func (r *Resolver) MeasureConnection() schema.MeasureConnectionResolver { return &measureConnectionResolver{r} } +// MembershipConnection returns schema.MembershipConnectionResolver implementation. +func (r *Resolver) MembershipConnection() schema.MembershipConnectionResolver { + return &membershipConnectionResolver{r} +} + // Mutation returns schema.MutationResolver implementation. func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} } @@ -5420,6 +5544,9 @@ func (r *Resolver) TrustCenterReferenceConnection() schema.TrustCenterReferenceC // User returns schema.UserResolver implementation. func (r *Resolver) User() schema.UserResolver { return &userResolver{r} } +// UserConnection returns schema.UserConnectionResolver implementation. +func (r *Resolver) UserConnection() schema.UserConnectionResolver { return &userConnectionResolver{r} } + // Vendor returns schema.VendorResolver implementation. func (r *Resolver) Vendor() schema.VendorResolver { return &vendorResolver{r} } @@ -5476,8 +5603,10 @@ type evidenceConnectionResolver struct{ *Resolver } type fileResolver struct{ *Resolver } type frameworkResolver struct{ *Resolver } type frameworkConnectionResolver struct{ *Resolver } +type invitationConnectionResolver struct{ *Resolver } type measureResolver struct{ *Resolver } type measureConnectionResolver struct{ *Resolver } +type membershipConnectionResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type nonconformityResolver struct{ *Resolver } type nonconformityConnectionResolver struct{ *Resolver } @@ -5502,6 +5631,7 @@ type trustCenterDocumentAccessConnectionResolver struct{ *Resolver } type trustCenterReferenceResolver struct{ *Resolver } type trustCenterReferenceConnectionResolver struct{ *Resolver } type userResolver struct{ *Resolver } +type userConnectionResolver struct{ *Resolver } type vendorResolver struct{ *Resolver } type vendorBusinessAssociateAgreementResolver struct{ *Resolver } type vendorComplianceReportResolver struct{ *Resolver } diff --git a/pkg/server/api/trust/v1/resolver.go b/pkg/server/api/trust/v1/resolver.go index 93931eb0e..82e8581a6 100644 --- a/pkg/server/api/trust/v1/resolver.go +++ b/pkg/server/api/trust/v1/resolver.go @@ -26,17 +26,18 @@ import ( "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/transport" + "github.com/getprobo/probo/pkg/auth" + "github.com/getprobo/probo/pkg/authz" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/probo" console_v1 "github.com/getprobo/probo/pkg/server/api/console/v1" - "github.com/getprobo/probo/pkg/server/api/trust/v1/auth" "github.com/getprobo/probo/pkg/server/api/trust/v1/schema" + "github.com/getprobo/probo/pkg/server/api/trust/v1/trustauth" gqlutils "github.com/getprobo/probo/pkg/server/graphql" "github.com/getprobo/probo/pkg/server/session" "github.com/getprobo/probo/pkg/statelesstoken" "github.com/getprobo/probo/pkg/trust" - "github.com/getprobo/probo/pkg/usrmgr" "github.com/go-chi/chi/v5" "go.gearno.de/kit/log" ) @@ -79,31 +80,32 @@ func UserFromContext(ctx context.Context) *coredata.User { return user } -func TokenAccessFromContext(ctx context.Context) *auth.TokenAccessData { - tokenAccess, _ := ctx.Value(tokenAccessContextKey).(*auth.TokenAccessData) +func TokenAccessFromContext(ctx context.Context) *trustauth.TokenAccessData { + tokenAccess, _ := ctx.Value(tokenAccessContextKey).(*trustauth.TokenAccessData) return tokenAccess } -// UserFromContext implements auth.ContextAccessor interface +// UserFromContext implements trustauth.ContextAccessor interface func (r *Resolver) UserFromContext(ctx context.Context) *coredata.User { return UserFromContext(ctx) } -// TokenAccessFromContext implements auth.ContextAccessor interface -func (r *Resolver) TokenAccessFromContext(ctx context.Context) *auth.TokenAccessData { +// TokenAccessFromContext implements trustauth.ContextAccessor interface +func (r *Resolver) TokenAccessFromContext(ctx context.Context) *trustauth.TokenAccessData { return TokenAccessFromContext(ctx) } func NewMux( logger *log.Logger, - usrmgrSvc *usrmgr.Service, + authSvc *auth.Service, + authzSvc *authz.Service, trustSvc *trust.Service, authCfg console_v1.AuthConfig, trustAuthCfg TrustAuthConfig, ) *chi.Mux { r := chi.NewMux() - r.Handle("/graphql", graphqlHandler(logger, usrmgrSvc, trustSvc, authCfg, trustAuthCfg)) + r.Handle("/graphql", graphqlHandler(logger, authSvc, authzSvc, trustSvc, authCfg, trustAuthCfg)) r.Post("/auth/authenticate", authTokenHandler(trustSvc, trustAuthCfg)) r.Delete("/auth/logout", trustCenterLogoutHandler(authCfg, trustAuthCfg)) @@ -111,7 +113,7 @@ func NewMux( return r } -func graphqlHandler(logger *log.Logger, usrmgrSvc *usrmgr.Service, trustSvc *trust.Service, authCfg console_v1.AuthConfig, trustAuthCfg TrustAuthConfig) http.HandlerFunc { +func graphqlHandler(logger *log.Logger, authSvc *auth.Service, authzSvc *authz.Service, trustSvc *trust.Service, authCfg console_v1.AuthConfig, trustAuthCfg TrustAuthConfig) http.HandlerFunc { resolver := &Resolver{ trustCenterSvc: trustSvc, authCfg: authCfg, @@ -122,7 +124,7 @@ func graphqlHandler(logger *log.Logger, usrmgrSvc *usrmgr.Service, trustSvc *tru Resolvers: resolver, } - c.Directives.MustBeAuthenticated = auth.MustBeAuthenticatedDirective(resolver) + c.Directives.MustBeAuthenticated = trustauth.MustBeAuthenticatedDirective(resolver) es := schema.NewExecutableSchema(c) @@ -137,7 +139,7 @@ func graphqlHandler(logger *log.Logger, usrmgrSvc *usrmgr.Service, trustSvc *tru srv.SetRecoverFunc(gqlutils.RecoverFunc) - return WithSession(usrmgrSvc, trustSvc, authCfg, trustAuthCfg, srv.ServeHTTP) + return WithSession(authSvc, authzSvc, trustSvc, authCfg, trustAuthCfg, srv.ServeHTTP) } func (r *Resolver) RootTrustService(ctx context.Context) *trust.TenantService { @@ -149,14 +151,14 @@ func (r *Resolver) PublicTrustService(ctx context.Context, tenantID gid.TenantID } func (r *Resolver) PrivateTrustService(ctx context.Context, tenantID gid.TenantID) (*trust.TenantService, error) { - if err := auth.ValidateTenantAccess(ctx, r, userTenantContextKey, tenantID); err != nil { + if err := trustauth.ValidateTenantAccess(ctx, r, userTenantContextKey, tenantID); err != nil { return nil, fmt.Errorf("cannot access trust center: %w", err) } return r.trustCenterSvc.WithTenant(tenantID), nil } -func WithSession(usrmgrSvc *usrmgr.Service, trustSvc *trust.Service, authCfg console_v1.AuthConfig, trustAuthCfg TrustAuthConfig, next http.HandlerFunc) http.HandlerFunc { +func WithSession(authSvc *auth.Service, authzSvc *authz.Service, trustSvc *trust.Service, authCfg console_v1.AuthConfig, trustAuthCfg TrustAuthConfig, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -168,9 +170,9 @@ func WithSession(usrmgrSvc *usrmgr.Service, trustSvc *trust.Service, authCfg con return } - if authCtx := trySessionAuth(ctx, w, r, usrmgrSvc, authCfg); authCtx != nil { + if authCtx := trySessionAuth(ctx, w, r, authSvc, authzSvc, authCfg); authCtx != nil { next(w, r.WithContext(authCtx)) - updateSessionIfNeeded(authCtx, usrmgrSvc) + updateSessionIfNeeded(authCtx, authSvc) return } @@ -178,7 +180,7 @@ func WithSession(usrmgrSvc *usrmgr.Service, trustSvc *trust.Service, authCfg con } } -func trySessionAuth(ctx context.Context, w http.ResponseWriter, r *http.Request, usrmgrSvc *usrmgr.Service, authCfg console_v1.AuthConfig) context.Context { +func trySessionAuth(ctx context.Context, w http.ResponseWriter, r *http.Request, authSvc *auth.Service, authzSvc *authz.Service, authCfg console_v1.AuthConfig) context.Context { sessionAuthCfg := session.AuthConfig{ CookieName: authCfg.CookieName, CookieSecret: authCfg.CookieSecret, @@ -199,7 +201,7 @@ func trySessionAuth(ctx context.Context, w http.ResponseWriter, r *http.Request, }, } - authResult := session.TryAuth(ctx, w, r, usrmgrSvc, sessionAuthCfg, errorHandler) + authResult := session.TryAuth(ctx, w, r, authSvc, authzSvc, sessionAuthCfg, errorHandler) if authResult == nil { return nil } @@ -235,7 +237,7 @@ func tryTokenAuth(ctx context.Context, w http.ResponseWriter, r *http.Request, t return nil } - tokenAccess := &auth.TokenAccessData{ + tokenAccess := &trustauth.TokenAccessData{ TrustCenterID: basicPayload.Data.TrustCenterID, Email: basicPayload.Data.Email, TenantID: tenantID, @@ -258,10 +260,10 @@ func clearTokenCookie(w http.ResponseWriter, trustAuthCfg TrustAuthConfig) { }) } -func updateSessionIfNeeded(ctx context.Context, usrmgrSvc *usrmgr.Service) { +func updateSessionIfNeeded(ctx context.Context, authSvc *auth.Service) { session := SessionFromContext(ctx) if session != nil { - if err := usrmgrSvc.UpdateSession(ctx, session); err != nil { + if _, err := authSvc.UpdateSession(ctx, session.ID); err != nil { panic(fmt.Errorf("failed to update session: %w", err)) } } diff --git a/pkg/server/api/trust/v1/auth/auth.go b/pkg/server/api/trust/v1/trustauth/auth.go similarity index 99% rename from pkg/server/api/trust/v1/auth/auth.go rename to pkg/server/api/trust/v1/trustauth/auth.go index bcbaea4d5..8d16c91d9 100644 --- a/pkg/server/api/trust/v1/auth/auth.go +++ b/pkg/server/api/trust/v1/trustauth/auth.go @@ -12,7 +12,7 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -package auth +package trustauth import ( "context" diff --git a/pkg/server/server.go b/pkg/server/server.go index c65ab426d..5b8794a98 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -21,6 +21,8 @@ import ( "strings" "github.com/getprobo/probo/pkg/agents" + "github.com/getprobo/probo/pkg/auth" + "github.com/getprobo/probo/pkg/authz" "github.com/getprobo/probo/pkg/connector" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/probo" @@ -30,7 +32,6 @@ import ( "github.com/getprobo/probo/pkg/server/trust" "github.com/getprobo/probo/pkg/server/web" trust_pkg "github.com/getprobo/probo/pkg/trust" - "github.com/getprobo/probo/pkg/usrmgr" "github.com/go-chi/chi/v5" "go.gearno.de/kit/log" ) @@ -40,9 +41,10 @@ type Config struct { AllowedOrigins []string ExtraHeaderFields map[string]string Probo *probo.Service - Usrmgr *usrmgr.Service + Auth *auth.Service + Authz *authz.Service Trust *trust_pkg.Service - Auth api.ConsoleAuthConfig + ConsoleAuth api.ConsoleAuthConfig TrustAuth api.TrustAuthConfig ConnectorRegistry *connector.ConnectorRegistry Agent *agents.Agent @@ -68,9 +70,10 @@ func NewServer(cfg Config) (*Server, error) { apiCfg := api.Config{ AllowedOrigins: cfg.AllowedOrigins, Probo: cfg.Probo, - Usrmgr: cfg.Usrmgr, - Trust: cfg.Trust, Auth: cfg.Auth, + Authz: cfg.Authz, + Trust: cfg.Trust, + ConsoleAuth: cfg.ConsoleAuth, TrustAuth: cfg.TrustAuth, ConnectorRegistry: cfg.ConnectorRegistry, SafeRedirect: cfg.SafeRedirect, diff --git a/pkg/server/session/session.go b/pkg/server/session/session.go index 68b9c9a04..5da667e28 100644 --- a/pkg/server/session/session.go +++ b/pkg/server/session/session.go @@ -19,10 +19,11 @@ import ( "errors" "net/http" + "github.com/getprobo/probo/pkg/authz" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/securecookie" - "github.com/getprobo/probo/pkg/usrmgr" + "github.com/getprobo/probo/pkg/auth" ) type AuthConfig struct { @@ -48,7 +49,8 @@ func TryAuth( ctx context.Context, w http.ResponseWriter, r *http.Request, - usrmgrSvc *usrmgr.Service, + authSvc *auth.Service, + authzSvc *authz.Service, authCfg AuthConfig, errorHandler ErrorHandler, ) *AuthResult { @@ -71,7 +73,7 @@ func TryAuth( return nil } - session, err := usrmgrSvc.GetSession(ctx, sessionID) + session, err := authSvc.GetSession(ctx, sessionID) if err != nil { if errorHandler.OnSessionError != nil { errorHandler.OnSessionError(w, authCfg) @@ -79,7 +81,7 @@ func TryAuth( return nil } - user, err := usrmgrSvc.GetUserBySession(ctx, sessionID) + user, err := authSvc.GetUserBySession(ctx, sessionID) if err != nil { if errorHandler.OnUserError != nil { errorHandler.OnUserError(w, authCfg) @@ -87,7 +89,7 @@ func TryAuth( return nil } - tenantIDs, err := usrmgrSvc.ListTenantsForUserID(ctx, user.ID) + organizations, err := authzSvc.GetAllUserOrganizations(ctx, user.ID) if err != nil { if errorHandler.OnTenantError != nil { errorHandler.OnTenantError(err) @@ -95,6 +97,11 @@ func TryAuth( return nil } + tenantIDs := make([]gid.TenantID, len(organizations)) + for i, org := range organizations { + tenantIDs[i] = org.ID.TenantID() + } + return &AuthResult{ Session: session, User: user, diff --git a/pkg/trust/service.go b/pkg/trust/service.go index f261b034e..85cc55b2d 100644 --- a/pkg/trust/service.go +++ b/pkg/trust/service.go @@ -16,13 +16,13 @@ package trust import ( "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/getprobo/probo/pkg/auth" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/crypto/cipher" "github.com/getprobo/probo/pkg/filemanager" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/html2pdf" "github.com/getprobo/probo/pkg/probo" - "github.com/getprobo/probo/pkg/usrmgr" "go.gearno.de/kit/pg" ) @@ -34,7 +34,7 @@ type ( proboSvc *probo.Service encryptionKey cipher.EncryptionKey tokenSecret string - usrmgr *usrmgr.Service + auth *auth.Service html2pdfConverter *html2pdf.Converter fileManager *filemanager.Service } @@ -47,7 +47,7 @@ type ( proboSvc *probo.Service encryptionKey cipher.EncryptionKey tokenSecret string - usrmgr *usrmgr.Service + auth *auth.Service html2pdfConverter *html2pdf.Converter fileManager *filemanager.Service TrustCenters *TrustCenterService @@ -68,7 +68,7 @@ func NewService( bucket string, encryptionKey cipher.EncryptionKey, tokenSecret string, - usrmgr *usrmgr.Service, + auth *auth.Service, html2pdfConverter *html2pdf.Converter, fileManagerService *filemanager.Service, ) *Service { @@ -78,7 +78,7 @@ func NewService( bucket: bucket, encryptionKey: encryptionKey, tokenSecret: tokenSecret, - usrmgr: usrmgr, + auth: auth, html2pdfConverter: html2pdfConverter, fileManager: fileManagerService, } @@ -93,7 +93,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { proboSvc: s.proboSvc, encryptionKey: s.encryptionKey, tokenSecret: s.tokenSecret, - usrmgr: s.usrmgr, + auth: s.auth, html2pdfConverter: s.html2pdfConverter, fileManager: s.fileManager, } @@ -103,7 +103,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { tenantService.Audits = &AuditService{svc: tenantService} tenantService.Vendors = &VendorService{svc: tenantService} tenantService.Frameworks = &FrameworkService{svc: tenantService} - tenantService.TrustCenterAccesses = &TrustCenterAccessService{svc: tenantService, usrmgr: s.usrmgr} + tenantService.TrustCenterAccesses = &TrustCenterAccessService{svc: tenantService, auth: s.auth} tenantService.TrustCenterReferences = &TrustCenterReferenceService{svc: tenantService} tenantService.Reports = &ReportService{svc: tenantService} tenantService.Organizations = &OrganizationService{svc: tenantService} diff --git a/pkg/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index 1986c2ba0..a93be9876 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -24,14 +24,14 @@ import ( "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" - "github.com/getprobo/probo/pkg/usrmgr" + "github.com/getprobo/probo/pkg/auth" "go.gearno.de/kit/pg" ) type ( TrustCenterAccessService struct { svc *TenantService - usrmgr *usrmgr.Service + auth *auth.Service } RequestTrustCenterAccessRequest struct { diff --git a/pkg/usrmgr/usrmgr.go b/pkg/usrmgr/usrmgr.go deleted file mode 100644 index 4f9e0eb18..000000000 --- a/pkg/usrmgr/usrmgr.go +++ /dev/null @@ -1,953 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package usrmgr - -import ( - "context" - "errors" - "fmt" - "net/mail" - "net/url" - "time" - - "github.com/getprobo/probo/pkg/coredata" - "github.com/getprobo/probo/pkg/crypto/passwdhash" - "github.com/getprobo/probo/pkg/gid" - "github.com/getprobo/probo/pkg/page" - "github.com/getprobo/probo/pkg/statelesstoken" - "go.gearno.de/kit/pg" -) - -type ( - Service struct { - pg *pg.Client - hp *passwdhash.Profile - hostname string - tokenSecret string - disableSignup bool - invitationTokenValidity time.Duration - } - - ErrInvalidCredentials struct { - message string - } - - ErrInvalidEmail struct { - email string - } - - ErrInvalidPassword struct { - minLength int - maxLength int - } - - ErrInvalidFullName struct { - fullName string - } - - ErrUserAlreadyExists struct { - message string - } - - ErrSessionNotFound struct { - message string - } - - ErrSessionExpired struct { - message string - } - - ErrInvalidTokenType struct { - message string - } - - ErrSignupDisabled struct{} - - EmailConfirmationData struct { - UserID gid.GID `json:"uid"` - Email string `json:"email"` - } - - InvitationData struct { - OrganizationID gid.GID `json:"organization_id"` - Email string `json:"email"` - FullName string `json:"full_name"` - CreatePeople bool `json:"create_people"` - } - - PasswordResetData struct { - Email string `json:"email"` - } -) - -// Token types -const ( - TokenTypeEmailConfirmation = "email_confirmation" - TokenTypeOrganizationInvitation = "organization_invitation" - TokenTypePasswordReset = "password_reset" -) - -var ( - signupEmailSubject = "Confirm your email address" - signupEmailTemplate = ` - Thanks joining Probo! - Please confirm your email address by clicking the link below[1] - - [1] %s - ` - - invitationEmailSubject = "Join Probo" - invitationEmailTemplate = ` - You have been invited to join Probo! - Please click the link below to sign up[1] - - [1] %s - ` - - passwordResetEmailSubject = "Reset your password" - passwordResetEmailTemplate = ` - You have requested a password reset for your Probo account. - Please click the link below to reset your password[1] - - If you did not request this password reset, please ignore this email. - - [1] %s - ` -) - -func (e ErrInvalidCredentials) Error() string { - return e.message -} - -func (e ErrUserAlreadyExists) Error() string { - return e.message -} - -func (e ErrSessionNotFound) Error() string { - return e.message -} - -func (e ErrSessionExpired) Error() string { - return e.message -} - -func (e ErrInvalidEmail) Error() string { - return fmt.Sprintf("invalid email: %s", e.email) -} - -func (e ErrInvalidPassword) Error() string { - return fmt.Sprintf("invalid password: the length must be between %d and %d characters", e.minLength, e.maxLength) -} - -func (e ErrInvalidFullName) Error() string { - return fmt.Sprintf("invalid full name: %s", e.fullName) -} - -func (e ErrInvalidTokenType) Error() string { - return e.message -} - -func (e ErrSignupDisabled) Error() string { - return "signup is disabled, contact the owner of the Probo instance" -} - -func NewService( - ctx context.Context, - pgClient *pg.Client, - hp *passwdhash.Profile, - tokenSecret string, - hostname string, - disableSignup bool, - invitationTokenValidity time.Duration, -) (*Service, error) { - return &Service{ - pg: pgClient, - hp: hp, - hostname: hostname, - tokenSecret: tokenSecret, - disableSignup: disableSignup, - invitationTokenValidity: invitationTokenValidity, - }, nil -} - -func (s Service) ForgetPassword( - ctx context.Context, - email string, -) error { - // Always generate a new token to avoid timing attacks and leaking information - // about existing emails - passwordResetToken, err := statelesstoken.NewToken( - s.tokenSecret, - TokenTypePasswordReset, - 1*time.Hour, - PasswordResetData{Email: email}, - ) - if err != nil { - return fmt.Errorf("cannot generate password reset token: %w", err) - } - - resetPasswordUrl := url.URL{ - Scheme: "https", - Host: s.hostname, - Path: "/auth/reset-password", - RawQuery: url.Values{ - "token": []string{passwordResetToken}, - }.Encode(), - } - - user := &coredata.User{} - - err = s.pg.WithTx( - ctx, - func(tx pg.Conn) error { - if err := user.LoadByEmail(ctx, tx, email); err != nil { - var errUserNotFound *coredata.ErrUserNotFound - - if errors.As(err, &errUserNotFound) { - // We don't want to leak information about existing emails - // Return success even if the email doesn't exist - return nil - } - return fmt.Errorf("cannot load user by %q email: %w", email, err) - } - - resetPasswordEmail := coredata.NewEmail( - user.FullName, - user.EmailAddress, - passwordResetEmailSubject, - fmt.Sprintf(passwordResetEmailTemplate, resetPasswordUrl.String()), - ) - - if err := resetPasswordEmail.Insert(ctx, tx); err != nil { - return fmt.Errorf("cannot insert email: %w", err) - } - - return nil - }, - ) - if err != nil { - return err - } - - return nil -} - -func (s Service) SignUp( - ctx context.Context, - email, password, fullName string, -) (*coredata.User, *coredata.Session, error) { - if s.disableSignup { - return nil, nil, &ErrSignupDisabled{} - } - - if _, err := mail.ParseAddress(email); err != nil { - return nil, nil, &ErrInvalidEmail{email} - } - - if len(password) < 8 || len(password) > 128 { - return nil, nil, &ErrInvalidPassword{minLength: 8, maxLength: 128} - } - - if fullName == "" { - return nil, nil, &ErrInvalidFullName{fullName} - } - - hashedPassword, err := s.hp.HashPassword([]byte(password)) - if err != nil { - return nil, nil, fmt.Errorf("cannot hash password: %w", err) - } - - now := time.Now() - user := &coredata.User{ - ID: gid.New(gid.NilTenant, coredata.UserEntityType), - EmailAddress: email, - HashedPassword: hashedPassword, - FullName: fullName, - CreatedAt: now, - UpdatedAt: now, - } - - session := &coredata.Session{ - ID: gid.New(gid.NilTenant, coredata.SessionEntityType), - UserID: user.ID, - ExpiredAt: now.Add(24 * time.Hour), - CreatedAt: now, - UpdatedAt: now, - } - - confirmationToken, err := statelesstoken.NewToken( - s.tokenSecret, - TokenTypeEmailConfirmation, - 1*time.Hour, - EmailConfirmationData{UserID: user.ID, Email: user.EmailAddress}, - ) - if err != nil { - return nil, nil, fmt.Errorf("cannot generate confirmation token: %w", err) - } - - confirmationEmailUrl := url.URL{ - Scheme: "https", - Host: s.hostname, - Path: "/auth/confirm-email", - RawQuery: url.Values{ - "token": []string{confirmationToken}, - }.Encode(), - } - - confirmationEmail := coredata.NewEmail( - user.FullName, - user.EmailAddress, - signupEmailSubject, - fmt.Sprintf(signupEmailTemplate, confirmationEmailUrl.String()), - ) - - err = s.pg.WithTx( - ctx, - func(tx pg.Conn) error { - if err := user.Insert(ctx, tx); err != nil { - return fmt.Errorf("cannot insert user: %w", err) - } - - if err := session.Insert(ctx, tx); err != nil { - return fmt.Errorf("cannot insert session: %w", err) - } - - if err := confirmationEmail.Insert(ctx, tx); err != nil { - return fmt.Errorf("cannot insert email: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, nil, err - } - - return user, session, nil -} - -func (s Service) SignIn( - ctx context.Context, - email, password string, -) (*coredata.User, *coredata.Session, error) { - now := time.Now() - user := &coredata.User{} - session := &coredata.Session{ - ID: gid.New(gid.NilTenant, coredata.SessionEntityType), - UserID: gid.Nil, - ExpiredAt: now.Add(24 * time.Hour), - CreatedAt: now, - UpdatedAt: now, - } - - if len(password) < 8 || len(password) > 128 { - return nil, nil, &ErrInvalidPassword{minLength: 8, maxLength: 128} - } - - err := s.pg.WithTx( - ctx, - func(tx pg.Conn) error { - if err := user.LoadByEmail(ctx, tx, email); err != nil { - _, _ = s.hp.ComparePasswordAndHash([]byte("this-compare-should-never-succeed"), []byte("it-just-to-prevent-timing-attack")) - - var errUserNotFound *coredata.ErrUserNotFound - - if errors.As(err, &errUserNotFound) { - return &ErrInvalidCredentials{message: "invalid email or password"} - } - - return fmt.Errorf("cannot load user by email: %w", err) - } - - ok, err := s.hp.ComparePasswordAndHash([]byte(password), user.HashedPassword) - if err != nil { - return fmt.Errorf("cannot compare password: %w", err) - } - - if !ok { - return &ErrInvalidCredentials{message: "invalid email or password"} - } - - session.UserID = user.ID - - if err := session.Insert(ctx, tx); err != nil { - return fmt.Errorf("cannot insert session: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, nil, err - } - - return user, session, nil -} - -func (s Service) SignOut( - ctx context.Context, - sessionID gid.GID, -) error { - return s.pg.WithConn( - ctx, - func(tx pg.Conn) error { - err := coredata.DeleteSession(ctx, tx, sessionID) - if err != nil { - return fmt.Errorf("cannot delete session: %w", err) - } - - return nil - }, - ) -} - -func (s Service) GetSession( - ctx context.Context, - sessionID gid.GID, -) (*coredata.Session, error) { - session := &coredata.Session{} - - err := s.pg.WithTx( - ctx, - func(tx pg.Conn) error { - if err := session.LoadByID(ctx, tx, sessionID); err != nil { - return &ErrSessionNotFound{message: "session not found"} - } - - if time.Now().After(session.ExpiredAt) { - if err := coredata.DeleteSession(ctx, tx, sessionID); err != nil { - return fmt.Errorf("cannot delete expired session: %w", err) - } - return &ErrSessionExpired{message: "session expired"} - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return session, nil -} - -func (s Service) GetUserByID( - ctx context.Context, - userID gid.GID, -) (*coredata.User, error) { - user := &coredata.User{} - - err := s.pg.WithTx( - ctx, - func(tx pg.Conn) error { - if err := user.LoadByID(ctx, tx, userID); err != nil { - return fmt.Errorf("user not found: %w", err) - } - return nil - }, - ) - - if err != nil { - return nil, err - } - - return user, nil -} - -func (s Service) GetUserBySession( - ctx context.Context, - sessionID gid.GID, -) (*coredata.User, error) { - session, err := s.GetSession(ctx, sessionID) - if err != nil { - return nil, err - } - - return s.GetUserByID(ctx, session.UserID) -} - -func (s Service) ListOrganizationsForUserID( - ctx context.Context, - userID gid.GID, -) (coredata.Organizations, error) { - - uos := coredata.UserOrganizations{} - organizations := []*coredata.Organization{} - - err := s.pg.WithConn( - ctx, - func(conn pg.Conn) error { - if err := uos.ForUserID(ctx, conn, userID); err != nil { - return fmt.Errorf("cannot list user organizations: %w", err) - } - - for _, uo := range uos { - scope := coredata.NewScope(uo.OrganizationID.TenantID()) - organization := &coredata.Organization{} - if err := organization.LoadByID(ctx, conn, scope, uo.OrganizationID); err != nil { - return fmt.Errorf("cannot load organization by id: %w", err) - } - organizations = append(organizations, organization) - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return organizations, nil -} - -// Tenant id scope is not applied in this functions because we want to access all user's organizations. -func (s Service) ListOrganizationsForUserIDPaginated( - ctx context.Context, - userID gid.GID, - cursor *page.Cursor[coredata.OrganizationOrderField], -) (coredata.Organizations, error) { - organizations := coredata.Organizations{} - - err := s.pg.WithConn( - ctx, - func(conn pg.Conn) error { - err := organizations.ListForUserID(ctx, conn, userID, cursor) - if err != nil { - return fmt.Errorf("cannot list user organizations: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return organizations, nil -} - -func (s Service) ListTenantsForUserID( - ctx context.Context, - userID gid.GID, -) ([]gid.TenantID, error) { - - uos := coredata.UserOrganizations{} - - err := s.pg.WithConn( - ctx, - func(tx pg.Conn) error { - return uos.ForUserID(ctx, tx, userID) - }, - ) - - if err != nil { - return nil, err - } - - tenantIDs := make([]gid.TenantID, len(uos)) - for _, uo := range uos { - tenantIDs = append(tenantIDs, uo.OrganizationID.TenantID()) - } - - return tenantIDs, nil -} - -func (s Service) EnrollUserInOrganization( - ctx context.Context, - userID gid.GID, - organizationID gid.GID, -) error { - - uo := coredata.UserOrganization{ - UserID: userID, - OrganizationID: organizationID, - CreatedAt: time.Now(), - } - - return s.pg.WithConn( - ctx, - func(tx pg.Conn) error { - return uo.Insert(ctx, tx) - }, - ) -} - -func (s Service) UpdateSession( - ctx context.Context, - session *coredata.Session, -) error { - session.UpdatedAt = time.Now() - session.ExpiredAt = time.Now().Add(24 * time.Hour) - - return s.pg.WithTx( - ctx, - func(tx pg.Conn) error { - return session.Update(ctx, tx) - }, - ) -} - -func (s Service) ConfirmEmail(ctx context.Context, tokenString string) error { - token, err := statelesstoken.ValidateToken[EmailConfirmationData]( - s.tokenSecret, - TokenTypeEmailConfirmation, - tokenString, - ) - if err != nil { - return fmt.Errorf("cannot validate email confirmation token: %w", err) - } - - return s.pg.WithTx( - ctx, - func(tx pg.Conn) error { - user := &coredata.User{} - - if err := user.LoadByID(ctx, tx, token.Data.UserID); err != nil { - return fmt.Errorf("user not found: %w", err) - } - - if user.EmailAddress != token.Data.Email { - return fmt.Errorf("token email does not match user email") - } - - if err := user.UpdateEmailVerification(ctx, tx, true); err != nil { - return fmt.Errorf("cannot update user email verification: %w", err) - } - - return nil - }, - ) -} - -func (s Service) ListUsersForTenant( - ctx context.Context, - organizationID gid.GID, - cursor *page.Cursor[coredata.UserOrderField], -) (*page.Page[*coredata.User, coredata.UserOrderField], error) { - users := coredata.Users{} - - err := s.pg.WithConn( - ctx, - func(tx pg.Conn) error { - return users.LoadByOrganizationID(ctx, tx, organizationID, cursor) - }, - ) - - if err != nil { - return nil, err - } - - return page.NewPage(users, cursor), nil -} - -func (s Service) InviteUser( - ctx context.Context, - organizationID gid.GID, - fullName string, - emailAddress string, - createPeople bool, -) error { - if _, err := mail.ParseAddress(emailAddress); err != nil { - return &ErrInvalidEmail{emailAddress} - } - if fullName == "" { - return &ErrInvalidFullName{fullName} - } - - var userExists bool - err := s.pg.WithConn( - ctx, - func(tx pg.Conn) error { - user := &coredata.User{} - - if err := user.LoadByEmail(ctx, tx, emailAddress); err != nil { - var errUserNotFound *coredata.ErrUserNotFound - - if errors.As(err, &errUserNotFound) { - userExists = false - return nil - } - - return fmt.Errorf("cannot load user by email: %w", err) - } - - userExists = true - uo := coredata.UserOrganization{ - UserID: user.ID, - OrganizationID: organizationID, - CreatedAt: time.Now(), - } - - if err := uo.Insert(ctx, tx); err != nil { - return fmt.Errorf("cannot insert user organization: %w", err) - } - - if createPeople { - people := &coredata.People{} - scope := coredata.NewScope(organizationID.TenantID()) - if err := people.LoadByEmail(ctx, tx, scope, emailAddress); err != nil { - var errPeopleNotFound *coredata.ErrPeopleNotFound - - if errors.As(err, &errPeopleNotFound) { - people = &coredata.People{ - ID: gid.New(organizationID.TenantID(), coredata.PeopleEntityType), - OrganizationID: organizationID, - UserID: &user.ID, - FullName: fullName, - PrimaryEmailAddress: emailAddress, - Kind: coredata.PeopleKindContractor, - AdditionalEmailAddresses: []string{}, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - if err := people.Insert(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot insert people: %w", err) - } - } else { - return fmt.Errorf("cannot load people by email: %w", err) - } - } else { - people.UserID = &user.ID - people.FullName = fullName - people.UpdatedAt = time.Now() - - if err := people.Update(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot update people: %w", err) - } - } - } - - return nil - }, - ) - - if err != nil { - return err - } - - if userExists { - return nil - } - - confirmationToken, err := statelesstoken.NewToken( - s.tokenSecret, - TokenTypeOrganizationInvitation, - s.invitationTokenValidity, - InvitationData{OrganizationID: organizationID, Email: emailAddress, FullName: fullName, CreatePeople: createPeople}, - ) - if err != nil { - return fmt.Errorf("cannot generate confirmation token: %w", err) - } - - confirmationInvitationUrl := url.URL{ - Scheme: "https", - Host: s.hostname, - Path: "/auth/confirm-invitation", - RawQuery: url.Values{ - "token": []string{confirmationToken}, - }.Encode(), - } - - confirmationEmail := coredata.NewEmail( - fullName, - emailAddress, - invitationEmailSubject, - fmt.Sprintf(invitationEmailTemplate, confirmationInvitationUrl.String()), - ) - - return s.pg.WithConn( - ctx, - func(conn pg.Conn) error { - if err := confirmationEmail.Insert(ctx, conn); err != nil { - return fmt.Errorf("cannot insert email: %w", err) - } - - return nil - }, - ) -} - -func (s Service) ConfirmInvitation(ctx context.Context, tokenString string, password string) (*coredata.User, error) { - token, err := statelesstoken.ValidateToken[InvitationData]( - s.tokenSecret, - TokenTypeOrganizationInvitation, - tokenString, - ) - if err != nil { - return nil, fmt.Errorf("cannot validate organization invitation token: %w", err) - } - - if len(password) < 8 || len(password) > 128 { - return nil, &ErrInvalidPassword{minLength: 8, maxLength: 128} - } - - now := time.Now() - - hashedPassword, err := s.hp.HashPassword([]byte(password)) - if err != nil { - return nil, fmt.Errorf("cannot hash password: %w", err) - } - - user := &coredata.User{} - - err = s.pg.WithTx( - ctx, - func(tx pg.Conn) error { - - if err := user.LoadByEmail(ctx, tx, token.Data.Email); err != nil { - var errUserNotFound *coredata.ErrUserNotFound - - if errors.As(err, &errUserNotFound) { - user = &coredata.User{ - ID: gid.New(gid.NilTenant, coredata.UserEntityType), - EmailAddress: token.Data.Email, - HashedPassword: hashedPassword, - EmailAddressVerified: true, - FullName: token.Data.FullName, - CreatedAt: now, - UpdatedAt: now, - } - - if err := user.Insert(ctx, tx); err != nil { - return fmt.Errorf("cannot insert user: %w", err) - } - } - } - - uo := coredata.UserOrganization{ - UserID: user.ID, - OrganizationID: token.Data.OrganizationID, - CreatedAt: now, - } - - if err := uo.Insert(ctx, tx); err != nil { - return fmt.Errorf("cannot insert user organization: %w", err) - } - - if token.Data.CreatePeople { - people := &coredata.People{} - scope := coredata.NewScope(token.Data.OrganizationID.TenantID()) - - if err := people.LoadByEmail(ctx, tx, scope, token.Data.Email); err != nil { - var errPeopleNotFound *coredata.ErrPeopleNotFound - - if errors.As(err, &errPeopleNotFound) { - peopleID := gid.New(token.Data.OrganizationID.TenantID(), coredata.PeopleEntityType) - people = &coredata.People{ - ID: peopleID, - OrganizationID: token.Data.OrganizationID, - UserID: &user.ID, - FullName: token.Data.FullName, - PrimaryEmailAddress: token.Data.Email, - Kind: coredata.PeopleKindEmployee, - AdditionalEmailAddresses: []string{}, - CreatedAt: now, - UpdatedAt: now, - } - - if err := people.Insert(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot insert people: %w", err) - } - } else { - return fmt.Errorf("cannot load people by email: %w", err) - } - } else { - people.UserID = &user.ID - people.FullName = token.Data.FullName - people.UpdatedAt = now - - if err := people.Update(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot update people: %w", err) - } - } - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return user, nil -} - -func (s Service) RemoveUser(ctx context.Context, organizationID gid.GID, userID gid.GID) error { - return s.pg.WithConn( - ctx, - func(tx pg.Conn) error { - uo := coredata.UserOrganization{ - UserID: userID, - OrganizationID: organizationID, - } - - if err := uo.Delete(ctx, tx); err != nil { - return fmt.Errorf("cannot delete user organization: %w", err) - } - - return nil - }, - ) -} - -func (s Service) ResetPassword(ctx context.Context, tokenString string, newPassword string) error { - token, err := statelesstoken.ValidateToken[PasswordResetData]( - s.tokenSecret, - TokenTypePasswordReset, - tokenString, - ) - if err != nil { - return fmt.Errorf("cannot validate password reset token: %w", err) - } - - if len(newPassword) < 8 || len(newPassword) > 128 { - return &ErrInvalidPassword{minLength: 8, maxLength: 128} - } - - hashedPassword, err := s.hp.HashPassword([]byte(newPassword)) - if err != nil { - return fmt.Errorf("cannot hash password: %w", err) - } - - return s.pg.WithTx( - ctx, - func(tx pg.Conn) error { - user := &coredata.User{} - - if err := user.LoadByEmail(ctx, tx, token.Data.Email); err != nil { - var errUserNotFound *coredata.ErrUserNotFound - - if errors.As(err, &errUserNotFound) { - return fmt.Errorf("user not found: %w", err) - } - - return fmt.Errorf("cannot load user by email: %w", err) - } - - if err := user.UpdatePassword(ctx, tx, hashedPassword); err != nil { - return fmt.Errorf("cannot update user password: %w", err) - } - - return nil - }, - ) -}