Skip to content

Add Google OAuth sessions and team access UI#8

Merged
fakechris merged 4 commits intomainfrom
feature/google-oauth-team-access
Apr 5, 2026
Merged

Add Google OAuth sessions and team access UI#8
fakechris merged 4 commits intomainfrom
feature/google-oauth-team-access

Conversation

@fakechris
Copy link
Copy Markdown
Owner

@fakechris fakechris commented Apr 5, 2026

Summary

  • add Google OAuth-backed browser sessions while keeping trusted CLI/dev auth paths
  • add team visibility and membership RBAC primitives plus access-control guards
  • add a minimal Access page for managing team visibility and memberships
  • update docs and env examples for the new auth/runtime model

Verification

  • pnpm --filter @involute/server typecheck
  • pnpm --filter @involute/server test --run src/auth.test.ts src/access-control.test.ts src/index.test.ts
  • pnpm --filter @involute/web typecheck
  • pnpm --filter @involute/web exec vitest --config vitest.config.ts --run src/App.access.test.tsx

Open with Devin

Summary by CodeRabbit

  • New Features

    • Google OAuth browser sign‑in with session cookies, session-aware UI, sign‑out, and session endpoint
    • Team visibility (PUBLIC/PRIVATE), role-based permissions (VIEWER/EDITOR/OWNER), and GraphQL mutations to manage access
    • Access page UI for managing team visibility and memberships (invite/upsert/remove)
  • Documentation

    • README and .env example updated with APP_ORIGIN, Google OAuth vars, and SESSION_TTL_SECONDS guidance
  • Tests

    • Added/updated unit, integration, and e2e tests for auth, sessions, and access flows
  • Chores

    • Dev/test startup and container init adjusted for deployment/runtime env options

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8228300-5a7d-4929-9fbf-c849007b602f

📥 Commits

Reviewing files that changed from the base of the PR and between 0352b36 and beb3fa5.

📒 Files selected for processing (2)
  • docker-compose.yml
  • packages/server/package.json
✅ Files skipped from review due to trivial changes (1)
  • packages/server/package.json

📝 Walkthrough

Walkthrough

Adds Google OAuth and session-backed authentication, Prisma-backed sessions and team memberships, RBAC for team visibility and membership roles, auth/session HTTP endpoints and client session bootstrapping, access-management UI and GraphQL mutations, plus docs and environment/config updates.

Changes

