Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
4 changes: 4 additions & 0 deletions admin-ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ plugins_repo
.env.*
*.local

# Cursor
.cursor/
.cursorrules

# ESLint cache
.eslintcache
.vscode/*
Expand Down
13 changes: 8 additions & 5 deletions admin-ui/app/redux/api/backend-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { AppConfigResponse } from 'JansConfigApi'
import type {
ApiTokenResponse,
FetchUserInfoParams,
FetchUserInfoResult,
PolicyStoreApiResponse,
PutServerConfigPayload,
UserActionPayload,
UserIpAndLocationResponse,
Expand All @@ -13,6 +15,7 @@ import { devLogger } from '@/utils/devLogger'
export type {
ApiTokenResponse,
FetchUserInfoParams,
FetchUserInfoResult,
PutServerConfigPayload,
UserActionPayload,
UserIpAndLocationResponse,
Expand Down Expand Up @@ -55,15 +58,15 @@ export const getUserIpAndLocation = (): Promise<UserIpAndLocationResponse | -1>
})
}

// Retrieve user information
// Retrieve user information (OIDC userinfo endpoint returns JWT string)
export const fetchUserInformation = ({
userInfoEndpoint,
token_type,
access_token,
}: FetchUserInfoParams): Promise<Record<string, unknown> | -1> => {
}: FetchUserInfoParams): Promise<FetchUserInfoResult> => {
const headers = { Authorization: `${token_type} ${access_token}` }
return axios
.get<Record<string, unknown>>(userInfoEndpoint, { headers })
.get<string>(userInfoEndpoint, { headers })
.then((response) => response.data)
.catch((error) => {
devLogger.error('Problems fetching user information with the provided code.', error)
Expand Down Expand Up @@ -126,12 +129,12 @@ export const fetchApiTokenWithDefaultScopes = (): Promise<ApiTokenResponse> => {

export const fetchPolicyStore = (
token?: string,
): Promise<{ status?: number; data?: Record<string, string | number | boolean> }> => {
): Promise<{ status?: number; data?: PolicyStoreApiResponse }> => {
const config = token
? { headers: { Authorization: `Bearer ${token}` } }
: { withCredentials: true }
return axios
.get('/admin-ui/security/policyStore', config)
.get<PolicyStoreApiResponse>('/admin-ui/security/policyStore', config)
.then((response) => ({ status: response.status, data: response.data }))
.catch((error) => {
devLogger.error('Problems fetching policy store.', error)
Expand Down
7 changes: 7 additions & 0 deletions admin-ui/app/redux/api/types/BackendApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export interface FetchUserInfoParams {
access_token: string
}

export type FetchUserInfoResult = string | -1

/** Policy store API response shape */
export interface PolicyStoreApiResponse {
responseObject: string
}

