Skip to content

Commit 88a0a90

Browse files
authored
fix(web): redirect Google auth through callback (#1723)
* Refine sync lifecycle and simplify related UI * fix(web): validate google callback scopes * fix(e2e): cover google auth callback flow * fix: remove committed screen log * refactor(web): deepen google authorization flow
1 parent fed7e31 commit 88a0a90

63 files changed

Lines changed: 1547 additions & 1330 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTEXT.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ _Avoid_: non-Google user
4343
**Google-connected user**:
4444
An authenticated user with usable Google credentials stored by the backend.
4545

46+
**Google authorization**:
47+
The Google approval step that lets Compass sign a user in with Google or connect
48+
Google Calendar to an existing Compass session.
49+
_Avoid_: Google login mode
50+
51+
**Google authorization intent**:
52+
The user's Compass purpose for a Google authorization: Google sign-in/up or
53+
Google Calendar connect/reconnect.
54+
_Avoid_: auth mode
55+
4656
**Google revoked**:
4757
The state where Google access is no longer usable and Google-origin data should
4858
be pruned, ignored, or reconnected.
@@ -164,6 +174,8 @@ during Import or Public watch notification handling.
164174
**Events** without becoming a **Google-connected user**.
165175
- A **Google-connected user** can import from Google and mirror eligible Compass
166176
event changes to Google.
177+
- A **Google authorization** must preserve its **Google authorization intent**
178+
instead of inferring the user's goal from the later session state.
167179
- **Public watch notifications** are separate from browser API and **SSE**
168180
traffic; browser traffic can be local, but Google webhook posts need public
169181
HTTPS when continuous sync is expected.

docs/README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Internal documentation for engineers and agents working in the Compass repo.
44

55
Start with [AGENTS.md](../AGENTS.md) for repo rules and command defaults. Use this index for codebase shape, subsystem behavior, and acceptance runbooks.
66

7+
## How docs get published
8+
9+
Markdown files in this `docs/` directory are automatically mirrored to [docs.compasscalendar.com](https://docs.compasscalendar.com). A GitHub Action detects any push to `main` that touches `docs/**` and syncs the changes to the doc site. Just edit any of the markdown in `docs/` and the doc site will update itself upon merge.
10+
711
## Start Here
812

913
- [Repo Architecture](./architecture/repo-architecture.md)
@@ -56,9 +60,3 @@ User-visible behavior runbooks for manual verification and expected outcomes:
5660
- [Recurring Events](./acceptance/recurring-events.md)
5761
- [Shortcuts](./acceptance/shortcuts.md)
5862
- [Tasks](./acceptance/tasks.md)
59-
60-
## How docs get published
61-
62-
Markdown files in this `docs/` directory are automatically mirrored to [docs.compasscalendar.com](https://docs.compasscalendar.com). A GitHub Action detects any push to `main` that touches `docs/**` and syncs the changes to the doc site. Just edit any of the markdown in `docs/` and the doc site will update itself upon merge.
63-
64-

docs/acceptance/auth.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,21 +132,21 @@ The forgot-password flow should avoid leaking whether an email exists. The reset
132132

133133
### UX
134134

135-
The auth modal should still allow Google sign in from a logged-out state. A successful Google flow should authenticate the user. Closing the popup should behave like cancellation, not like a hard auth failure.
135+
The auth modal should still allow Google sign in from a logged-out state. A successful Google redirect flow should authenticate the user and return to Compass.
136136

137137
### Steps
138138

139139
1. Open the auth modal from `/day?auth=login`.
140140
2. Select `Continue with Google`.
141-
3. Complete Google OAuth successfully.
141+
3. Complete the Google authorization redirect with the intended Google account.
142142
4. Log out.
143-
5. Start Google sign in again, but close the popup before finishing.
143+
5. Start Google sign in again, but cancel at Google before finishing.
144144

145145
### Expected Results
146146

147-
- A successful Google sign-in authenticates the user and returns them to the app.
148-
- Closing the popup clears the loading state.
149-
- Popup cancellation does not leave the app stuck in an auth error state.
147+
- The Google authorization redirect returns to Compass through `/auth/google/callback`.
148+
- A successful Google sign-in authenticates the user and returns them to the saved app path.
149+
- A canceled Google redirect returns to Compass and shows a recoverable auth error.
150150

151151
## Scenario 6: Password-Only Compass Usage Before Google Connect
152152

docs/acceptance/google-sync.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ A password-authenticated user can connect Google Calendar from inside the app us
4949
1. Sign up or log in with email/password. Do not connect Google.
5050
2. Create at least one Compass event so there is pre-existing data.
5151
3. Open the command palette (Cmd+K) and select Connect Google Calendar, or click the Google status icon in the sidebar.
52-
4. Complete the Google OAuth popup with the intended Google account.
52+
4. Complete the Google authorization redirect with the intended Google account.
5353
5. Return to Compass and observe the sidebar status icon.
5454
6. Reload the page.
5555

5656
### Expected Results
5757

58-
- The OAuth popup opens and closes cleanly without redirecting away from the app shell.
58+
- The Google authorization redirect returns to Compass through `/auth/google/callback`.
5959
- The sidebar status transitions away from NOT_CONNECTED into an importing state.
6060
- Pre-existing Compass events remain visible on the calendar.
6161
- The network flow uses `POST /api/auth/google/connect`, not the logged-out sign-in path.
@@ -72,7 +72,7 @@ After connecting Google, Compass imports all events from the user's Google calen
7272
### Steps
7373

7474
1. Connect Google Calendar (see Scenario 1), or start with an account that has `importGCal` flagged for restart.
75-
2. Observe the header immediately after the OAuth popup closes.
75+
2. Observe the header immediately after the Google authorization redirect returns.
7676
3. Continue using the app normally while the import runs (navigate to different dates, create a Compass event).
7777
4. Wait for the header spinner to disappear.
7878
5. Check the calendar for newly imported Google events.
@@ -207,12 +207,12 @@ After revocation, the user can reconnect Google using the same flow as the initi
207207

208208
1. Complete Scenario 7 so the connection is in the NOT_CONNECTED state.
209209
2. Open the command palette and select Connect Google Calendar.
210-
3. Complete the Google OAuth popup.
210+
3. Complete the Google authorization redirect.
211211
4. Wait for the import to complete.
212212

213213
### Expected Results
214214

215-
- The OAuth popup opens and closes without error.
215+
- The Google authorization redirect returns to Compass without error.
216216
- The import spinner appears in the header.
217217
- Google events repopulate the calendar after import completes.
218218
- The sidebar status returns to HEALTHY.

docs/development/feature-file-map.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ Use this document to find the first files to inspect for common Compass changes.
1616

1717
- Session initialization and SuperTokens wiring: `packages/web/src/auth/session/SessionProvider.tsx`
1818
- User profile bootstrap: `packages/web/src/auth/context/UserProvider.tsx`
19-
- Google OAuth app flow: `packages/web/src/auth/hooks/google/useGoogleAuth/useGoogleAuth.ts`, `packages/web/src/auth/hooks/google/useGoogleAuthWithOverlay/useGoogleAuthWithOverlay.ts`
20-
- Google OAuth provider wrapper: `packages/web/src/auth/hooks/google/useGoogleLogin/useGoogleLogin.ts`
21-
- Popup-cancel classification for Google OAuth: `packages/web/src/auth/google/google-oauth-error.util.ts`
19+
- Google authorization app flow: `packages/web/src/auth/google/authorization`
20+
- Google redirect callback: `packages/web/src/views/GoogleAuthCallback/GoogleAuthCallback.tsx`
21+
- Google authorization intent storage: `packages/web/src/auth/google/authorization/google-authorization.storage.ts`
2222
- Auth schemas: `packages/web/src/auth/schemas/auth.schemas.ts`
2323
- Backend auth routes: `packages/backend/src/auth/auth.routes.config.ts`
2424
- Backend auth controllers/services: `packages/backend/src/auth/controllers`, `packages/backend/src/auth/services`

docs/development/troubleshoot.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ If that pre-connect local sync fails, connect is intentionally aborted and the u
132132

133133
What this means operationally:
134134

135-
- this is a local-to-cloud event migration failure, not an OAuth popup failure
135+
- this is a local-to-cloud event migration failure, not a Google redirect failure
136136
- backend `connectGoogleToCurrentUser` is not called for that attempt
137137
- no Google import restart should be observed from that click
138138

docs/features/password-auth-flow.md

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,33 @@ Design intent:
7777
- logged-in Google attach is an authenticated Compass backend flow
7878
- logout is decoupled from Google state and succeeds even when no Google account
7979
is linked
80+
- Google authorization uses a full-page redirect. Both Google sign-in/up and
81+
Google Calendar connect/reconnect return to `/auth/google/callback`.
82+
- Before leaving for Google, the web app stores a short-lived Google
83+
authorization intent and same-origin return path in `sessionStorage`, keyed by
84+
the OAuth `state` value. The callback validates that state, finishes the saved
85+
intent, removes it, and then returns the user to the page that started the
86+
flow. If the saved return path is missing or unsafe, the callback falls back
87+
to `/day`.
88+
- The backend accepts only the configured Compass Google callback URL as the
89+
OAuth redirect URI when exchanging a Google code. The callback URL is derived
90+
from backend `FRONTEND_URL` plus `/auth/google/callback`.
91+
- The callback page is intentionally transitional: it shows a simple completion
92+
status, finishes or fails the saved Google authorization intent, and navigates
93+
back into the app.
94+
- Google authorization no longer uses a blocking overlay. The callback page is
95+
the only Google authorization loading surface.
96+
- When a user first signs up or signs in, Compass should sync local events the
97+
user created themselves, but should not sync seeded demo events such as
98+
"Morning standup" or "Try Compass" into the new account.
99+
- Seeded demo events should be marked only in browser IndexedDB. The marker is
100+
used to skip demo events during local-event sync and must not be sent to the
101+
backend or stored as account event data.
102+
- Editing a seeded demo event does not make it a user-created event for sync
103+
purposes; it should still be skipped.
104+
- Logged-out Google sign-in keeps the shared post-auth completion behavior for
105+
now: after the session is created, Compass syncs local events to the account
106+
and warns if those events remain device-local.
80107

81108
## Web Entry Points
82109

@@ -262,19 +289,31 @@ does not return a new one.
262289

263290
When a logged-in password user chooses `Connect Google Calendar`:
264291

265-
1. the web client completes the Google popup flow
266-
2. `useConnectGoogle()` sends the auth-code payload to
292+
1. the web client syncs pending local events to the server
293+
2. if local-event sync succeeds, the web client redirects through Google and
294+
returns to
295+
`/auth/google/callback`
296+
3. the callback sends the auth-code payload to
267297
`POST /api/auth/google/connect`
268-
3. `connectGoogleToCurrentUser()` exchanges the code for Google tokens
269-
4. backend verifies the Google account is not already owned by a different
298+
4. `connectGoogleToCurrentUser()` exchanges the code for Google tokens
299+
5. backend verifies the Google account is not already owned by a different
270300
Compass user
271-
5. backend persists Google credentials onto the current Compass user
272-
6. backend marks metadata sync flags as `"RESTART"` and restarts sync in the
301+
6. backend persists Google credentials onto the current Compass user
302+
7. backend marks metadata sync flags as `"RESTART"` and restarts sync in the
273303
background
274304

275305
This path does not call SuperTokens `signInUpPOST` and does not depend on
276306
SuperTokens account linking.
277307

308+
Redirect implementation should include focused tests for:
309+
310+
- matching OAuth `state` before completing the callback
311+
- routing Google sign-in/up and Google Calendar connect/reconnect to the correct
312+
backend endpoint
313+
- rejecting unsafe return paths and falling back to `/day`
314+
- using the configured `/auth/google/callback` URL when exchanging Google codes
315+
- syncing user-created local events while skipping demo-marked local events
316+
278317
### Google connect conflict contract
279318

280319
If a logged-in user attempts to connect a Google account that is already linked
@@ -403,3 +442,6 @@ session-linking failure mode.
403442
- A Google account can belong to only one Compass user. In-session connect
404443
returns a conflict if the Google account is already attached elsewhere.
405444
- Dated-route redirects preserve existing query params (including `auth=verify`), but `useAuthUrlParam()` only handles `login`, `signup`, `forgot`, and `reset`.
445+
- Future UX question: first-time Google sign-in may need a choice before syncing
446+
anonymous local events into the account, especially when those events are demo
447+
or placeholder data.

docs/frontend/frontend-runtime-flow.md

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,29 +73,13 @@ Once a user has ever authenticated, the app records that fact in local auth-stat
7373

7474
When a user re-authenticates with Google, auth-state utilities also clear any in-memory "Google revoked" flag so normal remote sync can resume.
7575

76-
## Google OAuth Popup Cancellation Semantics
76+
## Google Authorization Redirect
7777

78-
Files:
79-
80-
- `packages/web/src/auth/hooks/google/useGoogleAuth/useGoogleAuth.ts`
81-
- `packages/web/src/auth/hooks/google/useGoogleLogin/useGoogleLogin.ts`
82-
- `packages/web/src/auth/google/google-oauth-error.util.ts`
83-
84-
The web auth flow intentionally treats popup-close outcomes as cancellation, not authentication failure.
85-
86-
Cancellation detection (`isGooglePopupClosedError`) returns true when any of these match:
87-
88-
- `type === "popup_closed"`
89-
- `error`, `error_description`, or `message` equals `"popup_closed"` (case-insensitive)
90-
- `error`, `error_description`, or `message` contains `"popup window closed"` (case-insensitive)
91-
92-
When cancellation is detected in the auth hooks:
78+
Google sign-in/up and Google Calendar connect/reconnect leave Compass through a full-page Google redirect and return through `/auth/google/callback`.
9379

94-
- auth state is reset (`resetAuth`)
95-
- OAuth overlay closes because `selectIsAuthenticating` becomes false
96-
- generic auth failure state is not dispatched for that event
80+
Before redirecting, the web app stores a short-lived authorization intent in `sessionStorage` keyed by OAuth `state`. The callback validates that state, finishes the saved intent, removes it, and returns the user to the original same-origin path or `/day`.
9781

98-
For non-cancellation errors, normal auth-failure handling still applies.
82+
The old blocking overlay is not used for Google authorization.
9983

10084
## User Bootstrap
10185

docs/self-hosting/google-calendar.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@ Authorized JavaScript origins:
4343
http://localhost:9080
4444
4545
Authorized redirect URIs:
46-
http://localhost:9080
46+
http://localhost:9080/auth/google/callback
4747
```
4848

49-
Compass sends the browser origin as the OAuth redirect URI. That means the redirect URI is the app origin itself, not a longer callback path.
49+
Compass sends the dedicated Google callback page as the OAuth redirect URI.
50+
That means the redirect URI includes `/auth/google/callback`.
5051

5152
This path doesn't make your local backend public. It's for sign-in and one-time import only.
5253

@@ -74,17 +75,19 @@ Local setups do not create a public HTTPS URL, so they can't receive these. You
7475

7576
For a public server install, create a Google OAuth client with **Web application** as the client type.
7677

77-
Use your public Compass origin for both OAuth fields:
78+
Use your public Compass origin for JavaScript origins and the Compass callback
79+
page for redirect URIs:
7880

7981
```text
8082
Authorized JavaScript origins:
8183
https://cal.example.com
8284
8385
Authorized redirect URIs:
84-
https://cal.example.com
86+
https://cal.example.com/auth/google/callback
8587
```
8688

87-
Replace `https://cal.example.com` with your own Compass URL. Do not add `/api`, `/auth/callback`, or another path to the redirect URI.
89+
Replace `https://cal.example.com` with your own Compass URL. Do not add `/api`
90+
to the redirect URI.
8891

8992
Also check these in Google Cloud:
9093

0 commit comments

Comments
 (0)