Cohort / File(s) Summary
Docs & env
\.env.example, README.md, docs/milestones.md, docs/review/triage.md, docs/vision.md
Add APP_ORIGIN, GOOGLE_OAUTH_*, SESSION_TTL_SECONDS; reclassify AUTH_TOKEN/VIEWER_ASSERTION_SECRET as trusted/dev; document OAuth sign-in, session cookies, and team permission primitives.
Prisma schema & seed
packages/server/prisma/schema.prisma, packages/server/prisma/seed-helpers.ts
Add enums (GlobalRole, TeamVisibility, TeamMembershipRole), models (Session, TeamMembership), extend User/Team, and set seeded admin globalRole.
Auth core & context
packages/server/src/auth.ts, packages/server/src/auth.test.ts
Introduce resolveRequestAuthentication, add authMode/isTrustedSystem to GraphQL context, per-request auth caching, session-first then token fallback, update auth plugin API and tests.
Session primitives
packages/server/src/session.ts
Implement token generation/hashing, cookie parsing/serialization, and Prisma-backed create/get/delete session functions with TTL handling.
OAuth flows & routes
packages/server/src/google-oauth.ts, packages/server/src/auth-routes.ts
Build Google auth URL, exchange code for profile, upsert Google user, create sessions, and expose /auth/google/start, /auth/google/callback, /auth/session, /auth/logout with origin/CORS and cookie semantics.
Access control & errors
packages/server/src/access-control.ts, packages/server/src/access-control.test.ts, packages/server/src/errors.ts
Add readable/visible Prisma where builders, assert helpers (read/write/manage), comment deletion guards, error constants and mappings, and unit tests for write authorization.
GraphQL schema & resolvers
packages/server/src/schema.ts, packages/server/src/index.ts, packages/server/src/index.test.ts
Add Query.viewer, TeamMembership types, mutations (teamUpdateAccess, teamMembershipUpsert, teamMembershipRemove); enforce access asserts in resolvers; wire auth routes and OAuth/session config into server startup and tests.
Client session & Apollo
packages/web/src/lib/session.ts, packages/web/src/lib/apollo.tsx
Add session-fetching, getGoogleLoginUrl, logoutSession; configure Apollo with credentials:'include'; update bootstrap error messaging to recommend Google sign-in or trusted dev token fallback.
Access UI, types & queries
packages/web/src/routes/AccessPage.tsx, packages/web/src/board/queries.ts, packages/web/src/board/types.ts
New AccessPage route and related queries/types/mutations; RBAC gating, deterministic member ordering, optimistic updates and error handling.
App UI, styles & tests
packages/web/src/App.tsx, packages/web/src/styles/app.css, packages/web/src/App.access.test.tsx, packages/web/src/App.error-states.test.tsx, packages/web/src/test/app-test-helpers.tsx
Load session on mount, show sign-in/sign-out and viewer info, add Access nav, session UI styles, tests and test helpers including session mocking.
Integration & infra changes
packages/server/src/index.test.ts, packages/server/src/graphql-mutations.test.ts, e2e/setup-backend.sh, playwright.config.ts, docker-compose.yml, packages/server/package.json
Add session-based GraphQL tests and mutation checks for access constraints; forward APP_ORIGIN/web port for e2e; conditional Prisma push (accept-data-loss flag) in compose/tests.
Misc tests
packages/server/src/access-control.test.ts, packages/web/src/App.access.test.tsx, packages/web/src/App.error-states.test.tsx
New and updated tests covering access control, access UI flows, and updated bootstrap messages.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser
    participant AppUI
    participant Server
    participant Google

    User->>AppUI: Click "Sign in with Google"
    AppUI->>Server: GET /auth/google/start
    Server->>Server: Generate state, build auth URL
    Server-->>Browser: 302 redirect + oauth state cookie
    Browser->>Google: GET authorization URL
    Google-->>Browser: 302 redirect to /auth/google/callback?code=...
    Browser->>Server: GET /auth/google/callback?code=...&state=...
    Server->>Server: Validate state cookie
    Server->>Google: POST /token (exchange code)
    Google-->>Server: access_token
    Server->>Google: GET /userinfo
    Google-->>Server: user profile
    Server->>Server: Upsert user, create session record
    Server-->>Browser: 302 redirect to appOrigin + session cookie
    Browser->>AppUI: Fetch session -> authenticated
Loading
sequenceDiagram
    participant Browser
    participant Server
    participant AuthCache as AuthCache
    participant Prisma

    Browser->>Server: POST /graphql (cookies + headers)
    Server->>AuthCache: lookup(Request)
    alt cached
        AuthCache-->>Server: RequestAuthentication
    else not cached
        Server->>Server: read session cookie
        alt session present
            Server->>Prisma: find session by tokenHash
            Prisma-->>Server: session + user
            Server->>AuthCache: cache session auth
        else no session
            Server->>Server: check Authorization header
            alt valid token
                Server->>AuthCache: cache token auth (trusted)
            else
                Server->>AuthCache: cache unauthenticated
            end
        end
        AuthCache-->>Server: RequestAuthentication
    end
    Server->>Server: build GraphQL context (viewer, authMode, isTrustedSystem)
    Server-->>Browser: GraphQL response (authorization applied)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 I hopped to code a cookie bright,

States and tokens tucked at night,
Owners, editors, viewers cheer,
Sessions snug and access near,
A rabbit guards your team tonight ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Add Google OAuth sessions and team access UI' clearly and accurately summarizes the main changes: Google OAuth authentication, session management, and a new access control UI component.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/google-oauth-team-access

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a comprehensive authentication and authorization system, introducing Google OAuth support and session-based authentication via cookies. It establishes a Role-Based Access Control (RBAC) framework for teams and issues, including visibility settings and membership roles. The updates include database schema changes, new server-side auth routes, and a dedicated Access management page in the web UI. Feedback suggests refining the GraphQL mutation wrapper to ensure authorization errors are correctly propagated and improving the accuracy of error messages when team memberships are not found.

Comment thread packages/server/src/schema.ts Outdated
Comment on lines 1140 to 1142
if (error instanceof GraphQLError || getExposedError(error) || isPrismaInvalidInputError(error)) {
return fallback;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The error handling in runMutation is too broad. The condition error instanceof GraphQLError will catch and suppress authorization errors (e.g., FORBIDDEN, NOT_FOUND) that should be propagated to the client. This prevents the UI from showing specific feedback about why an action failed (e.g. permission denied).

Exposed errors should be re-thrown to be handled by the GraphQL layer, while other specific, non-critical errors can be converted into a { success: false } payload.

    if (getExposedError(error)) {
      throw error;
    }

    if (isPrismaInvalidInputError(error)) {
      return fallback;
    }

Comment thread packages/server/src/schema.ts Outdated
});