/** Geolocation API response (geolocation-db.com) */
export interface UserIpAndLocationResponse {
[key: string]: unknown
Expand Down
15 changes: 14 additions & 1 deletion admin-ui/app/redux/sagas/LicenseSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
setValidatingFlow,
setApiDefaultToken,
setLicenseError,
setBackendStatus,
checkLicensePresent,
checkUserLicenceKey,
checkLicenseConfigValid,
Expand All @@ -32,17 +33,25 @@ import MauApi from 'Redux/api/MauApi'
import { getYearMonth } from '../../utils/Util'
import { devLogger } from '@/utils/devLogger'
import * as JansConfigApi from 'jans_config_api'
import type { SagaError } from './types/audit'
import type { ApiErrorLike, SagaError } from './types/audit'

let defaultToken: ApiTokenResponse | undefined

const getBackendStatusFromError = (error: unknown) => {
const err = error as ApiErrorLike
const statusCode = typeof err?.response?.status === 'number' ? err.response.status : null
const errorMessage = err?.response?.data?.responseMessage ?? err?.response?.data?.message ?? null
return { active: false as const, errorMessage: errorMessage ?? null, statusCode }
}

export function* getAccessToken() {
if (!defaultToken) {
try {
defaultToken = (yield call(fetchApiTokenWithDefaultScopes)) as ApiTokenResponse
yield put(setApiDefaultToken(defaultToken))
} catch (error) {
devLogger.error('Failed to fetch API token with default scopes', error)
yield put(setBackendStatus(getBackendStatusFromError(error)))
throw error
}
}
Expand Down Expand Up @@ -120,6 +129,10 @@ function* retrieveLicenseKey(_action?: { type: string }) {
yield put(checkLicensePresentResponse({ isLicenseValid: false }))
yield put(generateTrialLicenseResponse(null))
}
} else {
yield put(retrieveLicenseKeyResponse({ isNoValidLicenseKeyFound: true }))
yield put(checkLicensePresentResponse({ isLicenseValid: false }))
yield put(generateTrialLicenseResponse(null))
}
} catch (err) {
yield put(setLicenseError(getLicenseErrorMessage(err as Error | SagaError)))
Expand Down
15 changes: 6 additions & 9 deletions admin-ui/app/utils/ApiKeyRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,10 @@ const ApiKeyRedirect = ({
isNoValidLicenseKeyFound

const showRedirectingLoader =
!islicenseCheckResultLoaded ||
isConfigValid === null ||
(isConfigValid !== false &&
!isTimeout &&
isUnderThresholdLimit &&
backendStatus.active &&
!shouldShowApiKey)
isConfigValid !== false &&
(!islicenseCheckResultLoaded ||
isConfigValid === null ||
(!isTimeout && isUnderThresholdLimit && backendStatus.active && !shouldShowApiKey))

if (showRedirectingLoader) {
return (
Expand All @@ -75,9 +72,9 @@ const ApiKeyRedirect = ({
return (
<React.Fragment>
<Container>
{isConfigValid === false ? (
{isConfigValid === false && backendStatus.active ? (
<UploadSSA />
) : !isTimeout && isUnderThresholdLimit ? (
) : isConfigValid === false ? null : !isTimeout && isUnderThresholdLimit ? (
shouldShowApiKey ? (
<ApiKey />
) : null
Expand Down
23 changes: 15 additions & 8 deletions admin-ui/app/utils/AppAuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, type ReactNode } from 'react'
import React, { useState, useEffect, useRef, type ReactNode } from 'react'
import ApiKeyRedirect from './ApiKeyRedirect'
import { useLocation } from 'react-router'
import { NoHashQueryStringUtils, saveIssuer, getIssuer } from './TokenController'
Expand All @@ -25,7 +25,11 @@ import {
AuthorizationError,
} from '@openid/appauth'
import type { AuthorizationResponse } from '@openid/appauth'
import { fetchPolicyStore, fetchUserInformation } from 'Redux/api/backend-api'
import {
fetchPolicyStore,
fetchUserInformation,
type FetchUserInfoResult,
} from 'Redux/api/backend-api'
import { jwtDecode } from 'jwt-decode'
import type { UserInfo } from '@/redux/features/types/authTypes'

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

const hasDispatchedConfigCheck = useRef(false)
useEffect(() => {
const params = queryString.parse(location.search)
if (!(params.code && params.scope && params.state)) {
if (!(params.code && params.scope && params.state) && !hasDispatchedConfigCheck.current) {
hasDispatchedConfigCheck.current = true
dispatch(checkLicenseConfigValid(undefined))
}
}, [])
}, [dispatch])

useEffect(() => {
const params = queryString.parse(location.search)
Expand All @@ -99,7 +105,7 @@ export default function AppAuthProvider({ children }: Readonly<AppAuthProviderPr
if (hasSession) {
fetchPolicyStore()
.then((policyStoreResponse) => {
if (isMounted) {
if (isMounted && policyStoreResponse.data) {
const policyStoreJson = policyStoreResponse.data.responseObject
dispatch({
type: 'cedarPermissions/setPolicyStoreJson',
Expand Down Expand Up @@ -192,7 +198,7 @@ export default function AppAuthProvider({ children }: Readonly<AppAuthProviderPr
})

let authConfigs: AuthorizationServiceConfiguration | null = null
// Config is fetched after getAPIAccessToken (in saga) to avoid a second api-protection-token call

let idToken: string | undefined
let oauthAccessToken: string | undefined

Expand All @@ -210,8 +216,9 @@ export default function AppAuthProvider({ children }: Readonly<AppAuthProviderPr
token_type: token.tokenType,
})
})
.then((ujwt: string) => {
if (!ujwt) return
.then((value: FetchUserInfoResult) => {
if (value === -1) return
const ujwt = value

const decoded = jwtDecode<UserInfo>(ujwt)
dispatch(
Expand Down
10 changes: 9 additions & 1 deletion admin-ui/app/utils/styles/UploadSSA.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ const useStyles = makeStyles<{ themeColors: ThemeConfig }>()((theme, { themeColo
},
dropzone: {
'marginTop': theme.spacing(1),
'minHeight': '80px',
'display': 'flex',
'alignItems': 'center',
'justifyContent': 'center',
'padding': theme.spacing(2),
'boxSizing': 'border-box',
'borderRadius': `${MAPPING_SPACING.INFO_ALERT_BORDER_RADIUS}px`,
'border': `1px solid ${themeColors.infoAlert.border}`,
'background': themeColors.infoAlert.background,
Expand All @@ -30,7 +36,7 @@ const useStyles = makeStyles<{ themeColors: ThemeConfig }>()((theme, { themeColo
},
error: {
color: themeColors.errorColor,
fontSize: fontSizes.xl,
fontSize: fontSizes.content,
fontWeight: fontWeights.semiBold,
marginTop: theme.spacing(2),
display: 'block',
Expand All @@ -42,6 +48,8 @@ const useStyles = makeStyles<{ themeColors: ThemeConfig }>()((theme, { themeColo
fontStyle: 'normal',
fontWeight: fontWeights.medium,
lineHeight: lineHeights.tight,
margin: 0,
textAlign: 'center',
},
button: {
display: 'inline-flex',
Expand Down
Loading