sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #07844
[Merge] ~petermakowski/maas-site-manager:refactor-auth-context-useReducer into maas-site-manager:main
Peter Makowski has proposed merging ~petermakowski/maas-site-manager:refactor-auth-context-useReducer into maas-site-manager:main.
Commit message:
refactor: auth context useReducer
- split context providers into AppContext and AuthContext
Requested reviews:
MAAS Committers (maas-committers)
For more details, see:
https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/442091
refactor auth context to use useReducer
next step will be extracting async logic to middleware
QA Steps
Verify you can login to site manager and logout successfully
--
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:refactor-auth-context-useReducer into maas-site-manager:main.
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index cee9a8b..51431fb 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -2,10 +2,9 @@ import "./App.scss";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
-import { AppContextProvider, AuthContextProvider } from "./context";
-import routes from "./routes";
-
import apiClient from "@/api";
+import { AppContextProvider, AuthContextProvider } from "@/context";
+import routes from "@/routes";
const queryClient = new QueryClient();
const router = createBrowserRouter(routes);
diff --git a/frontend/src/components/MainLayout/MainLayout.tsx b/frontend/src/components/MainLayout/MainLayout.tsx
index ee4e970..3347511 100644
--- a/frontend/src/components/MainLayout/MainLayout.tsx
+++ b/frontend/src/components/MainLayout/MainLayout.tsx
@@ -5,13 +5,12 @@ import { Col, Row, useOnEscapePressed, usePrevious } from "@canonical/react-comp
import classNames from "classnames";
import { matchPath, Outlet, useLocation } from "react-router-dom";
-import SecondaryNavigation from "../SecondaryNavigation";
-
import { routesConfig } from "@/base/routesConfig";
import type { RoutePath } from "@/base/routesConfig";
import DocumentTitle from "@/components/DocumentTitle/DocumentTitle";
import Navigation from "@/components/Navigation";
import RemoveRegions from "@/components/RemoveRegions";
+import SecondaryNavigation from "@/components/SecondaryNavigation";
import { useAppContext, useAuthContext } from "@/context";
import TokensCreate from "@/pages/tokens/create";
diff --git a/frontend/src/context/AppContext.tsx b/frontend/src/context/AppContext.tsx
new file mode 100644
index 0000000..c7c41ec
--- /dev/null
+++ b/frontend/src/context/AppContext.tsx
@@ -0,0 +1,26 @@
+import { createContext, useContext, useState } from "react";
+
+import type { OnChangeFn, RowSelectionState } from "@tanstack/react-table";
+
+export const AppContext = createContext<{
+ rowSelection: RowSelectionState;
+ setRowSelection: OnChangeFn<RowSelectionState>;
+ sidebar: "removeRegions" | "createToken" | null;
+ setSidebar: (sidebar: "removeRegions" | "createToken" | null) => void;
+}>({
+ rowSelection: {},
+ setRowSelection: () => ({}),
+ sidebar: null,
+ setSidebar: () => null,
+});
+
+export const AppContextProvider = ({ children }: { children: React.ReactNode }) => {
+ const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
+ const [sidebar, setSidebar] = useState<"removeRegions" | "createToken" | null>(null);
+
+ return (
+ <AppContext.Provider value={{ rowSelection, setRowSelection, sidebar, setSidebar }}>{children}</AppContext.Provider>
+ );
+};
+
+export const useAppContext = () => useContext(AppContext);
diff --git a/frontend/src/context.tsx b/frontend/src/context/AuthContext.tsx
similarity index 51%
rename from frontend/src/context.tsx
rename to frontend/src/context/AuthContext.tsx
index 9cd0980..69387ba 100644
--- a/frontend/src/context.tsx
+++ b/frontend/src/context/AuthContext.tsx
@@ -1,40 +1,16 @@
-import { createContext, useContext, useEffect, useState } from "react";
+import type { Reducer } from "react";
+import React, { createContext, useContext, useReducer } from "react";
-import type { OnChangeFn, RowSelectionState } from "@tanstack/react-table";
import type { AxiosInstance } from "axios";
import useLocalStorageState from "use-local-storage-state";
-import type { LoginError } from "@/hooks/api";
import { useLoginMutation } from "@/hooks/api";
-
-export const AppContext = createContext<{
- rowSelection: RowSelectionState;
- setRowSelection: OnChangeFn<RowSelectionState>;
- sidebar: "removeRegions" | "createToken" | null;
- setSidebar: (sidebar: "removeRegions" | "createToken" | null) => void;
-}>({
- rowSelection: {},
- setRowSelection: () => ({}),
- sidebar: null,
- setSidebar: () => null,
-});
-
-export const AppContextProvider = ({ children }: { children: React.ReactNode }) => {
- const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
- const [sidebar, setSidebar] = useState<"removeRegions" | "createToken" | null>(null);
-
- return (
- <AppContext.Provider value={{ rowSelection, setRowSelection, sidebar, setSidebar }}>{children}</AppContext.Provider>
- );
-};
+import type { LoginError } from "@/hooks/api";
type AuthStatus = "initial" | "authenticated" | "unauthorised";
interface AuthContextType {
status: AuthStatus;
- setStatus: (status: AuthStatus) => void;
- setAuthToken: (token: string) => void;
- removeAuthToken: VoidFunction;
login: ({ username, password }: { username: string; password: string }) => void;
logout: (callback: VoidFunction) => void;
isError: boolean;
@@ -43,15 +19,52 @@ interface AuthContextType {
export const AuthContext = createContext<AuthContextType>({
status: "initial",
- setStatus: () => null,
- setAuthToken: () => null,
- removeAuthToken: () => null,
login: () => null,
logout: () => null,
isError: false,
failureReason: null,
});
+export const actionTypes = {
+ LOGIN_SUCCESS: "LOGIN_SUCCESS",
+ LOGIN_ERROR: "LOGIN_ERROR",
+ LOGOUT: "LOGOUT",
+} as const;
+
+const status = {
+ AUTHENTICATED: "authenticated",
+ UNAUTHORISED: "unauthorised",
+} as const;
+
+type Status = (typeof status)[keyof typeof status];
+type ActionType = (typeof actionTypes)[keyof typeof actionTypes];
+type AuthToken = string | null;
+type AuthState = {
+ authToken: AuthToken;
+ status: Status;
+};
+type AuthAction =
+ | {
+ type: typeof actionTypes.LOGIN_SUCCESS;
+ payload: AuthToken;
+ }
+ | {
+ type: Exclude<ActionType, "LOGIN_SUCCESS">;
+ };
+
+const authReducer: Reducer<AuthState, AuthAction> = (state, action) => {
+ switch (action.type) {
+ case actionTypes.LOGIN_SUCCESS:
+ return { ...state, status: "authenticated", authToken: action.payload };
+ case actionTypes.LOGIN_ERROR:
+ return { ...state, status: "unauthorised", authToken: null };
+ case actionTypes.LOGOUT:
+ return { ...state, status: "unauthorised", authToken: null };
+ default:
+ return state;
+ }
+};
+
export const AuthContextProvider = ({
apiClient,
children,
@@ -59,65 +72,70 @@ export const AuthContextProvider = ({
apiClient: AxiosInstance;
children: React.ReactNode;
}) => {
- const [authToken, setAuthToken, { removeItem: removeAuthToken }] = useLocalStorageState("jwtToken");
- const [status, setStatus] = useState<AuthStatus>("initial");
+ const [persistedAuthToken, setPersistedAuthToken, { removeItem: removePersistedAuthToken }] =
+ useLocalStorageState<string>("jwtToken");
const { mutateAsync, isError, failureReason } = useLoginMutation();
- useEffect(() => {
- if (!authToken) {
- setStatus("unauthorised");
- } else {
- setStatus("authenticated");
- }
- }, [apiClient, authToken]);
+ const initialState: AuthState = {
+ authToken: persistedAuthToken ? persistedAuthToken : null,
+ status: persistedAuthToken ? "authenticated" : "unauthorised",
+ };
+
+ const [state, dispatch] = useReducer(authReducer, initialState);
useEffect(() => {
- if (authToken) {
+ if (state.status === "authenticated") {
apiClient.interceptors.request.use(function (config) {
- if (authToken) {
- config.headers.Authorization = `Bearer ${authToken}`;
+ if (state.authToken) {
+ config.headers.Authorization = `Bearer ${state.authToken}`;
}
return config;
});
}
- }, [apiClient, authToken]);
+ }, [apiClient.interceptors.request, state.authToken, state.status]);
useEffect(() => {
apiClient.interceptors.response.use(
(response) => response,
(error) => {
- if (error.response.status === 401) {
- removeAuthToken();
+ if (error?.response?.status === 401 || error?.status === 401) {
+ dispatch({ type: actionTypes.LOGOUT });
+ removePersistedAuthToken();
}
return Promise.reject(error);
},
);
- }, [apiClient, removeAuthToken]);
+ }, [apiClient, removePersistedAuthToken]);
const login = async ({ username, password }: { username: string; password: string }) => {
try {
const response = await mutateAsync({ username, password });
- setAuthToken(response.access_token);
- setStatus("authenticated");
+ dispatch({ type: actionTypes.LOGIN_SUCCESS, payload: response.access_token });
+ setPersistedAuthToken(response.access_token);
} catch (error) {
- setStatus("unauthorised");
+ dispatch({ type: actionTypes.LOGIN_ERROR });
}
};
const logout = (callback: VoidFunction) => {
- removeAuthToken();
- setStatus("unauthorised");
+ dispatch({ type: actionTypes.LOGOUT });
+ removePersistedAuthToken();
callback();
};
return (
<AuthContext.Provider
- value={{ status, setStatus, setAuthToken, removeAuthToken, login, logout, isError, failureReason }}
+ value={{
+ status: state.status,
+ login,
+ logout,
+ isError,
+ failureReason,
+ }}
>
{children}
</AuthContext.Provider>
);
};
-export const useAppContext = () => useContext(AppContext);
export const useAuthContext = () => useContext(AuthContext);
diff --git a/frontend/src/context/index.ts b/frontend/src/context/index.ts
new file mode 100644
index 0000000..d778afb
--- /dev/null
+++ b/frontend/src/context/index.ts
@@ -0,0 +1,2 @@
+export { AuthContext, AuthContextProvider, useAuthContext } from "./AuthContext";
+export { AppContext, AppContextProvider, useAppContext } from "./AppContext";
diff --git a/frontend/src/test-utils.tsx b/frontend/src/test-utils.tsx
index cabf08d..b247e2a 100644
--- a/frontend/src/test-utils.tsx
+++ b/frontend/src/test-utils.tsx
@@ -7,9 +7,8 @@ import { screen, render } from "@testing-library/react";
import type { MemoryRouterProps } from "react-router-dom";
import { MemoryRouter } from "react-router-dom";
-import { AppContextProvider, AuthContextProvider } from "./context";
-
import apiClient from "@/api";
+import { AppContextProvider, AuthContextProvider } from "@/context";
const queryClient = new QueryClient({
defaultOptions: {
Follow ups
-
[Merge] ~petermakowski/maas-site-manager:refactor-auth-context-useReducer into maas-site-manager:main
From: MAAS Lander, 2023-05-01
-
Re: [UNITTESTS] -b refactor-auth-context-useReducer lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS PASS
From: MAAS Lander, 2023-05-01
-
[Merge] ~petermakowski/maas-site-manager:refactor-auth-context-useReducer into maas-site-manager:main
From: Peter Makowski, 2023-05-01
-
[Merge] ~petermakowski/maas-site-manager:refactor-auth-context-useReducer into maas-site-manager:main
From: MAAS Lander, 2023-05-01
-
Re: [Merge] -b refactor-auth-context-useReducer lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - LANDING FAILED
From: MAAS Lander, 2023-05-01
-
Re: [UNITTESTS] -b refactor-auth-context-useReducer lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS FAILED
From: MAAS Lander, 2023-05-01
-
[Merge] ~petermakowski/maas-site-manager:refactor-auth-context-useReducer into maas-site-manager:main
From: Peter Makowski, 2023-05-01
-
[Merge] ~petermakowski/maas-site-manager:refactor-auth-context-useReducer into maas-site-manager:main
From: MAAS Lander, 2023-04-28
-
Re: [Merge] -b refactor-auth-context-useReducer lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - LANDING FAILED
From: MAAS Lander, 2023-04-28
-
[Merge] ~petermakowski/maas-site-manager:refactor-auth-context-useReducer into maas-site-manager:main
From: Peter Makowski, 2023-04-28
-
Re: [Merge] ~petermakowski/maas-site-manager:refactor-auth-context-useReducer into maas-site-manager:main
From: Nick De Villiers, 2023-04-28
-
Re: [UNITTESTS] -b refactor-auth-context-useReducer lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS PASS
From: MAAS Lander, 2023-04-28