From 6e7f4fee0853eb01772ff5748cbb60a9af86a2ea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Mar 2026 09:43:12 +0000 Subject: [PATCH] docs(auth): document google connect conflict behavior Co-authored-by: Tyler Dane --- docs/backend/api-documentation.md | 34 ++++++++++++++++++ .../google-sync-and-websocket-flow.md | 23 ++++++++++++ docs/features/password-auth-flow.md | 29 +++++++++++++++ docs/manual-testing/auth-testing.md | 35 +++++++++++++++++-- 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/docs/backend/api-documentation.md b/docs/backend/api-documentation.md index 37ffffe3c..1d5337312 100644 --- a/docs/backend/api-documentation.md +++ b/docs/backend/api-documentation.md @@ -112,6 +112,23 @@ Authenticated Compass-defined Google attach/reconnect endpoint: } ``` +`POST /api/auth/google/connect` request example: + +```json +{ + "thirdPartyId": "google", + "clientType": "web", + "redirectURIInfo": { + "redirectURIOnProviderDashboard": "https://example.com/day", + "redirectURIQueryParams": { + "code": "oauth-authorization-code", + "scope": "openid email profile", + "state": "opaque-state" + } + } +} +``` + Behavior: - intended only for users who already have an active Compass session @@ -121,6 +138,23 @@ Behavior: Compass user - marks Google sync metadata for restart and starts background sync +Conflict contract (Google account already owned by another Compass user): + +- status code: `409 CONFLICT` +- payload shape (BaseError client payload): + +```json +{ + "result": "User not connected", + "message": "Google account is already connected to another Compass user" +} +``` + +Operational notes: + +- conflict exits before credential persistence, so no sync restart is triggered +- clients should treat this as an ownership conflict and keep the current Compass session + ### SuperTokens-managed auth endpoints (runtime) Files: diff --git a/docs/features/google-sync-and-websocket-flow.md b/docs/features/google-sync-and-websocket-flow.md index 7e215a928..cd489aa5c 100644 --- a/docs/features/google-sync-and-websocket-flow.md +++ b/docs/features/google-sync-and-websocket-flow.md @@ -262,6 +262,29 @@ Primary files: - `packages/web/src/auth/hooks/oauth/useConnectGoogle.ts` - `packages/web/src/common/repositories/event/event.repository.util.ts` +### Connect-Later Ownership Conflict Triage + +If `POST /api/auth/google/connect` returns `409` while a user is trying to +connect Google from an existing password session: + +1. confirm whether the Google account is already linked by checking for an + existing Compass user with the same `google.googleId` +2. verify backend conflict payload: + - `result: "User not connected"` + - `message: "Google account is already connected to another Compass user"` +3. verify no reconnect side effects were applied for the current session user: + - no new Google credential write + - no metadata transition to `sync.importGCal = "RESTART"` + - no reconnect/import websocket lifecycle (`IMPORT_GCAL_START` / + `IMPORT_GCAL_END`) for that failed request + +Expected operator action: + +- treat as ownership protection, not as an OAuth transport failure +- have the user authenticate into the Compass account that already owns that + Google identity (or disconnect/recover ownership through an explicit support + path) + ## User Metadata Shape Used By Socket And UI `UserMetadata` includes Google connection state alongside sync state: diff --git a/docs/features/password-auth-flow.md b/docs/features/password-auth-flow.md index 0a06a2804..21a0ed0f5 100644 --- a/docs/features/password-auth-flow.md +++ b/docs/features/password-auth-flow.md @@ -266,6 +266,35 @@ When a logged-in password user chooses `Connect Google Calendar`: This path does not call SuperTokens `signInUpPOST` and does not depend on SuperTokens account linking. +### Google connect conflict contract + +If a logged-in user attempts to connect a Google account that is already linked +to a different Compass user, backend connect intentionally fails with a conflict +instead of reassigning ownership. + +Source path: + +- `googleAuthService.connectGoogleToCurrentUser(...)` + +Response contract: + +- status: `409 CONFLICT` +- payload shape: + +```json +{ + "result": "User not connected", + "message": "Google account is already connected to another Compass user" +} +``` + +Operational implications: + +- no Google credentials are persisted for the current session user on conflict +- metadata sync flags are not set to `"RESTART"` for that failed request +- clients should keep the current Compass session and prompt users to sign in + with the account that already owns the Google connection + ### Email/password sign-up and sign-in The `EmailPassword` recipe is overridden in two places. diff --git a/docs/manual-testing/auth-testing.md b/docs/manual-testing/auth-testing.md index c35ab519a..6f2250d66 100644 --- a/docs/manual-testing/auth-testing.md +++ b/docs/manual-testing/auth-testing.md @@ -217,7 +217,35 @@ land in the same Compass account rather than creating a duplicate account. - Existing Compass data remains visible. - No duplicate or empty account is created. -## Scenario 9: Session-Expired Re-Auth +## Scenario 9: Connect Conflict (Google Account Already Linked Elsewhere) + +### UX + +If a logged-in user tries to connect a Google account that already belongs to a +different Compass user, the connect action should fail safely without replacing +or mutating the current account session. + +### Steps + +1. Prepare two distinct Compass users (User A and User B). +2. Connect Google account G to User A and confirm success. +3. Log out User A. +4. Log in as User B (email/password session is easiest for setup). +5. Trigger `Connect Google Calendar`. +6. Complete OAuth with the same Google account G. +7. Observe network and UI behavior after OAuth returns. + +### Expected Results + +- `POST /api/auth/google/connect` returns `409`. +- Response payload includes: + - `result: "User not connected"` + - `message: "Google account is already connected to another Compass user"` +- User B remains signed in as User B (session is not replaced). +- User B's existing Compass data remains visible. +- Google connection status for User B does not transition to connected/importing. + +## Scenario 10: Session-Expired Re-Auth ### UX @@ -238,7 +266,7 @@ When a previously authenticated session becomes invalid, the app should guide th - Clicking `Sign in` opens the login modal. - Re-authenticating restores normal app usage. -## Scenario 10: Logout And Persisted Gate State +## Scenario 11: Logout And Persisted Gate State ### UX @@ -270,7 +298,8 @@ If time is limited, run these checks before shipping auth changes: 6. `Connect Google Calendar` works from an authenticated password session without losing existing Compass data. 7. After connect-later, logged-out Google sign-in lands in the same Compass account. 8. Session expiry opens the login modal from the toast. -9. Logging out preserves the rollout gate for that browser session. +9. Connect conflict returns `409` and does not change active Compass session. +10. Logging out preserves the rollout gate for that browser session. ## Current Caveats