From 7512bf990ff729aaea1e3832624dfae33b2ad910 Mon Sep 17 00:00:00 2001 From: Stefan Kairinos Date: Fri, 25 Oct 2024 10:13:35 +0100 Subject: [PATCH] feat: Contributor frontend 11 (#66) * fix: session state * export * fix: use session metadata * fix: export session slice * indexes * api not func * fix: rename env to settings * fix: unused arg * reinstate auth utils --- .eslintrc.json | 16 --------------- src/api/createApi.ts | 28 ++++++++----------------- src/api/endpoints/index.ts | 1 + src/api/endpoints/session.ts | 40 ++++++++++++++++++++++++++++++++++++ src/hooks/auth.tsx | 11 +++++++--- src/middlewares/index.ts | 1 + src/middlewares/session.ts | 16 +++++++++++++++ src/{env.ts => settings.ts} | 6 ++++++ src/slices/createSlice.ts | 8 ++++++++ src/slices/index.ts | 2 ++ src/slices/session.ts | 32 +++++++++++++++++++++++++++++ src/utils/auth.ts | 12 ++++++++--- 12 files changed, 131 insertions(+), 42 deletions(-) create mode 100644 src/api/endpoints/session.ts create mode 100644 src/middlewares/index.ts create mode 100644 src/middlewares/session.ts rename src/{env.ts => settings.ts} (81%) create mode 100644 src/slices/createSlice.ts create mode 100644 src/slices/index.ts create mode 100644 src/slices/session.ts diff --git a/.eslintrc.json b/.eslintrc.json index c7c92f71..8d3e80cc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,22 +26,6 @@ { "fixStyle": "inline-type-imports" } - ], - "@typescript-eslint/no-restricted-imports": [ - 2, - { - "paths": [ - { - "name": "react-redux", - "importNames": [ - "useSelector", - "useStore", - "useDispatch" - ], - "message": "Please use pre-typed versions from `src/app/hooks.ts` instead." - } - ] - } ] }, "overrides": [ diff --git a/src/api/createApi.ts b/src/api/createApi.ts index 4b5dd66e..73d9f506 100644 --- a/src/api/createApi.ts +++ b/src/api/createApi.ts @@ -3,9 +3,10 @@ import { fetchBaseQuery, } from "@reduxjs/toolkit/query/react" -import { SERVICE_API_URL } from "../env" -import { getCsrfCookie, logout } from "../utils/auth" +import { SERVICE_API_URL } from "../settings" import defaultTagTypes from "./tagTypes" +import { buildLogoutEndpoint } from "./endpoints/session" +import { getCsrfCookie } from "../utils/auth" // TODO: decide if we want to keep any of this. // export function handleResponseError(error: FetchBaseQueryError): void { @@ -72,25 +73,12 @@ export default function createApi({ return await fetch(args, api, extraOptions) }, tagTypes: [...defaultTagTypes, ...tagTypes], + endpoints: () => ({}), + }) + + return api.injectEndpoints({ endpoints: build => ({ - logout: build.mutation({ - query: () => ({ - url: "session/logout/", - method: "POST", - }), - async onQueryStarted(_, { dispatch, queryFulfilled }) { - try { - await queryFulfilled - } catch (error) { - console.error("Failed to call logout endpoint...", error) - } finally { - logout() - dispatch(api.util.resetApiState()) - } - }, - }), + logout: buildLogoutEndpoint(api, build), }), }) - - return api } diff --git a/src/api/endpoints/index.ts b/src/api/endpoints/index.ts index 516a6f55..3e18dbf4 100644 --- a/src/api/endpoints/index.ts +++ b/src/api/endpoints/index.ts @@ -4,5 +4,6 @@ export * from "./klass" export { default as getReadClassEndpoints } from "./klass" export * from "./school" export { default as getReadSchoolEndpoints } from "./school" +export * from "./session" export * from "./user" export { default as getReadUserEndpoints } from "./user" diff --git a/src/api/endpoints/session.ts b/src/api/endpoints/session.ts new file mode 100644 index 00000000..56eb50b2 --- /dev/null +++ b/src/api/endpoints/session.ts @@ -0,0 +1,40 @@ +import { type EndpointBuilder, type Api } from "@reduxjs/toolkit/query/react" + +import { login, logout } from "../../slices/session" + +export function buildLoginEndpoint( + build: EndpointBuilder, + url: string = "session/login/", +) { + return build.mutation({ + query: body => ({ url, method: "POST", body }), + async onQueryStarted(_, { dispatch, queryFulfilled }) { + try { + await queryFulfilled + dispatch(login()) + } catch (error) { + console.error("Failed to call login endpoint...", error) + } + }, + }) +} + +export function buildLogoutEndpoint( + api: Api, + build: EndpointBuilder, + url: string = "session/logout/", +) { + return build.mutation({ + query: () => ({ url, method: "POST" }), + async onQueryStarted(_, { dispatch, queryFulfilled }) { + try { + await queryFulfilled + } catch (error) { + console.error("Failed to call logout endpoint...", error) + } finally { + dispatch(logout()) + dispatch(api.util.resetApiState()) + } + }, + }) +} diff --git a/src/hooks/auth.tsx b/src/hooks/auth.tsx index f89b47b0..1ac73416 100644 --- a/src/hooks/auth.tsx +++ b/src/hooks/auth.tsx @@ -1,8 +1,11 @@ import Cookies from "js-cookie" import { useEffect, type ReactNode } from "react" import { createSearchParams, useLocation, useNavigate } from "react-router-dom" +import { useSelector } from "react-redux" import { type AuthFactor, type User } from "../api" +import { SESSION_METADATA_COOKIE_NAME } from "../settings" +import { selectIsLoggedIn } from "../slices/session" export interface SessionMetadata { user_id: User["id"] @@ -12,9 +15,11 @@ export interface SessionMetadata { } export function useSessionMetadata(): SessionMetadata | undefined { - const sessionMetadata = Cookies.get("session_metadata") - - return sessionMetadata ? JSON.parse(sessionMetadata) : undefined + return useSelector(selectIsLoggedIn) + ? (JSON.parse( + Cookies.get(SESSION_METADATA_COOKIE_NAME)!, + ) as SessionMetadata) + : undefined } export type UseSessionChildrenFunction = ( diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 00000000..0f5f6353 --- /dev/null +++ b/src/middlewares/index.ts @@ -0,0 +1 @@ +export * from "./session" diff --git a/src/middlewares/session.ts b/src/middlewares/session.ts new file mode 100644 index 00000000..a86407a2 --- /dev/null +++ b/src/middlewares/session.ts @@ -0,0 +1,16 @@ +import { type Middleware, isAction } from "@reduxjs/toolkit" + +import { logout } from "../utils/auth" + +export const logoutMiddleware: Middleware = _ => next => action => { + const response = next(action) + + // The backend should delete these cookie upon calling the logout endpoint. + // However, as a precaution, we also delete the session cookies in case the + // backend fails to delete the cookies. + if (isAction(action) && action.type === "session/logout") { + logout() + } + + return response +} diff --git a/src/env.ts b/src/settings.ts similarity index 81% rename from src/env.ts rename to src/settings.ts index 14a66346..65c73581 100644 --- a/src/env.ts +++ b/src/settings.ts @@ -28,3 +28,9 @@ export const SERVICE_BASE_URL = // The api url of the current service. export const SERVICE_API_URL = `${SERVICE_BASE_URL}/api` + +// The names of cookies. +export const CSRF_COOKIE_NAME = `${SERVICE_NAME}_csrftoken` +export const SESSION_COOKIE_NAME = env.VITE_SESSION_COOKIE_NAME ?? "session_key" +export const SESSION_METADATA_COOKIE_NAME = + env.VITE_SESSION_METADATA_COOKIE_NAME ?? "session_metadata" diff --git a/src/slices/createSlice.ts b/src/slices/createSlice.ts new file mode 100644 index 00000000..7ba139e9 --- /dev/null +++ b/src/slices/createSlice.ts @@ -0,0 +1,8 @@ +import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit" + +// `buildCreateSlice` allows us to create a slice with async thunks. +const createSlice = buildCreateSlice({ + creators: { asyncThunk: asyncThunkCreator }, +}) + +export default createSlice diff --git a/src/slices/index.ts b/src/slices/index.ts new file mode 100644 index 00000000..f21ae4cc --- /dev/null +++ b/src/slices/index.ts @@ -0,0 +1,2 @@ +export { default as createSlice } from "./createSlice" +export { default as sessionSlice, type SessionState } from "./session" diff --git a/src/slices/session.ts b/src/slices/session.ts new file mode 100644 index 00000000..5a4afcc0 --- /dev/null +++ b/src/slices/session.ts @@ -0,0 +1,32 @@ +import Cookies from "js-cookie" + +import { SESSION_METADATA_COOKIE_NAME } from "../settings" +import createSlice from "./createSlice" + +export interface SessionState { + isLoggedIn: boolean +} + +const initialState: SessionState = { + isLoggedIn: Boolean(Cookies.get(SESSION_METADATA_COOKIE_NAME)), +} + +const sessionSlice = createSlice({ + name: "session", + initialState, + reducers: create => ({ + login: create.reducer(state => { + state.isLoggedIn = true + }), + logout: create.reducer(state => { + state.isLoggedIn = false + }), + }), + selectors: { + selectIsLoggedIn: session => session.isLoggedIn, + }, +}) + +export default sessionSlice +export const { login, logout } = sessionSlice.actions +export const { selectIsLoggedIn } = sessionSlice.selectors diff --git a/src/utils/auth.ts b/src/utils/auth.ts index af4619ad..a0a05981 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,11 +1,17 @@ import Cookies from "js-cookie" +import { + SESSION_COOKIE_NAME, + SESSION_METADATA_COOKIE_NAME, + CSRF_COOKIE_NAME, +} from "../settings" + export function logout() { - Cookies.remove("session_key") - Cookies.remove("session_metadata") + Cookies.remove(SESSION_COOKIE_NAME) + Cookies.remove(SESSION_METADATA_COOKIE_NAME) } // https://docs.djangoproject.com/en/3.2/ref/csrf/ export function getCsrfCookie() { - return Cookies.get(`${import.meta.env.VITE_SERVICE_NAME}_csrftoken`) + return Cookies.get(CSRF_COOKIE_NAME) }