Skip to content

feat: enhance account update logic with admin checks and authentication#388

Open
pradipthaadhi wants to merge 3 commits intotestfrom
yuliusupwork/myc-4524-enforce-authentication-and-identity-bound-updates-for-patch
Open

feat: enhance account update logic with admin checks and authentication#388
pradipthaadhi wants to merge 3 commits intotestfrom
yuliusupwork/myc-4524-enforce-authentication-and-identity-bound-updates-for-patch

Conversation

@pradipthaadhi
Copy link
Copy Markdown
Collaborator

@pradipthaadhi pradipthaadhi commented Apr 1, 2026

  • Added authentication validation to the PATCH /api/accounts endpoint using validateAuthContext.
  • Implemented logic to restrict accountId overrides to admin users only.
  • Updated validateUpdateAccountBody to make accountId optional.
  • Introduced unit tests for various scenarios including authentication errors, account updates, and admin checks.

Summary by CodeRabbit

  • Bug Fixes

    • Account updates now enforce authentication and authorization; non-admins cannot modify other accounts (requests return 403).
  • New Features

    • Account ID in update requests is now optional.
    • Admins can optionally override the target account when updating.
  • Validation

    • Update requests must include at least one updatable field (e.g., name, organization, image, role); otherwise validation fails.

- Added authentication validation to the PATCH /api/accounts endpoint using validateAuthContext.
- Implemented logic to restrict accountId overrides to admin users only.
- Updated validateUpdateAccountBody to make accountId optional.
- Introduced unit tests for various scenarios including authentication errors, account updates, and admin checks.
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 1, 2026

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

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Apr 3, 2026 4:00pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

PATCH /api/accounts now delegates request handling to a new patchAccountHandler(req). The update body schema makes accountId optional and requires at least one profile field. The handler authenticates the request, validates body, resolves target accountId (body override or auth), enforces admin-only overrides, and calls updateAccountHandler.

Changes

Cohort / File(s) Summary
Route: delegation
app/api/accounts/route.ts
Replaced inline PATCH logic with delegation to patchAccountHandler(req); removed local parsing/validation and direct updateAccountHandler invocation.
Schema: request validation
lib/accounts/validateUpdateAccountBody.ts
Made accountId optional and added a refinement enforcing at least one update field (name, instruction, organization, image, jobTitle, roleType, companyName, knowledges). Validation returns first issue in existing error shape.
Handler: new flow
lib/accounts/patchAccountHandler.ts
Added exported patchAccountHandler(req) which: validates auth via validateAuthContext, parses & validates body, resolves target accountId (with admin-only override checks using checkIsAdmin and 403+CORS on failure), and calls updateAccountHandler with resolved accountId and validated update data.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as "app/api/accounts/route.ts"
    participant Handler as "patchAccountHandler"
    participant Auth as "validateAuthContext"
    participant Admin as "checkIsAdmin"
    participant Update as "updateAccountHandler"

    Client->>Route: PATCH /api/accounts
    Route->>Handler: patchAccountHandler(req)
    Handler->>Auth: validateAuthContext(req)
    Auth-->>Handler: authResult { accountId, ... }
    Handler->>Handler: safeParseJson(req) & validateUpdateAccountBody
    Handler->>Handler: resolve targetAccountId (body.accountId || authResult.accountId)
    alt override provided && target != authResult.accountId
        Handler->>Admin: checkIsAdmin(authResult.accountId)
        Admin-->>Handler: isAdmin?
        alt not admin
            Handler-->>Client: 403 Forbidden + CORS headers
        else is admin
            Handler->>Update: updateAccountHandler(validatedData + targetAccountId)
            Update-->>Client: 200/Success
        end
    else same account
        Handler->>Update: updateAccountHandler(validatedData + targetAccountId)
        Update-->>Client: 200/Success
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • sweetmantech

Poem

A PATCH sets sail with checks in tow,
IDs may wander, but auth must know,
Schema asks kindly: just one thing to change,
Admins may steer when ranges rearrange,
Updates march forward—neat, secure, and slow. 🛳️

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning The patchAccountHandler violates SRP by consolidating five concerns (auth, parsing, validation, authorization, business logic) in one 46-line function and repeats error-handling conditionals twice, violating DRY. Most critically, it accepts accountId from the request body to override the authenticated account, contradicting the security principle that account_id should never be in request bodies. Remove accountId from request body logic and derive target account strictly from authResult.accountId. For cross-account admin updates, create a separate privileged route. Extract repeated error-handling patterns into a helper function and split the handler into focused functions, reducing it to under 20 lines.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yuliusupwork/myc-4524-enforce-authentication-and-identity-bound-updates-for-patch

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

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

🧹 Nitpick comments (1)
app/api/accounts/route.ts (1)

61-69: Consider wrapping checkIsAdmin in error handling for resilience.

The authorization logic is sound—the admin check correctly guards accountId overrides. However, checkIsAdmin calls validateOrganizationAccess which may throw on database errors (see lib/admins/checkIsAdmin.ts:12-15). An unhandled exception here would result in a 500 rather than a graceful error response.

For consistency with the structured error responses elsewhere in this handler, consider:

♻️ Proposed resilient error handling
   if (validated.accountId && validated.accountId !== authResult.accountId) {
-    const isAdmin = await checkIsAdmin(authResult.accountId);
-    if (!isAdmin) {
+    let isAdmin = false;
+    try {
+      isAdmin = await checkIsAdmin(authResult.accountId);
+    } catch {
+      return NextResponse.json(
+        { status: "error", error: "Unable to verify admin status" },
+        { status: 500, headers: getCorsHeaders() },
+      );
+    }
+    if (!isAdmin) {
       return NextResponse.json(
         { status: "error", error: "accountId override is only allowed for admin accounts" },
         { status: 403, headers: getCorsHeaders() },
       );
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/accounts/route.ts` around lines 61 - 69, Wrap the call to
checkIsAdmin used in the accountId override guard in a try/catch to handle
possible exceptions from validateOrganizationAccess; specifically around the
existing call in the if (validated.accountId && validated.accountId !==
authResult.accountId) block, catch errors from
checkIsAdmin(authResult.accountId) and return a structured NextResponse.json
error (use the same shape as other handler errors and include getCorsHeaders())
with an appropriate 500 status and a generic message like "failed to verify
admin status" while avoiding leaking internal details; ensure the catch path
logs the error if logging exists and preserves the existing non-admin 403
response when checkIsAdmin resolves to false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/api/accounts/route.ts`:
- Around line 61-69: Wrap the call to checkIsAdmin used in the accountId
override guard in a try/catch to handle possible exceptions from
validateOrganizationAccess; specifically around the existing call in the if
(validated.accountId && validated.accountId !== authResult.accountId) block,
catch errors from checkIsAdmin(authResult.accountId) and return a structured
NextResponse.json error (use the same shape as other handler errors and include
getCorsHeaders()) with an appropriate 500 status and a generic message like
"failed to verify admin status" while avoiding leaking internal details; ensure
the catch path logs the error if logging exists and preserves the existing
non-admin 403 response when checkIsAdmin resolves to false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f0e3603d-fae9-4dfa-b4b0-7f74addc3334

📥 Commits

Reviewing files that changed from the base of the PR and between fd93602 and 6c230aa.

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

- Updated the updateAccountBodySchema to include a refinement that enforces at least one update field to be provided in the request body.
- Added unit tests for validateUpdateAccountBody to ensure proper validation behavior, including cases with no fields and valid updates.
{
message: "At least one update field is required",
path: ["body"],
},
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.

Suggestion: This chained || check works but is fragile — if a new field gets added to the schema, it's easy to forget to add it here too.

A more maintainable approach:

.refine(
  body => {
    const { accountId, ...updateFields } = body;
    return Object.values(updateFields).some(v => v !== undefined);
  },
  {
    message: "At least one update field is required",
    path: ["body"],
  },
)

This destructures out accountId (not an update field) and dynamically checks the rest. Any new fields added to the schema are automatically covered.

Comment on lines 48 to 75
const authResult = await validateAuthContext(req);
if (authResult instanceof NextResponse) {
return authResult;
}

const body = await safeParseJson(req);

const validated = validateUpdateAccountBody(body);
if (validated instanceof NextResponse) {
return validated;
}

return updateAccountHandler(validated as UpdateAccountBody);
const targetAccountId = validated.accountId ?? authResult.accountId;
if (validated.accountId && validated.accountId !== authResult.accountId) {
const isAdmin = await checkIsAdmin(authResult.accountId);
if (!isAdmin) {
return NextResponse.json(
{ status: "error", error: "accountId override is only allowed for admin accounts" },
{ status: 403, headers: getCorsHeaders() },
);
}
}

return updateAccountHandler({
...(validated as UpdateAccountBody),
accountId: targetAccountId,
});
}
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.

SRP - handler function.

…dler

- Replaced the previous update logic with a dedicated patchAccountHandler for handling account updates.
- Updated validation to ensure at least one profile field is provided, with optional accountId for admin overrides.
- Refactored tests to cover new patchAccountHandler functionality and validation scenarios.
- Added unit tests for patchAccountHandler to verify authentication, admin checks, and validation errors.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/patchAccountHandler.ts`:
- Around line 31-45: The code currently lets validated.accountId become the
write target (targetAccountId) and passes it to updateAccountHandler,
reintroducing a client-controlled account selector; instead, always derive the
account to update from authResult.accountId when calling updateAccountHandler
(do not use validated.accountId as the accountId argument), keep the
checkIsAdmin block only for deciding whether to reject non-admins but do not use
it to allow body-based overrides, and if cross-account admin updates are
required implement them via a separate privileged route or server-only param
rather than using validated.accountId; update the use of
targetAccountId/validated.accountId/authResult.accountId and ensure
updateAccountHandler receives authResult.accountId.
🪄 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: 71951922-f18e-47dc-951d-8426af9ae12d

📥 Commits

Reviewing files that changed from the base of the PR and between 0cf4a28 and a38159d.

⛔ Files ignored due to path filters (3)
  • app/api/accounts/__tests__/route.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • lib/accounts/__tests__/patchAccountHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/accounts/__tests__/validateUpdateAccountBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (3)
  • app/api/accounts/route.ts
  • lib/accounts/patchAccountHandler.ts
  • lib/accounts/validateUpdateAccountBody.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/api/accounts/route.ts
  • lib/accounts/validateUpdateAccountBody.ts

Comment on lines +31 to +45
const targetAccountId = validated.accountId ?? authResult.accountId;
if (validated.accountId && validated.accountId !== authResult.accountId) {
const isAdmin = await checkIsAdmin(authResult.accountId);
if (!isAdmin) {
return NextResponse.json(
{ status: "error", error: "accountId override is only allowed for admin accounts" },
{ status: 403, headers: getCorsHeaders() },
);
}
}

return updateAccountHandler({
...(validated as UpdateAccountBody),
accountId: targetAccountId,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep the target account derived from auth, not the PATCH body.

validated.accountId still becomes the authoritative write target here, and updateAccountHandler uses that accountId for all downstream reads and writes. Even with the admin gate, this reintroduces a public body parameter for account selection on /api/accounts; if admins need cross-account updates, move that to a separate privileged route or a server-only parameter instead.

Based on learnings: "Never use account_id in request bodies or tool schemas; always derive the account ID from authentication".

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

In `@lib/accounts/patchAccountHandler.ts` around lines 31 - 45, The code currently
lets validated.accountId become the write target (targetAccountId) and passes it
to updateAccountHandler, reintroducing a client-controlled account selector;
instead, always derive the account to update from authResult.accountId when
calling updateAccountHandler (do not use validated.accountId as the accountId
argument), keep the checkIsAdmin block only for deciding whether to reject
non-admins but do not use it to allow body-based overrides, and if cross-account
admin updates are required implement them via a separate privileged route or
server-only param rather than using validated.accountId; update the use of
targetAccountId/validated.accountId/authResult.accountId and ensure
updateAccountHandler receives authResult.accountId.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants