{"record":{"id":"46b7c214aae75642","repo":"toeverything/AFFiNE","slug":"email-verification-required","errorCode":"email_verification_required","errorMessage":"You must verify your email before accessing this resource.","messagePattern":"You must verify your email before accessing this resource\\.","errorType":"exception","errorClass":"EmailVerificationRequired","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/core/auth/resolver.ts","lineNumber":170,"sourceCode":"    await this.auth.sendNotificationChangeEmail(email);\n\n    return user;\n  }\n\n  @Mutation(() => Boolean)\n  async sendChangePasswordEmail(\n    @CurrentUser() user: CurrentUser,\n    @Args('callbackUrl') callbackUrl: string,\n    @Args('email', {\n      type: () => String,\n      nullable: true,\n      deprecationReason: 'fetched from signed in user',\n    })\n    _email: string | undefined,\n    @Context() context: GraphqlContext\n  ) {\n    if (!user.emailVerified) {\n      throw new EmailVerificationRequired();\n    }\n\n    const { token, expiresAt } =\n      await this.models.verificationToken.createWithExpiresAt(\n        TokenType.ChangePassword,\n        user.id\n      );\n\n    const url = this.url.safeLink(callbackUrl, { userId: user.id, token });\n\n    return await this.auth.sendChangePasswordEmail(\n      user.email,\n      url,\n      this.mailMetadata(context, expiresAt)\n    );\n  }\n\n  @Mutation(() => Boolean)","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/resolver.ts#L152-L188","documentation":"The sendChangePasswordEmail mutation requires the authenticated user to have emailVerified=true; if user.emailVerified is false, EmailVerificationRequired is thrown before any token is minted or email sent. This prevents unverified accounts from triggering password-change flows, ensuring the account's email ownership is established first.","triggerScenarios":"An authenticated but email-unverified user calls sendChangePasswordEmail. Common for accounts created via a method that skips verification, or that never completed the verify-email step.","commonSituations":"User signed up via a provider/admin path that didn't mark emailVerified. User hasn't clicked the verification email yet. Imported accounts where emailVerified wasn't set.","solutions":["Complete email verification first: call sendVerifyEmail and click the link to set emailVerified=true.","If the user cannot verify (no access to inbox), an admin can mark the email verified out-of-band.","Re-check that the user record's emailVerified column is set after verification completes.","Guide the user through the verify-email flow before exposing the change-password UI."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// guard the UI: only show 'change password' after email verification\nif (!currentUser.emailVerified) {\n  showVerifyEmailFirst();\n  return;\n}","typeGuard":"function isEmailVerificationRequired(err: unknown): boolean {\n  return (\n    !!err &&\n    typeof err === 'object' &&\n    (err as { code?: string }).code === 'email_verification_required'\n  );\n}","tryCatchPattern":"try {\n  await sendChangePasswordEmail({ callbackUrl });\n} catch (err) {\n  if (isEmailVerificationRequired(err)) {\n    redirectToVerifyEmail();\n    return;\n  }\n  throw err;\n}","preventionTips":["Gate sensitive-flow UI behind a currentUser.emailVerified check.","Drive users through email verification at sign-up.","Have admins set emailVerified for accounts without inbox access.","Re-check the flag after the verify-email step completes."],"tags":["auth","email-verification","password","precondition","graphql"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}