Skip to content

Commit 68bb7c4

Browse files
authored
🐛 Fixed contributor profile editor navigation on close (TryGhost#25629)
ref https://linear.app/ghost/issue/BER-3039 When a contributor attempted to close their profile page they'd be re-directed back to it. This was caused by competing rules and side effects in the Settings component and the Ember router. This was fixed by lifting the redirect into the Router, which removed the conflict and allowed contributors to navigate away from their profile page.
1 parent 2e30e48 commit 68bb7c4

4 files changed

Lines changed: 29 additions & 14 deletions

File tree

apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,8 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
220220
if (canAccessSettings(currentUser)) {
221221
updateRoute('staff');
222222
} else {
223-
updateRoute({isExternal: true, route: 'analytics'});
223+
// Contributors can't access settings, exit to let the shell handle navigation
224+
updateRoute({isExternal: true, route: ''});
224225
}
225226
}, [currentUser, updateRoute]);
226227

apps/admin-x-settings/src/main-content.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const Page: React.FC<{children: ReactNode}> = ({children}) => {
2222

2323
const MainContent: React.FC = () => {
2424
const {currentUser} = useGlobalData();
25-
const {route, updateRoute, loadingModal} = useRouting();
25+
const {loadingModal} = useRouting();
2626
const {isDirty} = useGlobalDirtyState();
2727

2828
const navigateAway = (escLocation: string) => {
@@ -50,12 +50,8 @@ const MainContent: React.FC = () => {
5050
toast.remove();
5151
}, []);
5252

53-
useEffect(() => {
54-
if (!canAccessSettings(currentUser) && route !== `staff/${currentUser.slug}`) {
55-
updateRoute(`staff/${currentUser.slug}`);
56-
}
57-
}, [currentUser, route, updateRoute]);
58-
53+
// Contributors/Authors only see their profile modal (rendered via routing)
54+
// Don't render the main settings content for them
5955
if (!canAccessSettings(currentUser)) {
6056
return null;
6157
}

apps/admin-x-settings/test/acceptance/permissions.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,22 @@ test.describe('User permissions', async () => {
1616
await expect(page.getByTestId('title-and-description')).toBeHidden();
1717
});
1818

19+
// Note: Author/Contributor redirect to profile is handled by the Ember router (settings-x.js),
20+
// not by the React app. These tests verify the UI renders correctly when on the profile route.
1921
test('Authors can only see their own profile', async ({page}) => {
2022
await mockApi({page, requests: {
2123
...globalDataRequests,
2224
browseMe: {...globalDataRequests.browseMe, response: meWithRole('Author')}
2325
}});
2426

25-
await page.goto('/');
27+
// Navigate directly to profile route using hash-based routing
28+
// (Ember router handles redirect in production)
29+
await page.goto('/#/settings/staff/owner');
2630

2731
await expect(page.getByTestId('user-detail-modal')).toBeVisible();
2832
await expect(page.getByTestId('sidebar')).toBeHidden();
2933
await expect(page.getByTestId('users')).toBeHidden();
3034
await expect(page.getByTestId('title-and-description')).toBeHidden();
31-
32-
expect(page.url()).toMatch(/\/owner$/);
3335
});
3436

3537
test('Contributors can only see their own profile', async ({page}) => {
@@ -38,13 +40,13 @@ test.describe('User permissions', async () => {
3840
browseMe: {...globalDataRequests.browseMe, response: meWithRole('Contributor')}
3941
}});
4042

41-
await page.goto('/');
43+
// Navigate directly to profile route using hash-based routing
44+
// (Ember router handles redirect in production)
45+
await page.goto('/#/settings/staff/owner');
4246

4347
await expect(page.getByTestId('user-detail-modal')).toBeVisible();
4448
await expect(page.getByTestId('sidebar')).toBeHidden();
4549
await expect(page.getByTestId('users')).toBeHidden();
4650
await expect(page.getByTestId('title-and-description')).toBeHidden();
47-
48-
expect(page.url()).toMatch(/\/owner$/);
4951
});
5052
});

ghost/admin/app/routes/settings-x.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ export default class SettingsXRoute extends AuthenticatedRoute {
77
@service ui;
88
@service modals;
99

10+
beforeModel(transition) {
11+
super.beforeModel(...arguments);
12+
13+
// Contributors and Authors can only access their own profile in settings
14+
if (this.session.user.isAuthorOrContributor) {
15+
// Check if they're trying to access their own profile route
16+
const subPath = transition.to?.params?.sub;
17+
const ownProfilePath = `staff/${this.session.user.slug}`;
18+
19+
// Only allow access to their own profile, redirect everything else
20+
if (subPath !== ownProfilePath) {
21+
return this.transitionTo('settings-x.settings-x', ownProfilePath);
22+
}
23+
}
24+
}
25+
1026
activate() {
1127
super.activate(...arguments);
1228

0 commit comments

Comments
 (0)