Skip to content

Commit 48da38e

Browse files
authored
fix: logout error (#460)
* fix: redirect to logout page on fetch error * fix: redirect logout * test: logout route * feat: add logout page/route * fix: clear errors before logout * test: logout errors * test: logout * test: logout * fix: redirect logout page * fix: logout component * test: logout session * test: logout * test: logout * fix: session error * test: fetch error * test: error comp * test: logout * test: logout * test: logout * test: logout * test: logout * test: logout * test: socket * test: logout * test: logout * test: logout * test: logout * test: logout * test: logout * test: logout * test: logout * test: logout * fix: logout bug * feat: improve logout page comments
1 parent 4335406 commit 48da38e

File tree

6 files changed

+78
-19
lines changed

6 files changed

+78
-19
lines changed

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import Project from 'pages/Project'
5454
import Policy from 'pages/Policy'
5555
import Maintenance from 'pages/Maintenance'
5656
import PrivateRoute from 'components/AuthzRoute'
57+
import Logout from 'pages/Logout'
5758
import { HttpErrorBadRequest } from './utils/error'
5859
import { NotistackProvider, SnackbarUtilsConfigurator } from './utils/snack'
5960

@@ -175,6 +176,7 @@ function App() {
175176
<PrivateRoute path='/teams/:teamId/services' component={Services} exact />
176177
<PrivateRoute path='/teams/:teamId/services/:serviceId' component={Service} exact />
177178
<PrivateRoute path='/maintenance' component={Maintenance} platformAdminRoute exact />
179+
<Route path='/logout' component={Logout} />
178180
<Route path='*'>
179181
<Error error={new HttpErrorBadRequest()} />
180182
</Route>

src/components/AccountPopover.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useDeleteCloudttyMutation } from 'redux/otomiApi'
55
import { useSession } from 'providers/Session'
66
import { getDomain, getEmailNoSymbols, getUserTeams } from 'layouts/Shell'
77
import { clearLocalStorage } from 'hooks/useLocalStorage'
8+
import { useHistory } from 'react-router-dom'
89
import MenuPopover from './MenuPopover'
910
import { IconButtonAnimate } from './animate'
1011
import SettingMode from './SettingMode'
@@ -14,6 +15,7 @@ type Props = {
1415
}
1516

1617
export default function AccountPopover({ email }: Props) {
18+
const history = useHistory()
1719
const [open, setOpen] = useState<HTMLElement | null>(null)
1820
const [del] = useDeleteCloudttyMutation()
1921
const { user, oboTeamId } = useSession()
@@ -30,16 +32,13 @@ export default function AccountPopover({ email }: Props) {
3032
setOpen(null)
3133
}
3234

33-
const handleLogout = async () => {
34-
try {
35-
await del({
36-
body: { teamId: oboTeamId, domain, emailNoSymbols, isAdmin: user.isPlatformAdmin, userTeams },
37-
})
38-
} catch (error) {
39-
// cloudtty resources will be automatically deleted by the API after a 2-hour timeout
40-
}
41-
clearLocalStorage('oboTeamId')
42-
window.location.href = '/logout-otomi'
35+
const handleLogout = () => {
36+
del({
37+
body: { teamId: oboTeamId, domain, emailNoSymbols, isAdmin: user.isPlatformAdmin, userTeams },
38+
}).finally(() => {
39+
clearLocalStorage('oboTeamId')
40+
history.push('/logout')
41+
})
4342
}
4443

4544
return (

src/components/Error.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Helmet from 'react-helmet'
44
import { useTranslation } from 'react-i18next'
55
import { useAppDispatch, useAppSelector } from 'redux/hooks'
66
import { setError } from 'redux/reducers'
7+
import Logout from 'pages/Logout'
78
import { ApiErrorUnauthorized, ApiErrorUnauthorizedNoGroups, HttpError } from '../utils/error'
89
import Iconify from './Iconify'
910

@@ -18,9 +19,8 @@ export default function ({ error }: Props): React.ReactElement {
1819
// END HOOKS
1920
const err = error ?? globalError
2021
if (!err) return null
21-
// redirect to login page if the error is a fetch error (session expired)
22-
// automatically triggers Keycloak to route the user to the Keycloak login page
23-
if (err?.status === 'FETCH_ERROR') window.location.href = '/'
22+
// return the logout page if the error is a fetch error (session expired)
23+
if (err?.status === 'FETCH_ERROR') return <Logout fetchError />
2424
const { title, message, data, code, originalStatus, status } = err || {}
2525
const errorMessage = title ? `${title}: ${message}` : message || data?.error
2626
const errorCode = code || originalStatus || status || message || data?.error
@@ -36,6 +36,7 @@ export default function ({ error }: Props): React.ReactElement {
3636
case 403:
3737
icon = 'ic:baseline-do-not-disturb'
3838
break
39+
case 503:
3940
case 504:
4041
icon = 'ant-design:api-outlined'
4142
break
@@ -50,7 +51,12 @@ export default function ({ error }: Props): React.ReactElement {
5051
{text}
5152
</Button>
5253
)
53-
if (code === 504 || err instanceof ApiErrorUnauthorized || err instanceof ApiErrorUnauthorizedNoGroups) {
54+
if (
55+
code === 503 ||
56+
code === 504 ||
57+
err instanceof ApiErrorUnauthorized ||
58+
err instanceof ApiErrorUnauthorizedNoGroups
59+
) {
5460
return renderButton(t('Logout', { ns: 'error' }) as string, () => {
5561
window.location.href = '/logout-otomi'
5662
})

src/pages/Logout.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import LoadingScreen from 'components/LoadingScreen'
2+
import React, { useEffect } from 'react'
3+
4+
interface Props {
5+
fetchError?: boolean
6+
}
7+
8+
export default function Logout({ fetchError = false }: Props): React.ReactElement {
9+
// This component manages the logout process for users authenticated with Keycloak.
10+
// - If a fetch error occurs, the page reloads automatically to handle potential session issues.
11+
// - If no fetch error occurs, the user is redirected to the Keycloak logout page ('/logout-otomi' route).
12+
// - On component unmount, the page reloads to ensure a clean and consistent state.
13+
useEffect(() => {
14+
if (fetchError) window.location.reload()
15+
else window.location.href = '/logout-otomi'
16+
return () => {
17+
window.location.reload()
18+
}
19+
}, [fetchError])
20+
return <LoadingScreen />
21+
}

src/providers/Session.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { skipToken } from '@reduxjs/toolkit/dist/query'
12
import { setSpec } from 'common/api-spec'
23
import LinkCommit from 'components/LinkCommit'
34
import LoadingScreen from 'components/LoadingScreen'
@@ -6,8 +7,10 @@ import MessageTekton from 'components/MessageTekton'
67
import MessageTrans from 'components/MessageTrans'
78
import { useLocalStorage } from 'hooks/useLocalStorage'
89
import { ProviderContext, SnackbarKey } from 'notistack'
10+
import Logout from 'pages/Logout'
911
import React, { useContext, useEffect, useMemo, useState } from 'react'
1012
import { useTranslation } from 'react-i18next'
13+
import { useLocation } from 'react-router-dom'
1114
import { useAppSelector } from 'redux/hooks'
1215
import {
1316
GetSessionApiResponse,
@@ -18,7 +21,12 @@ import {
1821
useGetSettingsInfoQuery,
1922
} from 'redux/otomiApi'
2023
import { useSocket, useSocketEvent } from 'socket.io-react-hook'
21-
import { ApiErrorGatewayTimeout, ApiErrorUnauthorized, ApiErrorUnauthorizedNoGroups } from 'utils/error'
24+
import {
25+
ApiErrorGatewayTimeout,
26+
ApiErrorServiceUnavailable,
27+
ApiErrorUnauthorized,
28+
ApiErrorUnauthorizedNoGroups,
29+
} from 'utils/error'
2230
import snack from 'utils/snack'
2331

2432
export interface SessionContext extends GetSessionApiResponse {
@@ -90,17 +98,28 @@ type DroneBuildEvent = {
9098
}
9199

92100
export default function SessionProvider({ children }: Props): React.ReactElement {
101+
const { pathname } = useLocation()
102+
const skipFetch = pathname === '/logout' || pathname === '/logout-otomi'
93103
const [oboTeamId, setOboTeamId] = useLocalStorage<string>('oboTeamId', undefined)
94-
const { data: session, isLoading: isLoadingSession, refetch: refetchSession } = useGetSessionQuery()
104+
const {
105+
data: session,
106+
isLoading: isLoadingSession,
107+
refetch: refetchSession,
108+
error: sessionError,
109+
} = useGetSessionQuery(skipFetch && skipToken)
95110
const url = `${window.location.origin.replace(/^http/, 'ws')}`
96111
const path = '/api/ws'
97-
const { data: settings, isLoading: isLoadingSettings, refetch: refetchSettings } = useGetSettingsInfoQuery()
112+
const {
113+
data: settings,
114+
isLoading: isLoadingSettings,
115+
refetch: refetchSettings,
116+
} = useGetSettingsInfoQuery(skipFetch && skipToken)
98117
const {
99118
data: apps,
100119
isLoading: isLoadingApps,
101120
refetch: refetchAppsEnabled,
102121
} = useGetAppsQuery({ teamId: oboTeamId, picks: ['id', 'enabled'] }, { skip: !oboTeamId })
103-
const { data: apiDocs, isLoading: isLoadingApiDocs, error: errorApiDocs } = useApiDocsQuery()
122+
const { data: apiDocs, isLoading: isLoadingApiDocs, error: errorApiDocs } = useApiDocsQuery(skipFetch && skipToken)
104123
const { socket, error: errorSocket } = useSocket({ url, path })
105124
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
106125
const { lastMessage: lastDbMessage } = useSocketEvent<DbMessage>(socket, 'db')
@@ -278,7 +297,13 @@ export default function SessionProvider({ children }: Props): React.ReactElement
278297
if (errorSocket)
279298
keys.socket = snack.warning(`${t('Could not establish socket connection. Retrying...')}`, { key: keys.socket })
280299
// no error and we stopped loading, so we can check the user
281-
if (!session) throw new ApiErrorGatewayTimeout()
300+
if (sessionError) {
301+
const { originalStatus, status } = sessionError as any
302+
if (originalStatus === 503) throw new ApiErrorServiceUnavailable()
303+
if (originalStatus === 504) throw new ApiErrorGatewayTimeout()
304+
// return the logout page if the error is a fetch error (session expired)
305+
if (status === 'FETCH_ERROR') return <Logout fetchError />
306+
}
282307
if (!session.user.isPlatformAdmin && session.user.teams.length === 0) throw new ApiErrorUnauthorizedNoGroups()
283308
if (isLoadingApiDocs || isLoadingApps || isLoadingSession || isLoadingSettings) return <LoadingScreen />
284309
if (apiDocs) setSpec(apiDocs)

src/utils/error.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ class HttpErrorForbidden extends HttpError {
2828
}
2929
}
3030

31+
class ApiErrorServiceUnavailable extends HttpError {
32+
constructor() {
33+
super(e['The API could not be reached.'], 503)
34+
}
35+
}
3136
class ApiErrorGatewayTimeout extends HttpError {
3237
constructor() {
3338
super(e['The API could not be reached.'], 504)
@@ -56,6 +61,7 @@ export {
5661
HttpError,
5762
HttpErrorBadRequest,
5863
HttpErrorForbidden,
64+
ApiErrorServiceUnavailable,
5965
ApiErrorGatewayTimeout,
6066
ApiErrorUnauthorized,
6167
ApiErrorUnauthorizedNoGroups,

0 commit comments

Comments
 (0)