Skip to content

[20230204026] feat: implement Google OAuth sign-in flow (#243) - #252

Merged
ali-ahnaf merged 1 commit into
ali-ahnaf:developfrom
mubasshirahin:feat-google-oauth
Jul 18, 2026
Merged

[20230204026] feat: implement Google OAuth sign-in flow (#243)#252
ali-ahnaf merged 1 commit into
ali-ahnaf:developfrom
mubasshirahin:feat-google-oauth

Conversation

@mubasshirahin

@mubasshirahin mubasshirahin commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Description

Adds Google OAuth sign-in as an alternative to email/password login.

Closes #243

Changes

  • API: New POST /api/auth/google route that verifies Google ID tokens
  • Entity: Added nullable googleId column to User entity (with migration)
  • Auth Service: googleSignIn() handles three cases — existing link, email match (link), new user
  • UI: GoogleSignInButton + GoogleAuthProvider components integrated into sign-in/sign-up pages
  • Tests: Updated auth and vaults service tests

Roll Number

20230204026

Summary by CodeRabbit

  • New Features
    • Added “Login with Google” options to sign-in and sign-up.
    • Added Google account linking and automatic account creation with a default vault.
    • Added Google OAuth callback handling with success and failure redirects.
    • Expanded available avatar and icon selections.
  • Bug Fixes
    • Improved email normalization and handling for Google-only accounts.
    • Updated the app cache so users receive the latest assets.
  • Configuration
    • Added configuration placeholders for Google authentication in API and UI environments.

@github-actions

Copy link
Copy Markdown
Contributor

Greetings, @mubasshirahin. Thy scroll hath arrived unblemished — the runes align and no conflict bars the path. The council shall now convene over its contents, weighing each incantation by candlelight. Tarry a while, brave adventurer; we shall investigate and return to thee with our verdict.

@ali-ahnaf ali-ahnaf added the question Further information is requested label Jul 11, 2026
@ali-ahnaf

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Google OAuth sign-in

Layer / File(s) Summary
Google identity contract and storage
packages/api/.env.example, packages/api/package.json, packages/api/src/entities/*, packages/api/src/migrations/*, packages/api/src/repositories/*, packages/shared/src/*, packages/ui/src/lib/helpers/static.ts
Adds Google OAuth configuration, nullable password and unique Google identity fields, migration support, repository lookup, and shared avatar exports.
API Google authentication flow
packages/api/src/index.ts, packages/api/src/routes/auth/*, packages/api/src/services/auth.service.ts, packages/api/src/tests/*
Adds OAuth initiation and callback routes, state cookies, Google token validation, account linking or creation, passwordless-account handling, and service tests.
UI Google sign-in and callback
packages/ui/.env.*, packages/ui/src/app/auth/google/*, packages/ui/src/app/signin/page.tsx, packages/ui/src/app/signup/page.tsx, packages/ui/src/components/*
Adds Google sign-in controls, callback token processing, session initialization, failure redirects, and public callback routing.

Shared UI assets

Layer / File(s) Summary
Icon and avatar asset exports
packages/ui/src/lib/helpers/static.ts
Expands the available icon list and removes the obsolete icon inspection script.

Service worker cache version

Layer / File(s) Summary
Service worker cache update
packages/ui/public/sw.js
Changes the service worker cache version from v1 to v2.

Repository maintenance

Layer / File(s) Summary
Configuration and client cleanup
.agents/settings.json, .gitignore, packages/shared/src/index.ts, packages/ui/src/lib/api/ApiClient.ts
Updates local configuration and ignore patterns, reformats the shared expense category type, and removes API error console logging.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant GoogleSignInButton
  participant API
  participant Google
  participant AuthService
  participant UI
  User->>GoogleSignInButton: Click sign-in
  GoogleSignInButton->>API: GET /api/auth/google
  API->>Google: Redirect for authorization
  Google-->>API: Callback with authorization code
  API->>AuthService: Exchange and verify code
  AuthService-->>API: JWT auth result
  API-->>UI: Redirect with token
  UI->>UI: Seed session and redirect home
Loading

Possibly related PRs

Suggested reviewers: ali-ahnaf

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The diff also changes unrelated areas like the service worker cache version and removes a test-only script, which are outside Google OAuth scope. Move unrelated cleanup or cache-busting changes into a separate PR, or remove them unless they are required for the OAuth flow.
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 (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: adding Google OAuth sign-in.
Linked Issues check ✅ Passed The changes cover Google sign-in buttons, OAuth routes, user linking, JWT issuance, and passwordless OAuth accounts as requested.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
packages/api/src/services/auth.service.ts-118-133 (1)

118-133: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize emails before account lookup/linking. findByEmail(email) is case-sensitive, so a password account saved with different casing can be missed and Google sign-in will create a duplicate user instead of linking. Apply the same normalization in sign-up/sign-in and the Google flow.

🤖 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 `@packages/api/src/services/auth.service.ts` around lines 118 - 133, Normalize
email addresses consistently in sign-up, sign-in, and the Google authentication
flow before persistence or lookup, including the email passed to
users.findByEmail in the shown linking logic. Reuse the existing normalization
convention or helper so equivalent casing resolves to the same account and
Google identity linking does not create duplicates.
🧹 Nitpick comments (3)
packages/api/src/services/debts.service.ts (1)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial

Use one canonical logger instance across the services layer.

packages/api/src/services/index.ts constructs a logger separately from the instance exported by packages/api/src/services/logger.service.ts. These changes make six services use the latter while other barrel consumers may still use the former. Update the barrel to re-export the canonical logger, or verify that separate instances are intentional.

  • packages/api/src/services/debts.service.ts#L6-L6: keep the direct import only if it resolves to the canonical instance.
  • packages/api/src/services/preferences.service.ts#L5-L5: use the same canonical logger instance.
  • packages/api/src/services/prompt.service.ts#L4-L4: use the same canonical logger instance.
  • packages/api/src/services/recurring.service.ts#L8-L8: use the same canonical logger instance.
  • packages/api/src/services/tags.service.ts#L6-L6: use the same canonical logger instance.
  • packages/api/src/services/transactions.service.ts#L6-L6: use the same canonical logger instance.
🤖 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 `@packages/api/src/services/debts.service.ts` at line 6, Use the logger
instance exported by services/logger.service.ts as the single canonical logger
throughout the services layer. Update services/index.ts to re-export that
instance instead of constructing a separate one, and ensure the imports in
packages/api/src/services/debts.service.ts:6-6,
packages/api/src/services/preferences.service.ts:5-5,
packages/api/src/services/prompt.service.ts:4-4,
packages/api/src/services/recurring.service.ts:8-8,
packages/api/src/services/tags.service.ts:6-6, and
packages/api/src/services/transactions.service.ts:6-6 all resolve to it; keep
the direct debts import only if it already references the canonical export.
packages/api/src/services/auth.service.ts (1)

23-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inject googleClient like the other dependencies for consistency and testability.

users and vaults are constructor-injected with defaults (per this class's own doc comment, explicitly for unit-testability), but googleClient is instantiated as a fixed class field, so tests can't substitute a mock without reaching into google-auth-library internals.

♻️ Proposed refactor
 export class AuthService {
-  private readonly googleClient = new OAuth2Client(GOOGLE_CLIENT_ID);
-
   constructor(
     private readonly users: UsersRepository = usersRepository,
     private readonly vaults: VaultsRepository = vaultsRepository,
+    private readonly googleClient: OAuth2Client = new OAuth2Client(GOOGLE_CLIENT_ID),
   ) {}
🤖 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 `@packages/api/src/services/auth.service.ts` around lines 23 - 29, Update
AuthService to constructor-inject the OAuth2Client dependency like users and
vaults, supplying the existing Google client as its default value. Remove the
fixed googleClient class-field instantiation and preserve the service’s existing
use of the injected googleClient so tests can provide a mock.
packages/ui/src/app/signin/page.tsx (1)

49-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated Google sign-in success handler.

Both pages implement an identical handleGoogleSuccess: clear errors, set loading, call authApi.google(credential), call setSession with the same field mapping, redirect to /, and set the same fallback error message on failure. This is copy-pasted logic that will drift if the session-handling contract changes.

  • packages/ui/src/app/signin/page.tsx#L49-L62: replace this handler with a shared hook/helper (e.g., useGoogleAuthHandler or a completeGoogleSignIn(credential) helper colocated with AuthApi.ts) that both pages call.
  • packages/ui/src/app/signup/page.tsx#L55-L68: same replacement, reusing the same shared helper instead of a second copy.
🤖 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 `@packages/ui/src/app/signin/page.tsx` around lines 49 - 62, The Google sign-in
success logic is duplicated across both pages. In
packages/ui/src/app/signin/page.tsx lines 49-62 and
packages/ui/src/app/signup/page.tsx lines 55-68, replace each local
handleGoogleSuccess implementation with a shared useGoogleAuthHandler or
completeGoogleSignIn helper that preserves error clearing, loading state,
authApi.google, setSession mapping, redirect, and fallback error behavior; both
sites require the same replacement.
🤖 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 `@packages/api/src/migrations/1783695914180-Migration.ts`:
- Around line 18-21: Update the down migration’s users table recreation and data
copy around the migration’s down method so Google-only users with null passwords
can be restored successfully. Preserve existing password values while allowing
null passwords in the recreated users schema and ensuring the INSERT from
temporary_users does not violate the schema constraint.

In `@packages/ui/src/components/GoogleSignInButton.tsx`:
- Around line 16-36: The handleClick flow in GoogleSignInButton currently relies
on prompt() without handling skipped or suppressed One Tap states. Add a
moment_callback to the Google Identity initialize configuration that detects
isNotDisplayed() or isSkippedMoment(), then invoke the persistent sign-in
fallback via renderButton() or the existing OAuth popup path while preserving
onError for actual prompt failures.

---

Other comments:
In `@packages/api/src/services/auth.service.ts`:
- Around line 118-133: Normalize email addresses consistently in sign-up,
sign-in, and the Google authentication flow before persistence or lookup,
including the email passed to users.findByEmail in the shown linking logic.
Reuse the existing normalization convention or helper so equivalent casing
resolves to the same account and Google identity linking does not create
duplicates.

---

Nitpick comments:
In `@packages/api/src/services/auth.service.ts`:
- Around line 23-29: Update AuthService to constructor-inject the OAuth2Client
dependency like users and vaults, supplying the existing Google client as its
default value. Remove the fixed googleClient class-field instantiation and
preserve the service’s existing use of the injected googleClient so tests can
provide a mock.

In `@packages/api/src/services/debts.service.ts`:
- Line 6: Use the logger instance exported by services/logger.service.ts as the
single canonical logger throughout the services layer. Update services/index.ts
to re-export that instance instead of constructing a separate one, and ensure
the imports in packages/api/src/services/debts.service.ts:6-6,
packages/api/src/services/preferences.service.ts:5-5,
packages/api/src/services/prompt.service.ts:4-4,
packages/api/src/services/recurring.service.ts:8-8,
packages/api/src/services/tags.service.ts:6-6, and
packages/api/src/services/transactions.service.ts:6-6 all resolve to it; keep
the direct debts import only if it already references the canonical export.

In `@packages/ui/src/app/signin/page.tsx`:
- Around line 49-62: The Google sign-in success logic is duplicated across both
pages. In packages/ui/src/app/signin/page.tsx lines 49-62 and
packages/ui/src/app/signup/page.tsx lines 55-68, replace each local
handleGoogleSuccess implementation with a shared useGoogleAuthHandler or
completeGoogleSignIn helper that preserves error clearing, loading state,
authApi.google, setSession mapping, redirect, and fallback error behavior; both
sites require the same replacement.
🪄 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: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 7659f4d8-525d-49eb-bac4-5a6ad02d8515

📥 Commits

Reviewing files that changed from the base of the PR and between 9676def and 5d7bbb8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (28)
  • README.md
  • packages/api/.env.example
  • packages/api/package.json
  • packages/api/src/entities/User.entity.ts
  • packages/api/src/migrations/1783695914180-Migration.ts
  • packages/api/src/repositories/users.repository.ts
  • packages/api/src/routes/auth.routes.ts
  • packages/api/src/routes/auth/google.route.ts
  • packages/api/src/services/auth.service.ts
  • packages/api/src/services/debts.service.ts
  • packages/api/src/services/preferences.service.ts
  • packages/api/src/services/prompt.service.ts
  • packages/api/src/services/recurring.service.ts
  • packages/api/src/services/tags.service.ts
  • packages/api/src/services/transactions.service.ts
  • packages/api/src/services/users.service.ts
  • packages/api/src/services/vaults.service.ts
  • packages/api/src/tests/auth.service.test.ts
  • packages/api/src/tests/vaults.service.test.ts
  • packages/shared/src/contracts/auth.ts
  • packages/ui/.env.development
  • packages/ui/public/sw.js
  • packages/ui/src/app/layout.tsx
  • packages/ui/src/app/signin/page.tsx
  • packages/ui/src/app/signup/page.tsx
  • packages/ui/src/components/GoogleAuthProvider.tsx
  • packages/ui/src/components/GoogleSignInButton.tsx
  • packages/ui/src/lib/api/AuthApi.ts

Comment on lines +18 to +21
await queryRunner.query(`ALTER TABLE "users" RENAME TO "temporary_users"`);
await queryRunner.query(`CREATE TABLE "users" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar(100) NOT NULL, "email" varchar(255) NOT NULL, "avatar" varchar(255) NOT NULL DEFAULT (''), "password" varchar(255) NOT NULL, "createdAt" datetime DEFAULT (datetime('now')), "updatedAt" datetime DEFAULT (datetime('now')), "deletedAt" datetime, "disableAiPrompt" boolean NOT NULL DEFAULT (0), "googleId" varchar(255), CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"), CONSTRAINT "UQ_93124f92af3bd84a60e424644bb" UNIQUE ("googleId"))`);
await queryRunner.query(`INSERT INTO "users"("id", "name", "email", "avatar", "password", "createdAt", "updatedAt", "deletedAt", "disableAiPrompt", "googleId") SELECT "id", "name", "email", "avatar", "password", "createdAt", "updatedAt", "deletedAt", "disableAiPrompt", "googleId" FROM "temporary_users"`);
await queryRunner.query(`DROP TABLE "temporary_users"`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Migration file ==\n'
sed -n '1,220p' packages/api/src/migrations/1783695914180-Migration.ts

printf '\n== User entity/searches ==\n'
rg -n "password|googleId|disableAiPrompt|deletedAt|isNull|nullable" packages/api/src -g '!**/dist/**' -g '!**/build/**'

printf '\n== Relevant user entity file(s) ==\n'
fd -a "User" packages/api/src | head -n 20
fd -a "user" packages/api/src | head -n 20

Repository: ali-ahnaf/pocket_pixel

Length of output: 45453


Down migration breaks for Google-only users. down() recreates users with password NOT NULL and then copies rows straight from temporary_users; any account created via Google with password = null will abort the rollback.

🤖 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 `@packages/api/src/migrations/1783695914180-Migration.ts` around lines 18 - 21,
Update the down migration’s users table recreation and data copy around the
migration’s down method so Google-only users with null passwords can be restored
successfully. Preserve existing password values while allowing null passwords in
the recreated users schema and ensuring the INSERT from temporary_users does not
violate the schema constraint.

Comment on lines +16 to +36
const handleClick = useCallback(() => {
if (!ready || !clientId) {
onError?.();
return;
}

if (!initialized.current) {
window.google!.accounts.id.initialize({
client_id: clientId,
callback: (response) => onSuccess(response.credential),
cancel_on_tap_outside: true,
});
initialized.current = true;
}

try {
window.google!.accounts.id.prompt();
} catch {
onError?.();
}
}, [ready, clientId, onSuccess, onError]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby references.
ast-grep outline packages/ui/src/components/GoogleSignInButton.tsx --view expanded || true
echo "----"
wc -l packages/ui/src/components/GoogleSignInButton.tsx
echo "----"
cat -n packages/ui/src/components/GoogleSignInButton.tsx | sed -n '1,220p'
echo "----"
rg -n "GoogleSignInButton|accounts\.id\.prompt|accounts\.id\.initialize|renderButton\(" packages/ui/src -S

Repository: ali-ahnaf/pocket_pixel

Length of output: 4329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check for any local handling of Google Identity Services prompt notifications / callback refs.
rg -n "isNotDisplayed|isSkippedMoment|prompt\\((notification|.*=>)|onSuccessRef|onErrorRef|initialized\\.current" packages/ui/src -S

Repository: ali-ahnaf/pocket_pixel

Length of output: 332


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect signin/signup pages around GoogleSignInButton usage and handler identities.
cat -n packages/ui/src/app/signin/page.tsx | sed -n '1,240p'
echo "----"
cat -n packages/ui/src/app/signup/page.tsx | sed -n '1,280p'

Repository: ali-ahnaf/pocket_pixel

Length of output: 20074


🌐 Web query:

Google Identity Services prompt moment listener suppressed after dismissal Safari Firefox ITP renderButton documentation

💡 Result:

When a user manually dismisses or closes the Google Identity Services (GIS) One Tap prompt, the system automatically suppresses future prompts to respect the user's preference and reduce annoyance [1]. This suppression is standard behavior across all browsers, including those subject to Intelligent Tracking Prevention (ITP) like Safari and Firefox [2][3]. Key points regarding this behavior include: 1. Automatic Suppression: If a user explicitly closes the One Tap prompt (e.g., by clicking the 'Close' button or tapping outside the prompt area, depending on configuration), the library flags this state. Consequently, subsequent calls to google.accounts.id.prompt will not display the prompt again for that user session or until the suppression cooldown period resets [1]. 2. Prompt Moment Listener: The prompt method allows you to pass a momentListener to receive notifications about the UI status, such as skipped or dismissed moments [4][5]. - Dismissed Moment: This occurs when a user explicitly closes the prompt, including when you programmatically call google.accounts.id.cancel [4][5]. - Skipped Moment: This occurs due to automatic cancellation, user cancellation, or if the user is not signed into a Google session [4][5]. - Note on FedCM: If FedCM is enabled (use_fedcm_for_prompt: true), the behavior of the momentListener may change, and notifications can be delayed by up to 1 minute to improve user privacy [6]. 3. ITP Browsers (Safari/Firefox): ITP does not independently suppress the GIS prompt in a way that differs from standard browsers; rather, browsers with ITP restrictions (like Safari and Firefox) often trigger a specific "upgraded" UX [2][3]. This UX may begin with a welcome page or a pop-up window [3]. The GIS library is designed to handle these environments, but you must ensure your implementation properly utilizes the moment_callback to handle states where the prompt is not displayed [5][7]. 4. Mitigation and Troubleshooting: - If your prompt is not reappearing during testing, ensure you are not hitting the "cooldown" period. - Verify that your momentListener is correctly handling 'dismissed' and 'skipped' moments, as relying on these callbacks is the recommended way to determine if you should fall back to other identity providers or alternative login methods [5]. - If using FedCM, be aware of the potential for notification delays [6]. For further implementation details, refer to the official GIS documentation on managing prompt UI status notifications and ITP support [5][2].

Citations:


prompt() needs a fallback path for skipped One Tap states.
This button relies on google.accounts.id.prompt() alone, so if One Tap is suppressed or skipped (for example after a manual dismissal) the click can appear to do nothing and never call onError. Add a moment_callback for isNotDisplayed() / isSkippedMoment() and fall back to renderButton() or an OAuth popup for a persistent sign-in button.

🤖 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 `@packages/ui/src/components/GoogleSignInButton.tsx` around lines 16 - 36, The
handleClick flow in GoogleSignInButton currently relies on prompt() without
handling skipped or suppressed One Tap states. Add a moment_callback to the
Google Identity initialize configuration that detects isNotDisplayed() or
isSkippedMoment(), then invoke the persistent sign-in fallback via
renderButton() or the existing OAuth popup path while preserving onError for
actual prompt failures.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/api/src/services/prompt.service.ts (1)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import logger from the canonical module.

Line 6 still imports logger from ., while packages/api/src/services/index.ts creates a separate Logger instance from the singleton exported by packages/api/src/services/logger.service.ts. Keep only tagsService and vaultsService from the barrel.

Suggested import split
-import { logger, tagsService, vaultsService } from '.';
+import { logger } from './logger.service';
+import { tagsService, vaultsService } from '.';
🤖 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 `@packages/api/src/services/prompt.service.ts` at line 6, Update the imports in
prompt.service.ts so logger comes from the canonical logger service module,
while the barrel import from '.' retains only tagsService and vaultsService.
Ensure prompt.service.ts uses the shared logger singleton rather than the
separately created barrel instance.
🤖 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 `@packages/api/src/services/transactions.service.ts`:
- Around line 1-8: Remove the duplicate transactionsRepository and logger
imports at the top of the transactions service. Keep the combined repository
import from ../repositories containing transactionsRepository and
vaultsRepository, and retain the explicit logger import from ./logger.service.
- Around line 101-106: Update the transfer persistence flow containing the
expense and income saves so both save calls and the conditional replaceTags
calls execute within a single AppDataSource.transaction boundary. Preserve the
existing tagIds behavior and use the transaction-scoped repository/manager for
every write, ensuring any failure rolls back the entire transfer.

---

Nitpick comments:
In `@packages/api/src/services/prompt.service.ts`:
- Line 6: Update the imports in prompt.service.ts so logger comes from the
canonical logger service module, while the barrel import from '.' retains only
tagsService and vaultsService. Ensure prompt.service.ts uses the shared logger
singleton rather than the separately created barrel instance.
🪄 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: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 1e532b5e-1fe4-41a4-bd07-4eacedb36f3a

📥 Commits

Reviewing files that changed from the base of the PR and between 5d7bbb8 and df6938f.

📒 Files selected for processing (4)
  • README.md
  • packages/api/src/services/debts.service.ts
  • packages/api/src/services/prompt.service.ts
  • packages/api/src/services/transactions.service.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/api/src/services/prompt.service.ts (1)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import logger from the canonical module.

Line 6 still imports logger from ., while packages/api/src/services/index.ts creates a separate Logger instance from the singleton exported by packages/api/src/services/logger.service.ts. Keep only tagsService and vaultsService from the barrel.

Suggested import split
-import { logger, tagsService, vaultsService } from '.';
+import { logger } from './logger.service';
+import { tagsService, vaultsService } from '.';
🤖 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 `@packages/api/src/services/prompt.service.ts` at line 6, Update the imports in
prompt.service.ts so logger comes from the canonical logger service module,
while the barrel import from '.' retains only tagsService and vaultsService.
Ensure prompt.service.ts uses the shared logger singleton rather than the
separately created barrel instance.
🤖 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 `@packages/api/src/services/transactions.service.ts`:
- Around line 1-8: Remove the duplicate transactionsRepository and logger
imports at the top of the transactions service. Keep the combined repository
import from ../repositories containing transactionsRepository and
vaultsRepository, and retain the explicit logger import from ./logger.service.
- Around line 101-106: Update the transfer persistence flow containing the
expense and income saves so both save calls and the conditional replaceTags
calls execute within a single AppDataSource.transaction boundary. Preserve the
existing tagIds behavior and use the transaction-scoped repository/manager for
every write, ensuring any failure rolls back the entire transfer.

---

Nitpick comments:
In `@packages/api/src/services/prompt.service.ts`:
- Line 6: Update the imports in prompt.service.ts so logger comes from the
canonical logger service module, while the barrel import from '.' retains only
tagsService and vaultsService. Ensure prompt.service.ts uses the shared logger
singleton rather than the separately created barrel instance.
🪄 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: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 1e532b5e-1fe4-41a4-bd07-4eacedb36f3a

📥 Commits

Reviewing files that changed from the base of the PR and between 5d7bbb8 and df6938f.

📒 Files selected for processing (4)
  • README.md
  • packages/api/src/services/debts.service.ts
  • packages/api/src/services/prompt.service.ts
  • packages/api/src/services/transactions.service.ts
🛑 Comments failed to post (2)
packages/api/src/services/transactions.service.ts (2)

1-8: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicate imports; this file will not compile.

transactionsRepository and logger are each declared twice. Keep the combined repository import and the explicit ./logger.service import only.

Suggested cleanup
-import { transactionsRepository } from '../repositories';
 import { logger } from './logger.service';
 import { transactionsRepository, vaultsRepository } from '../repositories';
-import { logger } from '.';
📝 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.

import { CreateTransactionInput, CreateTransferInput, ListTransactionsQuery, TransactionDto, UpdateTransactionInput } from '`@expense-tracker/shared`';
import { Expense } from '../entities/Expense.entity';
import { AppError } from '../errors/app-error';
import { TransactionsRepository, TransactionDateFilter } from '../repositories/transactions.repository';
import { logger } from './logger.service';
import { transactionsRepository, vaultsRepository } from '../repositories';
🧰 Tools
🪛 GitHub Actions: Build / 2_test.txt

[error] 5-5: TS2300: Duplicate identifier 'transactionsRepository'.


[error] 6-6: TS2300: Duplicate identifier 'logger'.


[error] 7-7: TS2300: Duplicate identifier 'transactionsRepository'.


[error] 8-8: TS2300: Duplicate identifier 'logger'.


[error] 1-1: Test step failed: jest exited with code 1 (npm lifecycle script test failed).

🪛 GitHub Actions: Build / test

[error] 5-5: TypeScript TS2300: Duplicate identifier 'transactionsRepository'.


[error] 6-6: TypeScript TS2300: Duplicate identifier 'logger'.


[error] 7-7: TypeScript TS2300: Duplicate identifier 'transactionsRepository'.


[error] 8-8: TypeScript TS2300: Duplicate identifier 'logger'.

🤖 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 `@packages/api/src/services/transactions.service.ts` around lines 1 - 8, Remove
the duplicate transactionsRepository and logger imports at the top of the
transactions service. Keep the combined repository import from ../repositories
containing transactionsRepository and vaultsRepository, and retain the explicit
logger import from ./logger.service.

101-106: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== transactions.service.ts outline ==\n'
ast-grep outline packages/api/src/services/transactions.service.ts --view expanded || true

printf '\n== relevant slices ==\n'
sed -n '1,220p' packages/api/src/services/transactions.service.ts

printf '\n== search for transaction usage in api services ==\n'
rg -n "transaction|manager|QueryRunner|save\\(|replaceTags\\(" packages/api/src/services packages/api/src -g '*.ts' | sed -n '1,220p'

Repository: ali-ahnaf/pocket_pixel

Length of output: 32567


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== transactions.repository.ts outline ==\n'
ast-grep outline packages/api/src/repositories/transactions.repository.ts --view expanded || true

printf '\n== transactions.repository.ts ==\n'
sed -n '1,260p' packages/api/src/repositories/transactions.repository.ts

printf '\n== search replaceTags implementation and transaction wrappers ==\n'
rg -n "replaceTags|AppDataSource\\.transaction|manager\\.save|manager\\.delete|QueryRunner" packages/api/src/repositories packages/api/src/services -g '*.ts' | sed -n '1,220p'

Repository: ali-ahnaf/pocket_pixel

Length of output: 4966


Wrap transfer writes in a transaction. packages/api/src/services/transactions.service.ts:101-106save(expenseTx), save(incomeTx), and both replaceTags() calls are independent; if any later step fails, this can leave a partial transfer or mismatched tags. Run them inside one AppDataSource.transaction(...) boundary, or equivalent.

🤖 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 `@packages/api/src/services/transactions.service.ts` around lines 101 - 106,
Update the transfer persistence flow containing the expense and income saves so
both save calls and the conditional replaceTags calls execute within a single
AppDataSource.transaction boundary. Preserve the existing tagIds behavior and
use the transaction-scoped repository/manager for every write, ensuring any
failure rolls back the entire transfer.

@ali-ahnaf
ali-ahnaf force-pushed the feat-google-oauth branch from df6938f to d0af0d9 Compare July 16, 2026 10:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/api/src/migrations/1783695914180-Migration.ts (1)

19-24: 🗄️ Data Integrity & Integration | 🟠 Major

Make down() safe for Google-only accounts.

The supplied packages/api/src/services/auth.service.ts creates users with password: null, but Line 19 recreates users.password as NOT NULL and Line 20 copies those null values. Even if that copy were relaxed, Lines 23-24 repeat the same failure when restoring the historical schema. A downgrade after a Google-only signup therefore aborts. Define and enforce a downgrade policy, such as rejecting the downgrade with an explicit precondition or converting OAuth-only users to a valid legacy representation.

🤖 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 `@packages/api/src/migrations/1783695914180-Migration.ts` around lines 19 - 24,
Make the migration’s down() path explicitly handle users with null passwords
before recreating the NOT NULL users.password column. Choose and enforce a clear
downgrade policy—either reject when Google-only accounts exist with an explicit
precondition, or convert them to a valid legacy representation—and apply it
consistently to both users table reconstruction and historical schema
restoration around the INSERT statements.
🧹 Nitpick comments (1)
packages/api/src/migrations/1783695914180-Migration.ts (1)

1-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Format the migration with Prettier.

Line 1 uses double quotes, and the class declaration uses non-Prettier indentation/semicolon formatting. Run the repository formatter on this file.

As per coding guidelines, TypeScript files must be formatted with Prettier using singleQuote, trailingComma: all, and printWidth: 200.

🤖 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 `@packages/api/src/migrations/1783695914180-Migration.ts` around lines 1 - 4,
Run Prettier on Migration1783695914180 and apply the repository’s TypeScript
formatting settings, including single quotes, trailing commas where applicable,
200-character print width, and normalized indentation and semicolons.

Source: Coding guidelines

🤖 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.

Duplicate comments:
In `@packages/api/src/migrations/1783695914180-Migration.ts`:
- Around line 19-24: Make the migration’s down() path explicitly handle users
with null passwords before recreating the NOT NULL users.password column. Choose
and enforce a clear downgrade policy—either reject when Google-only accounts
exist with an explicit precondition, or convert them to a valid legacy
representation—and apply it consistently to both users table reconstruction and
historical schema restoration around the INSERT statements.

---

Nitpick comments:
In `@packages/api/src/migrations/1783695914180-Migration.ts`:
- Around line 1-4: Run Prettier on Migration1783695914180 and apply the
repository’s TypeScript formatting settings, including single quotes, trailing
commas where applicable, 200-character print width, and normalized indentation
and semicolons.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 87aa6c11-3c03-457a-8d4c-5503fff7655a

📥 Commits

Reviewing files that changed from the base of the PR and between df6938f and d0af0d9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (26)
  • packages/api/.env.example
  • packages/api/package.json
  • packages/api/src/entities/User.entity.ts
  • packages/api/src/migrations/1783695914180-Migration.ts
  • packages/api/src/repositories/users.repository.ts
  • packages/api/src/routes/auth.routes.ts
  • packages/api/src/routes/auth/google.route.ts
  • packages/api/src/services/auth.service.ts
  • packages/api/src/services/debts.service.ts
  • packages/api/src/services/preferences.service.ts
  • packages/api/src/services/prompt.service.ts
  • packages/api/src/services/recurring.service.ts
  • packages/api/src/services/tags.service.ts
  • packages/api/src/services/users.service.ts
  • packages/api/src/services/vaults.service.ts
  • packages/api/src/tests/auth.service.test.ts
  • packages/api/src/tests/vaults.service.test.ts
  • packages/shared/src/contracts/auth.ts
  • packages/ui/.env.development
  • packages/ui/public/sw.js
  • packages/ui/src/app/layout.tsx
  • packages/ui/src/app/signin/page.tsx
  • packages/ui/src/app/signup/page.tsx
  • packages/ui/src/components/GoogleAuthProvider.tsx
  • packages/ui/src/components/GoogleSignInButton.tsx
  • packages/ui/src/lib/api/AuthApi.ts
💤 Files with no reviewable changes (1)
  • packages/api/src/services/prompt.service.ts
🚧 Files skipped from review as they are similar to previous changes (21)
  • packages/api/package.json
  • packages/api/.env.example
  • packages/api/src/services/vaults.service.ts
  • packages/ui/.env.development
  • packages/api/src/routes/auth/google.route.ts
  • packages/ui/src/app/signin/page.tsx
  • packages/ui/public/sw.js
  • packages/api/src/services/tags.service.ts
  • packages/api/src/tests/vaults.service.test.ts
  • packages/api/src/services/users.service.ts
  • packages/ui/src/components/GoogleSignInButton.tsx
  • packages/ui/src/components/GoogleAuthProvider.tsx
  • packages/api/src/services/preferences.service.ts
  • packages/api/src/tests/auth.service.test.ts
  • packages/api/src/entities/User.entity.ts
  • packages/ui/src/app/signup/page.tsx
  • packages/api/src/services/recurring.service.ts
  • packages/ui/src/lib/api/AuthApi.ts
  • packages/ui/src/app/layout.tsx
  • packages/api/src/services/auth.service.ts
  • packages/shared/src/contracts/auth.ts

@ali-ahnaf
ali-ahnaf force-pushed the feat-google-oauth branch from d0af0d9 to 546a27b Compare July 16, 2026 12:04
@ali-ahnaf
ali-ahnaf force-pushed the feat-google-oauth branch from 546a27b to 1a43579 Compare July 18, 2026 15:58
@ali-ahnaf
ali-ahnaf merged commit c24eb25 into ali-ahnaf:develop Jul 18, 2026
6 of 7 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

The quest is sealed, @mubasshirahin! Thy incantations have been woven into the main path. The realm grows stronger for thy contribution — accept the gratitude of this humble keeper of the codex. May thy commits forever merge cleanly, brave adventurer 💰

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
packages/ui/.env.example (1)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add the required ending blank line.

This leaves a dotenv-linter warning in the changed file.

🤖 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 `@packages/ui/.env.example` at line 2, Add a trailing blank line after
NEXT_PUBLIC_GOOGLE_CLIENT_ID in the environment example file so the file ends
with the required newline and passes dotenv-linter validation.

Source: Linters/SAST tools

🤖 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 `@packages/api/src/routes/auth/google-callback.route.ts`:
- Around line 28-32: Update the Google callback handler around
authService.googleSignInWithCode so it does not place result.token in the
redirect URL. Persist the authenticated session using the project’s supported
HttpOnly cookie mechanism, or issue a short-lived single-use exchange code, then
redirect without exposing the JWT in browser history, logs, or referrers.

In `@packages/api/src/routes/auth/google-sign-in.route.ts`:
- Around line 16-27: Update both OAuth routes to follow the approved utilService
response contract: in google-sign-in.route.ts, use an existing redirect-capable
utilService method or add the approved redirect mechanism before returning the
Google authorization result; in google-callback.route.ts, remove the local
try/catch, let asyncHandler forward failures to the global error handler, and
send successful output through the same utilService mechanism.

In `@packages/api/src/services/auth.service.ts`:
- Around line 165-169: Update the user-update flow around users.findByEmail and
users.save so an existing non-null user.googleId is never replaced when it
differs from the incoming googleId. Reject this conflict unless the request is
handled through an authenticated account-linking flow, while preserving linking
for users without a Google ID; add a regression test covering the conflicting
identity case.
- Around line 220-221: Update normalizeEmail and the UsersRepository.findByEmail
lookup so existing mixed-case email records remain discoverable, either by
backfilling legacy users.email values during migration or by making the
repository comparison case-insensitive. Preserve normalized email storage and
ensure both password sign-in and Google sign-in resolve an existing account
instead of creating duplicates.

In `@packages/ui/src/app/auth/google/callback/page.tsx`:
- Around line 42-54: Remove the JWT-based session creation from the Google
callback flow in the callback page, including token parsing and the setSession
call. Replace it with a server-mediated HttpOnly session-cookie completion or a
single-use callback-code exchange bound to the browser’s OAuth initiation
state/cookie, and preserve the existing failure redirect behavior for invalid or
failed callbacks.

---

Nitpick comments:
In `@packages/ui/.env.example`:
- Line 2: Add a trailing blank line after NEXT_PUBLIC_GOOGLE_CLIENT_ID in the
environment example file so the file ends with the required newline and passes
dotenv-linter validation.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: adde60d7-3207-48b4-aeb0-688c452edef8

📥 Commits

Reviewing files that changed from the base of the PR and between 546a27b and 1a43579.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (28)
  • .agents/settings.json
  • .gitignore
  • packages/api/.env.example
  • packages/api/package.json
  • packages/api/src/entities/User.entity.ts
  • packages/api/src/index.ts
  • packages/api/src/migrations/1783695914180-Migration.ts
  • packages/api/src/repositories/users.repository.ts
  • packages/api/src/routes/auth.routes.ts
  • packages/api/src/routes/auth/google-callback.route.ts
  • packages/api/src/routes/auth/google-sign-in.route.ts
  • packages/api/src/services/auth.service.ts
  • packages/api/src/tests/auth.service.google.test.ts
  • packages/api/src/tests/auth.service.test.ts
  • packages/api/src/tests/vaults.service.test.ts
  • packages/shared/src/constants.ts
  • packages/shared/src/index.ts
  • packages/ui/.env.development
  • packages/ui/.env.example
  • packages/ui/public/sw.js
  • packages/ui/src/app/auth/google/callback/page.tsx
  • packages/ui/src/app/signin/page.tsx
  • packages/ui/src/app/signup/page.tsx
  • packages/ui/src/components/AuthGuard.tsx
  • packages/ui/src/components/GoogleSignInButton.tsx
  • packages/ui/src/lib/api/ApiClient.ts
  • packages/ui/src/lib/helpers/static.ts
  • packages/ui/test-lucide.ts
💤 Files with no reviewable changes (4)
  • packages/ui/test-lucide.ts
  • packages/ui/src/lib/api/ApiClient.ts
  • .agents/settings.json
  • packages/ui/.env.development
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/api/src/routes/auth.routes.ts
  • packages/api/src/repositories/users.repository.ts
  • packages/api/src/tests/auth.service.test.ts
  • packages/api/src/entities/User.entity.ts
  • packages/api/src/migrations/1783695914180-Migration.ts
  • packages/api/src/tests/vaults.service.test.ts

Comment on lines +28 to +32
try {
const result = await authService.googleSignInWithCode(code);
return res.redirect(`${APP_URL}/auth/google/callback/?token=${encodeURIComponent(result.token)}`);
} catch {
return res.redirect(`${APP_URL}/auth/google/callback/?error=1`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not place the session JWT in the callback URL.

The token will enter browser history and can be exposed through logs or referrers. Use a secure HttpOnly session cookie with authenticated cookie support, or redirect with a short-lived single-use exchange code instead.

🤖 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 `@packages/api/src/routes/auth/google-callback.route.ts` around lines 28 - 32,
Update the Google callback handler around authService.googleSignInWithCode so it
does not place result.token in the redirect URL. Persist the authenticated
session using the project’s supported HttpOnly cookie mechanism, or issue a
short-lived single-use exchange code, then redirect without exposing the JWT in
browser history, logs, or referrers.

Comment on lines +16 to +27
router.get(
'/google',
asyncHandler(async (_req: Request, res: Response) => {
const state = randomBytes(16).toString('hex');
res.cookie(OAUTH_STATE_COOKIE, state, {
httpOnly: true,
secure: isProd,
sameSite: 'lax',
maxAge: 10 * 60 * 1000,
});
return res.redirect(authService.getGoogleAuthUrl(state));
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Keep OAuth routes within the API response and error-handling contract.

Both routes bypass utilService; the callback also catches service failures locally instead of allowing asyncHandler to forward them to the global handler.

  • packages/api/src/routes/auth/google-sign-in.route.ts#L16-L27: return the initiation result through the approved API response mechanism, or add an explicit approved redirect mechanism to it.
  • packages/api/src/routes/auth/google-callback.route.ts#L24-L32: remove the route-local try/catch and route output through the same approved mechanism.

As per coding guidelines, “Do not add try/catch blocks in API routes; wrap handlers with asyncHandler and let the global error handler middleware handle thrown errors.” As per coding guidelines, “Send API responses only through utilService.”

📍 Affects 2 files
  • packages/api/src/routes/auth/google-sign-in.route.ts#L16-L27 (this comment)
  • packages/api/src/routes/auth/google-callback.route.ts#L24-L32
🤖 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 `@packages/api/src/routes/auth/google-sign-in.route.ts` around lines 16 - 27,
Update both OAuth routes to follow the approved utilService response contract:
in google-sign-in.route.ts, use an existing redirect-capable utilService method
or add the approved redirect mechanism before returning the Google authorization
result; in google-callback.route.ts, remove the local try/catch, let
asyncHandler forward failures to the global error handler, and send successful
output through the same utilService mechanism.

Source: Coding guidelines

Comment on lines +165 to +169
user = await this.users.findByEmail(email);
if (user) {
user.googleId = googleId;
if (!user.avatar && profile.picture) user.avatar = profile.picture;
user = await this.users.save(user);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not overwrite an already-linked Google identity.

When email lookup finds a user whose googleId belongs to another Google subject, this path replaces that association. Reject conflicting non-null IDs and require an authenticated account-linking flow; add a regression test for this case.

Proposed fix
 if (user) {
+  if (user.googleId && user.googleId !== googleId) {
+    throw new AppError('Email is already linked to a different Google account', 409);
+  }
   user.googleId = googleId;
   if (!user.avatar && profile.picture) user.avatar = profile.picture;
📝 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
user = await this.users.findByEmail(email);
if (user) {
user.googleId = googleId;
if (!user.avatar && profile.picture) user.avatar = profile.picture;
user = await this.users.save(user);
user = await this.users.findByEmail(email);
if (user) {
if (user.googleId && user.googleId !== googleId) {
throw new AppError('Email is already linked to a different Google account', 409);
}
user.googleId = googleId;
if (!user.avatar && profile.picture) user.avatar = profile.picture;
user = await this.users.save(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 `@packages/api/src/services/auth.service.ts` around lines 165 - 169, Update the
user-update flow around users.findByEmail and users.save so an existing non-null
user.googleId is never replaced when it differs from the incoming googleId.
Reject this conflict unless the request is handled through an authenticated
account-linking flow, while preserving linking for users without a Google ID;
add a regression test covering the conflicting identity case.

Comment on lines +220 to +221
private normalizeEmail(email: string): string {
return email.trim().toLowerCase();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline packages/api/src/repositories/users.repository.ts --items all
ast-grep outline packages/api/src/entities/User.entity.ts --items all

rg -n -C3 'findByEmail|LOWER\(|ILIKE|email' packages/api/src/repositories/users.repository.ts packages/api/src/entities/User.entity.ts
fd -a '1783695914180-Migration.ts' packages/api/src/migrations --exec sed -n '1,260p' {}

Repository: ali-ahnaf/pocket_pixel

Length of output: 6262


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- auth.service relevant lines ---\n'
sed -n '1,320p' packages/api/src/services/auth.service.ts | cat -n | sed -n '1,320p'

printf '\n--- search for email normalization/canonicalization across migrations ---\n'
rg -n -C2 'normalizeEmail|LOWER\(|ILIKE|case[- ]?insensitive|email' packages/api/src/migrations packages/api/src/services packages/api/src/repositories packages/api/src/entities | sed -n '1,240p'

Repository: ali-ahnaf/pocket_pixel

Length of output: 31710


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- repository lookup implementation ---\n'
sed -n '1,120p' packages/api/src/repositories/users.repository.ts | cat -n

printf '\n--- users entity column definition ---\n'
sed -n '1,80p' packages/api/src/entities/User.entity.ts | cat -n

Repository: ali-ahnaf/pocket_pixel

Length of output: 2821


Backfill legacy emails or make lookups case-insensitive. normalizeEmail() only lower-cases the input; UsersRepository.findByEmail() still does an exact match, and the migrations copy existing users.email values as-is. Any mixed-case row will stop matching, which can break password sign-in and cause Google sign-in to create a duplicate account instead of linking it.

🤖 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 `@packages/api/src/services/auth.service.ts` around lines 220 - 221, Update
normalizeEmail and the UsersRepository.findByEmail lookup so existing mixed-case
email records remain discoverable, either by backfilling legacy users.email
values during migration or by making the repository comparison case-insensitive.
Preserve normalized email storage and ensure both password sign-in and Google
sign-in resolve an existing account instead of creating duplicates.

Comment on lines +42 to +54
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
const decoded = token ? decodeToken(token) : null;

if (params.get('error') || !token || !decoded) {
setFailed(true);

const timer = setTimeout(() => router.replace('/signin'), 2000);
return () => clearTimeout(timer);
}

setSession(token, { id: decoded.userId, name: decoded.name, email: decoded.email, avatar: decoded.avatar });
router.replace('/');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not establish sessions from a JWT in the callback URL.

A victim can be induced to visit this page with an attacker’s valid JWT, which setSession then persists as the victim’s session. This is login CSRF/session fixation; query-string JWTs can also leak through browser history, logs, and same-origin referrers. Complete the flow with an HttpOnly session cookie, or exchange a single-use callback code bound to the browser’s OAuth-initiation state/cookie instead.

🤖 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 `@packages/ui/src/app/auth/google/callback/page.tsx` around lines 42 - 54,
Remove the JWT-based session creation from the Google callback flow in the
callback page, including token parsing and the setSession call. Replace it with
a server-mediated HttpOnly session-cookie completion or a single-use
callback-code exchange bound to the browser’s OAuth initiation state/cookie, and
preserve the existing failure redirect behavior for invalid or failed callbacks.

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

Labels

question Further information is requested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Google OAuth sign-in flow

2 participants