if (!membership) {
throw createNotFoundError(TEAM_NOT_FOUND_MESSAGE);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message here is misleading. If a teamMembership is not found, the error thrown is TEAM_NOT_FOUND_MESSAGE, which states that the team could not be found. This could be confusing for debugging or for the client.

It would be more accurate to throw an error indicating that the membership was not found. You might consider adding a new error constant like TEAM_MEMBERSHIP_NOT_FOUND_MESSAGE and using it here.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (7)
packages/web/src/lib/apollo.tsx (1)

174-178: Consider adding error handling for malformed GraphQL URLs.

getServerBaseUrl() will throw if getGraphqlUrl() returns a malformed URL. While getGraphqlUrl() currently returns valid URLs in all branches, a defensive try-catch could prevent runtime errors if the URL validation logic changes.

🛡️ Optional defensive implementation
 export function getServerBaseUrl(): string {
-  const graphqlUrl = new URL(getGraphqlUrl());
-
-  return graphqlUrl.origin;
+  try {
+    const graphqlUrl = new URL(getGraphqlUrl());
+    return graphqlUrl.origin;
+  } catch {
+    return 'http://localhost:4200';
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/lib/apollo.tsx` around lines 174 - 178, Add defensive error
handling around the URL parsing in getServerBaseUrl: wrap the call to new
URL(getGraphqlUrl()) in a try-catch, catch any TypeError or RangeError from a
malformed URL, and return a safe fallback (e.g., empty string or a sensible
default) or rethrow a more descriptive error; update getServerBaseUrl to
reference getGraphqlUrl() inside the try block and ensure the catch logs or
surfaces the failure clearly so callers know the origin could not be determined.
packages/server/src/access-control.test.ts (1)

7-24: Assert that the bypass path never hits teamMembership.findUnique().

These tests only prove that assertCanWriteTeam() resolves. They would still pass if the implementation started querying membership before short-circuiting, which is exactly the behavior this PR is trying to preserve for trusted/admin requests. Capture the mock and assert expect(findUnique).not.toHaveBeenCalled().

Also applies to: 26-48

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/access-control.test.ts` around lines 7 - 24, The test for
assertCanWriteTeam should also verify the bypass short-circuits membership
lookups: capture the mocked teamMembership.findUnique used in the test (the
vi.fn() passed into the prisma stub) and add an assertion
expect(findUnique).not.toHaveBeenCalled() after awaiting assertCanWriteTeam so
the test fails if the implementation queries membership before short-circuiting;
apply the same change to the other similar test block covering lines 26-48 that
tests trusted/admin bypasses.
packages/web/src/App.access.test.tsx (1)

101-129: Assert the mutation payloads, not just that the mocks were called.

These assertions still pass if the UI sends the wrong teamId, role, email, or removes the wrong membership, because the mocks resolve regardless of their variables. Verifying the call payloads would also remove the brittle positional lookup on Line 126.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/App.access.test.tsx` around lines 101 - 129, Update the test
to assert the actual mutation payloads instead of only that the mocks were
called: after the visibility change, verify teamUpdateAccess was called with the
expected variables (e.g., { teamId: ..., visibility: 'PUBLIC' }); after adding
the member, assert teamMembershipUpsert was called with the correct { teamId,
email: 'editor@example.com', name: 'Editor User', role: 'EDITOR' }; and when
removing, assert teamMembershipRemove was called with the correct membership
identifier/variables rather than using a brittle positional button lookup—use
the membership's unique label/text to find the specific Remove button or capture
the membership id from the rendered item so you can assert the correct variables
were passed to teamMembershipRemove.
packages/web/src/lib/session.ts (1)

30-35: Consider returning success/failure status from logoutSession().

The function silently ignores any errors from the logout request. Callers might want to know if the logout failed (e.g., to show a retry option or warning).

♻️ Optional: return a boolean indicating success
-export async function logoutSession(): Promise<void> {
-  await fetch(`${getServerBaseUrl()}/auth/logout`, {
+export async function logoutSession(): Promise<boolean> {
+  try {
+    const response = await fetch(`${getServerBaseUrl()}/auth/logout`, {
+      credentials: 'include',
+      method: 'POST',
+    });
+    return response.ok;
+  } catch {
+    return false;
+  }
-    credentials: 'include',
-    method: 'POST',
-  });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/lib/session.ts` around lines 30 - 35, The logoutSession
function currently ignores errors and always resolves; update logoutSession to
return a Promise<boolean> (or similar success flag) by awaiting
fetch(`${getServerBaseUrl()}/auth/logout`, catching network exceptions, and
returning false on error, and also checking response.ok (return true for 2xx,
false otherwise). Make changes inside logoutSession (and update callers) to
propagate the boolean result so callers can react to failures.
packages/web/src/test/app-test-helpers.tsx (1)

63-92: Consider extracting duplicated mutation mock logic into a shared function.

The mutation mock implementation (handling CommentCreate, CommentDelete, IssueDelete, TeamUpdateAccess, etc.) is duplicated between the hoisted mock (lines 63-91) and the beforeEach block (lines 171-198). Extract this to a shared factory to reduce maintenance burden.

♻️ Suggested refactor
function createMutationMockImplementation() {
  return (document: unknown) => {
    const source = getDocumentSource(document);

    if (source.includes('mutation CommentCreate')) {
      return [vi.fn().mockResolvedValue({ data: { commentCreate: { success: true, comment: null } } })];
    }
    // ... rest of the cases
    return [vi.fn()];
  };
}

// Then use in both places:
const hoistedApolloMocks = vi.hoisted<ApolloMockSet>(() => ({
  useQuery: vi.fn(),
  useMutation: vi.fn(createMutationMockImplementation()),
}));

// In beforeEach:
apolloMocks.useMutation.mockImplementation(createMutationMockImplementation());

Also applies to: 171-199

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/test/app-test-helpers.tsx` around lines 63 - 92, Extract the
duplicated mutation mock logic used in useMutation into a shared factory
function (e.g., createMutationMockImplementation) that returns the
implementation callback which inspects getDocumentSource(document) and returns
the appropriate vi.fn().mockResolvedValue(...) cases for 'mutation
CommentCreate', 'mutation CommentDelete', 'mutation IssueDelete', 'mutation
TeamUpdateAccess', 'mutation TeamMembershipUpsert', and 'mutation
TeamMembershipRemove', defaulting to [vi.fn()]; then use that factory when
creating the hoisted mock (vi.hoisted returning useMutation:
vi.fn(createMutationMockImplementation())) and in your beforeEach by calling
apolloMocks.useMutation.mockImplementation(createMutationMockImplementation()).
packages/web/src/routes/AccessPage.tsx (1)

128-134: Remove unnecessary async keyword from updateSelectedTeam.

The function is declared async but doesn't use await internally. This causes callers to unnecessarily await on calls to this function (e.g., lines 145, 170, 211, 253). The function could be synchronous.

♻️ Suggested fix
-  async function updateSelectedTeam(updater: (team: TeamSummary) => TeamSummary) {
+  function updateSelectedTeam(updater: (team: TeamSummary) => TeamSummary) {
     if (!selectedTeam) {
       return;
     }
 
     setTeams((currentTeams) => currentTeams.map((team) => (team.id === selectedTeam.id ? updater(team) : team)));
   }

Then remove the await from calls at lines 145, 170, 211, 253.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/routes/AccessPage.tsx` around lines 128 - 134, Remove the
unnecessary async from updateSelectedTeam: change the declaration "async
function updateSelectedTeam(updater: (team: TeamSummary) => TeamSummary)" to a
plain synchronous function "function updateSelectedTeam(...)" since it does not
use await; keep the existing setTeams logic intact. Then update all call sites
that currently "await updateSelectedTeam(...)" to call it synchronously (remove
the await) so callers no longer await a non-async operation; specifically update
every usage of updateSelectedTeam in this module to call it without awaiting.
packages/web/src/board/types.ts (1)

5-8: Keep access-page team data separate from TeamSummary.

Making visibility and memberships optional on the shared board type weakens the query contract: the access UI can still type-check even if those fields disappear from its selection set. A dedicated AccessTeamSummary keeps board queries loose while making access-page requirements explicit.

Also applies to: 108-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/board/types.ts` around lines 5 - 8, The shared board-related
type currently makes visibility and memberships optional on TeamSummary which
weakens query contracts; create a new AccessTeamSummary type that includes
visibility: 'PRIVATE'|'PUBLIC' and memberships: { nodes: TeamMembershipSummary[]
} (required), revert TeamSummary to not include those optional fields, and
update the access-page types/usages to consume AccessTeamSummary instead of
TeamSummary (also adjust the other occurrences mentioned around the same block
108-113 to use AccessTeamSummary).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/server/src/auth-routes.ts`:
- Around line 233-253: The respondJson function must mark sensitive auth
responses as non-cacheable: add a Cache-Control header (e.g.,
response.setHeader('Cache-Control', 'no-store')) before ending the response in
respondJson so /auth/session and /auth/logout cannot be cached by browsers or
intermediaries; place this call alongside the existing content-type and
Set-Cookie header logic in respondJson (ensure it executes regardless of
options.setCookie).

In `@packages/server/src/errors.ts`:
- Around line 55-60: The getExposedError function currently allows any
GraphQLError that includes an extensions.code; tighten this by implementing an
allowlist of permitted codes (e.g. ALLOWED_ERROR_CODES) and only return a
GraphQLError when error is an instance of GraphQLError and error.extensions.code
is one of those allowed codes; when returning, normalize the output to a new
GraphQLError that contains only a safe message and the canonical extensions.code
(no other extension fields or original stack), and otherwise return null. Ensure
you update getExposedError to reference the allowlist and perform normalization
before exposing the error.

In `@packages/server/src/google-oauth.ts`:
- Around line 76-104: The two external fetches in
exchangeGoogleCodeForUserProfile (the POST to GOOGLE_TOKEN_URL producing
tokenResponse and the GET to GOOGLE_USER_INFO_URL producing userInfoResponse)
lack timeouts; fix by passing an AbortSignal from AbortSignal.timeout(ms) into
each fetch options so both fetch calls will abort on timeout (choose an
appropriate ms value), ensuring timeout errors propagate to the existing
try/catch in handleGoogleCallback.
- Around line 134-176: When linking/updating Google accounts in google-oauth.ts,
add guards to prevent hijacking: before performing the prisma.user.update for
existingByEmail or existingBySubject, check (1) if
existingByEmail?.googleSubject is set and differs from profile.subject, reject
the login (throw or return an auth error) instead of overwriting googleSubject,
and (2) if both existingBySubject and existingByEmail are non-null but
existingBySubject.id !== existingByEmail.id, reject as a conflict instead of
updating either record. Apply these checks where existingBySubject and
existingByEmail are used and only call prisma.user.update when the validation
passes.

In `@packages/server/src/index.ts`:
- Around line 120-125: Replace the current catch handler that returns
error.message with a safe exposure flow: call getExposedError(error) inside the
catch for the auth-route promise and use its return value for the JSON response
(falling back to a generic "Internal server error" string if getExposedError
returns nothing); also log the original error server-side (e.g., console.error
or existing logger) before sending the response so internal details are recorded
but not echoed to the browser — apply this change to the catch block that sets
response.statusCode/response.setHeader/response.end.

In `@packages/server/src/schema.ts`:
- Around line 700-702: The thrown error message is incorrect: in the membership
check (the block that checks membership and currently throws
createNotFoundError(TEAM_NOT_FOUND_MESSAGE) when `membership` is falsy) replace
the wrong message with a dedicated membership-not-found constant. Add a new
exported constant MEMBERSHIP_NOT_FOUND_MESSAGE (e.g. in errors.ts alongside
TEAM_NOT_FOUND_MESSAGE) with the text "Team membership not found." and update
the resolver to call createNotFoundError(MEMBERSHIP_NOT_FOUND_MESSAGE) when
`membership` is missing.

In `@packages/server/src/session.ts`:
- Around line 33-52: The cookie-parsing logic currently calls decodeURIComponent
which can throw on malformed percent-escapes; update the reducer in the cookie
parsing function (the block that computes cookies[key] =
decodeURIComponent(value)) to wrap decodeURIComponent(value) in a try/catch and
skip that entry on failure (i.e., do not rethrow, just return cookies) so
malformed cookie values are ignored and parsing continues for the rest of the
Cookie header used by readCookieValue / auth/session/logout/google-callback.

In `@packages/web/src/App.tsx`:
- Around line 14-39: The UI wrongly treats session === null as "not configured"
while that value is used for both "loading" and "loaded empty"; update the
session loading logic in the useEffect that calls fetchSessionState (and related
render branches at lines ~61-90) to distinguish loading vs loaded: introduce a
boolean state like isSessionLoaded (or switch session initial value to undefined
and set to null on an unauthenticated response), set it to true in both the
.then and .catch handlers (after calling setSession or setSessionError), and
update the render logic to only show the "Google OAuth not configured" /
unauthenticated sign-in copy when isSessionLoaded is true (or when session ===
null after switching to undefined initial). Ensure you reference and update
session, setSession, sessionError, fetchSessionState, and the useEffect handler
accordingly.

In `@packages/web/src/lib/session.ts`:
- Around line 17-24: fetchSessionState assumes fetch and response.json() always
succeed; wrap the network call and JSON parsing in a try/catch, check
response.ok before parsing, and handle non-JSON or network errors by logging the
error and returning a safe fallback SessionState (e.g., an unauthenticated/empty
state). Update the function fetchSessionState to validate response.ok, guard
against response.json() throwing, and return a default SessionState on any error
so callers never get an unhandled exception.

In `@packages/web/src/styles/app.css`:
- Around line 65-69: The .app-shell__brand-group flex container currently forces
a single row causing overflow on narrow viewports; update its stylesheet so it
can wrap (e.g., add flex-wrap: wrap and adjust gap/align-items) and/or add a
media query at the 780px breakpoint to set .app-shell__brand-group { flex-wrap:
wrap; gap: 0.5rem; align-items: center; } so the brand and nav pills stack/wrap
on small screens alongside .app-shell__nav.

---

Nitpick comments:
In `@packages/server/src/access-control.test.ts`:
- Around line 7-24: The test for assertCanWriteTeam should also verify the
bypass short-circuits membership lookups: capture the mocked
teamMembership.findUnique used in the test (the vi.fn() passed into the prisma
stub) and add an assertion expect(findUnique).not.toHaveBeenCalled() after
awaiting assertCanWriteTeam so the test fails if the implementation queries
membership before short-circuiting; apply the same change to the other similar
test block covering lines 26-48 that tests trusted/admin bypasses.

In `@packages/web/src/App.access.test.tsx`:
- Around line 101-129: Update the test to assert the actual mutation payloads
instead of only that the mocks were called: after the visibility change, verify
teamUpdateAccess was called with the expected variables (e.g., { teamId: ...,
visibility: 'PUBLIC' }); after adding the member, assert teamMembershipUpsert
was called with the correct { teamId, email: 'editor@example.com', name: 'Editor
User', role: 'EDITOR' }; and when removing, assert teamMembershipRemove was
called with the correct membership identifier/variables rather than using a
brittle positional button lookup—use the membership's unique label/text to find
the specific Remove button or capture the membership id from the rendered item
so you can assert the correct variables were passed to teamMembershipRemove.

In `@packages/web/src/board/types.ts`:
- Around line 5-8: The shared board-related type currently makes visibility and
memberships optional on TeamSummary which weakens query contracts; create a new
AccessTeamSummary type that includes visibility: 'PRIVATE'|'PUBLIC' and
memberships: { nodes: TeamMembershipSummary[] } (required), revert TeamSummary
to not include those optional fields, and update the access-page types/usages to
consume AccessTeamSummary instead of TeamSummary (also adjust the other
occurrences mentioned around the same block 108-113 to use AccessTeamSummary).

In `@packages/web/src/lib/apollo.tsx`:
- Around line 174-178: Add defensive error handling around the URL parsing in
getServerBaseUrl: wrap the call to new URL(getGraphqlUrl()) in a try-catch,
catch any TypeError or RangeError from a malformed URL, and return a safe
fallback (e.g., empty string or a sensible default) or rethrow a more
descriptive error; update getServerBaseUrl to reference getGraphqlUrl() inside
the try block and ensure the catch logs or surfaces the failure clearly so
callers know the origin could not be determined.

In `@packages/web/src/lib/session.ts`:
- Around line 30-35: The logoutSession function currently ignores errors and
always resolves; update logoutSession to return a Promise<boolean> (or similar
success flag) by awaiting fetch(`${getServerBaseUrl()}/auth/logout`, catching
network exceptions, and returning false on error, and also checking response.ok
(return true for 2xx, false otherwise). Make changes inside logoutSession (and
update callers) to propagate the boolean result so callers can react to
failures.

In `@packages/web/src/routes/AccessPage.tsx`:
- Around line 128-134: Remove the unnecessary async from updateSelectedTeam:
change the declaration "async function updateSelectedTeam(updater: (team:
TeamSummary) => TeamSummary)" to a plain synchronous function "function
updateSelectedTeam(...)" since it does not use await; keep the existing setTeams
logic intact. Then update all call sites that currently "await
updateSelectedTeam(...)" to call it synchronously (remove the await) so callers
no longer await a non-async operation; specifically update every usage of
updateSelectedTeam in this module to call it without awaiting.

In `@packages/web/src/test/app-test-helpers.tsx`:
- Around line 63-92: Extract the duplicated mutation mock logic used in
useMutation into a shared factory function (e.g.,
createMutationMockImplementation) that returns the implementation callback which
inspects getDocumentSource(document) and returns the appropriate
vi.fn().mockResolvedValue(...) cases for 'mutation CommentCreate', 'mutation
CommentDelete', 'mutation IssueDelete', 'mutation TeamUpdateAccess', 'mutation
TeamMembershipUpsert', and 'mutation TeamMembershipRemove', defaulting to
[vi.fn()]; then use that factory when creating the hoisted mock (vi.hoisted
returning useMutation: vi.fn(createMutationMockImplementation())) and in your
beforeEach by calling
apolloMocks.useMutation.mockImplementation(createMutationMockImplementation()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6d78db3-b6f9-40d3-8734-5e1a9dd36a43

📥 Commits

Reviewing files that changed from the base of the PR and between a525e74 and bce0eac.

📒 Files selected for processing (29)
  • .env.example
  • README.md
  • docs/milestones.md
  • docs/review/triage.md
  • docs/vision.md
  • packages/server/prisma/schema.prisma
  • packages/server/prisma/seed-helpers.ts
  • packages/server/src/access-control.test.ts
  • packages/server/src/access-control.ts
  • packages/server/src/auth-routes.ts
  • packages/server/src/auth.test.ts
  • packages/server/src/auth.ts
  • packages/server/src/environment.ts
  • packages/server/src/errors.ts
  • packages/server/src/google-oauth.ts
  • packages/server/src/index.test.ts
  • packages/server/src/index.ts
  • packages/server/src/schema.ts
  • packages/server/src/session.ts
  • packages/web/src/App.access.test.tsx
  • packages/web/src/App.error-states.test.tsx
  • packages/web/src/App.tsx
  • packages/web/src/board/queries.ts
  • packages/web/src/board/types.ts
  • packages/web/src/lib/apollo.tsx
  • packages/web/src/lib/session.ts
  • packages/web/src/routes/AccessPage.tsx
  • packages/web/src/styles/app.css
  • packages/web/src/test/app-test-helpers.tsx

Comment thread packages/server/src/auth-routes.ts
Comment thread packages/server/src/errors.ts
Comment thread packages/server/src/google-oauth.ts
Comment thread packages/server/src/google-oauth.ts
Comment thread packages/server/src/index.ts
Comment thread packages/server/src/schema.ts Outdated
Comment thread packages/server/src/session.ts
Comment thread packages/web/src/App.tsx Outdated
Comment thread packages/web/src/lib/session.ts
Comment on lines +65 to +69
.app-shell__brand-group {
display: flex;
align-items: center;
gap: 1rem;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Allow the nav brand group to wrap on small screens.

The 780px breakpoint only stacks .app-shell__nav; .app-shell__brand-group still stays on one row. With the added Access link, the brand + nav pills can overflow narrow viewports instead of wrapping.

Suggested change
 .app-shell__brand-group {
   display: flex;
   align-items: center;
   gap: 1rem;
+  flex-wrap: wrap;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.app-shell__brand-group {
display: flex;
align-items: center;
gap: 1rem;
}
.app-shell__brand-group {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/styles/app.css` around lines 65 - 69, The
.app-shell__brand-group flex container currently forces a single row causing
overflow on narrow viewports; update its stylesheet so it can wrap (e.g., add
flex-wrap: wrap and adjust gap/align-items) and/or add a media query at the
780px breakpoint to set .app-shell__brand-group { flex-wrap: wrap; gap: 0.5rem;
align-items: center; } so the brand and nav pills stack/wrap on small screens
alongside .app-shell__nav.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@fakechris fakechris merged commit 2e47969 into main Apr 5, 2026
3 checks passed
@fakechris fakechris deleted the feature/google-oauth-team-access branch April 5, 2026 04:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant