Skip to content

feat(auth): add OAuth2 login with Google and GitHub#493

Open
Honey-pg wants to merge 1 commit into
GitMetricsLab:mainfrom
Honey-pg:feat/oauth2-login
Open

feat(auth): add OAuth2 login with Google and GitHub#493
Honey-pg wants to merge 1 commit into
GitMetricsLab:mainfrom
Honey-pg:feat/oauth2-login

Conversation

@Honey-pg
Copy link
Copy Markdown
Contributor

@Honey-pg Honey-pg commented May 25, 2026

Adds OAuth2 authentication for login using Google and GitHub, while keeping existing email/password signup and login.

  • Passport strategies for Google (passport-google-oauth20) and GitHub (passport-github2)
  • User model extended with provider, providerId, and optional password for OAuth accounts
  • OAuth routes: /api/auth/google, /api/auth/github, callbacks, /api/auth/me, /api/auth/oauth/providers
  • Login page with Continue with Google and Continue with GitHub (always visible; shows setup hint if credentials are missing)
  • Shared authApi client with withCredentials for session cookies
  • README expanded with detailed Google and GitHub OAuth setup steps
  • Unit/integration tests for OAuth user helpers and updated auth specs
  • backend/.env.example and .env.example for required environment variables

Motivation

Improves security and UX with standardized third-party sign-in, aligned with the OAuth2 feature request.

Setup (for reviewers)

See README → OAuth2 sign-in (Google & GitHub):

  1. cp .env.example .env and cp backend/.env.example backend/.env
  2. Configure Google OAuth app and GitHub OAuth App (not PAT)
  3. Set callback URLs to {BACKEND_URL}/api/auth/google/callback and .../github/callback
  4. Set VITE_BACKEND_URL to match BACKEND_URL
  5. On macOS, use port 5001 if 5000 is taken by AirPlay

Test plan

  • Copy env examples and start MongoDB, backend, and frontend
  • Sign up and log in with email/password
  • With OAuth credentials in backend/.env, log in via Google
  • With OAuth credentials in backend/.env, log in via GitHub
  • Without OAuth credentials, confirm buttons show and setup message appears on click
  • GET /api/auth/me returns user when session is active
  • GET /api/auth/logout clears session
  • Run npx jasmine spec/**/*.spec.cjs with MongoDB running

Summary by CodeRabbit

  • New Features

    • Added Google and GitHub OAuth2 sign-in support to the login page.
    • Implemented OAuth callback handling and user session management.
    • Added configuration detection for available sign-in providers.
  • Documentation

    • Added comprehensive OAuth2 setup guide including credential configuration steps for Google and GitHub.
  • Tests

    • Added test coverage for OAuth user management and authentication flows.

Review Change Stack

Implement Passport OAuth strategies, extend the user model for provider
accounts, add auth routes and login UI, and document setup in the README.

Co-authored-by: Cursor <cursoragent@cursor.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented May 25, 2026

Deploy Preview for github-spy ready!

Name Link
🔨 Latest commit f9c97a5
🔍 Latest deploy log https://app.netlify.com/projects/github-spy/deploys/6a13dacd85be2a000841e70b
😎 Deploy Preview https://deploy-preview-493--github-spy.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

This PR adds complete OAuth2 authentication for Google and GitHub to the full-stack application. Changes include User model provider support, OAuth utility layer, Passport strategy configuration, backend auth routes with OAuth endpoints, server session security hardening, frontend API client, Login/Signup UI refactoring, environment configuration, and comprehensive documentation with tests.

Changes

OAuth2 Sign-In Implementation

