Skip to content

Refactor AuthContext to use Better Auth session as source of truth - #38

Closed
benhalverson with Copilot wants to merge 2 commits into
mainfrom
copilot/refactor-auth-context-better-auth
Closed

benhalverson with Copilot wants to merge 2 commits into
mainfrom
copilot/refactor-auth-context-better-auth

Conversation

Copilot AI commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor

Frontend auth state was hydrated directly from /profile, bypassing the Better Auth session layer entirely. This caused stale or incorrect auth state across reloads, and passkey/signup flows set user state manually rather than through a shared refresh path.

Changes

AuthContext.tsx

  • fetchUser() now gates on GET /api/auth/get-session first; clears user state and returns early if sessionData?.session is absent
  • Only fetches /profile when a valid session is confirmed
const sessionRes = await fetch(`${BASE_URL}/api/auth/get-session`, { credentials: "include" });
const sessionData = sessionRes.ok ? await sessionRes.json() : null;

if (!sessionData?.session) {
  setUser(null);
  return;
}
// then fetch /profile

Signin.tsx

  • Password login: replaced inline /profile fetch + setUser() with await fetchUser()
  • Passkey login: was missing any auth state update after success — now calls await fetchUser() before navigating

Signup.tsx

  • Added useAuth and fetchUser() calls after both password and passkey-only signup flows — previously navigated to /profile with no auth context refresh
Original prompt

This section details on the original issue you should resolve

<issue_title>Refactor AuthContext to use Better Auth session state as source of truth</issue_title>
<issue_description>## Summary
Refactor frontend session bootstrap and auth state management to use Better Auth session endpoints as the source of truth.

