Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2f2b534
fix: redirect to logout page on fetch error
ferruhcihan Nov 12, 2024
bdc89f1
fix: redirect logout
ferruhcihan Nov 13, 2024
ae511c7
test: logout route
ferruhcihan Nov 13, 2024
fa4b0dc
feat: add logout page/route
ferruhcihan Nov 13, 2024
29e6771
fix: clear errors before logout
ferruhcihan Nov 13, 2024
d39aa68
test: logout errors
ferruhcihan Nov 13, 2024
7055681
test: logout
ferruhcihan Nov 13, 2024
e8aa02c
test: logout
ferruhcihan Nov 13, 2024
faa7593
fix: redirect logout page
ferruhcihan Nov 13, 2024
7b8b1b5
fix: logout component
ferruhcihan Nov 13, 2024
a7946cf
test: logout session
ferruhcihan Nov 13, 2024
d3c54bf
test: logout
ferruhcihan Nov 13, 2024
2a43bd6
test: logout
ferruhcihan Nov 13, 2024
cab8a1f
fix: session error
ferruhcihan Nov 13, 2024
069feb1
test: fetch error
ferruhcihan Nov 13, 2024
8b66483
test: error comp
ferruhcihan Nov 13, 2024
f53437b
test: logout
ferruhcihan Nov 14, 2024
fe5b65a
test: logout
ferruhcihan Nov 14, 2024
02c58a6
test: logout
ferruhcihan Nov 14, 2024
4497522
test: logout
ferruhcihan Nov 14, 2024
8f7a3bc
test: logout
ferruhcihan Nov 14, 2024
a4a8ee2
test: logout
ferruhcihan Nov 14, 2024
9e9d95b
test: socket
ferruhcihan Nov 14, 2024
0597048
test: logout
ferruhcihan Nov 14, 2024
66af7c7
test: logout
ferruhcihan Nov 14, 2024
8bd4522
test: logout
ferruhcihan Nov 14, 2024
b088836
test: logout
ferruhcihan Nov 14, 2024
3d76075
test: logout
ferruhcihan Nov 14, 2024
0effc0f
test: logout
ferruhcihan Nov 14, 2024
5a1fe6f
test: logout
ferruhcihan Nov 14, 2024
c720cb9
test: logout
ferruhcihan Nov 14, 2024
3595ce1
test: logout
ferruhcihan Nov 14, 2024
eaa7061
fix: logout bug
ferruhcihan Nov 14, 2024
509bd95
feat: improve logout page comments
ferruhcihan Nov 15, 2024
3ea7ac6
Merge branch 'main' into APL-370
ferruhcihan Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import Project from 'pages/Project'
import Policy from 'pages/Policy'
import Maintenance from 'pages/Maintenance'
import PrivateRoute from 'components/AuthzRoute'
import Logout from 'pages/Logout'
import { HttpErrorBadRequest } from './utils/error'
import { NotistackProvider, SnackbarUtilsConfigurator } from './utils/snack'

Expand Down Expand Up @@ -175,6 +176,7 @@ function App() {
<PrivateRoute path='/teams/:teamId/services' component={Services} exact />
<PrivateRoute path='/teams/:teamId/services/:serviceId' component={Service} exact />
<PrivateRoute path='/maintenance' component={Maintenance} platformAdminRoute exact />
<Route path='/logout' component={Logout} />
<Route path='*'>
<Error error={new HttpErrorBadRequest()} />
</Route>
Expand Down
19 changes: 9 additions & 10 deletions src/components/AccountPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useDeleteCloudttyMutation } from 'redux/otomiApi'
import { useSession } from 'providers/Session'
import { getDomain, getEmailNoSymbols, getUserTeams } from 'layouts/Shell'
import { clearLocalStorage } from 'hooks/useLocalStorage'
import { useHistory } from 'react-router-dom'
import MenuPopover from './MenuPopover'
import { IconButtonAnimate } from './animate'
import SettingMode from './SettingMode'
Expand All @@ -14,6 +15,7 @@ type Props = {
}

