From cfcbda45194954ea84ea16d8d0b8a0308645b7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Thu, 8 Jan 2026 19:11:17 +0100 Subject: [PATCH] Fix uneeded assume + assume after invite accept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../iam/InvitationCardMutation.graphql.ts | 243 ++++++++++++-- .../MembershipCard_assumeMutation.graphql.ts | 18 +- ...DropdownMenuItem_assumeMutation.graphql.ts | 72 +++- .../iam/MembershipsPageQuery.graphql.ts | 307 ++++++++++++------ .../pages/iam/memberships/MembershipsPage.tsx | 27 +- .../_components/InvitationCard.tsx | 31 +- .../_components/MembershipCard.tsx | 1 + .../MembershipsDropdownMenuItem.tsx | 59 ++-- .../_components/ViewerMembershipDropdown.tsx | 2 +- packages/ui/src/Atoms/Badge/Badge.tsx | 64 ++-- pkg/iam/account_service.go | 8 +- pkg/server/api/connect/v1/schema.graphql | 1 + pkg/server/api/connect/v1/schema/schema.go | 66 ++++ pkg/server/api/connect/v1/types/types.go | 1 + pkg/server/api/connect/v1/v1_resolver.go | 3 +- 15 files changed, 681 insertions(+), 222 deletions(-) diff --git a/apps/console/src/__generated__/iam/InvitationCardMutation.graphql.ts b/apps/console/src/__generated__/iam/InvitationCardMutation.graphql.ts index 246dcbd9c..78378693d 100644 --- a/apps/console/src/__generated__/iam/InvitationCardMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/InvitationCardMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<8c8ece60966bd2f39668d4ee4459ae86>> + * @generated SignedSource<<2d970c5558233522bbd695a075469cad>> * @lightSyntaxTransform * @nogrep */ @@ -9,17 +9,27 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; export type AcceptInvitationInput = { invitationId: string; }; export type InvitationCardMutation$variables = { input: AcceptInvitationInput; + membershipConnections: ReadonlyArray; + pendingInvitationConnections: ReadonlyArray; }; export type InvitationCardMutation$data = { readonly acceptInvitation: { + readonly invitation: { + readonly id: string; + }; readonly membershipEdge: { readonly node: { readonly id: string; + readonly organization: { + readonly name: string; + } | null | undefined; + readonly " $fragmentSpreads": FragmentRefs<"MembershipCardFragment">; }; }; } | null | undefined; @@ -35,44 +45,101 @@ var v0 = [ "defaultValue": null, "kind": "LocalArgument", "name": "input" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "membershipConnections" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "pendingInvitationConnections" } ], v1 = [ { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "AcceptInvitationPayload", - "kind": "LinkedField", - "name": "acceptInvitation", - "plural": false, + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "InvitationCardMutation", "selections": [ { "alias": null, - "args": null, - "concreteType": "MembershipEdge", + "args": (v1/*: any*/), + "concreteType": "AcceptInvitationPayload", "kind": "LinkedField", - "name": "membershipEdge", + "name": "acceptInvitation", "plural": false, "selections": [ { "alias": null, "args": null, - "concreteType": "Membership", + "concreteType": "Invitation", "kind": "LinkedField", - "name": "node", + "name": "invitation", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "MembershipEdge", + "kind": "LinkedField", + "name": "membershipEdge", "plural": false, "selections": [ { "alias": null, "args": null, - "kind": "ScalarField", - "name": "id", + "concreteType": "Membership", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "MembershipCardFragment" + }, + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], "storageKey": null } ], @@ -82,16 +149,6 @@ v1 = [ "storageKey": null } ], - "storageKey": null - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "InvitationCardMutation", - "selections": (v1/*: any*/), "type": "Mutation", "abstractKey": null }, @@ -100,19 +157,137 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "InvitationCardMutation", - "selections": (v1/*: any*/) + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "AcceptInvitationPayload", + "kind": "LinkedField", + "name": "acceptInvitation", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "invitation", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "id", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "pendingInvitationConnections" + } + ] + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "MembershipEdge", + "kind": "LinkedField", + "name": "membershipEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "Session", + "kind": "LinkedField", + "name": "lastSession", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "logoUrl", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "membershipEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "membershipConnections" + } + ] + } + ], + "storageKey": null + } + ] }, "params": { - "cacheID": "592fad5ce23af0f457d7e0ccf5f7123f", + "cacheID": "15ed0089d35b0163252ef542a4b1b667", "id": null, "metadata": {}, "name": "InvitationCardMutation", "operationKind": "mutation", - "text": "mutation InvitationCardMutation(\n $input: AcceptInvitationInput!\n) {\n acceptInvitation(input: $input) {\n membershipEdge {\n node {\n id\n }\n }\n }\n}\n" + "text": "mutation InvitationCardMutation(\n $input: AcceptInvitationInput!\n) {\n acceptInvitation(input: $input) {\n invitation {\n id\n }\n membershipEdge {\n node {\n id\n ...MembershipCardFragment\n organization {\n name\n id\n }\n }\n }\n }\n}\n\nfragment MembershipCardFragment on Membership {\n lastSession {\n id\n expiresAt\n }\n organization {\n id\n name\n logoUrl\n }\n}\n" } }; })(); -(node as any).hash = "00a20b1db5dd8e98838c8c9659560bb9"; +(node as any).hash = "587f3fc115f88830e5ee49759f3ab7b5"; export default node; diff --git a/apps/console/src/__generated__/iam/MembershipCard_assumeMutation.graphql.ts b/apps/console/src/__generated__/iam/MembershipCard_assumeMutation.graphql.ts index 28bc7a4e2..271c39f23 100644 --- a/apps/console/src/__generated__/iam/MembershipCard_assumeMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/MembershipCard_assumeMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<1be91c8373db8ffb1155cb6e52beda6c>> + * @generated SignedSource<<0f048726f7e7371223919d924fa15d54>> * @lightSyntaxTransform * @nogrep */ @@ -23,6 +23,7 @@ export type MembershipCard_assumeMutation$data = { readonly membership: { readonly id: string; readonly lastSession: { + readonly expiresAt: string; readonly id: string; } | null | undefined; }; @@ -117,7 +118,14 @@ v3 = [ "name": "lastSession", "plural": false, "selections": [ - (v1/*: any*/) + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + } ], "storageKey": null } @@ -176,16 +184,16 @@ return { "selections": (v3/*: any*/) }, "params": { - "cacheID": "100893dbb5b232a19f4b11bc51598bc8", + "cacheID": "d106c9d8a5a2c3256da2f1f83fb8fde7", "id": null, "metadata": {}, "name": "MembershipCard_assumeMutation", "operationKind": "mutation", - "text": "mutation MembershipCard_assumeMutation(\n $input: AssumeOrganizationSessionInput!\n) {\n assumeOrganizationSession(input: $input) {\n result {\n __typename\n ... on OrganizationSessionCreated {\n membership {\n id\n lastSession {\n id\n }\n }\n }\n ... on PasswordRequired {\n reason\n }\n ... on SAMLAuthenticationRequired {\n reason\n redirectUrl\n }\n }\n }\n}\n" + "text": "mutation MembershipCard_assumeMutation(\n $input: AssumeOrganizationSessionInput!\n) {\n assumeOrganizationSession(input: $input) {\n result {\n __typename\n ... on OrganizationSessionCreated {\n membership {\n id\n lastSession {\n id\n expiresAt\n }\n }\n }\n ... on PasswordRequired {\n reason\n }\n ... on SAMLAuthenticationRequired {\n reason\n redirectUrl\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "f44a07beefbecb0a564858272e3cd244"; +(node as any).hash = "7acaa03357731962d7d29eb2c046660a"; export default node; diff --git a/apps/console/src/__generated__/iam/MembershipsDropdownMenuItem_assumeMutation.graphql.ts b/apps/console/src/__generated__/iam/MembershipsDropdownMenuItem_assumeMutation.graphql.ts index 7a06d5dd3..7c9b4bacc 100644 --- a/apps/console/src/__generated__/iam/MembershipsDropdownMenuItem_assumeMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/MembershipsDropdownMenuItem_assumeMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<4beff809268fb32711cc02699683b623>> * @lightSyntaxTransform * @nogrep */ @@ -19,6 +19,15 @@ export type MembershipsDropdownMenuItem_assumeMutation$variables = { export type MembershipsDropdownMenuItem_assumeMutation$data = { readonly assumeOrganizationSession: { readonly result: { + readonly __typename: "OrganizationSessionCreated"; + readonly membership: { + readonly id: string; + readonly lastSession: { + readonly expiresAt: string; + readonly id: string; + } | null | undefined; + }; + } | { readonly __typename: "PasswordRequired"; readonly reason: ReauthenticationReason; } | { @@ -46,13 +55,20 @@ var v0 = [ } ], v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { "alias": null, "args": null, "kind": "ScalarField", "name": "reason", "storageKey": null }, -v2 = [ +v3 = [ { "alias": null, "args": [ @@ -85,7 +101,45 @@ v2 = [ { "kind": "InlineFragment", "selections": [ - (v1/*: any*/) + { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "membership", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "Session", + "kind": "LinkedField", + "name": "lastSession", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "OrganizationSessionCreated", + "abstractKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + (v2/*: any*/) ], "type": "PasswordRequired", "abstractKey": null @@ -93,7 +147,7 @@ v2 = [ { "kind": "InlineFragment", "selections": [ - (v1/*: any*/), + (v2/*: any*/), { "alias": null, "args": null, @@ -118,7 +172,7 @@ return { "kind": "Fragment", "metadata": null, "name": "MembershipsDropdownMenuItem_assumeMutation", - "selections": (v2/*: any*/), + "selections": (v3/*: any*/), "type": "Mutation", "abstractKey": null }, @@ -127,19 +181,19 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "MembershipsDropdownMenuItem_assumeMutation", - "selections": (v2/*: any*/) + "selections": (v3/*: any*/) }, "params": { - "cacheID": "21ebf9c598bd39f2abcb0ace0d9c0c4b", + "cacheID": "96111606700d6d57e20584324fac44c3", "id": null, "metadata": {}, "name": "MembershipsDropdownMenuItem_assumeMutation", "operationKind": "mutation", - "text": "mutation MembershipsDropdownMenuItem_assumeMutation(\n $input: AssumeOrganizationSessionInput!\n) {\n assumeOrganizationSession(input: $input) {\n result {\n __typename\n ... on PasswordRequired {\n reason\n }\n ... on SAMLAuthenticationRequired {\n reason\n redirectUrl\n }\n }\n }\n}\n" + "text": "mutation MembershipsDropdownMenuItem_assumeMutation(\n $input: AssumeOrganizationSessionInput!\n) {\n assumeOrganizationSession(input: $input) {\n result {\n __typename\n ... on OrganizationSessionCreated {\n membership {\n id\n lastSession {\n id\n expiresAt\n }\n }\n }\n ... on PasswordRequired {\n reason\n }\n ... on SAMLAuthenticationRequired {\n reason\n redirectUrl\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "416f824d526d3f92797fec0eb9c2e590"; +(node as any).hash = "706b7dd33f09edc34377a52682d85037"; export default node; diff --git a/apps/console/src/__generated__/iam/MembershipsPageQuery.graphql.ts b/apps/console/src/__generated__/iam/MembershipsPageQuery.graphql.ts index d80724a9d..7aba165fd 100644 --- a/apps/console/src/__generated__/iam/MembershipsPageQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/MembershipsPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<1d247c2fede04c2292000421a9e49fa8>> * @lightSyntaxTransform * @nogrep */ @@ -14,6 +14,7 @@ export type MembershipsPageQuery$variables = Record; export type MembershipsPageQuery$data = { readonly viewer: { readonly memberships: { + readonly __id: string; readonly edges: ReadonlyArray<{ readonly node: { readonly id: string; @@ -25,6 +26,7 @@ export type MembershipsPageQuery$data = { }>; }; readonly pendingInvitations: { + readonly __id: string; readonly edges: ReadonlyArray<{ readonly node: { readonly id: string; @@ -40,35 +42,93 @@ export type MembershipsPageQuery = { }; const node: ConcreteRequest = (function(){ -var v0 = [ - { - "kind": "Literal", - "name": "first", - "value": 1000 - }, - { - "kind": "Literal", - "name": "orderBy", - "value": { - "direction": "DESC", - "field": "CREATED_AT" - } +var v0 = { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "DESC", + "field": "CREATED_AT" } +}, +v1 = [ + (v0/*: any*/) ], -v1 = { +v2 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v2 = { +v3 = { "alias": null, "args": null, "kind": "ScalarField", "name": "name", "storageKey": null -}; +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v6 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null +}, +v7 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}, +v8 = [ + { + "kind": "Literal", + "name": "first", + "value": 1000 + }, + (v0/*: any*/) +], +v9 = [ + "orderBy" +]; return { "fragment": { "argumentDefinitions": [], @@ -89,11 +149,11 @@ return { { "kind": "RequiredField", "field": { - "alias": null, - "args": (v0/*: any*/), + "alias": "memberships", + "args": (v1/*: any*/), "concreteType": "MembershipConnection", "kind": "LinkedField", - "name": "memberships", + "name": "__MembershipsPage_memberships_connection", "plural": false, "selections": [ { @@ -107,60 +167,60 @@ return { "plural": true, "selections": [ { - "kind": "RequiredField", - "field": { - "alias": null, - "args": null, - "concreteType": "Membership", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v1/*: any*/), - { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "MembershipCardFragment" + }, + { + "kind": "RequiredField", + "field": { + "alias": null, "args": null, - "kind": "FragmentSpread", - "name": "MembershipCardFragment" + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null }, - { - "kind": "RequiredField", - "field": { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "organization", - "plural": false, - "selections": [ - (v2/*: any*/) - ], - "storageKey": null - }, - "action": "THROW" - } - ], - "storageKey": null - }, - "action": "THROW" - } + "action": "THROW" + }, + (v4/*: any*/) + ], + "storageKey": null + }, + (v5/*: any*/) ], "storageKey": null }, "action": "THROW" - } + }, + (v6/*: any*/), + (v7/*: any*/) ], - "storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + "storageKey": "__MembershipsPage_memberships_connection(orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" }, "action": "THROW" }, { "kind": "RequiredField", "field": { - "alias": null, - "args": (v0/*: any*/), + "alias": "pendingInvitations", + "args": (v1/*: any*/), "concreteType": "InvitationConnection", "kind": "LinkedField", - "name": "pendingInvitations", + "name": "__MembershipsPage_pendingInvitations_connection", "plural": false, "selections": [ { @@ -174,33 +234,33 @@ return { "plural": true, "selections": [ { - "kind": "RequiredField", - "field": { - "alias": null, - "args": null, - "concreteType": "Invitation", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v1/*: any*/), - { - "args": null, - "kind": "FragmentSpread", - "name": "InvitationCardFragment" - } - ], - "storageKey": null - }, - "action": "THROW" - } + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "InvitationCardFragment" + }, + (v4/*: any*/) + ], + "storageKey": null + }, + (v5/*: any*/) ], "storageKey": null }, "action": "THROW" - } + }, + (v6/*: any*/), + (v7/*: any*/) ], - "storageKey": "pendingInvitations(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + "storageKey": "__MembershipsPage_pendingInvitations_connection(orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" }, "action": "THROW" } @@ -229,7 +289,7 @@ return { "selections": [ { "alias": null, - "args": (v0/*: any*/), + "args": (v8/*: any*/), "concreteType": "MembershipConnection", "kind": "LinkedField", "name": "memberships", @@ -251,7 +311,7 @@ return { "name": "node", "plural": false, "selections": [ - (v1/*: any*/), + (v2/*: any*/), { "alias": null, "args": null, @@ -260,7 +320,7 @@ return { "name": "lastSession", "plural": false, "selections": [ - (v1/*: any*/), + (v2/*: any*/), { "alias": null, "args": null, @@ -279,8 +339,8 @@ return { "name": "organization", "plural": false, "selections": [ - (v1/*: any*/), (v2/*: any*/), + (v3/*: any*/), { "alias": null, "args": null, @@ -290,19 +350,32 @@ return { } ], "storageKey": null - } + }, + (v4/*: any*/) ], "storageKey": null - } + }, + (v5/*: any*/) ], "storageKey": null - } + }, + (v6/*: any*/), + (v7/*: any*/) ], "storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" }, { "alias": null, - "args": (v0/*: any*/), + "args": (v8/*: any*/), + "filters": (v9/*: any*/), + "handle": "connection", + "key": "MembershipsPage_memberships", + "kind": "LinkedHandle", + "name": "memberships" + }, + { + "alias": null, + "args": (v8/*: any*/), "concreteType": "InvitationConnection", "kind": "LinkedField", "name": "pendingInvitations", @@ -324,7 +397,7 @@ return { "name": "node", "plural": false, "selections": [ - (v1/*: any*/), + (v2/*: any*/), { "alias": null, "args": null, @@ -347,37 +420,71 @@ return { "name": "organization", "plural": false, "selections": [ - (v1/*: any*/), - (v2/*: any*/) + (v2/*: any*/), + (v3/*: any*/) ], "storageKey": null - } + }, + (v4/*: any*/) ], "storageKey": null - } + }, + (v5/*: any*/) ], "storageKey": null - } + }, + (v6/*: any*/), + (v7/*: any*/) ], "storageKey": "pendingInvitations(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" }, - (v1/*: any*/) + { + "alias": null, + "args": (v8/*: any*/), + "filters": (v9/*: any*/), + "handle": "connection", + "key": "MembershipsPage_pendingInvitations", + "kind": "LinkedHandle", + "name": "pendingInvitations" + }, + (v2/*: any*/) ], "storageKey": null } ] }, "params": { - "cacheID": "70ba2b8cec46ba8154cf7a6a51e03932", + "cacheID": "a3dc3f3e775fa02b7d181fa57ade06b9", "id": null, - "metadata": {}, + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "viewer", + "memberships" + ] + }, + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "viewer", + "pendingInvitations" + ] + } + ] + }, "name": "MembershipsPageQuery", "operationKind": "query", - "text": "query MembershipsPageQuery {\n viewer {\n memberships(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...MembershipCardFragment\n organization {\n name\n id\n }\n }\n }\n }\n pendingInvitations(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationCardFragment\n }\n }\n }\n id\n }\n}\n\nfragment InvitationCardFragment on Invitation {\n id\n role\n createdAt\n organization {\n id\n name\n }\n}\n\nfragment MembershipCardFragment on Membership {\n lastSession {\n id\n expiresAt\n }\n organization {\n id\n name\n logoUrl\n }\n}\n" + "text": "query MembershipsPageQuery {\n viewer {\n memberships(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...MembershipCardFragment\n organization {\n name\n id\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n pendingInvitations(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n\nfragment InvitationCardFragment on Invitation {\n id\n role\n createdAt\n organization {\n id\n name\n }\n}\n\nfragment MembershipCardFragment on Membership {\n lastSession {\n id\n expiresAt\n }\n organization {\n id\n name\n logoUrl\n }\n}\n" } }; })(); -(node as any).hash = "4148521eb3eaba1670a2d181b6d2468f"; +(node as any).hash = "b21b9cae0ab7a4afdf12fb6ae516b64e"; export default node; diff --git a/apps/console/src/pages/iam/memberships/MembershipsPage.tsx b/apps/console/src/pages/iam/memberships/MembershipsPage.tsx index 5bdff27b3..4baf56578 100644 --- a/apps/console/src/pages/iam/memberships/MembershipsPage.tsx +++ b/apps/console/src/pages/iam/memberships/MembershipsPage.tsx @@ -17,9 +17,11 @@ export const membershipsPageQuery = graphql` query MembershipsPageQuery { viewer @required(action: THROW) { memberships(first: 1000, orderBy: { direction: DESC, field: CREATED_AT }) + @connection(key: "MembershipsPage_memberships") @required(action: THROW) { + __id edges @required(action: THROW) { - node @required(action: THROW) { + node { id ...MembershipCardFragment organization @required(action: THROW) { @@ -31,9 +33,12 @@ export const membershipsPageQuery = graphql` pendingInvitations( first: 1000 orderBy: { direction: DESC, field: CREATED_AT } - ) @required(action: THROW) { + ) + @connection(key: "MembershipsPage_pendingInvitations") + @required(action: THROW) { + __id edges @required(action: THROW) { - node @required(action: THROW) { + node { id ...InvitationCardFragment } @@ -54,8 +59,11 @@ export function MembershipsPage(props: { const { queryRef } = props; const { viewer: { - memberships: { edges: initialMemberships }, - pendingInvitations: { edges: invitations }, + memberships: { __id: membershipConnectionId, edges: initialMemberships }, + pendingInvitations: { + __id: pendingInvitationsConnectionId, + edges: invitations, + }, }, } = usePreloadedQuery(membershipsPageQuery, queryRef); @@ -81,7 +89,14 @@ export function MembershipsPage(props: { {__("Pending invitations")} {invitations.map(({ node }) => ( - + ))} )} diff --git a/apps/console/src/pages/iam/memberships/_components/InvitationCard.tsx b/apps/console/src/pages/iam/memberships/_components/InvitationCard.tsx index 0c5dd5fee..81c7bd5a9 100644 --- a/apps/console/src/pages/iam/memberships/_components/InvitationCard.tsx +++ b/apps/console/src/pages/iam/memberships/_components/InvitationCard.tsx @@ -1,10 +1,10 @@ import { formatDate, formatError } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; import { Button, Card, useToast } from "@probo/ui"; -import { graphql } from "relay-runtime"; +import { graphql, type DataID } from "relay-runtime"; import { useFragment, useMutation } from "react-relay"; import type { InvitationCardFragment$key } from "/__generated__/iam/InvitationCardFragment.graphql"; -import { useNavigate } from "react-router"; +import type { InvitationCardMutation } from "/__generated__/iam/InvitationCardMutation.graphql"; const fragment = graphql` fragment InvitationCardFragment on Invitation { @@ -19,11 +19,22 @@ const fragment = graphql` `; const acceptMutation = graphql` - mutation InvitationCardMutation($input: AcceptInvitationInput!) { + mutation InvitationCardMutation( + $input: AcceptInvitationInput! + $membershipConnections: [ID!]! + $pendingInvitationConnections: [ID!]! + ) { acceptInvitation(input: $input) { - membershipEdge { + invitation { + id @deleteEdge(connections: $pendingInvitationConnections) + } + membershipEdge @prependEdge(connections: $membershipConnections) { node { id + ...MembershipCardFragment + organization { + name + } } } } @@ -31,19 +42,22 @@ const acceptMutation = graphql` `; interface InvitationCardProps { + pendingInvitationsConnectionId: DataID; + membershipConnectionId: DataID; fKey: InvitationCardFragment$key; } export function InvitationCard(props: InvitationCardProps) { - const { fKey } = props; + const { pendingInvitationsConnectionId, membershipConnectionId, fKey } = + props; - const navigate = useNavigate(); const { __ } = useTranslate(); const { toast } = useToast(); const invitation = useFragment(fragment, fKey); - const [acceptInvitation, isAccepting] = useMutation(acceptMutation); + const [acceptInvitation, isAccepting] = + useMutation(acceptMutation); const handleAccept = () => { acceptInvitation({ @@ -51,6 +65,8 @@ export function InvitationCard(props: InvitationCardProps) { input: { invitationId: invitation.id, }, + pendingInvitationConnections: [pendingInvitationsConnectionId], + membershipConnections: [membershipConnectionId], }, onCompleted: (_, e) => { if (e) { @@ -61,7 +77,6 @@ export function InvitationCard(props: InvitationCardProps) { }); return; } - navigate(`/organizations/${invitation.organization.id}`); }, onError: (e) => { toast({ diff --git a/apps/console/src/pages/iam/memberships/_components/MembershipCard.tsx b/apps/console/src/pages/iam/memberships/_components/MembershipCard.tsx index 7b8fa58ca..57b8a117d 100644 --- a/apps/console/src/pages/iam/memberships/_components/MembershipCard.tsx +++ b/apps/console/src/pages/iam/memberships/_components/MembershipCard.tsx @@ -42,6 +42,7 @@ const assumeOrganizationSessionMutation = graphql` id lastSession { id + expiresAt } } } diff --git a/apps/console/src/pages/iam/organizations/_components/MembershipsDropdownMenuItem.tsx b/apps/console/src/pages/iam/organizations/_components/MembershipsDropdownMenuItem.tsx index 80c156279..5a3d9bf5b 100644 --- a/apps/console/src/pages/iam/organizations/_components/MembershipsDropdownMenuItem.tsx +++ b/apps/console/src/pages/iam/organizations/_components/MembershipsDropdownMenuItem.tsx @@ -35,6 +35,15 @@ const assumeOrganizationSessionMutation = graphql` assumeOrganizationSession(input: $input) { result { __typename + ... on OrganizationSessionCreated { + membership { + id + lastSession { + id + expiresAt + } + } + } ... on PasswordRequired { reason } @@ -67,32 +76,36 @@ export function MembershipsDropdownMenuItem(props: { ); const handleAssumeOrganizationSession = useCallback(() => { - assumeOrganizationSession({ - variables: { - input: { - organizationId: organization.id, + if (isAuthenticated) { + navigate(`/organizations/${organization.id}`); + } else { + assumeOrganizationSession({ + variables: { + input: { + organizationId: organization.id, + }, }, - }, - onCompleted: ({ assumeOrganizationSession }) => { - if (!assumeOrganizationSession) { - throw new Error("complete mutation result is empty"); - } + onCompleted: ({ assumeOrganizationSession }) => { + if (!assumeOrganizationSession) { + throw new Error("complete mutation result is empty"); + } - const { result } = assumeOrganizationSession; + const { result } = assumeOrganizationSession; - switch (result.__typename) { - case "PasswordRequired": - navigate("auth/login"); - break; - case "SAMLAuthenticationRequired": - window.location.href = result.redirectUrl; - break; - default: - navigate(`/organizations/${organization.id}`); - } - }, - }); - }, [assumeOrganizationSession, navigate, organization.id]); + switch (result.__typename) { + case "PasswordRequired": + navigate("/auth/login"); + break; + case "SAMLAuthenticationRequired": + window.location.href = result.redirectUrl; + break; + default: + navigate(`/organizations/${organization.id}`); + } + }, + }); + } + }, [assumeOrganizationSession, navigate, organization.id, isAuthenticated]); return ( diff --git a/apps/console/src/pages/iam/organizations/_components/ViewerMembershipDropdown.tsx b/apps/console/src/pages/iam/organizations/_components/ViewerMembershipDropdown.tsx index 0e91a1b86..a1b35f54e 100644 --- a/apps/console/src/pages/iam/organizations/_components/ViewerMembershipDropdown.tsx +++ b/apps/console/src/pages/iam/organizations/_components/ViewerMembershipDropdown.tsx @@ -68,7 +68,7 @@ export function ViewerMembershipDropdown(props: { }); return; } - window.location.reload(); + window.location.href = "/auth/login"; }, onError: (e) => { toast({ diff --git a/packages/ui/src/Atoms/Badge/Badge.tsx b/packages/ui/src/Atoms/Badge/Badge.tsx index cfb7985af..fa1620157 100644 --- a/packages/ui/src/Atoms/Badge/Badge.tsx +++ b/packages/ui/src/Atoms/Badge/Badge.tsx @@ -3,43 +3,45 @@ import { tv } from "tailwind-variants"; import { Slot } from "../Slot"; type Props = { - asChild?: boolean; - variant?: - | "success" - | "warning" - | "danger" - | "info" - | "neutral" - | "outline" - | "highlight"; - size?: "sm" | "md"; + asChild?: boolean; + variant?: + | "success" + | "warning" + | "danger" + | "info" + | "neutral" + | "outline" + | "highlight"; + size?: "sm" | "md"; } & HTMLAttributes; const badge = tv({ - base: "font-medium rounded-lg w-max flex gap-1 items-center group whitespace-nowrap", - variants: { - variant: { - success: "bg-success text-txt-success", - warning: "bg-warning text-txt-warning", - danger: "bg-danger text-txt-danger", - info: "bg-info text-txt-info", - neutral: "bg-subtle text-txt-secondary", - outline: "text-txt-tertiary border border-border-low", - highlight: "bg-highlight text-txt-primary", - }, - size: { - sm: "text-xs py-[2px] px-[6px]", - md: "text-sm py-[6px] px-2", - }, + base: "font-medium rounded-lg w-max flex gap-1 items-center group whitespace-nowrap", + variants: { + variant: { + success: "bg-success text-txt-success", + warning: "bg-warning text-txt-warning", + danger: "bg-danger text-txt-danger", + info: "bg-info text-txt-info", + neutral: "bg-subtle text-txt-secondary", + outline: "text-txt-tertiary border border-border-low", + highlight: "bg-highlight text-txt-primary", }, - defaultVariants: { - variant: "neutral", - size: "sm", + size: { + sm: "text-xs py-[2px] px-[6px]", + md: "text-sm py-[6px] px-2", }, + }, + defaultVariants: { + variant: "neutral", + size: "sm", + }, }); export function Badge(props: Props) { - const Component = props.asChild ? Slot : "div"; - const { size, variant, ...restProps } = props; - return ; + const Component = props.asChild ? Slot : "div"; + const { className, size, variant, ...restProps } = props; + return ( + + ); } diff --git a/pkg/iam/account_service.go b/pkg/iam/account_service.go index 1d1b8994b..e4b102c4d 100644 --- a/pkg/iam/account_service.go +++ b/pkg/iam/account_service.go @@ -198,17 +198,17 @@ func (s *AccountService) AcceptInvitation( ctx context.Context, identityID gid.GID, invitationID gid.GID, -) (*coredata.Membership, error) { +) (*coredata.Invitation, *coredata.Membership, error) { var ( now = time.Now() membership = &coredata.Membership{} + invitation = &coredata.Invitation{} ) err := s.pg.WithTx( ctx, func(tx pg.Conn) error { identity := coredata.Identity{} - invitation := coredata.Invitation{} err := identity.LoadByID(ctx, tx, identityID) if err != nil { @@ -306,10 +306,10 @@ func (s *AccountService) AcceptInvitation( ) if err != nil { - return nil, err + return nil, nil, err } - return membership, nil + return invitation, membership, nil } func (s *AccountService) ListPendingInvitations( diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index 0de8d2daa..108c8e760 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -829,6 +829,7 @@ type RemoveMemberPayload { type AcceptInvitationPayload { membershipEdge: MembershipEdge! + invitation: Invitation! } type DeleteInvitationPayload { diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index b331bf9f6..785de3d9d 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -72,6 +72,7 @@ type DirectiveRoot struct { type ComplexityRoot struct { AcceptInvitationPayload struct { + Invitation func(childComplexity int) int MembershipEdge func(childComplexity int) int } @@ -587,6 +588,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin _ = ec switch typeName + "." + field { + case "AcceptInvitationPayload.invitation": + if e.complexity.AcceptInvitationPayload.Invitation == nil { + break + } + + return e.complexity.AcceptInvitationPayload.Invitation(childComplexity), true case "AcceptInvitationPayload.membershipEdge": if e.complexity.AcceptInvitationPayload.MembershipEdge == nil { break @@ -3096,6 +3103,7 @@ type RemoveMemberPayload { type AcceptInvitationPayload { membershipEdge: MembershipEdge! + invitation: Invitation! } type DeleteInvitationPayload { @@ -3930,6 +3938,57 @@ func (ec *executionContext) fieldContext_AcceptInvitationPayload_membershipEdge( return fc, nil } +func (ec *executionContext) _AcceptInvitationPayload_invitation(ctx context.Context, field graphql.CollectedField, obj *types.AcceptInvitationPayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_AcceptInvitationPayload_invitation, + func(ctx context.Context) (any, error) { + return obj.Invitation, nil + }, + nil, + ec.marshalNInvitation2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐInvitation, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_AcceptInvitationPayload_invitation(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AcceptInvitationPayload", + 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) + case "status": + return ec.fieldContext_Invitation_status(ctx, field) + case "organization": + return ec.fieldContext_Invitation_organization(ctx, field) + case "permission": + return ec.fieldContext_Invitation_permission(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Invitation", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _AssumeOrganizationSessionPayload_result(ctx context.Context, field graphql.CollectedField, obj *types.AssumeOrganizationSessionPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -7658,6 +7717,8 @@ func (ec *executionContext) fieldContext_Mutation_acceptInvitation(ctx context.C switch field.Name { case "membershipEdge": return ec.fieldContext_AcceptInvitationPayload_membershipEdge(ctx, field) + case "invitation": + return ec.fieldContext_AcceptInvitationPayload_invitation(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AcceptInvitationPayload", field.Name) }, @@ -15154,6 +15215,11 @@ func (ec *executionContext) _AcceptInvitationPayload(ctx context.Context, sel as if out.Values[i] == graphql.Null { out.Invalids++ } + case "invitation": + out.Values[i] = ec._AcceptInvitationPayload_invitation(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/pkg/server/api/connect/v1/types/types.go b/pkg/server/api/connect/v1/types/types.go index cf6fe5c19..8fe176857 100644 --- a/pkg/server/api/connect/v1/types/types.go +++ b/pkg/server/api/connect/v1/types/types.go @@ -31,6 +31,7 @@ type AcceptInvitationInput struct { type AcceptInvitationPayload struct { MembershipEdge *MembershipEdge `json:"membershipEdge"` + Invitation *Invitation `json:"invitation"` } type AssumeOrganizationSessionInput struct { diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index 31b20027c..d7a1eafce 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -1006,7 +1006,7 @@ func (r *mutationResolver) AcceptInvitation(ctx context.Context, input types.Acc identity := IdentityFromContext(ctx) - membership, err := r.iam.AccountService.AcceptInvitation(ctx, identity.ID, input.InvitationID) + invitation, membership, err := r.iam.AccountService.AcceptInvitation(ctx, identity.ID, input.InvitationID) if err != nil { r.logger.ErrorCtx(ctx, "cannot accept invitation", log.Error(err)) return nil, gqlutils.Internal(ctx) @@ -1014,6 +1014,7 @@ func (r *mutationResolver) AcceptInvitation(ctx context.Context, input types.Acc return &types.AcceptInvitationPayload{ MembershipEdge: types.NewMembershipEdge(membership, coredata.MembershipOrderFieldCreatedAt), + Invitation: types.NewInvitation(invitation), }, nil }