Skip to content

feat: GET /api/accounts/{id} accepts email as identifier - #403

Merged
sweetmantech merged 7 commits into
testfrom
feat/accounts-id-email-lookup
Apr 6, 2026
Merged

feat: GET /api/accounts/{id} accepts email as identifier#403
sweetmantech merged 7 commits into
testfrom
feat/accounts-id-email-lookup

Conversation

@sweetmantech

@sweetmantechsweetmantech commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • GET /api/accounts/{id} now accepts an email address in addition to a UUID
  • When the path parameter contains @, resolves it via selectAccountByEmail before running auth/access checks
  • Returns 404 if no account is found for the email
  • UUID paths work unchanged
  • Part of REC-52: enables email-based account lookup for chat override

Test plan

  • RED: wrote 2 failing tests for email lookup (resolve + 404)
  • GREEN: all 6 tests pass
  • Verify on preview: GET /api/accounts/customer@example.com returns account
  • Verify UUID path still works
  • Verify 404 for unknown email

🤖 Generated with Claude Code


Summary by cubic

GET /api/accounts/{id} now accepts an email or a UUID. Resolution and a single auth/access check are centralized in validateGetAccountParams; email resolves first, then auth runs (REC-52 chat override).

  • New Features

    • If id contains @, delegate to resolveAccountIdByEmail (lookup via selectAccountByEmail; 404 on miss/null), then run one validateAuthContext; returns 401/403/404 as needed.
    • UUID paths remain supported; validated via validateAccountParams and the same auth path.
  • Refactors

    • Added validateGetAccountParams to handle UUID/email detection and a single validateAuthContext call; getAccountHandler now delegates to it.
    • Simplified resolveAccountIdByEmail to only perform email → accountId lookup by inlining selectAccountByEmail; removed resolveAccountIdFromEmail.

Written for commit 8c3a49a. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Account lookup now accepts either an account ID or an email address in the path.
    • Documentation updated to describe the "ID or email" path semantics and access behavior.
  • Bug Fixes

    • Improved access validation and clearer error responses when lookups fail (including 404 for unknown emails and consistent response formatting).

When the path parameter contains '@', resolves it as an email via
selectAccountByEmail before running auth checks. Returns 404 if no
account is found for the email. UUID paths work unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercelBot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
recoup-apiReadyReadyPreviewApr 6, 2026 9:35pm

Request Review

@coderabbitai

coderabbitaiBot commented Apr 6, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 21 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 19 minutes and 21 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e638d734-0d35-4c12-b4eb-53a644fca68d

📥 Commits

Reviewing files that changed from the base of the PR and between 5aebedc and 8c3a49a.

⛔ Files ignored due to path filters (2)
  • lib/accounts/__tests__/resolveAccountIdByEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/accounts/__tests__/validateGetAccountParams.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/accounts/resolveAccountIdByEmail.ts
  • lib/accounts/validateGetAccountParams.ts
📝 Walkthrough

Walkthrough

The GET account handler now accepts either a UUID or an email in the path id param. A new async validateGetAccountParams(request, id) centralizes validation/resolution: it either returns a NextResponse error or the resolved accountId. The handler then calls getAccountWithDetails(accountId). Documentation updated to reflect "ID or email" semantics.

Changes

Cohort / File(s)Summary
Handler
lib/accounts/getAccountHandler.ts
Replaced prior two-step validation with a single validateGetAccountParams(request, id) call. Handler now accepts resolved accountId (UUID) or returns early on NextResponse. Downstream call changed to getAccountWithDetails(accountId).
Email→ID resolver
lib/accounts/resolveAccountIdByEmail.ts
New helper: authenticates caller, queries selectAccountByEmail(email), returns 404 NextResponse if missing, then re-validates access via validateAuthContext(request, { accountId }). Returns resolved account_id string on success.
Param validator
lib/accounts/validateGetAccountParams.ts
New exported helper that distinguishes email (contains @) vs UUID paths. Delegates to resolveAccountIdByEmail for emails or validateAccountParams + validateAuthContext for UUIDs. Returns either string (accountId) or NextResponse on failure.
Docs
docs/... (updated docs mention)
Documentation updated to describe "ID or email" path parameter semantics and access behavior.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant Handler
participant Validator as validateGetAccountParams
participant Resolver as resolveAccountIdByEmail
participant Auth as validateAuthContext
participant DB
participant Service as getAccountWithDetails
Client->>Handler: GET /accounts/{id}
Handler->>Validator: validateGetAccountParams(request, id)
alt id contains "@"
Validator->>Resolver: resolveAccountIdByEmail(request, email)
Resolver->>Auth: validateAuthContext(request)
Auth-->>Resolver: auth OK / NextResponse
Resolver->>DB: selectAccountByEmail(email)
DB-->>Resolver: account_id
Resolver->>Auth: validateAuthContext(request, {accountId})
Auth-->>Resolver: auth OK / NextResponse
Resolver-->>Validator: accountId
else id is UUID
Validator->>Auth: validateAccountParams(id) -> validatedId
Auth-->>Validator: validatedId / NextResponse
Validator->>Auth: validateAuthContext(request, {accountId:validatedId})
Auth-->>Validator: OK / NextResponse
Validator-->>Handler: accountId
end
Handler->>Service: getAccountWithDetails(accountId)
Service-->>Handler: account details
Handler-->>Client: 200 JSON / or earlier NextResponse
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

✨ An ID or an email, both now heard,
A single gatekeeper checks every word.
Resolve, validate, then fetch the tale—
Clean flow, neat code, the happy trail. 🚀

🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check nameStatusExplanation
Solid & Clean Code✅ PassedCode demonstrates strong SOLID adherence: Single Responsibility (each file exports one focused function), Open/Closed (delegation over modification), Liskov Substitution (consistent return types), Interface Segregation (minimal dependencies), Dependency Inversion (abstracts error handling patterns). Clean code principles well-executed: descriptive naming, appropriate function sizes (37-44 lines), comprehensive JSDoc documentation, straightforward logic without over-engineering, and consistent error handling patterns throughout.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/accounts-id-email-lookup

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.

@coderabbitaicoderabbitaiBot 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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/accounts/getAccountHandler.ts`:
- Around line 28-34: The email lookup branch in getAccountHandler currently
returns a 404 for unknown emails before the handler's authentication check,
enabling account-probing; move or invoke the existing auth gate used later in
getAccountHandler (the same auth check currently at line ~45) before calling
selectAccountByEmail or returning any NextResponse.json about "No account
found", so unauthenticated requests get a 401/unauthorized and only
authenticated requests reach selectAccountByEmail and receive the
404/NextResponse.json with getCorsHeaders.
- Line 36: The code assigns emailAccount.account_id directly to accountId even
though the DB type is string | null; add an explicit null/empty check on
emailAccount.account_id before assigning and return an error NextResponse (using
getCorsHeaders()) if it's missing, then assign accountId and call
getAccountWithDetails(accountId); optionally validate the value as a UUID before
calling getAccountWithDetails to ensure the non-null string contract is
satisfied (refer to symbols emailAccount, accountId, getAccountWithDetails,
getCorsHeaders, NextResponse).
🪄 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

Run ID: 95eace95-ba18-49f3-a822-b111a55d0f89

📥 Commits

Reviewing files that changed from the base of the PR and between d01ff28 and 574ad31.

⛔ Files ignored due to path filters (1)
  • lib/accounts/__tests__/getAccountHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (1)
  • lib/accounts/getAccountHandler.ts

Comment threadlib/accounts/getAccountHandler.ts Outdated
Comment threadlib/accounts/getAccountHandler.ts Outdated
Comment threadlib/accounts/getAccountHandler.ts Outdated
Comment on lines +29 to +42
const emailAccount = await selectAccountByEmail(id);
if (!emailAccount) {
return NextResponse.json(
{ status: "error", error: "No account found for the provided email" },
{ status: 404, headers: getCorsHeaders() },
);
}
accountId = emailAccount.account_id;
} else {
const validatedParams = validateAccountParams(id);
if (validatedParams instanceof NextResponse) {
return validatedParams;
}
accountId = validatedParams.id;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Open Closed principle

  • actual: email check code added directly to existing getAccountHandler.
  • required: new lib file for email check code.

- Moved auth check before selectAccountByEmail to prevent unauthenticated
account-email probing (CodeRabbit critical)
- Added null guard on emailAccount.account_id (CodeRabbit major)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@cubic-dev-aicubic-dev-aiBot 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.

1 issue found across 2 files

Confidence score: 3/5

  • There is a concrete security risk in lib/accounts/getAccountHandler.ts: returning an email-not-found 404 before authentication can let unauthenticated callers enumerate valid emails via response differences.
  • Given the issue is medium severity (6/10) with reasonably high confidence (7/10), this introduces real user-impacting regression risk and lowers merge confidence until ordering is fixed.
  • This should be straightforward to address by authenticating first and only then returning resource-specific errors, which keeps behavior consistent for unauthorized requests.
  • Pay close attention to lib/accounts/getAccountHandler.ts - response ordering currently leaks account existence to unauthenticated callers.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/accounts/getAccountHandler.ts">
<violation number="1" location="lib/accounts/getAccountHandler.ts:30">
P2: Authenticate before returning the email-not-found 404; otherwise unauthenticated callers can probe which emails exist based on 404 vs 401/403 responses.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client
participant Handler as getAccountHandler
participant DB_Emails as DB: account_emails
participant Validator as validateAccountParams
participant Auth as validateAuthContext
participant DB_Accounts as DB: accounts
Client->>Handler: GET /api/accounts/{id}
Note over Handler,DB_Emails: ID Resolution Logic
alt NEW: id contains "@" (Email lookup)
Handler->>DB_Emails: selectAccountByEmail(email)
DB_Emails-->>Handler: account_id or null
opt Email not found
Handler-->>Client: 404 (No account found for email)
end
else Standard UUID
Handler->>Validator: validateAccountParams(id)
Validator-->>Handler: accountId
end
Note over Handler,DB_Accounts: Authorization & Retrieval
Handler->>Auth: validateAuthContext(request, { accountId })
alt Auth Success
Handler->>DB_Accounts: CHANGED: getAccountWithDetails(resolvedAccountId)
DB_Accounts-->>Handler: account record
alt Account exists
Handler-->>Client: 200 OK (Account Details)
else Account not found
Handler-->>Client: 404 (Account not found)
end
else Auth Failure (401/403)
Auth-->>Handler: Error Response
Handler-->>Client: Error Response
end
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment threadlib/accounts/getAccountHandler.ts Outdated
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sweetmantech

Copy link
Copy Markdown
ContributorAuthor

Preview Deployment Testing Results

Preview URL:recoup-api-git-feat-accounts-id-emai-33f78a-recoupable-ad724970.vercel.app

GET /api/accounts/sweetmantech@gmail.com

  • Returns status: "success" with full account details
  • Resolved account ID: fb678396-a68f-4294-ae50-b8cacf9ce77b
  • Includes account_emails, account_info, image, label, etc.
  • Auth enforced — requires valid API key
  • Access control enforced — validateAuthContext checks org membership/admin before returning

Security

  • Auth runs before email lookup (no account-email probing)
  • Null account_id guard in place
  • resolveAccountIdFromEmail extracted to own file (OCP)

Comment threadlib/accounts/getAccountHandler.ts Outdated
Comment on lines 26 to 61
let accountId: string;

if (id.includes("@")) {
// Authenticate before email lookup to prevent account-email probing
const authResult = await validateAuthContext(request);
if (authResult instanceof NextResponse) {
return authResult;
}

const resolved = await resolveAccountIdFromEmail(id);
if (resolved instanceof NextResponse) {
return resolved;
}
accountId = resolved;

// Verify caller can access this account
const accessResult = await validateAuthContext(request, {
accountId,
});
if (accessResult instanceof NextResponse) {
return accessResult;
}
} else {
const validatedParams = validateAccountParams(id);
if (validatedParams instanceof NextResponse) {
return validatedParams;
}
accountId = validatedParams.id;

const authResult = await validateAuthContext(request, {
accountId: validatedParams.id,
});
if (authResult instanceof NextResponse) {
return authResult;
const authResult = await validateAuthContext(request, {
accountId,
});
if (authResult instanceof NextResponse) {
return authResult;
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

SRP - move all lookup code to a standalone function out of the handler.

@cubic-dev-aicubic-dev-aiBot 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.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/accounts/resolveAccountIdFromEmail.ts">
<violation number="1" location="lib/accounts/resolveAccountIdFromEmail.ts:16">
P2: Do not return 404 for all `selectAccountByEmail` failures; database/query errors are being misreported as "email not found."</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment threadlib/accounts/resolveAccountIdFromEmail.ts
Moved auth + email resolve + access check into its own function.
Handler now delegates to resolveAccountIdByEmail for email paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@cubic-dev-aicubic-dev-aiBot 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.

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/accounts/resolveAccountIdByEmail.ts">
<violation number="1" location="lib/accounts/resolveAccountIdByEmail.ts:32">
P1: Returning the raw 403 from the access check leaks email existence (404 for unknown vs 403 for existing-but-forbidden). For email-based lookup, normalize forbidden access to the same not-found response.</violation>
</file>
<file name="lib/accounts/__tests__/resolveAccountIdByEmail.test.ts">
<violation number="1" location="lib/accounts/__tests__/resolveAccountIdByEmail.test.ts:25">
P3: The second mockResolvedValue overrides the first, so this test doesn’t exercise two different auth results. Use mockResolvedValueOnce for sequential calls so the first auth check and the access check are distinct.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment threadlib/accounts/resolveAccountIdByEmail.ts Outdated
Comment threadlib/accounts/__tests__/resolveAccountIdByEmail.test.ts Outdated
…tions
Moved all id resolution logic (email vs UUID detection, auth, access
control) into validateGetAccountParams. Handler is now a thin wrapper.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@cubic-dev-aicubic-dev-aiBot 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.

0 issues found across 4 files (changes from recent commits).

Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.

Comment threadlib/accounts/resolveAccountIdByEmail.ts

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

KISS - is this function required? why not move the call to selectAccountByEmail directly in lib/accounts/resolveAccountIdByEmail.ts and remove this lib?

…FromEmail
Removed unnecessary resolveAccountIdFromEmail abstraction. The
selectAccountByEmail call is now directly in resolveAccountIdByEmail.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…yEmail
- resolveAccountIdByEmail now only resolves email → accountId (no auth)
- validateGetAccountParams handles auth + access for both paths with one
validateAuthContext(request, { accountId }) call
- Both UUID and email paths now follow the same pattern: resolve, then auth
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sweetmantech
sweetmantech merged commit e5b1284 into testApr 6, 2026
4 of 5 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@sweetmantech