{"record":{"id":"9d8ad81e8d56c4b9","repo":"toeverything/AFFiNE","slug":"invalid-email-token-9d8ad8","errorCode":"invalid_email_token","errorMessage":"An invalid email token provided.","messagePattern":"An invalid email token provided\\.","errorType":"exception","errorClass":"InvalidEmailToken","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/auth/resolver.ts","lineNumber":122,"sourceCode":"    @Args('token') token: string,\n    @Args('newPassword') newPassword: string,\n    @Args('userId', { type: () => String, nullable: true }) userId?: string\n  ) {\n    if (!userId) {\n      throw new LinkExpired();\n    }\n\n    // NOTE: Set & Change password are using the same token type.\n    const valid = await this.models.verificationToken.verify(\n      TokenType.ChangePassword,\n      token,\n      {\n        credential: userId,\n      }\n    );\n\n    if (!valid) {\n      throw new InvalidEmailToken();\n    }\n\n    await this.auth.changePasswordAndRevokeSessions(userId, newPassword);\n\n    return true;\n  }\n\n  @Mutation(() => UserType)\n  async changeEmail(\n    @CurrentUser() user: CurrentUser,\n    @Args('token') token: string,\n    @Args('email') email: string\n  ) {\n    // @see [sendChangeEmail]\n    const valid = await this.models.verificationToken.verify(\n      TokenType.VerifyEmail,\n      token,\n      {","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/resolver.ts#L104-L140","documentation":"In the public changePassword mutation, after confirming userId is present, the server verifies the ChangePassword verification token via models.verificationToken.verify(TokenType.ChangePassword, token, { credential: userId }). If verify returns falsy — token not found, credential mismatch, or past expiresAt — InvalidEmailToken is thrown. NOTE: set-password and change-password share the same token type, so either flow's token is acceptable here.","triggerScenarios":"changePassword called with a token that is wrong, already used (one-time), expired, or whose stored credential does not equal the supplied userId. The verification_token row was deleted.","commonSituations":"User clicked an old reset link whose token expired. The link was already used (token consumed on a previous successful change). Token/userId mismatch because the link was crafted for a different account.","solutions":["Request a new change/set-password email and use the new link promptly.","Use the token exactly once, immediately after receipt, within its TTL.","Confirm the userId and token in the link correspond to the same account.","If self-hosted, verify verification_token rows are not pruned prematurely."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isInvalidEmailToken(err: unknown): boolean {\n  return (\n    !!err &&\n    typeof err === 'object' &&\n    (err as { code?: string }).code === 'invalid_email_token'\n  );\n}","tryCatchPattern":"try {\n  await changePassword({ userId, token, newPassword });\n} catch (err) {\n  if (isInvalidEmailToken(err)) {\n    showUser('This reset link has expired.');\n    redirectToRequestReset();\n    return;\n  }\n  throw err;\n}","preventionTips":["Use each reset token once, immediately after receiving the email.","Re-request the reset email when the token has expired.","Ensure userId and token in the link match the same account.","Do not prune verification_token rows prematurely on self-hosted deploys."],"tags":["auth","password","verification-token","expired-token","graphql"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}