Repository: couchers-org/couchers
Issue: "Invalid token" when trying to sign up with username already signed up

Sentry Issue: [BACKEND-2T](https://sentry.io/organizations/couchers/issues/2582011114/?referrer=github_integration)

```
UniqueViolation: duplicate key value violates unique constraint "uq_signup_flows_username"
DETAIL:  Key (username)=(esra) already exists.

  File "sqlalchemy/engine/base.py", line 1771, in _execute_context
    self.dialect.do_execute(
  File "sqlalchemy/engine/default.py", line 717, in do_execute
    cursor.execute(statement, parameters)

IntegrityError: (psycopg2.errors.UniqueViolation) duplicate key value violates unique constraint "uq_signup_flows_username"
DETAIL:  Key (username)=(esra) already exists.

[SQL: UPDATE signup_flows SET username=%(username)s, birthdate=%(birthdate)s, gender=%(gender)s, hosting_status=%(hosting_status)s, city=%(city)s, geom=CAST(CAST(ST_GeogFromText(%(param_1)s) AS geography(POINT,4326)) AS geometry(POINT,4326)), geom_radius=%(geom_radius)s, accepted_tos=%(accepted_tos)s WHERE signup_flows.id = %(signup_flows_id)s]
[param...
(15 additional frame(s) were not displayed)
...
  File "sqlalchemy/engine/base.py", line 1814, in _execute_context
    self._handle_dbapi_exception(
  File "sqlalchemy/engine/base.py", line 1995, in _handle_dbapi_exception
    util.raise_(
  File "sqlalchemy/util/compat.py", line 207, in raise_
    raise exception
  File "sqlalchemy/engine/base.py", line 1771, in _execute_context
    self.dialect.do_execute(
  File "sqlalchemy/engine/default.py", line 717, in do_execute
    cursor.execute(statement, parameters)
```

Changed files:
app/backend/src/couchers/servicers/auth.py, app/backend/src/couchers/errors.py

diff --git a/app/backend/src/couchers/errors.py b/app/backend/src/couchers/errors.py
index 456b83fbe..ba21e22b6 100644
--- a/app/backend/src/couchers/errors.py
+++ b/app/backend/src/couchers/errors.py
@@ -119,6 +119,7 @@ SIGNUP_FLOW_BASIC_FILLED = "Basic part already filled in."
 SIGNUP_FLOW_BASIC_NEEDED = "Basic part needed to sign up."
 SIGNUP_FLOW_EMAIL_STARTED_SIGNUP = "Please check your email for a link to continue signing up."
 SIGNUP_FLOW_EMAIL_TAKEN = "That email address is already associated with an account. Please log in instead!"
+SIGNUP_FLOW_USERNAME_TAKEN = "That username is already associated with an account"
 SIGNUP_FLOW_FEEDBACK_FILLED = "Feedback part already filled in."
 SIGNUP_FLOW_MISSING_FORM = "Missing contributor form."
 THREAD_NOT_FOUND = "Discussion thread not found."
diff --git a/app/backend/src/couchers/servicers/auth.py b/app/backend/src/couchers/servicers/auth.py
index 268f226bf..d5de3e952 100644
--- a/app/backend/src/couchers/servicers/auth.py
+++ b/app/backend/src/couchers/servicers/auth.py
@@ -156,6 +156,13 @@ class Auth(auth_pb2_grpc.AuthServicer):
                     ).scalar_one_or_none()
                     if existing_user:
                         context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_EMAIL_TAKEN)
+
+                    existing_username = session.execute(
+                        select(User).where(User.username == request.basic.username)
+                    ).scalar_one_or_none()
+                    if existing_username:
+                        context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_USERNAME_TAKEN)
+
                     existing_flow = session.execute(
                         select(SignupFlow).where(SignupFlow.email == request.basic.email)
                     ).scalar_one_or_none()
@@ -171,11 +178,7 @@ class Auth(auth_pb2_grpc.AuthServicer):
 
                     flow_token = cookiesafe_secure_token()
 
-                    flow = SignupFlow(
-                        flow_token=flow_token,
-                        name=request.basic.name,
-                        email=request.basic.email,
-                    )
+                    flow = SignupFlow(flow_token=flow_token, name=request.basic.name, email=request.basic.email)
                     session.add(flow)
                     session.flush()
                 else:
@@ -304,14 +307,8 @@ class Auth(auth_pb2_grpc.AuthServicer):
                 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),
-                )
+                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,
@@ -381,11 +378,7 @@ class Auth(auth_pb2_grpc.AuthServicer):
 
                 # create a session
                 token, expiry = create_session(context, session, user, False)
-                context.send_initial_metadata(
-                    [
-                        ("set-cookie", create_session_cookie(token, expiry)),
-                    ]
-                )
+                context.send_initial_metadata([("set-cookie", create_session_cookie(token, expiry))])
                 return _auth_res(user)
             else:
                 context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_TOKEN)
@@ -410,11 +403,7 @@ class Auth(auth_pb2_grpc.AuthServicer):
                     logger.debug(f"Right password")
                     # correct password
                     token, expiry = create_session(context, session, user, request.remember_device)
-                    context.send_initial_metadata(
-                        [
-                            ("set-cookie", create_session_cookie(token, expiry)),
-                        ]
-                    )
+                    context.send_initial_metadata([("set-cookie", create_session_cookie(token, expiry))])
                     return _auth_res(user)
                 else:
                     logger.debug(f"Wrong password")
@@ -438,11 +427,7 @@ class Auth(auth_pb2_grpc.AuthServicer):
             delete_session(token)
 
         # set the cookie to an empty string and expire immediately, should remove it from the browser
-        context.send_initial_metadata(
-            [
-                ("set-cookie", create_session_cookie("", now())),
-            ]
-        )
+        context.send_initial_metadata([("set-cookie", create_session_cookie("", now()))])
 
         return empty_pb2.Empty()