Skip to content

Commit df4eaf4

Browse files
fix(admin-ui): resolve navigation hang when redirecting to SSA page (#2672) (#2675)
* fix(admin-ui): resolve navigation hang when redirecting to SSA page (#2672) * coderabbit ai suggestions * show error page when Config API is unavailable instead of rendering SSA upload screen * show error page when Config API is unavailable instead of rendering SSA upload screen * show error page when Config API is unavailable instead of rendering SSA upload screen * code rabbit suggestions
1 parent 0168eda commit df4eaf4

File tree

7 files changed

+67
-24
lines changed

7 files changed

+67
-24
lines changed

admin-ui/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ plugins_repo
2222
.env.*
2323
*.local
2424

25+
# Cursor
26+
.cursor/
27+
.cursorrules
28+
2529
# ESLint cache
2630
.eslintcache
2731
.vscode/*

admin-ui/app/redux/api/backend-api.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { AppConfigResponse } from 'JansConfigApi'
22
import type {
33
ApiTokenResponse,
44
FetchUserInfoParams,
5+
FetchUserInfoResult,
6+
PolicyStoreApiResponse,
57
PutServerConfigPayload,
68
UserActionPayload,
79
UserIpAndLocationResponse,
@@ -13,6 +15,7 @@ import { devLogger } from '@/utils/devLogger'
1315
export type {
1416
ApiTokenResponse,
1517
FetchUserInfoParams,
18+
FetchUserInfoResult,
1619
PutServerConfigPayload,
1720
UserActionPayload,
1821
UserIpAndLocationResponse,
@@ -55,15 +58,15 @@ export const getUserIpAndLocation = (): Promise<UserIpAndLocationResponse | -1>
5558
})
5659
}
5760

58-
// Retrieve user information
61+
// Retrieve user information (OIDC userinfo endpoint returns JWT string)
5962
export const fetchUserInformation = ({
6063
userInfoEndpoint,
6164
token_type,
6265
access_token,
63-
}: FetchUserInfoParams): Promise<Record<string, unknown> | -1> => {
66+
}: FetchUserInfoParams): Promise<FetchUserInfoResult> => {
6467
const headers = { Authorization: `${token_type} ${access_token}` }
6568
return axios
66-
.get<Record<string, unknown>>(userInfoEndpoint, { headers })
69+
.get<string>(userInfoEndpoint, { headers })
6770
.then((response) => response.data)
6871
.catch((error) => {
6972
devLogger.error('Problems fetching user information with the provided code.', error)
@@ -126,12 +129,12 @@ export const fetchApiTokenWithDefaultScopes = (): Promise<ApiTokenResponse> => {
126129

127130
export const fetchPolicyStore = (
128131
token?: string,
129-
): Promise<{ status?: number; data?: Record<string, string | number | boolean> }> => {
132+
): Promise<{ status?: number; data?: PolicyStoreApiResponse }> => {
130133
const config = token
131134
? { headers: { Authorization: `Bearer ${token}` } }
132135
: { withCredentials: true }
133136
return axios
134-
.get('/admin-ui/security/policyStore', config)
137+
.get<PolicyStoreApiResponse>('/admin-ui/security/policyStore', config)
135138
.then((response) => ({ status: response.status, data: response.data }))
136139
.catch((error) => {
137140
devLogger.error('Problems fetching policy store.', error)

admin-ui/app/redux/api/types/BackendApi.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export interface FetchUserInfoParams {
3434
access_token: string
3535
}
3636

37+
export type FetchUserInfoResult = string | -1
38+
39+
/** Policy store API response shape */
40+
export interface PolicyStoreApiResponse {
41+
responseObject: string
42+
}
43+
3744
/** Geolocation API response (geolocation-db.com) */
3845
export interface UserIpAndLocationResponse {
3946
[key: string]: unknown

admin-ui/app/redux/sagas/LicenseSaga.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
setValidatingFlow,
1313
setApiDefaultToken,
1414
setLicenseError,
15+
setBackendStatus,
1516
checkLicensePresent,
1617
checkUserLicenceKey,
1718
checkLicenseConfigValid,
@@ -32,17 +33,29 @@ import MauApi from 'Redux/api/MauApi'
3233
import { getYearMonth } from '../../utils/Util'
3334
import { devLogger } from '@/utils/devLogger'
3435
import * as JansConfigApi from 'jans_config_api'
35-
import type { SagaError } from './types/audit'
36+
import type { ApiErrorLike, SagaError } from './types/audit'
3637

3738
let defaultToken: ApiTokenResponse | undefined
3839

40+
const getBackendStatusFromError = (error: unknown) => {
41+
const err = error as ApiErrorLike
42+
const statusCode = typeof err?.response?.status === 'number' ? err.response.status : null
43+
const errorMessage =
44+
err?.response?.data?.responseMessage ??
45+
err?.response?.data?.message ??
46+
(err instanceof Error ? err.message : error != null ? String(error) : 'Network error')
47+
return { active: false as const, errorMessage, statusCode }
48+
}
49+
3950
export function* getAccessToken() {
4051
if (!defaultToken) {
4152
try {
4253
defaultToken = (yield call(fetchApiTokenWithDefaultScopes)) as ApiTokenResponse
4354
yield put(setApiDefaultToken(defaultToken))
55+
yield put(setBackendStatus({ active: true, errorMessage: null, statusCode: null }))
4456
} catch (error) {
4557
devLogger.error('Failed to fetch API token with default scopes', error)
58+
yield put(setBackendStatus(getBackendStatusFromError(error)))
4659
throw error
4760
}
4861
}
@@ -120,6 +133,10 @@ function* retrieveLicenseKey(_action?: { type: string }) {
120133
yield put(checkLicensePresentResponse({ isLicenseValid: false }))
121134
yield put(generateTrialLicenseResponse(null))
122135
}
136+
} else {
137+
yield put(retrieveLicenseKeyResponse({ isNoValidLicenseKeyFound: true }))
138+
yield put(checkLicensePresentResponse({ isLicenseValid: false }))
139+
yield put(generateTrialLicenseResponse(null))
123140
}
124141
} catch (err) {
125142
yield put(setLicenseError(getLicenseErrorMessage(err as Error | SagaError)))

admin-ui/app/utils/ApiKeyRedirect.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,10 @@ const ApiKeyRedirect = ({
4545
isNoValidLicenseKeyFound
4646

4747
const showRedirectingLoader =
48-
!islicenseCheckResultLoaded ||
49-
isConfigValid === null ||
50-
(isConfigValid !== false &&
51-
!isTimeout &&
52-
isUnderThresholdLimit &&
53-
backendStatus.active &&
54-
!shouldShowApiKey)
48+
isConfigValid !== false &&
49+
(!islicenseCheckResultLoaded ||
50+
isConfigValid === null ||
51+
(!isTimeout && isUnderThresholdLimit && backendStatus.active && !shouldShowApiKey))
5552

5653
if (showRedirectingLoader) {
5754
return (
@@ -75,9 +72,9 @@ const ApiKeyRedirect = ({
7572
return (
7673
<React.Fragment>
7774
<Container>
78-
{isConfigValid === false ? (
75+
{isConfigValid === false && backendStatus.active ? (
7976
<UploadSSA />
80-
) : !isTimeout && isUnderThresholdLimit ? (
77+
) : isConfigValid === false ? null : !isTimeout && isUnderThresholdLimit ? (
8178
shouldShowApiKey ? (
8279
<ApiKey />
8380
) : null

admin-ui/app/utils/AppAuthProvider.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, type ReactNode } from 'react'
1+
import React, { useState, useEffect, useRef, type ReactNode } from 'react'
22
import ApiKeyRedirect from './ApiKeyRedirect'
33
import { useLocation } from 'react-router'
44
import { NoHashQueryStringUtils, saveIssuer, getIssuer } from './TokenController'
@@ -25,7 +25,11 @@ import {
2525
AuthorizationError,
2626
} from '@openid/appauth'
2727
import type { AuthorizationResponse } from '@openid/appauth'
28-
import { fetchPolicyStore, fetchUserInformation } from 'Redux/api/backend-api'
28+
import {
29+
fetchPolicyStore,
30+
fetchUserInformation,
31+
type FetchUserInfoResult,
32+
} from 'Redux/api/backend-api'
2933
import { jwtDecode } from 'jwt-decode'
3034
import type { UserInfo } from '@/redux/features/types/authTypes'
3135

@@ -77,12 +81,14 @@ export default function AppAuthProvider({ children }: Readonly<AppAuthProviderPr
7781
}
7882
}, [dispatch, hasSession, userinfo, userinfo_jwt])
7983

84+
const hasDispatchedConfigCheck = useRef(false)
8085
useEffect(() => {
8186
const params = queryString.parse(location.search)
82-
if (!(params.code && params.scope && params.state)) {
87+
if (!(params.code && params.scope && params.state) && !hasDispatchedConfigCheck.current) {
88+
hasDispatchedConfigCheck.current = true
8389
dispatch(checkLicenseConfigValid(undefined))
8490
}
85-
}, [])
91+
}, [dispatch])
8692

8793
useEffect(() => {
8894
const params = queryString.parse(location.search)
@@ -99,7 +105,7 @@ export default function AppAuthProvider({ children }: Readonly<AppAuthProviderPr
99105
if (hasSession) {
100106
fetchPolicyStore()
101107
.then((policyStoreResponse) => {
102-
if (isMounted) {
108+
if (isMounted && policyStoreResponse.data) {
103109
const policyStoreJson = policyStoreResponse.data.responseObject
104110
dispatch({
105111
type: 'cedarPermissions/setPolicyStoreJson',
@@ -192,7 +198,7 @@ export default function AppAuthProvider({ children }: Readonly<AppAuthProviderPr
192198
})
193199

194200
let authConfigs: AuthorizationServiceConfiguration | null = null
195-
// Config is fetched after getAPIAccessToken (in saga) to avoid a second api-protection-token call
201+
196202
let idToken: string | undefined
197203
let oauthAccessToken: string | undefined
198204

@@ -210,8 +216,9 @@ export default function AppAuthProvider({ children }: Readonly<AppAuthProviderPr
210216
token_type: token.tokenType,
211217
})
212218
})
213-
.then((ujwt: string) => {
214-
if (!ujwt) return
219+
.then((value: FetchUserInfoResult) => {
220+
if (value === -1) return
221+
const ujwt = value
215222

216223
const decoded = jwtDecode<UserInfo>(ujwt)
217224
dispatch(

admin-ui/app/utils/styles/UploadSSA.style.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ const useStyles = makeStyles<{ themeColors: ThemeConfig }>()((theme, { themeColo
1919
},
2020
dropzone: {
2121
'marginTop': theme.spacing(1),
22+
'minHeight': '80px',
23+
'display': 'flex',
24+
'alignItems': 'center',
25+
'justifyContent': 'center',
26+
'padding': theme.spacing(2),
27+
'boxSizing': 'border-box',
2228
'borderRadius': `${MAPPING_SPACING.INFO_ALERT_BORDER_RADIUS}px`,
2329
'border': `1px solid ${themeColors.infoAlert.border}`,
2430
'background': themeColors.infoAlert.background,
@@ -30,7 +36,7 @@ const useStyles = makeStyles<{ themeColors: ThemeConfig }>()((theme, { themeColo
3036
},
3137
error: {
3238
color: themeColors.errorColor,
33-
fontSize: fontSizes.xl,
39+
fontSize: fontSizes.content,
3440
fontWeight: fontWeights.semiBold,
3541
marginTop: theme.spacing(2),
3642
display: 'block',
@@ -42,6 +48,8 @@ const useStyles = makeStyles<{ themeColors: ThemeConfig }>()((theme, { themeColo
4248
fontStyle: 'normal',
4349
fontWeight: fontWeights.medium,
4450
lineHeight: lineHeights.tight,
51+
margin: 0,
52+
textAlign: 'center',
4553
},
4654
button: {
4755
display: 'inline-flex',

0 commit comments

Comments
 (0)