Searching for some examples on how to integrate keycloak with react admin application for authentication purposes.
App.js example:
// your other imports
import { ReactKeycloakProvider } from "#react-keycloak/web";
import Keycloak from "keycloak-js";
import Cookies from "js-cookie";
// we used cookies that backend is writing, because we had front as a production static build
const initOptions = {
url: Cookies.get("REACT_APP_KEYCLOAK_URL"),
realm: Cookies.get("REACT_APP_KEYCLOAK_REALM"),
clientId: Cookies.get("REACT_APP_KEYCLOAK_CLIENT_ID"),
onLoad: "login-required",
};
const keycloak = Keycloak(initOptions);
const onToken = () => {
if (keycloak.token && keycloak.refreshToken) {
localStorage.setItem("token", keycloak.token);
localStorage.setItem("refresh-token", keycloak.refreshToken);
}
};
const onTokenExpired = () => {
keycloak
.updateToken(30)
.then(() => {
console.log("successfully get a new token", keycloak.token);
})
.catch(() => {
console.error("failed to refresh token");
});
};
// for data provider, it writes token to an authorization header
const fetchJson = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: "application/json" });
}
if (keycloak.token) {
options.headers.set("Authorization", "Bearer " + keycloak.token);
} else if (localStorage.getItem("token")) {
options.headers.set(
"Authorization",
"Bearer " + localStorage.getItem("token")
);
}
return fetchUtils.fetchJson(url, options);
};
const customDataProvider = dataProvider("/api/v1", fetchJson);
const theme = createMuiTheme({
...defaultTheme,
sidebar: {
width: 110,
closedWidth: 40,
},
});
const fetchResources = (permissions) => {
let knownResources = [];
if (permissions) {
const resource = (
<Resource
name="feeds"
list={FeedList}
create={FeedCreate}
edit={StateEdit}
icon={CollectionsBookmark}
/>
);
knownResources.push(resource);
} else {
const resource = (
<Resource name="feeds" list={FeedList} icon={CollectionsBookmark} />
);
knownResources.push(resource);
}
return knownResources;
};
const CustomAdminWithKeycloak = () => {
const customAuthProvider = useAuthProvider(
Cookies.get("REACT_APP_KEYCLOAK_CLIENT_ID")
);
return (
<Admin
theme={theme}
dataProvider={customDataProvider}
authProvider={customAuthProvider}
loginPage={false}
title="Inventory Splitter"
layout={CustomLayout}
>
{fetchResources}
</Admin>
);
};
const CustomAdmin = () => {
return (
<Admin
theme={theme}
dataProvider={customDataProvider}
loginPage={false}
title="Inventory Splitter"
layout={CustomLayout}
>
<Resource
name="feeds"
list={FeedList}
create={FeedCreate}
edit={StateEdit}
icon={CollectionsBookmark}
/>
</Admin>
);
};
// we have a feature to completely switch off the authorization process through env variable on backend
const App = () => {
const useKeycloak = Cookies.get("USE_KEYCLOAK") === "true";
return useKeycloak ? (
<ReactKeycloakProvider
authClient={keycloak}
LoadingComponent={<div></div>}
initOptions={initOptions}
onTokens={onToken}
onTokenExpired={onTokenExpired}
>
<React.Fragment>
<CustomAdminWithKeycloak />
<ThemeProvider theme={theme}>
<Footer />
</ThemeProvider>
</React.Fragment>
</ReactKeycloakProvider>
) : (
<React.Fragment>
<CustomAdmin />
<ThemeProvider theme={theme}>
<Footer />
</ThemeProvider>
</React.Fragment>
);
};
export default App;
authProvider.js
import { useKeycloak } from '#react-keycloak/web'
import jwt_decode from 'jwt-decode'
const useAuthProvider = (clientID:string) => {
const { keycloak } = useKeycloak();
return ({
login: () => keycloak.login(),
checkError: () => Promise.resolve(),
checkAuth: () => {
return keycloak.authenticated &&
keycloak.token ? Promise.resolve() : Promise.reject("Failed to obtain access token.");
},
logout: () => keycloak.logout(),
getIdentity: () => {
if (keycloak.token) {
const decoded : any = jwt_decode(keycloak.token);
const id = decoded.sub
const fullName = decoded.name
return Promise.resolve({id, fullName});
}
return Promise.reject("Failed to get identity");
},
getPermissions:() => {
let hasRole = false;
if (keycloak.token) {
const decoded : any = jwt_decode(keycloak.token);
decoded.resource_access[clientID].roles.forEach((el: string) => {
if (el === "admin") {
hasRole = true;
return
}
});
}
if (hasRole) {
return Promise.resolve(true);
}
return Promise.resolve(false);
},
});
};
export default useAuthProvider;
if you want to hide some components depending on permissions, use smth like this:
import * as React from 'react';
import {
useListContext,
usePermissions,
TopToolbar,
CreateButton,
} from 'react-admin';
import PublishIcon from '#material-ui/icons/Publish';
const CustomListActions = (props) => {
const permissions = usePermissions();
const { basePath } = useListContext();
return (
<TopToolbar>
{permissions.permissions && <CreateButton label='Upload' basePath={basePath} icon={<PublishIcon/>}/>}
</TopToolbar>
);
};
export default CustomListActions;
Related
I'm learning React as I need to write an AWS app using Cognito. This series of videos is very helpful (https://www.youtube.com/watch?v=R-3uXlTudSQ&list=PLDckhLrNepPR8y-9mDXsLutiwsLhreOk1&index=3&t=300s) but it doesn't explain how you redirect your app after you've logged in.
my App.js is this:
export default () => {
return (
<Account>
<Status />
<Signup />
<Login />
<ForgotPassword />
<Settings />
</Account>
);
};
The Settings component will only appear for an authenticated user. However, once you've logged in it doesn't appear until you refresh the page. How do I get it to show the settings page without having to refresh the page?
The settings component is:
export default () => {
return (
<Account>
<Status />
<Signup />
<Login />
<ForgotPassword />
<Settings />
<SearchParms/>
</Account>
);
};
And the Accounts component is this:
import React, { createContext } from "react";
import { CognitoUser, AuthenticationDetails } from "amazon-cognito-identity-js";
import Pool from "../UserPool";
const AccountContext = createContext();
const Account = props => {
const getSession = async () =>
await new Promise((resolve, reject) => {
const user = Pool.getCurrentUser();
if (user) {
user.getSession(async (err, session) => {
if (err) {
reject();
} else {
const attributes = await new Promise((resolve, reject) => {
user.getUserAttributes((err, attributes) => {
if (err) {
reject(err);
} else {
const results = {};
for (let attribute of attributes) {
const { Name, Value } = attribute;
results[Name] = Value;
}
resolve(results);
}
});
});
resolve({
user,
...session,
...attributes
});
}
});
} else {
reject();
}
});
const authenticate = async (Username, Password) =>
await new Promise((resolve, reject) => {
Username = "nick.wright#maintel.co.uk";
Password = "C411m3di4**&";
const user = new CognitoUser({ Username, Pool });
//const authDetails = new AuthenticationDetails({ Username, Password });
const authDetails = new AuthenticationDetails({ Username, Password });
user.authenticateUser(authDetails, {
onSuccess: data => {
console.log("onSuccess:", data);
resolve(data);
},
onFailure: err => {
console.error("onFailure:", err);
reject(err);
},
newPasswordRequired: data => {
console.log("newPasswordRequired:", data);
resolve(data);
}
});
});
const logout = () => {
const user = Pool.getCurrentUser();
if (user) {
user.signOut();
}
};
return (
<AccountContext.Provider
value={{
authenticate,
getSession,
logout
}}
>
{props.children}
</AccountContext.Provider>
);
};
export { Account, AccountContext };
In Settings I have
import React, { useState, useEffect, useContext } from "react";
import { AccountContext } from "./Accounts";
import ChangePassword from "./ChangePassword";
import ChangeEmail from "./ChangeEmail";
// eslint-disable-next-line import/no-anonymous-default-export
export default () => {
const [loggedIn, setLoggedIn] = useState(false);
const { getSession } = useContext(AccountContext);
useEffect(() => {
getSession().then(() => {
setLoggedIn(true);
}).catch((err) => console.log("Catch", err) )
}, [getSession]);;
return (
<div>
{loggedIn && (
<>
<h1>Settings</h1>
<ChangePassword />
<ChangeEmail />
</>
)}
</div>
);
};
and at this line:
const { getSession } = useContext(AccountContext);
I'm getting an "AccountContext is not defined" error.
I haven't been able to find any online examples that solve this issue. Is there a way of dynamically showing/hiding each element when the login button is clicked.
In this case, there's no need to define getSession, authenticate and logout on the context. You can put those functions inside a auth folder and call them whenever you need wihout having to define them in the context. What you need to define in the context is whether the user is logged in or not, because that's the information that you want to share in your whole application. Regarding the AccountContext is not defined, I don't see any issues from what you have shared. https://codesandbox.io/s/adoring-sun-kz30yr?file=/src/Settings.js
In my Main.tsx:
import React, { FC, useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { useAppDispatch, useAppSelector } from '../../hook'
import { getProducts } from '../../store/ProductsSlice'
import Filter from '../Filter/Filter'
import Pagination from '../Pagination/Pagination'
import Products from '../Products/Products'
import { ErrorMessage, FilterError } from './styled'
const Main: FC = () => {
const products = useAppSelector((state) => state.products.list)
const dispatch = useAppDispatch()
const [errorId, setErrorId] = useState<string>('')
const [errorMessage, setErrorMessage] = useState<string>('')
const [page, setPage] = useState<number>(1)
const [filterId, setFilterId] = useState<number>()
const [pageParams, setPageParams] = useSearchParams()
pageParams.get(`page`) || ''
pageParams.get(`id`) || ''
useEffect(() => {
async function fetchProducts(id?: number, productsPage = 1) {
const itemsPerPage = 5
let url: string
if (id) {
url = `https://reqres.in/api/products/${id}`
} else {
url = `https://reqres.in/api/pr231oducts?per_page=${itemsPerPage}&page=${productsPage}`
}
const requestOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
fetch(url, requestOptions)
.then(async (response) => {
const data = await response.json()
if (response.ok) {
setErrorId('')
setErrorMessage('')
if (id) {
dispatch(
getProducts({
page: 1,
per_page: 1,
total: 1,
total_pages: 1,
data: [data.data],
})
)
setPageParams({ page: `1`, id: `${id}` })
} else {
dispatch(getProducts(data))
setPageParams({ page: `${productsPage}` })
}
} else {
const error = (data && data.message) || response.status
return Promise.reject(error)
}
setErrorMessage(data.id)
})
.catch((error) => {
setErrorId(error.toString())
console.error('There was an error!', error)
})
}
fetchProducts(filterId, page)
}, [filterId, page])
return (
<div>
{!products ? (
<>
{errorId ? <ErrorMessage>{errorId}</ErrorMessage> : null}
{errorMessage ? (
<ErrorMessage>
Something went wrong
{errorMessage}
</ErrorMessage>
) : null}
</>
) : (
<>
<Filter setFilterId={setFilterId} />
{errorId ? (
<FilterError>
{errorId}:
{errorId === '404'
? ' Product not found'
: `${errorId}: ${errorMessage}`}
</FilterError>
) : (
<Products />
)}
<Pagination setPage={setPage} />
</>
)}
</div>
)
}
export default Main
Filter.tsx:
import React, { FC } from 'react'
import { FilterContainer, FilterInput } from './styled'
const Filter: FC<{
setFilterId: React.Dispatch<React.SetStateAction<number | undefined>>
}> = ({ setFilterId }) => {
return (
<FilterContainer>
<FilterInput
onChange={(e) => {
if (e.target.value === '0') {
e.target.value = ''
}
setFilterId(Number(e.target.value))
}}
placeholder="Search by id"
type="number"
/>
</FilterContainer>
)
}
export default Filter
Pagination.tsx:
import { FC } from 'react'
import { useAppSelector } from '../../hook'
import ArrowBackIosIcon from '#mui/icons-material/ArrowBackIos'
import ArrowForwardIosIcon from '#mui/icons-material/ArrowForwardIos'
import { PaginationBtn, PaginationContainer } from './styled'
const Pagination: FC<{
setPage: React.Dispatch<React.SetStateAction<number>>
}> = ({ setPage }) => {
let pageNumber = useAppSelector((state) => state.products.list.page)
const totalPages = useAppSelector((state) => state.products.list.total_pages)
return (
<PaginationContainer>
<PaginationBtn
onClick={() => {
setPage((pageNumber -= 1))
}}
disabled={pageNumber <= 1}
>
<ArrowBackIosIcon fontSize="large" />
</PaginationBtn>
<PaginationBtn
onClick={() => {
setPage((pageNumber += 1))
}}
disabled={pageNumber >= totalPages}
>
<ArrowForwardIosIcon fontSize="large" />
</PaginationBtn>
</PaginationContainer>
)
}
export default Pagination
The fetchProducts function makes a request to the API, using the productPage and id variables passed to the function, the corresponding request is sent and the necessary information is displayed on the screen.
I'm going to take the page and id from the link and pass them to the fetchProducts function so that if something happens, the site opens immediately with the necessary information.
I have useSearchParams() with which I make a link that can be "sent to other users". But I don’t understand how to implement that when parameters are inserted into the link, they are applied and the page with the necessary data is loaded.
Now the correct link is generated, but if you copy it and paste it in another browser window, the standard "list of products" will be loaded
I have already an exemple for make you understand How to pass parameters from a URL link to a request:
App.js
function App() {
return (
<>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/add-book" element={<AddBook />} />
<Route path="/upd-book/:id" element={<UpdBook />} />
</Routes>
</BrowserRouter>
</>
)
}
export default App;
Home.js
<Link to={`/upd-book/${id}`} >Update</Link>
UpdatePage.js exemple url after you click to Link: localhost:3000/upd-book/30
import {useParams} from 'react-router-dom';
const {id} = useParams();
{id} is 30
I hope this exemple explain how this is work.
Issue
The Main component has competing "sources of truth", the queryString params and local state.
Solution
Use the queryString params as the source of truth for the API requests. Access the "page" and "id" query params in the component and pass as useEffect hook dependencies and on to the fetchProducts handler. Instead of enqueuing state updates enqueue navigation redirects that only update the URL queryString.
const Main: FC = () => {
const dispatch = useAppDispatch();
const products = useAppSelector((state) => state.products.list);
const [searchParams, setSearchParams] = useSearchParams();
// Read the queryString parameters, convert to number type
const page = Number(searchParams.get("page") || 1);
const filterId = Number(searchParams.get("id"));
const [errorId, setErrorId] = useState<string>('');
const [errorMessage, setErrorMessage] = useState<string>('');
useEffect(() => {
async function fetchProducts(id?: number, page: number = 1) {
const itemsPerPage = 5;
const url = id
? `https://reqres.in/api/products/${id}`
: `https://reqres.in/api/pr231oducts?per_page=${itemsPerPage}&page=${page}`;
const requestOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
try {
const response = await fetch(url, requestOptions);
const data = await response.json();
if (response.ok) {
setErrorId('');
setErrorMessage('');
if (id) {
dispatch(
getProducts({
page: 1,
per_page: 1,
total: 1,
total_pages: 1,
data: [data.data],
})
);
setSearchParams({ page, id }, { replace: true });
} else {
dispatch(getProducts(data));
setPageParams({ page }, { replace: true });
}
} else {
const error = data?.message || response.status;
return Promise.reject(error);
}
setErrorMessage(data.id);
} catch(error) {
setErrorId(error.toString());
console.error('There was an error!', error);
}
};
fetchProducts(filterId, page);
}, [filterId, page]);
// Callbacks to update the queryString parameters
const setPage = page => setSearchParams(params => {
params.set("page", page);
return params;
}, { replace: true });
const setFilterId = id => setSearchParams(params => {
params.set("id", id);
return params;
}, { replace: true });
return (
<div>
{!products ? (
<>
{errorId ? <ErrorMessage>{errorId}</ErrorMessage> : null}
{errorMessage && (
<ErrorMessage>
Something went wrong
{errorMessage}
</ErrorMessage>
)}
</>
) : (
<>
<Filter setFilterId={setFilterId} />
{errorId ? (
<FilterError>
{errorId}:
{errorId === '404'
? "Product not found"
: `${errorId}: ${errorMessage}`
}
</FilterError>
) : (
<Products />
)}
<Pagination setPage={setPage} />
</>
)}
</div>
);
};
Problem
I am looking for a way to mock my keycloak service called UserService. Actually I just need to mock the isLoggedIn and the login functions. I do not think this is a difficult task, but after some hours of trying I am still not able to get a working solution for this problem.
Code Samples
UserService.js
import Keycloak from "keycloak-js";
const _kc = new Keycloak({
url: "http://localhost:80",
realm: "realmName",
clientId: "clientName"
});
const initKeycloak = (onAuthenticatedCallback, onNotAuthenticatedCallback) => {
_kc.init(
{
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
pkceMethod: 'S256',
checkLoginIframe: false
}
)
.then((authenticated) => {
if (!authenticated) {
console.error("user is not authenticated..!");
}
onAuthenticatedCallback();
})
.catch((error) => {
onNotAuthenticatedCallback();
})
};
const doLogin = _kc.login;
const doLogout = _kc.logout;
const getToken = () => {
return _kc.token;
}
const isLoggedIn = () => {
if (_kc.token) {
return true;
} else {
return false;
}
}
const updateToken = (successCallback) => {
_kc.updateToken(5)
.then(successCallback)
.catch(doLogin);
}
const getUsername = () => _kc.tokenParsed?.preferred_username;
const hasRole = (roles) => {
return roles.some((role) => _kc.hasRealmRole(role));
}
const UserService = {
initKeycloak,
doLogin,
doLogout,
isLoggedIn,
getToken,
updateToken,
getUsername,
hasRole,
};
export default UserService;
I already tried to use the jest.mock() function. So now I can push the loginButton without an error, but the addButton still does not activate.
import { render, screen } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import { BrowserRouter } from "react-router-dom";
import OrganizationsPage from "./OrganizationsPage";
import { act } from "react-test-renderer";
jest.mock("../Services/UserService", () => ({
...jest.requireActual("../Services/UserService"),
doLogin: jest.fn(() => { return true }),
isLoggedIn: jest.fn(() => { return true }),
}));
const MockedOrganizationsPage = () => {
return (
<BrowserRouter>
<OrganizationsPage />
</BrowserRouter>
);
}
describe('OrganizationsPage Alter', () => {
it('should activate alter buttons on login', async () => {
// console.log(UserService.isLoggedIn())
await act(async () => render(<MockedOrganizationsPage />))
const loginButton = screen.getByText("Login");
userEvent.click(loginButton);
const addButton = screen.getByText("Add Organization");
userEvent.click(addButton);
})
})
So I found the solution:
I do not have to mock the UserService.js file but more the keycloak-js-module. For this I create a folder called __mocks__ and put in my mocked module. Jest will automatically know to call keycloak-js from the mocks folder instead of node-modules.
src/mocks/keycloak-js.js:
class Keycloak {
token = "token available";
url = null;
realm = null;
clientId = null;
tokenParsed = {
preferred_username: "dummy user",
}
constructor(keycloakConfig) {
this.url = keycloakConfig.url;
this.realm = keycloakConfig.realm;
this.clientId = keycloakConfig.clientId;
}
login = () => {
this.token = "Logged in";
}
logout = () => {
this.token = null;
}
updateToken = () => {
this.token = "Logged in";
return new Promise((resolve, reject) => {
if (!!this.token) {
resolve();
} else {
reject();
}
})
}
init = (startupConfig) => {
}
hasRealmRole = (role) => {
return true;
}
}
export default Keycloak;
We've implemented Google AdSense on our Next.JS website, but we'd like to have ads reload whenever the route changes.
Here is what I tried:
const AdBlock = props => {
const { userLoaded, currentUserIsSilverPlus } = useAuth()
useEffect(() => {
window.adsbygoogle = window.adsbygoogle || []
window.adsbygoogle.push({})
}, [])
const reloadAds = () => {
const { googletag } = window
if (googletag) {
googletag.cmd.push(function () {
googletag.pubads().refresh()
})
}
}
Router.events.on("routeChangeStart", reloadAds)
if (userLoaded && !currentUserIsSilverPlus) {
return (
<ins
className="adsbygoogle adbanner-customize"
style={{ display: "block" }}
data-ad-client="ca-pub-1702181491157560"
data-ad-slot={props.slot}
data-ad-format={props.format}
data-full-width-responsive={props.responsive}
/>
)
}
return ""
}
export default AdBlock
The ads load, however, they never appear to refresh. What am I missing?
My solution was to observe router event changes, when routeChangeStart ads had to be unmounted, when routeChangeComplete they had to be mounted remounted.
// components/Adsense.tsx
function Ads() {
const adsRef = useRef<HTMLModElement | null>(null);
useEffect(() => {
const executeWindowAds = () => {
try {
// #ts-ignore
(adsbygoogle = window.adsbygoogle || []).push({});
} catch (error: any) {
console.error('error ads======>', error.message);
}
};
const insHasChildren = adsRef.current?.childNodes.length;
if (!insHasChildren) {
executeWindowAds();
}
}, []);
return (
<ins
ref={adsRef}
className="adsbygoogle"
style={{ display: 'block' }}
data-ad-client="ca-pub-xxx"
data-ad-slot="xxx"
data-ad-format="auto"
data-full-width-responsive="true"
></ins>
);
}
export default function Adsense() {
const router = useRouter();
const [shouldMount, setShouldMount] = useState(true);
useEffect(() => {
const onRouteChangeStart = () => setShouldMount(false);
const onRouteChangeComplete = () => setShouldMount(true);
router.events.on('routeChangeStart', onRouteChangeStart);
router.events.on('routeChangeComplete', onRouteChangeComplete);
return () => {
router.events.off('routeChangeStart', onRouteChangeStart);
router.events.off('routeChangeComplete', onRouteChangeComplete);
};
}, [router.events]);
return shouldMount ? <Ads /> : null;
}
On your layout page, for example a sidebar where each page has the same sidebar:
// components/LayoutBlogPost.tsx
import dynamic from 'next/dynamic';
const Adsense = dynamic(() => import('./Adsense'), { ssr: false });
export default function LayoutBlogPost({children}: {children: any}) {
return (
<div>
{children}
<Adsense />
</div>
);
}
try useRouter hook and handling router events inside useEffect:
import { useRouter } from 'next/router'
export default function Page() {
const router = useRouter();
useEffect(() => {
router.events.on('routeChangeError', handleChange)
}, [])
return (
<>
</>
)
}
I create an API service like this:
export default class ApiService {
static init(store) {
axios.interceptors.request.use(config => {
const { token } = store.getState().user;
config.baseURL = ${process.env.NEXT_STATIC_API_URL};
if (token) {
config.headers.Authorization = Bearer ${token};
}
return config;
});
}
}
And init it here in the main app component:
class EnhancedApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
const { store } = ctx;
if (!isClient()) ApiService.init(store);
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
componentDidMount() {
ApiService.init(this.props.store);
}
render() {
const { Component, pageProps, store } = this.props;
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
}
export default withRedux(configureStore, { debug: true })(
withReduxSaga(EnhancedApp)
);
I'm using a wrapper to get user data for every page:
const checkAuth = async (ctx, isProtected) => {
const { auth } = nextCookie(ctx);
if (isProtected && !auth) {
if (!isClient() && ctx.res) {
ctx.res.writeHead(302, { Location: '/' });
ctx.res.end();
} else {
await Router.push('/');
}
}
return auth || null;
};
export const withAuth = (isProtected = false) => (WrappedComponent) => {
const WrappedWithAuth = (props) => {
const { token } = useSelector(
state => state.user
);
useEffect(() => {
if (isProtected && !token) Router.push('/');
}, [token]);
return <WrappedComponent {...props} />;
};
WrappedWithAuth.getInitialProps = async (ctx) => {
const token = await checkAuth(ctx, isProtected);
const { store } = ctx;
if (token) {
store.dispatch(userSetToken(token));
try {
const { data } = await UserService.getProfile();
store.dispatch(userGetProfileSuccess(data));
} catch (e) {
store.dispatch(userGetProfileFailure(e));
}
}
const componentProps =
WrappedComponent.getInitialProps &&
(await WrappedComponent.getInitialProps({ ...ctx, token }));
return { ...componentProps };
};
return WrappedWithAuth;
};
Everything works properly on the client-side, but when I change the user and refresh the page I see that get profile API call on the server-side continues to use a previous token.