Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Fix Account Switching ([#10234](https://github.com/linode/manager/pull/10234))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Fix to ensure ChildAccountList receives proper account token ([#10234](https://github.com/linode/manager/pull/10234))
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const assertAuthLocalStorage = (
expiry: string,
scopes: string
) => {
assertLocalStorageValue('authentication/token', token);
assertLocalStorageValue('authentication/token', `Bearer ${token}`);
assertLocalStorageValue('authentication/expire', expiry);
assertLocalStorageValue('authentication/scopes', scopes);
};
Expand Down Expand Up @@ -126,6 +126,23 @@ describe('Parent/Child account switching', () => {
.should('be.enabled')
.click();

// Prepare up mocks in advance of the account switch. As soon as the child account is clicked,
// Cloud will replace its stored token with the token provided by the API and then reload.
// From that point forward, we will not have a valid test account token stored in local storage,
// so all non-intercepted API requests will respond with a 401 status code and we will get booted to login.
// We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the
// individual requests as needed.
mockAllApiRequests();
mockGetLinodes([]);
mockGetRegions([]);
mockGetEvents([]);
mockGetNotifications([]);
mockGetAccount(mockChildAccount);
mockGetProfile(mockParentProfile);
mockGetUser(mockParentUser);

// Mock the account switch itself -- we have to do this after the mocks above
// to ensure that it is applied.
mockCreateChildAccountToken(mockChildAccount, mockChildAccountToken).as(
'switchAccount'
);
Expand All @@ -147,24 +164,6 @@ describe('Parent/Child account switching', () => {
mockChildAccountToken.scopes
);

// From this point forward, we will not have a valid test account token stored in local storage,
// so all non-intercepted API requests will respond with a 401 status code and we will get booted to login.
// We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the
// individual requests as needed.
mockAllApiRequests();
mockGetLinodes([]);
mockGetRegions([]);
mockGetEvents([]);
mockGetNotifications([]);

mockGetAccount(mockChildAccount);
mockGetProfile(mockParentProfile);
mockGetUser(mockParentUser);

// TODO Remove the call to `cy.reload()` once Cloud Manager automatically updates itself upon account switching.
// TODO Add assertions for toast upon account switch. This might involve improving mocks for events/notifications.
cy.reload();

// Confirm expected username and company are shown in user menu button.
assertUserMenuButton(
mockParentProfile.username,
Expand Down Expand Up @@ -205,6 +204,21 @@ describe('Parent/Child account switching', () => {
.click();
});

// Prepare up mocks in advance of the account switch. As soon as the child account is clicked,
// Cloud will replace its stored token with the token provided by the API and then reload.
// From that point forward, we will not have a valid test account token stored in local storage,
// so all non-intercepted API requests will respond with a 401 status code and we will get booted to login.
// We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the
// individual requests as needed.
mockAllApiRequests();
mockGetLinodes([]);
mockGetRegions([]);
mockGetEvents([]);
mockGetNotifications([]);
mockGetAccount(mockChildAccount);
mockGetProfile(mockParentProfile);
mockGetUser(mockParentUser);

// Click mock company name in "Switch Account" drawer.
mockCreateChildAccountToken(mockChildAccount, mockChildAccountToken).as(
'switchAccount'
Expand All @@ -227,23 +241,6 @@ describe('Parent/Child account switching', () => {
mockChildAccountToken.scopes
);

// From this point forward, we will not have a valid test account token stored in local storage,
// so all non-intercepted API requests will respond with a 401 status code and we will get booted to login.
// We'll mitigate this by broadly mocking ALL API-v4 requests, then applying more specific mocks to the
// individual requests as needed.
mockAllApiRequests();
mockGetLinodes([]);
mockGetRegions([]);
mockGetEvents([]);
mockGetNotifications([]);
mockGetAccount(mockChildAccount);
mockGetProfile(mockParentProfile);
mockGetUser(mockParentUser);

// TODO Remove the call to `cy.reload()` once Cloud Manager automatically updates itself upon account switching.
// TODO Add assertions for toast upon account switch. This might involve improving mocks for events/notifications.
cy.reload();

// Confirm expected username and company are shown in user menu button.
assertUserMenuButton(
mockParentProfile.username,
Expand Down
45 changes: 18 additions & 27 deletions packages/manager/src/features/Account/SwitchAccountDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createChildAccountPersonalAccessToken } from '@linode/api-v4';
import React from 'react';
import { useHistory } from 'react-router-dom';

import { StyledLinkButton } from 'src/components/Button/StyledLinkButton';
import { Drawer } from 'src/components/Drawer';
Expand All @@ -18,6 +17,7 @@ import { getStorage } from 'src/utilities/storage';

import { ChildAccountList } from './SwitchAccounts/ChildAccountList';

import type { Token } from '@linode/api-v4';
import type { APIError, ChildAccountPayload, UserType } from '@linode/api-v4';
import type { State as AuthState } from 'src/store/authentication';

Expand All @@ -38,7 +38,8 @@ export const SwitchAccountDrawer = (props: Props) => {
);

const currentTokenWithBearer = useCurrentToken() ?? '';
const history = useHistory();
const currentParentTokenWithBearer =
getStorage('authentication/parent_token/token') ?? '';

const handleClose = React.useCallback(() => {
onClose();
Expand Down Expand Up @@ -76,11 +77,9 @@ export const SwitchAccountDrawer = (props: Props) => {
[]
);

// Navigate to the current location, triggering a re-render without a full page reload.
const refreshPage = React.useCallback(() => {
// TODO: Parent/Child: We need to test this against the real API.
history.push(history.location.pathname);
}, [history]);
location.reload();
}, []);

const handleSwitchToChildAccount = React.useCallback(
async ({
Expand All @@ -97,17 +96,13 @@ export const SwitchAccountDrawer = (props: Props) => {
isProxyUser: boolean;
}) => {
try {
// TODO: Parent/Child: FOR MSW ONLY, REMOVE WHEN API IS READY
// ================================================================
// throw new Error(
// `Account switching failed. Try again.`
// );
// ================================================================

// We don't need to worry about this if we're a proxy user.
if (!isProxyUser) {
const parentToken = {
const parentToken: Token = {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

More type safety

created: getStorage('authentication/created'),
expiry: getStorage('authentication/expire'),
id: getStorage('authentication/id'),
label: getStorage('authentication/label'),
scopes: getStorage('authentication/scopes'),
token: currentTokenWithBearer ?? '',
};
Expand All @@ -128,7 +123,10 @@ export const SwitchAccountDrawer = (props: Props) => {

setTokenInLocalStorage({
prefix: 'authentication/proxy_token',
token: proxyToken,
token: {
...proxyToken,
token: `Bearer ${proxyToken.token}`,
},
});

updateCurrentTokenBasedOnUserType({
Expand All @@ -139,16 +137,6 @@ export const SwitchAccountDrawer = (props: Props) => {
refreshPage();
} catch (error) {
setIsProxyTokenError(error as APIError[]);

// TODO: Parent/Child: FOR MSW ONLY, REMOVE WHEN API IS READY
// ================================================================
// setIsProxyTokenError([
// {
// field: 'token',
// reason: error.message,
// },
// ]);
// ================================================================
}
},
[getProxyToken, refreshPage]
Expand All @@ -168,7 +156,8 @@ export const SwitchAccountDrawer = (props: Props) => {

updateCurrentTokenBasedOnUserType({ userType: 'parent' });
handleClose();
}, [handleClose, isProxyUser]);
refreshPage();
}, [handleClose, refreshPage]);

return (
<Drawer onClose={handleClose} open={open} title="Switch Account">
Expand Down Expand Up @@ -201,7 +190,9 @@ export const SwitchAccountDrawer = (props: Props) => {
.
</Typography>
<ChildAccountList
currentTokenWithBearer={currentTokenWithBearer}
currentTokenWithBearer={
isProxyUser ? currentParentTokenWithBearer : currentTokenWithBearer
}
isProxyUser={isProxyUser}
onClose={handleClose}
onSwitchAccount={handleSwitchToChildAccount}
Expand Down