fix(dashboard-v2): fix race conditions on /login & /logout calls

This commit is contained in:
Michał Leszczyk 2022-03-29 16:08:26 +02:00
parent 8cdf0f86b2
commit d3c252a0b0
No known key found for this signature in database
GPG Key ID: FA123CA8BAA2FBF4
4 changed files with 81 additions and 53 deletions

View File

@ -2,6 +2,7 @@ import { Link, navigate } from "gatsby";
import styled from "styled-components"; import styled from "styled-components";
import { screen } from "../../lib/cssHelpers"; import { screen } from "../../lib/cssHelpers";
import { useUser } from "../../contexts/user";
import { DropdownMenu, DropdownMenuLink } from "../DropdownMenu"; import { DropdownMenu, DropdownMenuLink } from "../DropdownMenu";
import { CogIcon, LockClosedIcon, SkynetLogoIcon } from "../Icons"; import { CogIcon, LockClosedIcon, SkynetLogoIcon } from "../Icons";
import { PageContainer } from "../PageContainer"; import { PageContainer } from "../PageContainer";
@ -49,48 +50,60 @@ const NavBarBody = styled.nav.attrs({
} }
`; `;
export const NavBar = () => ( export const NavBar = () => {
<NavBarContainer> const { mutate: setUserState } = useUser();
<PageContainer className="px-0">
<NavBarBody> const onLogout = async () => {
<NavBarSection className="logo-area pl-2 pr-4 md:px-0 md:w-[110px] justify-center sm:justify-start"> try {
<SkynetLogoIcon size={48} /> await accountsService.post("logout");
</NavBarSection> // Don't refresh user state from server, as it will now respond with UNAUTHORIZED
<NavBarSection className="navigation-area border-t border-palette-100"> // and user will be redirected to /auth/login with return_to query param (which we want empty).
<NavBarLink to="/" as={Link} activeClassName="!border-b-primary"> await setUserState(null, { revalidate: false });
Dashboard navigate("/auth/login");
</NavBarLink> } catch {
<NavBarLink to="/files" as={Link} activeClassName="!border-b-primary"> // Do nothing.
Files }
</NavBarLink> };
<NavBarLink to="/payments" as={Link} activeClassName="!border-b-primary">
Payments return (
</NavBarLink> <NavBarContainer>
</NavBarSection> <PageContainer className="px-0">
<NavBarSection className="dropdown-area justify-end"> <NavBarBody>
<DropdownMenu title="My account"> <NavBarSection className="logo-area pl-2 pr-4 md:px-0 md:w-[110px] justify-center sm:justify-start">
<DropdownMenuLink <SkynetLogoIcon size={48} />
to="/settings" </NavBarSection>
as={Link} <NavBarSection className="navigation-area border-t border-palette-100">
activeClassName="text-primary" <NavBarLink to="/" as={Link} activeClassName="!border-b-primary">
icon={CogIcon} Dashboard
label="Settings" </NavBarLink>
partiallyActive <NavBarLink to="/files" as={Link} activeClassName="!border-b-primary">
/> Files
<DropdownMenuLink </NavBarLink>
onClick={async () => { <NavBarLink to="/payments" as={Link} activeClassName="!border-b-primary">
await accountsService.post("logout"); Payments
navigate("/auth/login"); </NavBarLink>
// TODO: handle errors </NavBarSection>
}} <NavBarSection className="dropdown-area justify-end">
activeClassName="text-primary" <DropdownMenu title="My account">
className="cursor-pointer" <DropdownMenuLink
icon={LockClosedIcon} to="/settings"
label="Log out" as={Link}
/> activeClassName="text-primary"
</DropdownMenu> icon={CogIcon}
</NavBarSection> label="Settings"
</NavBarBody> partiallyActive
</PageContainer> />
</NavBarContainer> <DropdownMenuLink
); onClick={onLogout}
activeClassName="text-primary"
className="cursor-pointer"
icon={LockClosedIcon}
label="Log out"
/>
</DropdownMenu>
</NavBarSection>
</NavBarBody>
</PageContainer>
</NavBarContainer>
);
};

View File

@ -1,10 +1,10 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useSWR from "swr"; import useSWRImmutable from "swr/immutable";
import { UserContext } from "./UserContext"; import { UserContext } from "./UserContext";
export const UserProvider = ({ children }) => { export const UserProvider = ({ children }) => {
const { data: user, error, mutate } = useSWR("user"); const { data: user, error, mutate } = useSWRImmutable("user");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {

View File

@ -15,12 +15,15 @@ const redirectUnauthenticated = (key) =>
}); });
const redirectAuthenticated = (key) => const redirectAuthenticated = (key) =>
fetch(`${baseUrl}/${key}`).then((response) => { fetch(`${baseUrl}/${key}`).then(async (response) => {
if (response.status === StatusCodes.OK) { if (response.ok) {
navigate(`/`); await navigate("/");
return response.json();
} }
return response.json(); // If there was an error, let's throw so useSWR's "error" property is populated instead "data".
const data = await response.json();
throw new Error(data?.message || `Error occured when trying to fetch: ${key}`);
}); });
export const allUsers = { export const allUsers = {

View File

@ -1,19 +1,31 @@
import { useEffect } from "react";
import { navigate } from "gatsby"; import { navigate } from "gatsby";
import AuthLayout from "../../layouts/AuthLayout"; import AuthLayout from "../../layouts/AuthLayout";
import { LoginForm } from "../../components/forms"; import { LoginForm } from "../../components/forms";
import { useUser } from "../../contexts/user";
const LoginPage = ({ location }) => { const LoginPage = ({ location }) => {
const { user, mutate: refreshUserState } = useUser();
const query = new URLSearchParams(location.search); const query = new URLSearchParams(location.search);
const redirectTo = query.get("return_to"); const redirectTo = query.get("return_to");
useEffect(() => {
if (user) {
navigate(redirectTo || "/");
}
}, [user, redirectTo]);
return ( return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen"> <div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16"> <div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" /> <img src="/images/logo-black-text.svg" alt="Skynet" />
</div> </div>
<LoginForm onSuccess={() => navigate(redirectTo || "/")} /> <LoginForm
onSuccess={async () => {
await refreshUserState();
}}
/>
</div> </div>
); );
}; };