{"record":{"id":"e4a2a5fd5a245535","repo":"discourse/discourse","slug":"invite-existing-user-already-redemeed","errorCode":null,"errorMessage":"invite.existing_user_already_redemeed","messagePattern":"invite\\.existing_user_already_redemeed","errorType":"exception","errorClass":"Invite::UserExists","httpStatus":null,"severity":"warning","filePath":"app/models/invite_redeemer.rb","lineNumber":204,"sourceCode":"    # prefill the email and do not let the user modify it.\n    #\n    # Note that an invite link can also have a domain scope which must be checked.\n    email_to_check = redeeming_user&.email || email\n\n    if invite.email.present? && !invite.email_matches?(email_to_check)\n      raise ActiveRecord::RecordNotSaved.new(I18n.t(\"invite.not_matching_email\"))\n    end\n\n    if invite.domain.present? && !invite.domain_matches?(email_to_check)\n      raise ActiveRecord::RecordNotSaved.new(I18n.t(\"invite.domain_not_allowed\"))\n    end\n\n    # Anon user is trying to redeem an invitation, if an existing user already\n    # redeemed it then we cannot redeem now.\n    redeeming_user ||= User.where(admin: false, staged: false).find_by_email(email)\n    if redeeming_user.present? &&\n         InvitedUser.exists?(user_id: redeeming_user.id, invite_id: invite.id)\n      raise Invite::UserExists.new(I18n.t(\"invite.existing_user_already_redemeed\"))\n    end\n\n    true\n  end\n\n  # Note that the invited_user is returned by #redeemed, so other places\n  # (e.g. the InvitesController) can perform further actions on it, this\n  # is why things like send_welcome_message are set without being saved\n  # on the model.\n  def invited_user\n    return @invited_user if defined?(@invited_user)\n\n    # The redeeming user is an already logged in user or a user who is\n    # activating their account who is redeeming the invite,\n    # which is valid for existing users to be invited to topics or groups.\n    if redeeming_user.present?\n      @invited_user = redeeming_user\n      return @invited_user","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/discourse/discourse/blob/1b2d7253e854205d2c95c80b2c6a1cdd42e45059/app/models/invite_redeemer.rb#L186-L222","documentation":"InviteRedeemer#can_redeem_invite? raises Invite::UserExists with invite.existing_user_already_redemeed when an anonymous redeemer resolves to an existing user who already redeemed this invite (InvitedUser record exists).","triggerScenarios":"Opening an invite link while logged out with the email of a user who already redeemed it; re-clicking an invite after successful redemption in another browser.","commonSituations":"Users re-opening old invite links; double-submission of the redemption form.","solutions":["Log in normally - the account already has access from the earlier redemption","In custom flows, rescue Invite::UserExists and treat it as already-redeemed, routing to login"],"exampleFix":"# before\nInviteRedeemer.redeem(invite, email: email)\n\n# after\nbegin\n  InviteRedeemer.redeem(invite, email: email)\nrescue Invite::UserExists\n  # already redeemed; route to login\nend","handlingStrategy":"try-catch","validationCode":"return if InvitedUser.exists?(user_id: redeeming_user.id, invite_id: invite.id)","typeGuard":null,"tryCatchPattern":"rescue Invite::UserExists; treat as already-redeemed and route to login","preventionTips":["Expire single-use invite links after first redemption","Make redemption flows idempotent by rescuing UserExists as success"],"tags":["discourse","invites","duplicate-redemption"],"backgroundTag":"resource-already-consumed","analyzedSha":"1b2d7253e854205d2c95c80b2c6a1cdd42e45059","analyzedAt":"2026-08-26T03:11:08.302Z","schemaVersion":2},"datasetVersion":"2026-08-26T07:17:17.940Z"}