export default function AccountPopover({ email }: Props) {
const history = useHistory()
const [open, setOpen] = useState<HTMLElement | null>(null)
const [del] = useDeleteCloudttyMutation()
const { user, oboTeamId } = useSession()
Expand All @@ -30,16 +32,13 @@ export default function AccountPopover({ email }: Props) {
setOpen(null)
}

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

return (
Expand Down
14 changes: 10 additions & 4 deletions src/components/Error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Helmet from 'react-helmet'
import { useTranslation } from 'react-i18next'
import { useAppDispatch, useAppSelector } from 'redux/hooks'
import { setError } from 'redux/reducers'
import Logout from 'pages/Logout'
import { ApiErrorUnauthorized, ApiErrorUnauthorizedNoGroups, HttpError } from '../utils/error'
import Iconify from './Iconify'

Expand All @@ -18,9 +19,8 @@ export default function ({ error }: Props): React.ReactElement {
// END HOOKS
const err = error ?? globalError
if (!err) return null
// redirect to login page if the error is a fetch error (session expired)
// automatically triggers Keycloak to route the user to the Keycloak login page
if (err?.status === 'FETCH_ERROR') window.location.href = '/'
// return the logout page if the error is a fetch error (session expired)
if (err?.status === 'FETCH_ERROR') return <Logout fetchError />
const { title, message, data, code, originalStatus, status } = err || {}
const errorMessage = title ? `${title}: ${message}` : message || data?.error
const errorCode = code || originalStatus || status || message || data?.error
Expand All @@ -36,6 +36,7 @@ export default function ({ error }: Props): React.ReactElement {
case 403:
icon = 'ic:baseline-do-not-disturb'
break
case 503:
case 504:
icon = 'ant-design:api-outlined'
break
Expand All @@ -50,7 +51,12 @@ export default function ({ error }: Props): React.ReactElement {
{text}
</Button>
)
if (code === 504 || err instanceof ApiErrorUnauthorized || err instanceof ApiErrorUnauthorizedNoGroups) {
if (
code === 503 ||
code === 504 ||
err instanceof ApiErrorUnauthorized ||
err instanceof ApiErrorUnauthorizedNoGroups
) {
return renderButton(t('Logout', { ns: 'error' }) as string, () => {
window.location.href = '/logout-otomi'
})
Expand Down
21 changes: 21 additions & 0 deletions src/pages/Logout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import LoadingScreen from 'components/LoadingScreen'
import React, { useEffect } from 'react'

interface Props {
fetchError?: boolean
}

export default function Logout({ fetchError = false }: Props): React.ReactElement {
// This component manages the logout process for users authenticated with Keycloak.
// - If a fetch error occurs, the page reloads automatically to handle potential session issues.
// - If no fetch error occurs, the user is redirected to the Keycloak logout page ('/logout-otomi' route).
// - On component unmount, the page reloads to ensure a clean and consistent state.
useEffect(() => {
if (fetchError) window.location.reload()
else window.location.href = '/logout-otomi'
return () => {
window.location.reload()
}
}, [fetchError])
return <LoadingScreen />
}
35 changes: 30 additions & 5 deletions src/providers/Session.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { skipToken } from '@reduxjs/toolkit/dist/query'
import { setSpec } from 'common/api-spec'
import LinkCommit from 'components/LinkCommit'
import LoadingScreen from 'components/LoadingScreen'
Expand All @@ -6,8 +7,10 @@ import MessageTekton from 'components/MessageTekton'
import MessageTrans from 'components/MessageTrans'
import { useLocalStorage } from 'hooks/useLocalStorage'
import { ProviderContext, SnackbarKey } from 'notistack'
import Logout from 'pages/Logout'
import React, { useContext, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import { useAppSelector } from 'redux/hooks'
import {
GetSessionApiResponse,
Expand All @@ -18,7 +21,12 @@ import {
useGetSettingsInfoQuery,
} from 'redux/otomiApi'
import { useSocket, useSocketEvent } from 'socket.io-react-hook'
import { ApiErrorGatewayTimeout, ApiErrorUnauthorized, ApiErrorUnauthorizedNoGroups } from 'utils/error'
import {
ApiErrorGatewayTimeout,
ApiErrorServiceUnavailable,
ApiErrorUnauthorized,
ApiErrorUnauthorizedNoGroups,
} from 'utils/error'
import snack from 'utils/snack'

export interface SessionContext extends GetSessionApiResponse {
Expand Down Expand Up @@ -90,17 +98,28 @@ type DroneBuildEvent = {
}

export default function SessionProvider({ children }: Props): React.ReactElement {
const { pathname } = useLocation()
const skipFetch = pathname === '/logout' || pathname === '/logout-otomi'
const [oboTeamId, setOboTeamId] = useLocalStorage<string>('oboTeamId', undefined)
const { data: session, isLoading: isLoadingSession, refetch: refetchSession } = useGetSessionQuery()
const {
data: session,
isLoading: isLoadingSession,
refetch: refetchSession,
error: sessionError,
} = useGetSessionQuery(skipFetch && skipToken)
const url = `${window.location.origin.replace(/^http/, 'ws')}`
const path = '/api/ws'
const { data: settings, isLoading: isLoadingSettings, refetch: refetchSettings } = useGetSettingsInfoQuery()
const {
data: settings,
isLoading: isLoadingSettings,
refetch: refetchSettings,
} = useGetSettingsInfoQuery(skipFetch && skipToken)
const {
data: apps,
isLoading: isLoadingApps,
refetch: refetchAppsEnabled,
} = useGetAppsQuery({ teamId: oboTeamId, picks: ['id', 'enabled'] }, { skip: !oboTeamId })
const { data: apiDocs, isLoading: isLoadingApiDocs, error: errorApiDocs } = useApiDocsQuery()
const { data: apiDocs, isLoading: isLoadingApiDocs, error: errorApiDocs } = useApiDocsQuery(skipFetch && skipToken)
const { socket, error: errorSocket } = useSocket({ url, path })
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const { lastMessage: lastDbMessage } = useSocketEvent<DbMessage>(socket, 'db')
Expand Down Expand Up @@ -278,7 +297,13 @@ export default function SessionProvider({ children }: Props): React.ReactElement
if (errorSocket)
keys.socket = snack.warning(`${t('Could not establish socket connection. Retrying...')}`, { key: keys.socket })
// no error and we stopped loading, so we can check the user
if (!session) throw new ApiErrorGatewayTimeout()
if (sessionError) {
const { originalStatus, status } = sessionError as any
if (originalStatus === 503) throw new ApiErrorServiceUnavailable()
if (originalStatus === 504) throw new ApiErrorGatewayTimeout()
// return the logout page if the error is a fetch error (session expired)
if (status === 'FETCH_ERROR') return <Logout fetchError />
}
if (!session.user.isPlatformAdmin && session.user.teams.length === 0) throw new ApiErrorUnauthorizedNoGroups()
if (isLoadingApiDocs || isLoadingApps || isLoadingSession || isLoadingSettings) return <LoadingScreen />
if (apiDocs) setSpec(apiDocs)
Expand Down
6 changes: 6 additions & 0 deletions src/utils/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class HttpErrorForbidden extends HttpError {
}
}

class ApiErrorServiceUnavailable extends HttpError {
constructor() {
super(e['The API could not be reached.'], 503)
}
}
class ApiErrorGatewayTimeout extends HttpError {
constructor() {
super(e['The API could not be reached.'], 504)
Expand Down Expand Up @@ -56,6 +61,7 @@ export {
HttpError,
HttpErrorBadRequest,
HttpErrorForbidden,
ApiErrorServiceUnavailable,
ApiErrorGatewayTimeout,
ApiErrorUnauthorized,
ApiErrorUnauthorizedNoGroups,
Expand Down