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
6 changes: 6 additions & 0 deletions packages/manager/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).

## [2025-03-26] - v1.138.1

### Fixed:

- Authentication redirect issue ([#11925](https://github.com/linode/manager/pull/11925))

## [2025-03-25] - v1.138.0


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ describe('Parent/Child token expiration', () => {
.click();
});

cy.url().should('endWith', '/login');
cy.url().should('endWith', '/logout');
});
});
2 changes: 1 addition & 1 deletion packages/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "linode-manager",
"author": "Linode",
"description": "The Linode Manager website",
"version": "1.138.0",
"version": "1.138.1",
"private": true,
"type": "module",
"bugs": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('UserSSHKeyPanel', () => {
return HttpResponse.json(makeResourcePage([]));
}),
http.get('*/account/users', () => {
return HttpResponse.json(makeResourcePage([]));
return HttpResponse.json(makeResourcePage([]), { status: 401 });
})
);
const { queryByTestId } = renderWithTheme(
Expand All @@ -46,7 +46,7 @@ describe('UserSSHKeyPanel', () => {
return HttpResponse.json(makeResourcePage(sshKeys));
}),
http.get('*/account/users', () => {
return HttpResponse.json(makeResourcePage([]));
return HttpResponse.json(makeResourcePage([]), { status: 401 });
})
);
const { getByText } = renderWithTheme(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { NotFound } from 'src/components/NotFound';
import { PARENT_USER_SESSION_EXPIRED } from 'src/features/Account/constants';
import { useParentChildAuthentication } from 'src/features/Account/SwitchAccounts/useParentChildAuthentication';
import { setTokenInLocalStorage } from 'src/features/Account/SwitchAccounts/utils';
import { useCurrentToken } from 'src/hooks/useAuthentication';
import { sendSwitchToParentAccountEvent } from 'src/utilities/analytics/customEventAnalytics';
import { getAuthToken } from 'src/utilities/authentication';
import { getStorage, setStorage } from 'src/utilities/storage';

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

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

interface Props {
onClose: () => void;
Expand All @@ -22,7 +23,7 @@ interface Props {
}

interface HandleSwitchToChildAccountProps {
currentTokenWithBearer?: string;
currentTokenWithBearer?: AuthState['token'];
euuid: string;
event: React.MouseEvent<HTMLElement>;
onClose: (e: React.SyntheticEvent<HTMLElement>) => void;
Expand All @@ -38,9 +39,9 @@ export const SwitchAccountDrawer = (props: Props) => {
const [query, setQuery] = React.useState<string>('');

const isProxyUser = userType === 'proxy';
const currentParentTokenWithBearer: string =
const currentParentTokenWithBearer =
getStorage('authentication/parent_token/token') ?? '';
const currentTokenWithBearer = getAuthToken().token;
const currentTokenWithBearer = useCurrentToken() ?? '';

const {
createToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import {
isParentTokenValid,
updateCurrentTokenBasedOnUserType,
} from 'src/features/Account/SwitchAccounts/utils';
import { getAuthToken } from 'src/utilities/authentication';
import { useCurrentToken } from 'src/hooks/useAuthentication';
import { getStorage } from 'src/utilities/storage';

import type { Token, UserType } from '@linode/api-v4';

export const useParentChildAuthentication = () => {
const currentTokenWithBearer = getAuthToken().token;
const currentTokenWithBearer = useCurrentToken() ?? '';

const {
error: createTokenError,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getStorage, setStorage } from 'src/utilities/storage';

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

export interface ProxyTokenCreationParams {
/**
Expand All @@ -20,7 +21,7 @@ export interface ProxyTokenCreationParams {
export const updateParentTokenInLocalStorage = ({
currentTokenWithBearer,
}: {
currentTokenWithBearer?: string;
currentTokenWithBearer?: AuthState['token'];
}) => {
const parentToken: Token = {
created: getStorage('authentication/created'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as React from 'react';

import { profileFactory } from 'src/factories';
import { HttpResponse, http, server } from 'src/mocks/testServer';
import { clearAuthToken, setAuthToken } from 'src/utilities/authentication';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { TimezoneForm, getOptionLabel } from './TimezoneForm';
Expand All @@ -19,8 +18,6 @@ describe('Timezone change form', () => {
);
})
);

clearAuthToken();
});

it('should render input label', () => {
Expand All @@ -30,9 +27,9 @@ describe('Timezone change form', () => {
});

it('should show a message if an admin is logged in as a customer', async () => {
setAuthToken({ expiration: 'never', scopes: '*', token: 'admin 123' });

const { getByTestId } = renderWithTheme(<TimezoneForm />);
const { getByTestId } = renderWithTheme(<TimezoneForm />, {
customStore: { authentication: { loggedInAsCustomer: true } },
});

expect(getByTestId('admin-notice')).toBeInTheDocument();
});
Expand All @@ -44,9 +41,9 @@ describe('Timezone change form', () => {
});

it("should include text with the user's current time zone in the admin warning", async () => {
setAuthToken({ expiration: 'never', scopes: '*', token: 'admin 123' });

const { queryByTestId } = renderWithTheme(<TimezoneForm />);
const { queryByTestId } = renderWithTheme(<TimezoneForm />, {
customStore: { authentication: { loggedInAsCustomer: true } },
});

await waitFor(() => {
expect(queryByTestId('admin-notice')).toHaveTextContent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as React from 'react';
import { Controller, useForm } from 'react-hook-form';

import { timezones } from 'src/assets/timezones/timezones';
import { isLoggedInAsCustomer } from 'src/utilities/authentication';
import { useAuthentication } from 'src/hooks/useAuthentication';

import type { Profile } from '@linode/api-v4';

Expand Down Expand Up @@ -40,6 +40,7 @@ const timezoneOptions = getTimezoneOptions();
type Values = Pick<Profile, 'timezone'>;

export const TimezoneForm = () => {
const { loggedInAsCustomer } = useAuthentication();
const { enqueueSnackbar } = useSnackbar();
const { data: profile } = useProfile();
const { mutateAsync: updateProfile } = useMutateProfile();
Expand Down Expand Up @@ -67,7 +68,7 @@ export const TimezoneForm = () => {

return (
<form onSubmit={handleSubmit(onSubmit)}>
{isLoggedInAsCustomer() && (
{loggedInAsCustomer && (
<Notice dataTestId="admin-notice" variant="error">
While you are logged in as a customer, all times, dates, and graphs
will be displayed in the user&rsquo;s timezone ({profile?.timezone}).
Expand Down
6 changes: 4 additions & 2 deletions packages/manager/src/features/TopMenu/TopMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { AppBar } from 'src/components/AppBar';
import { Link } from 'src/components/Link';
import { StyledAkamaiLogo } from 'src/components/PrimaryNav/PrimaryNav.styles';
import { Toolbar } from 'src/components/Toolbar';
import { isLoggedInAsCustomer } from 'src/utilities/authentication';
import { useAuthentication } from 'src/hooks/useAuthentication';

import { Community } from './Community';
import { CreateMenu } from './CreateMenu/CreateMenu';
Expand Down Expand Up @@ -36,6 +36,8 @@ export interface TopMenuProps {
export const TopMenu = React.memo((props: TopMenuProps) => {
const { openSideMenu, username } = props;

const { loggedInAsCustomer } = useAuthentication();

const isNarrowViewport = useMediaQuery((theme: Theme) =>
theme.breakpoints.down(960)
);
Expand All @@ -45,7 +47,7 @@ export const TopMenu = React.memo((props: TopMenuProps) => {

return (
<>
{isLoggedInAsCustomer() && <InternalAdminBanner username={username} />}
{loggedInAsCustomer && <InternalAdminBanner username={username} />}
<AppBar data-qa-appbar>
<Toolbar variant="dense">
{isNarrowViewport && (
Expand Down
18 changes: 18 additions & 0 deletions packages/manager/src/hooks/useAuthentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react';
import { useSelector } from 'react-redux';

import { ApplicationState } from 'src/store';

export const useAuthentication = () => {
return useSelector((state: ApplicationState) => state.authentication);
};

// Returns the session token, using a ref to ensure the value is always fresh.
export const useCurrentToken = () => {
const { token } = useAuthentication();
const tokenRef = React.useRef(token);
React.useEffect(() => {
tokenRef.current = token;
});
return tokenRef.current;
};
11 changes: 7 additions & 4 deletions packages/manager/src/hooks/useInitialRequests.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useQueryClient } from '@tanstack/react-query';
import * as React from 'react';

import { useAuthentication } from 'src/hooks/useAuthentication';
import { usePendingUpload } from 'src/hooks/usePendingUpload';
import { accountQueries, profileQueries } from '@linode/queries';
import { redirectToLogin } from 'src/session';
import { isLoggedIn } from 'src/utilities/authentication';

/**
* This hook is responsible for making Cloud Manager's initial requests.
Expand All @@ -13,13 +13,16 @@ import { isLoggedIn } from 'src/utilities/authentication';
*/
export const useInitialRequests = () => {
const queryClient = useQueryClient();

const { token } = useAuthentication();
const isAuthenticated = Boolean(token);
const pendingUpload = usePendingUpload();

const [isLoading, setIsLoading] = React.useState(true);

React.useEffect(() => {
if (
!isLoggedIn() &&
!isAuthenticated &&
// Do not redirect to Login if there is a pending image upload.
!pendingUpload
) {
Expand All @@ -30,12 +33,12 @@ export const useInitialRequests = () => {
* this is the case where we've just come back from login and need
* to show the children onMount
*/
if (isLoggedIn()) {
if (isAuthenticated) {
makeInitialRequests();
}

// We only want this useEffect running when `isAuthenticated` changes.
}, []);
}, [isAuthenticated]);

/**
* We make a series of requests for data on app load. The flow is:
Expand Down
20 changes: 9 additions & 11 deletions packages/manager/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { CookieWarning } from 'src/components/CookieWarning';
import { Snackbar } from 'src/components/Snackbar/Snackbar';
import { SplashScreen } from 'src/components/SplashScreen';
import 'src/exceptionReporting';
import { Logout } from 'src/layouts/Logout';
import Logout from 'src/layouts/Logout';
import { setupInterceptors } from 'src/request';
import { storeFactory } from 'src/store';

Expand All @@ -33,16 +33,10 @@ const CancelLanding = React.lazy(() =>
}))
);

const LoginAsCustomerCallback = React.lazy(() =>
import('src/layouts/LoginAsCustomerCallback').then((module) => ({
default: module.LoginAsCustomerCallback,
}))
);
const OAuthCallback = React.lazy(() =>
import('src/layouts/OAuthCallback').then((module) => ({
default: module.OAuthCallback,
}))
const LoginAsCustomerCallback = React.lazy(
() => import('src/layouts/LoginAsCustomerCallback')
);
const OAuthCallbackPage = React.lazy(() => import('src/layouts/OAuth'));

const Main = () => {
if (!navigator.cookieEnabled) {
Expand All @@ -57,7 +51,11 @@ const Main = () => {
<React.Suspense fallback={<SplashScreen />}>
<Router>
<Switch>
<Route component={OAuthCallback} exact path="/oauth/callback" />
<Route
component={OAuthCallbackPage}
exact
path="/oauth/callback"
/>
<Route
component={LoginAsCustomerCallback}
exact
Expand Down
Loading
Loading