Skip to content

Commit

Permalink
[Feat] logout (#6)
Browse files Browse the repository at this point in the history
* feat(logout): add logout feature

* chore(home-page): add test logout and organization fetch as test

* fix(auth-guard): fix a breakage with empty localstorage
  • Loading branch information
sijav authored Sep 21, 2023
1 parent 1ae791b commit ce15c94
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 38 deletions.
26 changes: 18 additions & 8 deletions src/core/auth/AuthGuard.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import axios, { AxiosError } from 'axios'
import { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom'
import { StorageKeys } from 'src/shared/constants'
import { env } from 'src/shared/constants'
import { setAxiosWithAuth } from 'src/shared/utils/axios'
import { cookieMatch } from 'src/shared/utils/cookieMatch'
import { clearAllCookies, isAuthenticated } from 'src/shared/utils/cookie'
import { getAuthData, setAuthData } from 'src/shared/utils/localstorage'
import { UserContext, UserContextRealValues } from './UserContext'
import { logoutMutation } from './logout.mutation'
import { UserContext, UserContextRealValues, useUserProfile } from './UserContext'

export interface RequireAuthProps {}

export function RequireAuth() {
const location = useLocation()
const user = useUserProfile()

if (!cookieMatch(StorageKeys.authenticated, '1')) {
if (!user.isAuthenticated) {
return <Navigate to={{ pathname: '/login', search: `returnUrl=${location.pathname}${encodeURIComponent(location.search)}` }} replace />
}

Expand All @@ -22,7 +24,7 @@ export function RequireAuth() {
export interface AuthGuardProps {}

export function AuthGuard({ children }: PropsWithChildren<AuthGuardProps>) {
const [auth, setAuth] = useState(getAuthData)
const [auth, setAuth] = useState<UserContextRealValues | undefined>({ ...(getAuthData() || {}), isAuthenticated: isAuthenticated() })
const navigate = useNavigate()
const nextUrl = useRef<string>()

Expand All @@ -31,14 +33,22 @@ export function AuthGuard({ children }: PropsWithChildren<AuthGuardProps>) {
nextUrl.current = url
}, [])

const handleLogout = useCallback(() => {
logoutMutation().finally(() => {
clearAllCookies()
setAuth({ isAuthenticated: false })
})
}, [])

useEffect(() => {
if (auth?.isAuthenticated) {
const instance = axios.create({
baseURL: '/',
baseURL: env.apiUrl,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
withCredentials: true,
})
instance.interceptors.response.use(
(response) => response,
Expand All @@ -49,7 +59,7 @@ export function AuthGuard({ children }: PropsWithChildren<AuthGuardProps>) {
cause: 'Could not refresh token',
})
} else if (error?.response?.status === 403 || error?.response?.status === 401) {
setAuth(undefined)
setAuth({ isAuthenticated: false })
}
throw error
},
Expand All @@ -66,5 +76,5 @@ export function AuthGuard({ children }: PropsWithChildren<AuthGuardProps>) {
}
}, [auth, navigate])

return <UserContext.Provider value={{ ...auth, setAuth: handleSetAuth }}>{children}</UserContext.Provider>
return <UserContext.Provider value={{ ...auth, setAuth: handleSetAuth, logout: handleLogout }}>{children}</UserContext.Provider>
}
1 change: 1 addition & 0 deletions src/core/auth/UserContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type UserContextRealValues = {

export interface UserContextValue extends Partial<UserContextRealValues> {
setAuth: (value: UserContextRealValues, url?: string) => void
logout: () => void
}

export const UserContext = createContext<UserContextValue | null>(null)
Expand Down
6 changes: 6 additions & 0 deletions src/core/auth/logout.mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { endPoints } from 'src/shared/constants'
import { axiosWithAuth } from 'src/shared/utils/axios'

export const logoutMutation = async () => {
return axiosWithAuth.post<undefined>(endPoints.auth.jwt.logout)
}
12 changes: 0 additions & 12 deletions src/core/auth/refreshToken.mutation.ts

This file was deleted.

20 changes: 16 additions & 4 deletions src/pages/home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { Typography } from '@mui/material'
import { Button, Stack, Typography } from '@mui/material'
import { useQuery } from '@tanstack/react-query'
import { useUserProfile } from 'src/core/auth'
import { endPoints } from 'src/shared/constants'
import { axiosWithAuth } from 'src/shared/utils/axios'

export default function HomePage() {
const { data } = useQuery(['organization'], () => axiosWithAuth.get<Object>(endPoints.organizations.get).then((res) => res.data))
const { logout } = useUserProfile()
return (
<Typography variant="h1" color="secondary">
Setup cloud
</Typography>
<>
<Typography variant="h1" color="secondary" mb={2}>
Setup cloud
</Typography>
<Button onClick={logout} size="large" variant="contained">
Logout
</Button>
<Stack component={'pre'}>{JSON.stringify(data, null, ' ')}</Stack>
</>
)
}
5 changes: 4 additions & 1 deletion src/shared/constants/endPoints.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
export const endPoints = {
auth: {
jwt: {
refresh: '/api/auth/jwt/refresh',
logout: '/api/auth/jwt/logout',
},
oauthProviders: '/api/auth/oauth-providers',
},
organizations: {
get: '/api/organizations/',
},
}
40 changes: 40 additions & 0 deletions src/shared/utils/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { StorageKeys } from 'src/shared/constants'

export function cookieMatch(name: string, match: string | RegExp | ((value: string) => boolean)): boolean
export function cookieMatch(name: string, match?: undefined | never): string | undefined
export function cookieMatch(name: string, match?: string | RegExp | ((value: string) => boolean)) {
const splittedByName = window.document.cookie?.split(name + '=')
const value = splittedByName?.[1]?.split(';')?.[0]
if (typeof match === 'function') {
return match(value)
} else if (typeof match === 'string') {
return value === match
} else if (match && typeof match === 'object' && match instanceof RegExp) {
return match.test(value)
} else {
return value || undefined
}
}

export const isAuthenticated = () => cookieMatch(StorageKeys.authenticated, '1')

export const clearAllCookies = () => {
const cookies = window.document.cookie.split('; ')
for (let cookieIndex = 0; cookieIndex < cookies.length; cookieIndex++) {
const domain = window.location.hostname.split('.')
while (domain.length > 0) {
const cookieBase =
encodeURIComponent(cookies[cookieIndex].split(';')[0].split('=')[0]) +
'=; expires=Thu, 01-Jan-1970 00:00:01 GMT; domain=' +
domain.join('.') +
' ;path='
const schema = window.location.pathname.split('/')
window.document.cookie = cookieBase + '/'
while (schema.length > 0) {
window.document.cookie = cookieBase + schema.join('/')
schema.pop()
}
domain.shift()
}
}
}
13 changes: 0 additions & 13 deletions src/shared/utils/cookieMatch.ts

This file was deleted.

0 comments on commit ce15c94

Please sign in to comment.