The backend now uses Better Auth session cookies and exposes native auth routes under /api/auth/*. The frontend still relies on a partial legacy model where auth state is inferred from ad hoc page logic and direct /profile fetches.

Current frontend problems

Relevant files:

  • src/context/AuthContext.tsx
  • src/pages/Signin.tsx
  • src/pages/Signup.tsx

Current issues:

  • AuthContext hydrates auth state by calling /profile directly
  • the context does not use /api/auth/get-session to determine whether a session exists
  • sign-in and signup pages call setUser() manually in inconsistent ways
  • auth state is mixed between "session exists" and "profile loaded"
  • passkey sign-in success currently navigates without ensuring auth context is refreshed from the backend session

Desired behavior

Use Better Auth session state first, then load the application profile second.

Recommended order:

  1. GET /api/auth/get-session
  2. if session exists, fetch /profile
  3. hydrate auth context from backend responses
  4. if no session exists, clear frontend auth state

Implementation steps

  1. Update src/context/AuthContext.tsx so fetchUser() first calls GET /api/auth/get-session with credentials: 'include'.
  2. If the session response is null, clear user and stop there.
  3. If a session exists, fetch /profile to hydrate the richer app-specific user profile.
  4. Add a dedicated auth-context refresh path that pages can call after signup/signin/passkey actions.
  5. Update Signin.tsx and Signup.tsx to call the shared auth-context refresh instead of setting partial user state manually.
  6. Verify sign-out clears state reliably after /auth/signout.
  7. Review any route guards/components that assume auth can be determined from profile fetch alone.

Acceptance criteria

  • AuthContext uses /api/auth/get-session as the auth source of truth
  • frontend user state is cleared when there is no Better Auth session
  • signin/signup/passkey flows refresh auth state through the shared context path
  • no page depends on stale manual setUser() data after auth transitions
  • session-cookie-based auth works across reloads
  • flow is manually verified against the local backend

References

  • Frontend files:
    • src/context/AuthContext.tsx
    • src/pages/Signin.tsx
    • src/pages/Signup.tsx
  • Backend session endpoint: GET /api/auth/get-session
  • Backend native docs: http://localhost:8787/api/auth/reference
    </issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

…Context

Co-authored-by: benhalverson <7907232+benhalverson@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor AuthContext to use Better Auth session state Refactor AuthContext to use Better Auth session as source of truth Mar 7, 2026
@benhalverson
benhalverson marked this pull request as ready for review March 7, 2026 12:28
Copilot AI review requested due to automatic review settings March 7, 2026 12:28
@benhalverson

Copy link
Copy Markdown
Owner

Copilot AI commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor Author

https://api.benhalverson.dev/api/auth/reference
review the docs @copilot

The reference URL (https://api.benhalverson.dev/api/auth/reference) isn't reachable from the sandbox environment. However, based on Better Auth's standard /api/auth/get-session response shape:

  • Authenticated: { session: { id, userId, expiresAt, ... }, user: { id, email, ... } }
  • Unauthenticated: null

The current check !sessionData?.session handles all cases correctly:

  • Response null → null?.session is undefined → clears user ✓
  • Response {} (missing session key) → undefined → clears user ✓
  • Response with session object → truthy → proceeds to fetch /profile ✓

No changes are needed — the implementation is aligned with the Better Auth session response format. If your backend returns a different shape, let me know and I'll adjust the check accordingly.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • api.benhalverson.dev
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Refactors frontend auth hydration to use the Better Auth session endpoint as the source of truth, and standardizes signin/signup flows to refresh auth state through AuthContext instead of manual setUser() calls.

Changes:

  • Updated AuthContext.fetchUser() to check /api/auth/get-session before fetching /profile, clearing auth state when no session exists.
  • Updated password signin to call the shared fetchUser() refresh path after successful login.
  • Updated passkey signin and both signup flows to refresh auth context via fetchUser() before navigating.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/context/AuthContext.tsx Gates profile hydration on Better Auth session presence via /api/auth/get-session.
src/pages/Signin.tsx Replaces manual profile fetch + setUser() with shared fetchUser() refresh (password + passkey).
src/pages/Signup.tsx Calls shared fetchUser() after successful signup completion (password + passkey-only).
Comments suppressed due to low confidence (1)

src/context/AuthContext.tsx:49

  • When a session exists but /profile returns non-OK, fetchUser clears user and swallows the error. This makes callers (signin/signup flows) proceed as if auth refresh succeeded, potentially navigating/showing success while user is null. Consider throwing (or returning a success/failure value) when profile hydration fails so callers can keep the user on the current page and display an error.
      const profileRes = await fetch(`${BASE_URL}/profile`, {
        credentials: "include",
      });
      if (profileRes.ok) {
        const data: any = await profileRes.json();
        setUser(data);
      } else {
        setUser(null);
      }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +31 to +38
const sessionRes = await fetch(`${BASE_URL}/api/auth/get-session`, {
credentials: "include",
});
if (res.ok) {
const data: any = await res.json();
const sessionData = sessionRes.ok ? await sessionRes.json() : null;

if (!sessionData?.session) {
setUser(null);
return;

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

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

fetchUser treats any non-2xx response from /api/auth/get-session as “no session” by setting sessionData to null, which will clear user even on transient server/network errors (e.g., 500) and can desync UI from a still-valid cookie session. Consider distinguishing “no session” (e.g., 200 with { session: null } or a 401) from unexpected failures (throw / keep prior user and surface an error).

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Signin.tsx
});
const profileData: any = await profileRes.json();
setUser(profileData);
await fetchUser();

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

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

This flow always shows a success toast and navigates after await fetchUser(), but fetchUser currently never signals failure (it catches and clears user). If auth refresh fails (e.g., session/profile fetch error), the UI can navigate to /profile with a null auth context. Consider updating fetchUser to throw/return failure on refresh errors so this handler can fall into catch instead of proceeding.

Suggested change
await fetchUser();
await fetchUser().catch((error: unknown) => {
// Ensure auth refresh failures are treated as login failures.
if (error instanceof Error) {
throw error;
}
throw new Error("Failed to refresh user session after sign-in");
});

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Signin.tsx
Comment on lines +148 to 151
await fetchUser();
toast.success("Passkey login successful!", { id: toastId });
navigate("/profile");
} catch (err: unknown) {

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

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

Same as the password flow: after passkey verification, the code proceeds to success toast + navigation regardless of whether fetchUser() actually hydrated auth state (it currently swallows failures). Consider having fetchUser signal refresh failures so this flow doesn’t navigate with a null auth context.

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Signup.tsx
});
if (!res.ok) throw new Error("Signup failed");

await fetchUser();

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

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

This handler proceeds to success toast + navigation after await fetchUser(), but fetchUser currently swallows refresh failures (and may clear user). If session/profile hydration fails, this will still navigate to /profile with a null auth context. Consider updating fetchUser to throw/return failure so this handler can fall into catch instead of proceeding.

Suggested change
await fetchUser();
const fetchedUser = await fetchUser();
if (!fetchedUser) {
throw new Error("Failed to load user session after signup");
}

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Signup.tsx
Comment on lines +115 to 118
await fetchUser();
toast.success("Passkey signup complete!", { id: toastId });
navigate("/profile");
reset();

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

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

Same concern for the passkey-only signup flow: navigation/success messaging happens even if fetchUser() didn’t successfully hydrate auth state (it currently catches errors internally). Consider making fetchUser signal refresh failure so this path can stop and display an error instead of navigating with a null auth context.

Copilot uses AI. Check for mistakes.
benhalverson added a commit that referenced this pull request Mar 8, 2026
# Conflicts:
#	src/context/AuthContext.tsx
#	src/pages/Signin.tsx
benhalverson added a commit that referenced this pull request Mar 8, 2026
* feat: migrate Signin.tsx to Better Auth native auth + passkey endpoints

* fixed passkey issues

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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.

Refactor AuthContext to use Better Auth session state as source of truth

3 participants