def get_invite_code(request: Request) -> dict: """Generate a new invite code owned by the user.""" user = request.context if request.user.invite_codes_remaining < 1: raise HTTPForbidden("No invite codes remaining") # obtain a lock to prevent concurrent requests generating multiple codes request.obtain_lock("generate_invite_code", user.user_id) # it's possible to randomly generate an existing code, so we'll retry until we # create a new one (will practically always be the first try) while True: savepoint = request.tm.savepoint() code = UserInviteCode(user) request.db_session.add(code) try: request.db_session.flush() break except IntegrityError: savepoint.rollback() # doing an atomic decrement on request.user.invite_codes_remaining is going to make # it unusable as an integer in the template, so store the expected value after the # decrement first, to be able to use that instead num_remaining = request.user.invite_codes_remaining - 1 request.user.invite_codes_remaining = User.invite_codes_remaining - 1 return {"code": code, "num_remaining": num_remaining}
def _handle_invite_code(request: Request, invite_code: str) -> Optional[UserInviteCode]: # invite code not required -- leave empty if request.registry.settings["tildes.open_registration"]: return None # attempt to fetch and lock the row for the specified invite code (lock prevents # concurrent requests from using the same invite code) lookup_code = UserInviteCode.prepare_code_for_lookup(invite_code) code_row = ( request.query(UserInviteCode).filter( UserInviteCode.code == lookup_code, UserInviteCode.invitee_id == None, # noqa ).with_for_update(skip_locked=True).one_or_none()) if not code_row: incr_counter("invite_code_failures") raise HTTPUnprocessableEntity("Invalid invite code") return code_row
def post_register( request: Request, username: str, password: str, password_confirm: str, invite_code: str, ) -> HTTPFound: """Process a registration request.""" if not request.params.get("accepted_terms"): raise HTTPUnprocessableEntity( "Terms of Use and Privacy Policy must be accepted.") if password != password_confirm: raise HTTPUnprocessableEntity( "Password and confirmation do not match.") # attempt to fetch and lock the row for the specified invite code (lock prevents # concurrent requests from using the same invite code) lookup_code = UserInviteCode.prepare_code_for_lookup(invite_code) code_row = ( request.query(UserInviteCode).filter( UserInviteCode.code == lookup_code, UserInviteCode.invitee_id == None, # noqa ).with_for_update(skip_locked=True).one_or_none()) if not code_row: incr_counter("invite_code_failures") raise HTTPUnprocessableEntity("Invalid invite code") # create the user and set inviter to the owner of the invite code user = User(username, password) user.inviter_id = code_row.user_id # flush the user insert to db, will fail if username is already taken request.db_session.add(user) try: request.db_session.flush() except IntegrityError: raise HTTPUnprocessableEntity( "That username has already been registered.") # the flush above will generate the new user's ID, so use that to update the invite # code with info about the user that registered with it code_row.invitee_id = user.user_id # subscribe the new user to all groups except ~test all_groups = request.query(Group).all() for group in all_groups: if group.path == "test": continue request.db_session.add(GroupSubscription(user, group)) _send_welcome_message(user, request) incr_counter("registrations") # log the user in to the new account remember(request, user.user_id) # set request.user before logging so the user is associated with the event request.user = user request.db_session.add(Log(LogEventType.USER_REGISTER, request)) # redirect to the front page raise HTTPFound(location="/")