{"record":{"id":"12920541ba62e9a9","repo":"toeverything/AFFiNE","slug":"authentication-required-129205","errorCode":"authentication_required","errorMessage":"You must sign in first to access this resource.","messagePattern":"You must sign in first to access this resource\\.","errorType":"exception","errorClass":"AuthenticationRequired","httpStatus":401,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/resolvers/member.ts","lineNumber":587,"sourceCode":"    @Args('inviteId') inviteId: string,\n    @Args('workspaceId', { deprecationReason: 'never used', nullable: true })\n    _workspaceId: string,\n    @Args('sendAcceptMail', {\n      nullable: true,\n      deprecationReason: 'never used',\n    })\n    _sendAcceptMail: boolean\n  ) {\n    const role = await this.models.workspaceUser.getById(inviteId);\n    // invitation by email\n    if (role) {\n      if (user.id !== role.userId) {\n        throw new InvitationAccountMismatch();\n      }\n\n      await this.acceptInvitationByEmail(role);\n    } else {\n      // invitation by link\n      const invitation = await this.cache.get<{\n        workspaceId: string;\n        inviterUserId: string;\n      }>(`workspace:inviteLinkId:${inviteId}`);\n\n      if (!invitation) {\n        throw new InvalidInvitation();\n      }\n\n      const role = await this.models.workspaceUser.get(\n        invitation.workspaceId,\n        user.id\n      );\n\n      if (role) {\n        // if status is pending, should accept the invitation directly\n        if (role.status === WorkspaceMemberStatus.Pending) {\n          await this.acceptInvitationByEmail(role);","sourceCodeStart":569,"sourceCodeEnd":605,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/workspaces/resolvers/member.ts#L569-L605","documentation":"Thrown by acceptInvitation when inviteId does not match any email invitation (no workspaceUser record) so it is treated as an invitation-by-link, and there is no authenticated user in the GraphQL context. Link invitations require an existing account because the server must attach the user to the workspace.","triggerScenarios":"An anonymous visitor follows an invite link (e.g. /invite/<inviteId>) and the client fires the acceptInvitation mutation before a sign-in session exists.","commonSituations":"Invite link opened in an incognito window or after session expiry; frontend routing calls acceptInvitation before the auth provider finished restoring the session; API consumers calling the mutation without an Authorization header.","solutions":["Redirect the user to the sign-in page with the invite link as the return URL, then call acceptInvitation again after login.","If calling the API directly, send the session cookie or Authorization token for an existing account.","Register an account first if the user does not have one, then retry the link."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Only fire accept for link invites once a session exists\nconst me = await gql.request(CURRENT_USER_QUERY);\nif (!me.currentUser) {\n  // preserve the invite URL across login\n  router.push(`/sign-in?redirect=${encodeURIComponent(location.pathname)}`);\n} else {\n  await gql.request(ACCEPT_INVITATION, { inviteId });\n}","typeGuard":null,"tryCatchPattern":"try {\n  await acceptInvitation(inviteId);\n} catch (e) {\n  if (getErrorCode(e) === 'authentication_required') {\n    redirectToSignInWithReturnUrl(currentUrl);\n  }\n}","preventionTips":["Gate the invite-accept route behind an auth check so anonymous users are redirected to sign-in with the invite URL preserved.","Wait for the auth/session restoration promise to settle before firing invitation mutations."],"tags":["workspace","invitation","authentication","graphql"],"backgroundTag":"authentication-required","analyzedSha":"2af30773aecd567f09b346e7b72fc69143144057","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}