Layer / File(s) Summary
User Model Provider Support
backend/models/User.js, spec/user.model.spec.cjs
Schema adds provider enum and providerId string fields; password becomes conditionally required only for "local" provider; compound unique sparse index ensures { provider, providerId } uniqueness; comparePassword() safely returns false when password is absent; tests verify OAuth user creation without password.
OAuth User Utilities
backend/utils/oauthUser.js, spec/oauthUser.spec.cjs
Core module exports findOrCreateOAuthUser() for OAuth login with email-based account linking, ensureUniqueUsername() for collision-free username generation, toSessionUser() for session serialization, and isOAuthProviderConfigured() for provider validation; tests verify creation, idempotency, and username collision handling.
Passport Configuration
backend/config/passportConfig.js, backend/package.json
LocalStrategy updated to use toSessionUser(); Google and GitHub OAuth strategies conditionally configured with callback URLs and findOrCreateOAuthUser(); deserializeUser returns session user shape; exports getFrontendUrl and isOAuthProviderConfigured; dependencies add passport-github2 and passport-google-oauth20.
Backend Authentication Routes
backend/routes/auth.js, spec/auth.routes.spec.cjs
/signup and /login routes refactored to use JSON responses and req.logIn(); new /me returns authenticated user or 401; /oauth/providers exposes configured providers; /google, /github, and callback routes handle full OAuth flow with frontend redirect encoding; test verifies /oauth/providers response.
Server Session Security
backend/server.js
Express session middleware now sets httpOnly: true, production-based secure flag, environment-dependent sameSite ('none' in production, 'lax' otherwise), and 7-day maxAge for session cookies.
Frontend Authentication API Client
src/utils/authApi.ts
New Axios client configured with VITE_BACKEND_URL and withCredentials: true; exports AuthUser and OAuthProviders types; provides fetchOAuthProviders(), fetchCurrentUser(), and getOAuthLoginUrl() helper functions for frontend integration.
Frontend Login Page OAuth UI
src/pages/Login/Login.tsx
Fetches OAuth provider config on mount; displays "Continue with Google/GitHub" buttons when providers available; reads oauth=success|error query params for login status feedback; email/password flow switched to authApi client; message banner styling now differentiates success, OAuth setup missing, and error states.
Frontend Signup API Refactoring
src/pages/Signup/Signup.tsx
Signup HTTP request refactored from direct axios.post(${backendUrl}/api/auth/signup) to authApi.post("/signup"); imports updated to use shared authApi client; error and success handling unchanged.
Environment Configuration
.env.example, backend/.env.example
Frontend adds VITE_BACKEND_URL; backend adds PORT, MONGO_URI, SESSION_SECRET, BACKEND_URL, FRONTEND_URL, and OAuth credential placeholders for Google and GitHub with optional callback URL overrides.
Documentation and Setup Guide
README.md
New comprehensive "OAuth2 sign-in (Google & GitHub)" section covering required environment variables, Google OAuth consent screen + client credentials setup, GitHub OAuth App registration with callback URLs, verification steps, and missing-credentials behavior; test files list includes spec/oauthUser.spec.cjs.

Sequence Diagram

sequenceDiagram
  participant User
  participant Frontend as Frontend (Login)
  participant Backend as Backend (OAuth Routes)
  participant PassportLib as Passport + OAuth Strategies
  participant OAuthProvider as Google/GitHub OAuth
  participant Database as MongoDB User

  User->>Frontend: Click "Continue with Google"
  Frontend->>Backend: GET /auth/google
  Backend->>PassportLib: passport.authenticate('google')
  PassportLib->>OAuthProvider: Redirect to OAuth consent
  OAuthProvider->>User: OAuth consent screen
  User->>OAuthProvider: Grant permission
  OAuthProvider->>Backend: Redirect with auth code
  Backend->>PassportLib: Handle OAuth callback
  PassportLib->>OAuthProvider: Exchange code for profile
  PassportLib->>Backend: Call GoogleStrategy callback
  Backend->>Backend: findOrCreateOAuthUser()
  Backend->>Database: Query by {provider, providerId}
  Database-->>Backend: Existing user or null
  alt New OAuth User
    Backend->>Backend: ensureUniqueUsername()
    Backend->>Database: Create user (no password)
  end
  Database-->>Backend: User document
  Backend->>Backend: toSessionUser() → session shape
  Backend->>Backend: req.logIn(user, ...)
  Backend-->>Frontend: Redirect to /login?oauth=success
  Frontend->>Backend: Fetch current user (GET /me)
  Backend-->>Frontend: {user: AuthUser}
  Frontend->>User: Redirect to dashboard
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • GitMetricsLab/github_tracker#269: Overlaps with signup submission handling and loading/error UX in src/pages/Signup/Signup.tsx, which both PRs modify with different implementations.

