def test_signup_verification_email(db): request_email = f"{random_hex(12)}@couchers.org.invalid" with session_scope() as session: flow = SignupFlow(name="Frodo", email=request_email) with patch("couchers.email.queue_email") as mock: send_signup_email(flow) assert mock.call_count == 1 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args assert recipient == request_email assert flow.email_token in plain assert flow.email_token in html
def test_signup_email(db): user, api_token = generate_user() request_email = f"{random_hex(12)}@couchers.org.invalid" with session_scope() as session: token, expiry_text = new_signup_token(session, request_email) with patch("couchers.email.queue_email") as mock: send_signup_email(request_email, token, expiry_text) assert mock.call_count == 1 (sender_name, sender_email, recipient, subject, plain, html), _ = mock.call_args assert recipient == request_email assert token.token in plain assert token.token in html
def Signup(self, request, context): """ First step of Signup flow. If the email is not a valid email (by regexp), returns INVALID_EMAIL If the email already exists, returns EMAIL_EXISTS. Otherwise, creates a signup token and sends an email, then returns SENT_SIGNUP_EMAIL. """ logging.debug(f"Signup with {request.email=}") if not is_valid_email(request.email): return auth_pb2.SignupRes(next_step=auth_pb2.SignupRes.SignupStep.INVALID_EMAIL) with session_scope(self._Session) as session: user = session.query(User).filter(User.email == request.email).one_or_none() if not user: token, expiry_text = new_signup_token(session, request.email) send_signup_email(request.email, token, expiry_text) return auth_pb2.SignupRes(next_step=auth_pb2.SignupRes.SignupStep.SENT_SIGNUP_EMAIL) else: return auth_pb2.SignupRes(next_step=auth_pb2.SignupRes.SignupStep.EMAIL_EXISTS)
def test_signup_email(db): user, api_token = generate_user(db) request_email = f"{random_hex(12)}@couchers.org.invalid" message_id = random_hex(64) with session_scope(db) as session: token, expiry_text = new_signup_token(session, request_email) @create_autospec def mock_send_email(sender_name, sender_email, recipient, subject, plain, html): assert recipient == request_email assert token.token in plain assert token.token in html return message_id with patch("couchers.email.send_email", mock_send_email) as mock: send_signup_email(request_email, token, expiry_text) assert mock.call_count == 1
def SignupFlow(self, request, context): with session_scope() as session: if request.email_token: # the email token can either be for verification or just to find an existing signup flow = session.execute( select(SignupFlow). where(SignupFlow.email_verified == False).where( SignupFlow.email_token == request.email_token).where( SignupFlow.token_is_valid)).scalar_one_or_none() if flow: # find flow by email verification token and mark it as verified flow.email_verified = True flow.email_token = None flow.email_token_expiry = None session.flush() else: # just try to find the flow by flow token, no verification is done flow = session.execute( select(SignupFlow).where( SignupFlow.flow_token == request.email_token)).scalar_one_or_none() if not flow: context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_TOKEN) else: if not request.flow_token: # fresh signup if not request.HasField("basic"): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.SIGNUP_FLOW_BASIC_NEEDED) # TODO: unique across both tables existing_user = session.execute( select(User).where(User.email == request.basic.email) ).scalar_one_or_none() if existing_user: context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_EMAIL_TAKEN) existing_flow = session.execute( select(SignupFlow).where( SignupFlow.email == request.basic.email)).scalar_one_or_none() if existing_flow: send_signup_email(existing_flow) session.commit() context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_EMAIL_STARTED_SIGNUP) if not is_valid_email(request.basic.email): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_EMAIL) if not is_valid_name(request.basic.name): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME) flow_token = cookiesafe_secure_token() flow = SignupFlow( flow_token=flow_token, name=request.basic.name, email=request.basic.email, ) session.add(flow) session.flush() else: # not fresh signup flow = session.execute( select(SignupFlow).where( SignupFlow.flow_token == request.flow_token)).scalar_one_or_none() if not flow: context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_TOKEN) if request.HasField("basic"): context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_BASIC_FILLED) # we've found and/or created a new flow, now sort out other parts if request.HasField("account"): if flow.account_is_filled: context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_ACCOUNT_FILLED) # check username validity if not is_valid_username(request.account.username): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_USERNAME) if not self._username_available(request.account.username): context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.USERNAME_NOT_AVAILABLE) abort_on_invalid_password(request.account.password, context) hashed_password = hash_password(request.account.password) birthdate = parse_date(request.account.birthdate) if not birthdate or birthdate >= minimum_allowed_birthdate( ): context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.INVALID_BIRTHDATE) if not request.account.hosting_status: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOSTING_STATUS_REQUIRED) if request.account.lat == 0 and request.account.lng == 0: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE) if not request.account.accept_tos: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MUST_ACCEPT_TOS) flow.username = request.account.username flow.hashed_password = hashed_password flow.birthdate = birthdate flow.gender = request.account.gender flow.hosting_status = hostingstatus2sql[ request.account.hosting_status] flow.city = request.account.city flow.geom = create_coordinate(request.account.lat, request.account.lng) flow.geom_radius = request.account.radius flow.accepted_tos = TOS_VERSION session.flush() if request.HasField("feedback"): if flow.filled_feedback: context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_FEEDBACK_FILLED) form = request.feedback flow.filled_feedback = True flow.ideas = form.ideas flow.features = form.features flow.experience = form.experience flow.contribute = contributeoption2sql[form.contribute] flow.contribute_ways = form.contribute_ways flow.expertise = form.expertise session.flush() if request.HasField("accept_community_guidelines"): if not request.accept_community_guidelines.value: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MUST_ACCEPT_COMMUNITY_GUIDELINES) flow.accepted_community_guidelines = GUIDELINES_VERSION session.flush() # send verification email if needed if not flow.email_sent: send_signup_email(flow) session.flush() # finish the signup if done if flow.is_completed: user = User( name=flow.name, email=flow.email, username=flow.username, hashed_password=flow.hashed_password, birthdate=flow.birthdate, gender=flow.gender, hosting_status=flow.hosting_status, city=flow.city, geom=flow.geom, geom_radius=flow.geom_radius, accepted_tos=flow.accepted_tos, accepted_community_guidelines=flow. accepted_community_guidelines, onboarding_emails_sent=1, last_onboarding_email_sent=func.now(), ) session.add(user) form = ContributorForm( user=user, ideas=flow.ideas or None, features=flow.features or None, experience=flow.experience or None, contribute=flow.contribute or None, contribute_ways=flow.contribute_ways, expertise=flow.expertise or None, ) session.add(form) user.filled_contributor_form = form.is_filled session.delete(flow) session.commit() enforce_community_memberships_for_user(session, user) if form.is_filled: user.filled_contributor_form = True maybe_send_contributor_form_email(form) send_onboarding_email(user, email_number=1) token, expiry = create_session(context, session, user, False) context.send_initial_metadata([ ("set-cookie", create_session_cookie(token, expiry)), ]) return auth_pb2.SignupFlowRes(auth_res=_auth_res(user), ) else: return auth_pb2.SignupFlowRes( flow_token=flow.flow_token, need_account=not flow.account_is_filled, need_feedback=not flow.filled_feedback, need_verify_email=not flow.email_verified, need_accept_community_guidelines=flow. accepted_community_guidelines < GUIDELINES_VERSION, )