From ce15c9487d946c10809d6c9169da9d44eef27f1c Mon Sep 17 00:00:00 2001 From: Sina Date: Thu, 21 Sep 2023 18:21:59 +0200 Subject: [PATCH] [Feat] logout (#6) * 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 --- src/core/auth/AuthGuard.tsx | 26 +++++++++++------ src/core/auth/UserContext.tsx | 1 + src/core/auth/logout.mutation.ts | 6 ++++ src/core/auth/refreshToken.mutation.ts | 12 -------- src/pages/home/HomePage.tsx | 20 ++++++++++--- src/shared/constants/endPoints.ts | 5 +++- src/shared/utils/cookie.ts | 40 ++++++++++++++++++++++++++ src/shared/utils/cookieMatch.ts | 13 --------- 8 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 src/core/auth/logout.mutation.ts delete mode 100644 src/core/auth/refreshToken.mutation.ts create mode 100644 src/shared/utils/cookie.ts delete mode 100644 src/shared/utils/cookieMatch.ts diff --git a/src/core/auth/AuthGuard.tsx b/src/core/auth/AuthGuard.tsx index 2753dbb2..32c82f6f 100644 --- a/src/core/auth/AuthGuard.tsx +++ b/src/core/auth/AuthGuard.tsx @@ -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 } @@ -22,7 +24,7 @@ export function RequireAuth() { export interface AuthGuardProps {} export function AuthGuard({ children }: PropsWithChildren) { - const [auth, setAuth] = useState(getAuthData) + const [auth, setAuth] = useState({ ...(getAuthData() || {}), isAuthenticated: isAuthenticated() }) const navigate = useNavigate() const nextUrl = useRef() @@ -31,14 +33,22 @@ export function AuthGuard({ children }: PropsWithChildren) { 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, @@ -49,7 +59,7 @@ export function AuthGuard({ children }: PropsWithChildren) { cause: 'Could not refresh token', }) } else if (error?.response?.status === 403 || error?.response?.status === 401) { - setAuth(undefined) + setAuth({ isAuthenticated: false }) } throw error }, @@ -66,5 +76,5 @@ export function AuthGuard({ children }: PropsWithChildren) { } }, [auth, navigate]) - return {children} + return {children} } diff --git a/src/core/auth/UserContext.tsx b/src/core/auth/UserContext.tsx index 9491ef8c..5ae0367d 100644 --- a/src/core/auth/UserContext.tsx +++ b/src/core/auth/UserContext.tsx @@ -6,6 +6,7 @@ export type UserContextRealValues = { export interface UserContextValue extends Partial { setAuth: (value: UserContextRealValues, url?: string) => void + logout: () => void } export const UserContext = createContext(null) diff --git a/src/core/auth/logout.mutation.ts b/src/core/auth/logout.mutation.ts new file mode 100644 index 00000000..b49e22a5 --- /dev/null +++ b/src/core/auth/logout.mutation.ts @@ -0,0 +1,6 @@ +import { endPoints } from 'src/shared/constants' +import { axiosWithAuth } from 'src/shared/utils/axios' + +export const logoutMutation = async () => { + return axiosWithAuth.post(endPoints.auth.jwt.logout) +} diff --git a/src/core/auth/refreshToken.mutation.ts b/src/core/auth/refreshToken.mutation.ts deleted file mode 100644 index ec5434a4..00000000 --- a/src/core/auth/refreshToken.mutation.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { endPoints } from 'src/shared/constants' -import { simpleAxios } from 'src/shared/utils/axios' - -export const refreshTokenMutation = async (refreshToken: string) => { - return simpleAxios - .post(endPoints.auth.jwt.refresh, null, { - headers: { - Authorization: `Bearer ${refreshToken}`, - }, - }) - .then((res) => res.data) -} diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index a4411c87..58b730c5 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -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(endPoints.organizations.get).then((res) => res.data)) + const { logout } = useUserProfile() return ( - - Setup cloud - + <> + + Setup cloud + + + {JSON.stringify(data, null, ' ')} + ) } diff --git a/src/shared/constants/endPoints.ts b/src/shared/constants/endPoints.ts index eab80a97..063faefe 100644 --- a/src/shared/constants/endPoints.ts +++ b/src/shared/constants/endPoints.ts @@ -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/', + }, } diff --git a/src/shared/utils/cookie.ts b/src/shared/utils/cookie.ts new file mode 100644 index 00000000..f131fa7b --- /dev/null +++ b/src/shared/utils/cookie.ts @@ -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() + } + } +} diff --git a/src/shared/utils/cookieMatch.ts b/src/shared/utils/cookieMatch.ts deleted file mode 100644 index 54b707b2..00000000 --- a/src/shared/utils/cookieMatch.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const cookieMatch = (name: string, match?: string | RegExp | ((value: string) => boolean)) => { - const splittedByName = 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 - } -}