Skip to content

Commit

Permalink
feat: Contributor frontend 11 (#66)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
SKairinos authored Oct 25, 2024
1 parent 204a118 commit 7512bf9
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 42 deletions.
16 changes: 0 additions & 16 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
28 changes: 8 additions & 20 deletions src/api/createApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -72,25 +73,12 @@ export default function createApi<TagTypes extends string = never>({
return await fetch(args, api, extraOptions)
},
tagTypes: [...defaultTagTypes, ...tagTypes],
endpoints: () => ({}),
})

return api.injectEndpoints({
endpoints: build => ({
logout: build.mutation<null, null>({
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<null, null>(api, build),
}),
})

return api
}
1 change: 1 addition & 0 deletions src/api/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
40 changes: 40 additions & 0 deletions src/api/endpoints/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { type EndpointBuilder, type Api } from "@reduxjs/toolkit/query/react"

import { login, logout } from "../../slices/session"

export function buildLoginEndpoint<ResultType, QueryArg>(
build: EndpointBuilder<any, any, any>,
url: string = "session/login/",
) {
return build.mutation<ResultType, QueryArg>({
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<ResultType, QueryArg>(
api: Api<any, any, any, any, any>,
build: EndpointBuilder<any, any, any>,
url: string = "session/logout/",
) {
return build.mutation<ResultType, QueryArg>({
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())
}
},
})
}
11 changes: 8 additions & 3 deletions src/hooks/auth.tsx
Original file line number Diff line number Diff line change
@@ -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"]
Expand All @@ -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<Required extends boolean> = (
Expand Down
1 change: 1 addition & 0 deletions src/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./session"
16 changes: 16 additions & 0 deletions src/middlewares/session.ts
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions src/env.ts → src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
8 changes: 8 additions & 0 deletions src/slices/createSlice.ts
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions src/slices/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as createSlice } from "./createSlice"
export { default as sessionSlice, type SessionState } from "./session"
32 changes: 32 additions & 0 deletions src/slices/session.ts
Original file line number Diff line number Diff line change
@@ -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
12 changes: 9 additions & 3 deletions src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 7512bf9

Please sign in to comment.