Skip to content

Commit 7b8450d

Browse files
committed
fix(dashboard): use updateActiveAdministrator for profile page
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 #5037
1 parent db8482a commit 7b8450d

3 files changed

Lines changed: 68 additions & 6 deletions

File tree

packages/dashboard/e2e/tests/settings/profile.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { expect, test } from '@playwright/test';
22

3+
import { VendureAdminClient } from '../../utils/vendure-admin-client.js';
4+
35
test.describe('Profile', () => {
46
test('should display profile page with form fields', async ({ page }) => {
57
await page.goto('/profile');
@@ -87,4 +89,65 @@ test.describe('Profile', () => {
8789
// Reset to original value (don't submit)
8890
await firstNameInput.fill(originalValue);
8991
});
92+
93+
// #5037 — an admin without UpdateAdministrator permission must still be able to
94+
// edit their own profile (the page uses updateActiveAdministrator, gated by Owner)
95+
test('should allow an admin without UpdateAdministrator permission to update own profile', async ({
96+
page,
97+
browser,
98+
}) => {
99+
const suffix = Date.now();
100+
const emailAddress = `restricted-${suffix}@test.com`;
101+
const password = 'test-password';
102+
103+
const client = new VendureAdminClient(page);
104+
await client.login();
105+
const { createRole } = await client.gql(
106+
`mutation ($input: CreateRoleInput!) { createRole(input: $input) { id } }`,
107+
{
108+
input: {
109+
code: `restricted-${suffix}`,
110+
description: 'No admin permissions',
111+
permissions: ['ReadCatalog'],
112+
},
113+
},
114+
);
115+
await client.gql(
116+
`mutation ($input: CreateAdministratorInput!) { createAdministrator(input: $input) { id } }`,
117+
{
118+
input: {
119+
firstName: 'Restricted',
120+
lastName: 'Admin',
121+
emailAddress,
122+
password,
123+
roleIds: [createRole.id],
124+
},
125+
},
126+
);
127+
128+
// Fresh context so we browse as the restricted admin, not the superadmin
129+
const context = await browser.newContext({ storageState: { cookies: [], origins: [] } });
130+
const restrictedPage = await context.newPage();
131+
await restrictedPage.goto('/login');
132+
await restrictedPage.getByPlaceholder('Email').fill(emailAddress);
133+
await restrictedPage.getByPlaceholder('Password').fill(password);
134+
await restrictedPage.getByRole('button', { name: 'Sign in' }).click();
135+
await expect(restrictedPage).not.toHaveURL(/\/login/, { timeout: 15_000 });
136+
137+
await restrictedPage.goto('/profile');
138+
const firstNameField = restrictedPage.locator('[data-slot="field"]').filter({
139+
has: restrictedPage.locator('[data-slot="field-label"]').getByText('First name', { exact: true }),
140+
});
141+
await firstNameField.getByRole('textbox').fill('Renamed');
142+
await restrictedPage.getByRole('button', { name: 'Update' }).click();
143+
144+
await expect(
145+
restrictedPage.locator('[data-sonner-toast]').filter({ hasText: 'Successfully updated profile' }),
146+
).toBeVisible({ timeout: 10_000 });
147+
148+
await restrictedPage.reload();
149+
await expect(firstNameField.getByRole('textbox')).toHaveValue('Renamed');
150+
151+
await context.close();
152+
});
90153
});

packages/dashboard/src/app/routes/_authenticated/_profile/profile.graphql.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export const activeAdministratorDocument = graphql(`
2121
}
2222
`);
2323

24-
export const updateAdministratorDocument = graphql(`
25-
mutation UpdateAdministrator($input: UpdateAdministratorInput!) {
26-
updateAdministrator(input: $input) {
24+
export const updateActiveAdministratorDocument = graphql(`
25+
mutation UpdateActiveAdministrator($input: UpdateActiveAdministratorInput!) {
26+
updateActiveAdministrator(input: $input) {
2727
id
2828
}
2929
}

packages/dashboard/src/app/routes/_authenticated/_profile/profile.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
2020
import { Trans, useLingui } from '@lingui/react/macro';
2121
import { createFileRoute } from '@tanstack/react-router';
2222
import { toast } from 'sonner';
23-
import { activeAdministratorDocument, updateAdministratorDocument } from './profile.graphql.js';
23+
import { activeAdministratorDocument, updateActiveAdministratorDocument } from './profile.graphql.js';
2424

2525
const pageId = 'profile';
2626

@@ -49,11 +49,10 @@ function ProfilePage() {
4949
const { form, submitHandler, isPending, entity } = useDetailPage({
5050
queryDocument: activeAdministratorDocument,
5151
entityField: 'activeAdministrator',
52-
updateDocument: updateAdministratorDocument,
52+
updateDocument: updateActiveAdministratorDocument,
5353
pageId,
5454
setValuesForUpdate: entity => {
5555
return {
56-
id: entity.id,
5756
firstName: entity.firstName,
5857
lastName: entity.lastName,
5958
emailAddress: entity.emailAddress,

0 commit comments

Comments
 (0)