Skip to content

fix(dashboard): Use updateActiveAdministrator for profile page - #5055

Open
Ryrahul wants to merge 1 commit into
vendurehq:masterfrom
Ryrahul:fix/profile-update-permission
Open

fix(dashboard): Use updateActiveAdministrator for profile page#5055
Ryrahul wants to merge 1 commit into
vendurehq:masterfrom
Ryrahul:fix/profile-update-permission

Conversation

@Ryrahul

@Ryrahul Ryrahul commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Description

The profile page (/profile) uses the updateAdministrator mutation, which requires Permission.UpdateAdministrator. This means any administrator whose role does not include that permission cannot edit their own first name, last name, email, or password — the form loads fine (the query uses activeAdministrator gated by Permission.Owner), but saving fails with a FORBIDDEN error.

The Admin API already has updateActiveAdministrator for exactly this case — it's gated by Permission.Owner and resolves the target from ctx.activeUserId, so it can only ever modify the calling user's own record.

This PR switches the profile page to use updateActiveAdministrator.

Fixes #5037

What changed

File Change
profile.graphql.ts updateAdministratorupdateActiveAdministrator (mutation name + input type)
profile.tsx Updated import, updateDocument reference, removed id from setValuesForUpdate (UpdateActiveAdministratorInput has no id field — the server infers it from the active user)

Why this is safe

  • updateActiveAdministrator is @Allow(Permission.Owner) — any authenticated admin can call it
  • The resolver resolves the target admin from ctx.activeUserId (cannot modify other admins)
  • UpdateActiveAdministratorInput has no roleIds field — prevents privilege escalation
  • customFields is dynamically added via graphql-custom-fields.ts extend input, so custom fields on the profile page continue to work
  • The separate admin detail page (/administrators/:id) still uses updateAdministrator with Permission.UpdateAdministrator — unchanged

Breaking changes

None.

Checklist

📌 Always:

  • I have set a clear title
  • My PR is small and contains a single feature
  • I have checked my own PR

👍 Most of the time:

  • I have added or updated test cases
  • I have updated the README if needed

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
vendure-storybook Ready Ready Preview Jul 31, 2026 10:31am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The profile GraphQL document now uses UpdateActiveAdministrator and UpdateActiveAdministratorInput. The profile route passes this mutation to useDetailPage. An end-to-end test verifies that an administrator without UpdateAdministrator permission can update and persist their own first name.

Possibly related PRs

  • vendurehq/vendure#4727: Also covers the updateActiveAdministrator flow and administrator authorization behavior.
  • vendurehq/vendure#4995: Changes the same profile files to use the active-administrator mutation and adds permission coverage.

Suggested reviewers: michaelbromley

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #5037 by enabling self-service profile updates without UpdateAdministrator and adding a regression test for restricted administrators.
Out of Scope Changes check ✅ Passed All changes are directly related to the profile mutation fix and its regression coverage; no unrelated code changes are identified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and concisely identifies the main fix: using updateActiveAdministrator for the dashboard profile page.
Description check ✅ Passed The description includes the change summary, linked issue, safety rationale, breaking-change status, and checklist; the optional screenshots section is not needed.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@michaelbromley michaelbromley added the T1: Fast track Clearly understood fix with limited blast radius. Fast lane. label Jul 30, 2026
@biggamesmallworld

biggamesmallworld commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Thanks @Ryrahul, checked out the branch and reviewed it. The change is correct:

  • updateActiveAdministrator is @Allow(Permission.Owner) and resolves the target from ctx.activeUserId, so it can only ever touch the caller's own record.
  • Dropping id from setValuesForUpdate is required, not cosmetic — UpdateActiveAdministratorInput has no id, and useDetailPage submits the form values verbatim, so nothing else needed changing.
  • Custom fields still work: addActiveAdministratorCustomFields is wired unconditionally for the Admin API.
  • This matches what the Angular admin-ui has always done, so it's a dashboard parity fix rather than a new pattern.

Two things before merge:

1. The commit message says Fixes #5035 (a collection batch-loading perf issue). The PR body correctly says #5037. Please amend the commit so the squash body doesn't close the wrong issue.

2. Needs a test. The existing packages/dashboard/e2e/tests/settings/profile.spec.ts tests all run as superadmin, who already has UpdateAdministrator — they passed before this change and pass after, so they don't protect against a regression. The test needs to be "a restricted admin can save their profile".

I wrote one locally and confirmed it fails on master and passes with this PR. Please add it to the existing spec file, along with the import import { VendureAdminClient } from '../../utils/vendure-admin-client.js';:

    // #5037 — an admin without UpdateAdministrator permission must still be able to
    // edit their own profile (the page uses updateActiveAdministrator, gated by Owner)
    test('should allow an admin without UpdateAdministrator permission to update own profile', async ({
        page,
        browser,
    }) => {
        const suffix = Date.now();
        const emailAddress = `restricted-${suffix}@test.com`;
        const password = 'test-password';

        const client = new VendureAdminClient(page);
        await client.login();
        const { createRole } = await client.gql(
            `mutation ($input: CreateRoleInput!) { createRole(input: $input) { id } }`,
            {
                input: {
                    code: `restricted-${suffix}`,
                    description: 'No admin permissions',
                    permissions: ['ReadCatalog'],
                },
            },
        );
        await client.gql(
            `mutation ($input: CreateAdministratorInput!) { createAdministrator(input: $input) { id } }`,
            {
                input: {
                    firstName: 'Restricted',
                    lastName: 'Admin',
                    emailAddress,
                    password,
                    roleIds: [createRole.id],
                },
            },
        );

        // Fresh context so we browse as the restricted admin, not the superadmin
        const context = await browser.newContext({ storageState: { cookies: [], origins: [] } });
        const restrictedPage = await context.newPage();
        await restrictedPage.goto('/login');
        await restrictedPage.getByPlaceholder('Email').fill(emailAddress);
        await restrictedPage.getByPlaceholder('Password').fill(password);
        await restrictedPage.getByRole('button', { name: 'Sign in' }).click();
        await expect(restrictedPage).not.toHaveURL(/\/login/, { timeout: 15_000 });

        await restrictedPage.goto('/profile');
        const firstNameField = restrictedPage.locator('[data-slot="field"]').filter({
            has: restrictedPage.locator('[data-slot="field-label"]').getByText('First name', { exact: true }),
        });
        await firstNameField.getByRole('textbox').fill('Renamed');
        await restrictedPage.getByRole('button', { name: 'Update' }).click();

        await expect(
            restrictedPage.locator('[data-sonner-toast]').filter({ hasText: 'Successfully updated profile' }),
        ).toBeVisible({ timeout: 10_000 });

        await restrictedPage.reload();
        await expect(firstNameField.getByRole('textbox')).toHaveValue('Renamed');

        await context.close();
    });

Run with:

CI=true VITE_TEST_PORT=5176 bunx playwright test --config e2e/playwright.config.ts e2e/tests/settings/profile.spec.ts --reporter=list

Happy to approve once the test is in and the commit trailer is fixed.

The profile page used the updateAdministrator mutation which requires
Permission.UpdateAdministrator, preventing administrators without that
permission from editing their own profile (name, email, password).

Switch to updateActiveAdministrator which is gated by Permission.Owner
and resolves the target from ctx.activeUserId, so any authenticated
administrator can edit their own profile without needing elevated
permissions.

- Replace updateAdministratorDocument with updateActiveAdministratorDocument
- Remove id from setValuesForUpdate (UpdateActiveAdministratorInput has
  no id field; the server infers it from the active user)
- Add e2e test verifying a restricted admin (without UpdateAdministrator)
  can update their own profile

Fixes vendurehq#5037

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/dashboard/e2e/tests/settings/profile.spec.ts (2)

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

Consider cleaning up the created role and administrator.

The test creates a role and an administrator through client.gql calls but never deletes them afterward. Over repeated CI runs, this accumulates test-only administrators and roles in the database, unless the test environment resets between runs.

Add deleteAdministrator and deleteRole calls at the end of the test to keep the test data isolated.

🤖 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/dashboard/e2e/tests/settings/profile.spec.ts` around lines 103 -
126, The test setup creates persistent administrator and role records without
cleanup. At the end of the test containing the createAdministrator and
createRole mutations, add client.gql calls for deleteAdministrator and
deleteRole using the created administrator and role IDs, ensuring cleanup runs
after the test assertions complete.

129-152: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard context.close() with try/finally.

context.close() at line 151 only runs if every prior assertion (lines 131-150) succeeds. If any assertion fails, the browser context created at line 129 stays open for the rest of the test run.

Wrap the interactions in a try/finally block so the context is always closed, even when an assertion throws.

♻️ Proposed fix
         const context = await browser.newContext({ storageState: { cookies: [], origins: [] } });
-        const restrictedPage = await context.newPage();
-        await restrictedPage.goto('/login');
-        await restrictedPage.getByPlaceholder('Email').fill(emailAddress);
-        await restrictedPage.getByPlaceholder('Password').fill(password);
-        await restrictedPage.getByRole('button', { name: 'Sign in' }).click();
-        await expect(restrictedPage).not.toHaveURL(/\/login/, { timeout: 15_000 });
-
-        await restrictedPage.goto('/profile');
-        const firstNameField = restrictedPage.locator('[data-slot="field"]').filter({
-            has: restrictedPage.locator('[data-slot="field-label"]').getByText('First name', { exact: true }),
-        });
-        await firstNameField.getByRole('textbox').fill('Renamed');
-        await restrictedPage.getByRole('button', { name: 'Update' }).click();
-
-        await expect(
-            restrictedPage.locator('[data-sonner-toast]').filter({ hasText: 'Successfully updated profile' }),
-        ).toBeVisible({ timeout: 10_000 });
-
-        await restrictedPage.reload();
-        await expect(firstNameField.getByRole('textbox')).toHaveValue('Renamed');
-
-        await context.close();
+        try {
+            const restrictedPage = await context.newPage();
+            await restrictedPage.goto('/login');
+            await restrictedPage.getByPlaceholder('Email').fill(emailAddress);
+            await restrictedPage.getByPlaceholder('Password').fill(password);
+            await restrictedPage.getByRole('button', { name: 'Sign in' }).click();
+            await expect(restrictedPage).not.toHaveURL(/\/login/, { timeout: 15_000 });
+
+            await restrictedPage.goto('/profile');
+            const firstNameField = restrictedPage.locator('[data-slot="field"]').filter({
+                has: restrictedPage.locator('[data-slot="field-label"]').getByText('First name', { exact: true }),
+            });
+            await firstNameField.getByRole('textbox').fill('Renamed');
+            await restrictedPage.getByRole('button', { name: 'Update' }).click();
+
+            await expect(
+                restrictedPage.locator('[data-sonner-toast]').filter({ hasText: 'Successfully updated profile' }),
+            ).toBeVisible({ timeout: 10_000 });
+
+            await restrictedPage.reload();
+            await expect(firstNameField.getByRole('textbox')).toHaveValue('Renamed');
+        } finally {
+            await context.close();
+        }
🤖 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/dashboard/e2e/tests/settings/profile.spec.ts` around lines 129 -
152, Wrap the restrictedPage login, profile update, assertions, and reload flow
in a try/finally block, keeping context.close() in the finally clause. Preserve
the existing test interactions and assertions while ensuring the browser context
created by browser.newContext is closed even when any step fails.
🤖 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.

Nitpick comments:
In `@packages/dashboard/e2e/tests/settings/profile.spec.ts`:
- Around line 103-126: The test setup creates persistent administrator and role
records without cleanup. At the end of the test containing the
createAdministrator and createRole mutations, add client.gql calls for
deleteAdministrator and deleteRole using the created administrator and role IDs,
ensuring cleanup runs after the test assertions complete.
- Around line 129-152: Wrap the restrictedPage login, profile update,
assertions, and reload flow in a try/finally block, keeping context.close() in
the finally clause. Preserve the existing test interactions and assertions while
ensuring the browser context created by browser.newContext is closed even when
any step fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e48dae48-31ea-4173-a676-fb995eed8cda

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab495e and 7b8450d.

📒 Files selected for processing (3)
  • packages/dashboard/e2e/tests/settings/profile.spec.ts
  • packages/dashboard/src/app/routes/_authenticated/_profile/profile.graphql.ts
  • packages/dashboard/src/app/routes/_authenticated/_profile/profile.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/dashboard/src/app/routes/_authenticated/_profile/profile.graphql.ts
  • packages/dashboard/src/app/routes/_authenticated/_profile/profile.tsx

@Ryrahul Ryrahul changed the title fix(dashboard): use updateActiveAdministrator for profile page Fix(dashboard): use updateActiveAdministrator for profile page Aug 6, 2026
@Ryrahul Ryrahul changed the title Fix(dashboard): use updateActiveAdministrator for profile page fix(dashboard): Use updateActiveAdministrator for profile page Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T1: Fast track Clearly understood fix with limited blast radius. Fast lane.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard profile page saves via updateAdministrator, so admins without the UpdateAdministrator permission cannot edit their own profile

3 participants