Suggested labels

level:intermediate, quality:clean

Poem

🐰 A Hop Through OAuth's Garden

With Google and GitHub as garden gates,
Our rabbit now hops through auth's estates,
Sessions stay cozy, passwords take rest,
Provider-bound users pass every test,
Welcome Back blooms where OAuth's paths twist! 🌻

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat(auth): add OAuth2 login with Google and GitHub' accurately summarizes the main change, is concise and clear, and directly reflects the primary objective of adding OAuth2 authentication.
Description check ✅ Passed The pull request description covers key aspects including feature implementation, motivation, and setup instructions, though it does not strictly follow the provided template structure with sections like 'Related Issue', 'How Has This Been Tested', and proper 'Type of Change' checkboxes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Contributor

@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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/models/User.js`:
- Line 34: The current unique index on UserSchema (UserSchema.index({ provider:
1, providerId: 1 }, { unique: true, sparse: true })) still conflicts for local
users because provider always exists and providerId can be missing; replace this
with a partial unique index so uniqueness only applies when providerId is
present by calling UserSchema.index(...) with a partialFilterExpression such as
{ providerId: { $exists: true } } (and keep unique: true) so only documents that
actually have a providerId are constrained.

In `@backend/utils/oauthUser.js`:
- Around line 32-45: OAuth email lookup lowercases the address
(`normalizedEmail`) but local accounts are stored/queried with the raw case,
causing missed matches; fix by canonicalizing emails consistently across both
auth flows: normalize (trim().toLowerCase()) before saving local user records
and use the same normalized value (or a dedicated canonical field) for all
User.findOne lookups (including the OAuth path using normalizedEmail) and for
User.create/update logic so lookups (User.findOne) and persistence share the
same normalized key.
- Around line 47-53: The code is overwriting user.provider for existing local
accounts (user.provider === "local") which breaks LocalStrategy; instead do not
change user.provider when it's "local": remove the assignment user.provider =
provider and avoid converting the account to an OAuth-only type; if you need to
record the OAuth identity, store provider/providerId in a separate linked
identity field (e.g., linkedProviders or identities) or only set user.providerId
(without changing user.provider), save the user, and return toSessionUser(user)
so email/password login remains intact.

In `@README.md`:
- Around line 102-103: Replace hardcoded callback URLs in the README with a
BACKEND_URL-based example so they match backend/.env.example; update the
occurrences that currently show "http://localhost:5001/api/auth/google/callback"
(and the related provider examples at 119-120) to use
"{BACKEND_URL}/api/auth/{provider}/callback" or explicitly show
"http://localhost:5000/api/auth/{provider}/callback" with
"http://localhost:5001" as an optional fallback so the documented callback
host/port aligns with BACKEND_URL/default port.

In `@src/pages/Login/Login.tsx`:
- Around line 50-52: The oauthMessage value coming from
searchParams.get("message") is already decoded, so remove the extra
decodeURIComponent call in the oauth error branch; change the logic in the block
that checks oauthStatus === "error" (where setMessage(...) is called) to use
oauthMessage directly (or fall back to "OAuth sign-in failed") and then call
setSearchParams({}, { replace: true }) as before—update references to
oauthStatus, oauthMessage, setMessage, and setSearchParams accordingly to avoid
double-decoding and potential URIError.

In `@src/utils/authApi.ts`:
- Around line 27-33: The fetchCurrentUser function currently returns null for
any error; change the catch to inspect the thrown error (from authApi.get) and
only return null when the response indicates an unauthorized status (e.g.,
status === 401 or 403), otherwise rethrow the error so network/backend failures
bubble up; locate the fetchCurrentUser function and modify its catch to check
the error's response.status (or use AxiosError type guards) and handle 401/403
with return null, rethrow for all other cases.
🪄 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: 7461be07-7ff8-4080-8e06-59d3da916e2c

📥 Commits

Reviewing files that changed from the base of the PR and between 6c6bc3e and f9c97a5.

📒 Files selected for processing (15)
  • .env.example
  • README.md
  • backend/.env.example
  • backend/config/passportConfig.js
  • backend/models/User.js
  • backend/package.json
  • backend/routes/auth.js
  • backend/server.js
  • backend/utils/oauthUser.js
  • spec/auth.routes.spec.cjs
  • spec/oauthUser.spec.cjs
  • spec/user.model.spec.cjs
  • src/pages/Login/Login.tsx
  • src/pages/Signup/Signup.tsx
  • src/utils/authApi.ts

Comment thread backend/models/User.js
// ✅ FIXED: no next()
UserSchema.pre('save', async function () {
if (!this.isModified('password')) return;
UserSchema.index({ provider: 1, providerId: 1 }, { unique: true, sparse: true });
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant portion of the schema
sed -n '1,120p' backend/models/User.js | cat -n

# Print index-related lines specifically
rg -n "UserSchema\.index|index\(" backend/models/User.js

# Check whether provider/providerId are set in defaults or validation logic
rg -n "providerId|provider" backend/models/User.js

Repository: GitMetricsLab/github_tracker

Length of output: 1791


🌐 Web query:

MongoDB sparse unique compound index semantics provider providerId missing providerId $exists true partialFilterExpression

💡 Result:

MongoDB semantics for a “sparse unique compound index where providerId is missing / $exists:true / partialFilterExpression provider.providerId …” 1) sparse + unique means “uniqueness only among indexed documents; missing fields are allowed” - MongoDB sparse indexes only include documents that have the indexed field(s) present, and a sparse unique index prevents duplicates for the indexed key values while allowing multiple documents that omit the key [1]. - For sparse compound indexes, MongoDB indexes a document if it contains at least one of the indexed fields; the combination of sparse-ness with compound keys affects whether documents without all fields get included in the index entry [1]. 2) You cannot combine sparse with partialFilterExpression in a single index definition - MongoDB partial indexes explicitly say: “You cannot specify both the partialFilterExpression option and the sparse option.” [2][3]. So the “sparse unique … partialFilterExpression …” combination is not valid as a single index spec. 3) If your real goal is “unique only when provider.providerId exists (and maybe is non-null),” use a partial unique index (not sparse) - Partial indexes include only documents that match partialFilterExpression, and when a unique constraint is defined on that partial index, the uniqueness constraint applies only to documents that meet the partial filter [2][3]. - The docs example shows partial unique indexes expressed with { $exists: true } (e.g., password: { $exists: true }) [2][3]. Example pattern (provider/providerId): - Index that enforces uniqueness of (provider, providerId) only for documents where providerId exists: db.collection.createIndex( { "provider": 1, "provider.providerId": 1 }, { unique: true, partialFilterExpression: { "provider.providerId": { $exists: true } } }) This matches the documented behavior that partial unique constraints apply only to documents satisfying partialFilterExpression [2][3], and it avoids any need for sparse. 4) $exists:true in queries and index usage: sparse can help, but it’s not the right tool for “uniqueness conditioned on field presence” - The $exists operator matches documents that contain the field (including documents where the field value is null) [4]. - MongoDB docs also note that you can make $exists: true queries more efficient by creating a sparse index on that field [4]. However, for uniqueness guarantees specifically, partial indexes provide the conditional uniqueness semantics you want (unique only for docs matching the filter), whereas sparse unique semantics are based on presence rules of sparse indexing and compound sparse indexing can still index documents with only partial key field presence [1]. 5) Practical takeaway for “providerId missing” cases - If you use sparse+unique on a compound key, documents where providerId is missing can still end up being indexed depending on which compound fields are present (sparse compound indexes index a document if it contains at least one indexed field) [1]. That can lead to surprising uniqueness behavior. - If you instead use a partial unique index with partialFilterExpression: { "provider.providerId": { $exists: true } }, then documents missing providerId are simply not part of the unique index constraint [2][3].

Citations:


Fix User unique index to avoid local-providerId collisions

  • UserSchema.index({ provider: 1, providerId: 1 }, { unique: true, sparse: true }) can still index local documents because provider always exists; when providerId is missing, those documents share the same indexed key value for uniqueness, so the 2nd local signup can hit a duplicate-key error.
  • Apply uniqueness only to documents that actually have providerId via a partial unique index.
Proposed fix
-UserSchema.index({ provider: 1, providerId: 1 }, { unique: true, sparse: true });
+UserSchema.index(
+  { provider: 1, providerId: 1 },
+  {
+    unique: true,
+    partialFilterExpression: { providerId: { $exists: true } },
+  }
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/models/User.js` at line 34, The current unique index on UserSchema
(UserSchema.index({ provider: 1, providerId: 1 }, { unique: true, sparse: true
})) still conflicts for local users because provider always exists and
providerId can be missing; replace this with a partial unique index so
uniqueness only applies when providerId is present by calling
UserSchema.index(...) with a partialFilterExpression such as { providerId: {
$exists: true } } (and keep unique: true) so only documents that actually have a
providerId are constrained.

Comment on lines +32 to +45
const normalizedEmail = email?.trim().toLowerCase();

if (!providerId) {
throw new Error("OAuth profile is missing a provider id");
}

let user = await User.findOne({ provider, providerId });

if (user) {
return toSessionUser(user);
}

if (normalizedEmail) {
user = await User.findOne({ email: normalizedEmail });
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Canonicalize email consistently across local and OAuth auth paths.

Lines 32-45 lowercase the OAuth email before lookup, but local accounts in this PR are still persisted and queried with raw input elsewhere. A local account like Alice@Example.com can therefore miss this match and get a second OAuth account instead of being linked to the existing user.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/utils/oauthUser.js` around lines 32 - 45, OAuth email lookup
lowercases the address (`normalizedEmail`) but local accounts are stored/queried
with the raw case, causing missed matches; fix by canonicalizing emails
consistently across both auth flows: normalize (trim().toLowerCase()) before
saving local user records and use the same normalized value (or a dedicated
canonical field) for all User.findOne lookups (including the OAuth path using
normalizedEmail) and for User.create/update logic so lookups (User.findOne) and
persistence share the same normalized key.

Comment on lines +47 to +53
if (user) {
if (user.provider === "local" && user.password) {
user.provider = provider;
user.providerId = providerId;
await user.save();
return toSessionUser(user);
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't convert local accounts into OAuth-only accounts here.

Lines 47-53 overwrite user.provider on an existing local account. After that, LocalStrategy rejects the same user, so email/password login is no longer retained for anyone who signs in with OAuth once. Keep the local account type intact here, or model linked identities separately.

Minimal safe change
       if (user.provider === "local" && user.password) {
-        user.provider = provider;
-        user.providerId = providerId;
-        await user.save();
         return toSessionUser(user);
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/utils/oauthUser.js` around lines 47 - 53, The code is overwriting
user.provider for existing local accounts (user.provider === "local") which
breaks LocalStrategy; instead do not change user.provider when it's "local":
remove the assignment user.provider = provider and avoid converting the account
to an OAuth-only type; if you need to record the OAuth identity, store
provider/providerId in a separate linked identity field (e.g., linkedProviders
or identities) or only set user.providerId (without changing user.provider),
save the user, and return toSessionUser(user) so email/password login remains
intact.

Comment thread README.md
Comment thread src/pages/Login/Login.tsx
Comment on lines +50 to +52
if (oauthStatus === "error") {
setMessage(oauthMessage ? decodeURIComponent(oauthMessage) : "OAuth sign-in failed");
setSearchParams({}, { replace: true });
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does URLSearchParams.get() return decoded values, and can decodeURIComponent throw URIError when input contains a raw '%'?

💡 Result:

Yes. URLSearchParams.get returns decoded values. For decoding: MDN’s URLSearchParams docs state that when constructing from a query string, “percent-encoding is decoded,” and that when retrieving values “only decoded values are returned”; likewise, when getting an individual value, you use the decoded key and get the decoded value [1][2]. About decodeURIComponent and raw “%”: decodeURIComponent throws URIError when its input contains a “%” that is not followed by two hexadecimal digits (i.e., not valid percent-encoding) [3]. Separately, it’s also documented that decodeURIComponent throws URIError if encodedURI contains a “%” not followed by two hexadecimal digits [3]. So if the raw input you pass to decodeURIComponent contains a literal “%” that isn’t part of a valid %XX sequence (e.g., just “%” or “a%AFc” where the % sequence is not valid/complete for the decoder’s expectations), decodeURIComponent can throw URIError [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/pages/Login/Login.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

# Show the relevant region with line numbers
nl -ba "$FILE" | sed -n '1,140p' | sed -n '40,90p'

Repository: GitMetricsLab/github_tracker

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/pages/Login/Login.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

# Print lines ~40-90 with line numbers via awk
awk 'NR>=35 && NR<=90 {printf "%5d: %s\n", NR, $0}' "$FILE"

# Also locate where oauthMessage is defined
rg -n "oauthMessage" "$FILE" || true

Repository: GitMetricsLab/github_tracker

Length of output: 2415


Avoid double-decoding the OAuth message query param (can throw URIError for raw %).

In src/pages/Login/Login.tsx, oauthMessage is already obtained via searchParams.get("message") (URLSearchParams returns decoded values). Calling decodeURIComponent again can throw when the decoded message contains a literal % (e.g., produced from an original %25). This can crash the login screen.

Suggested fix
     if (oauthStatus === "error") {
-      setMessage(oauthMessage ? decodeURIComponent(oauthMessage) : "OAuth sign-in failed");
+      setMessage(oauthMessage || "OAuth sign-in failed");
       setSearchParams({}, { replace: true });
     }
📝 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
if (oauthStatus === "error") {
setMessage(oauthMessage ? decodeURIComponent(oauthMessage) : "OAuth sign-in failed");
setSearchParams({}, { replace: true });
if (oauthStatus === "error") {
setMessage(oauthMessage || "OAuth sign-in failed");
setSearchParams({}, { replace: true });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/Login/Login.tsx` around lines 50 - 52, The oauthMessage value
coming from searchParams.get("message") is already decoded, so remove the extra
decodeURIComponent call in the oauth error branch; change the logic in the block
that checks oauthStatus === "error" (where setMessage(...) is called) to use
oauthMessage directly (or fall back to "OAuth sign-in failed") and then call
setSearchParams({}, { replace: true }) as before—update references to
oauthStatus, oauthMessage, setMessage, and setSearchParams accordingly to avoid
double-decoding and potential URIError.

Comment thread src/utils/authApi.ts
Comment on lines +27 to +33
export async function fetchCurrentUser(): Promise<AuthUser | null> {
try {
const { data } = await authApi.get<{ user: AuthUser }>("/me");
return data.user;
} catch {
return null;
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle only unauthenticated responses as null in fetchCurrentUser().

Returning null for every failure hides backend/network outages as “logged out” and makes auth state unreliable. Return null only for unauthorized responses, and rethrow everything else.

Suggested fix
 export async function fetchCurrentUser(): Promise<AuthUser | null> {
   try {
     const { data } = await authApi.get<{ user: AuthUser }>("/me");
     return data.user;
-  } catch {
-    return null;
+  } catch (error: unknown) {
+    const status =
+      typeof error === "object" &&
+      error !== null &&
+      "response" in error &&
+      typeof (error as { response?: { status?: unknown } }).response?.status === "number"
+        ? (error as { response?: { status?: number } }).response?.status
+        : undefined;
+
+    if (status === 401) return null;
+    throw error;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/authApi.ts` around lines 27 - 33, The fetchCurrentUser function
currently returns null for any error; change the catch to inspect the thrown
error (from authApi.get) and only return null when the response indicates an
unauthorized status (e.g., status === 401 or 403), otherwise rethrow the error
so network/backend failures bubble up; locate the fetchCurrentUser function and
modify its catch to check the error's response.status (or use AxiosError type
guards) and handle 401/403 with return null, rethrow for all other cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants