diff --git a/apps/services/bff/src/app/modules/auth/auth.service.ts b/apps/services/bff/src/app/modules/auth/auth.service.ts index 4c8ee5b5b02b..282cbf868f7b 100644 --- a/apps/services/bff/src/app/modules/auth/auth.service.ts +++ b/apps/services/bff/src/app/modules/auth/auth.service.ts @@ -277,14 +277,24 @@ export class AuthService { }) } - let loginAttemptData: LoginAttemptData | undefined + const loginAttemptCacheKey = this.cacheService.createSessionKeyType( + 'attempt', + query.state, + ) + // Get login attempt data from the cache + const loginAttemptData = await this.cacheService.get( + loginAttemptCacheKey, + // Do not throw an error if the key is not found + false, + ) - try { - // Get login attempt from cache - loginAttemptData = await this.cacheService.get( - this.cacheService.createSessionKeyType('attempt', query.state), - ) + if (!loginAttemptData) { + this.logger.warn(this.cacheService.createKeyError(loginAttemptCacheKey)) + return this.redirectWithError(res) + } + + try { // Get tokens and user information from the authorization code const tokenResponse = await this.idsService.getTokens({ code: query.code, diff --git a/apps/web/screens/Grants/Grant/GrantSidebar.tsx b/apps/web/screens/Grants/Grant/GrantSidebar.tsx index c785d595a2b1..fa83f4377950 100644 --- a/apps/web/screens/Grants/Grant/GrantSidebar.tsx +++ b/apps/web/screens/Grants/Grant/GrantSidebar.tsx @@ -1,6 +1,14 @@ import { useMemo } from 'react' -import { Box, Button, LinkV2, Stack, Text } from '@island.is/island-ui/core' +import { + Box, + BoxProps, + Button, + LinkV2, + Stack, + Text, +} from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' import { Locale } from '@island.is/shared/types' import { isDefined } from '@island.is/shared/utils' import { InstitutionPanel } from '@island.is/web/components' @@ -8,7 +16,6 @@ import { Grant } from '@island.is/web/graphql/schema' import { LinkType, useLinkResolver } from '@island.is/web/hooks' import { m } from '../messages' -import { useLocale } from '@island.is/localization' import { generateStatusTag } from '../utils' interface Props { @@ -30,6 +37,20 @@ const generateLine = (heading: string, content?: React.ReactNode) => { ) } +const generateSidebarPanel = ( + data: Array, + background: BoxProps['background'], +) => { + if (!data) { + return undefined + } + return ( + + {data} + + ) +} + export const GrantSidebar = ({ grant, locale }: Props) => { const { linkResolver } = useLinkResolver() const { formatMessage } = useLocale() @@ -100,6 +121,7 @@ export const GrantSidebar = ({ grant, locale }: Props) => { return ( @@ -113,6 +135,35 @@ export const GrantSidebar = ({ grant, locale }: Props) => { [grant.files], ) + const supportLinksPanelData = useMemo( + () => + grant.supportLinks + ?.map((link) => { + if (!link.url || !link.text || !link.id) { + return null + } + return ( + + + + ) + }) + .filter(isDefined) ?? [], + [grant.supportLinks], + ) + return ( { img={grant.fund?.parentOrganization.logo?.url} locale={locale} /> - {detailPanelData.length ? ( - - {detailPanelData} - - ) : undefined} - {filesPanelData.length ? ( - - {filesPanelData} - - ) : undefined} + {generateSidebarPanel(detailPanelData, 'blue100')} + {generateSidebarPanel(filesPanelData, 'red100')} + {generateSidebarPanel(supportLinksPanelData, 'purple100')} ) } diff --git a/apps/web/screens/queries/Grants.ts b/apps/web/screens/queries/Grants.ts index 22b40ec4d2cf..59160d38847b 100644 --- a/apps/web/screens/queries/Grants.ts +++ b/apps/web/screens/queries/Grants.ts @@ -77,6 +77,12 @@ export const GET_GRANT_QUERY = gql` id title } + supportLinks { + id + text + url + date + } files { ...AssetFields } diff --git a/libs/cms/src/lib/generated/contentfulTypes.d.ts b/libs/cms/src/lib/generated/contentfulTypes.d.ts index a03f5f97171d..ec191a4968b0 100644 --- a/libs/cms/src/lib/generated/contentfulTypes.d.ts +++ b/libs/cms/src/lib/generated/contentfulTypes.d.ts @@ -1853,6 +1853,9 @@ export interface IGrantFields { /** Files */ grantFiles?: Asset[] | undefined + /** Support links */ + grantSupportLinks?: ILink[] | undefined + /** Category tags */ grantCategoryTags?: IGenericTag[] | undefined diff --git a/libs/cms/src/lib/models/grant.model.ts b/libs/cms/src/lib/models/grant.model.ts index 90e0b9a031bd..b1f21752a40a 100644 --- a/libs/cms/src/lib/models/grant.model.ts +++ b/libs/cms/src/lib/models/grant.model.ts @@ -7,6 +7,7 @@ import { mapDocument, SliceUnion } from '../unions/slice.union' import { Asset, mapAsset } from './asset.model' import { ReferenceLink, mapReferenceLink } from './referenceLink.model' import { Fund, mapFund } from './fund.model' +import { Link, mapLink } from './link.model' export enum GrantStatus { CLOSED, @@ -66,6 +67,9 @@ export class Grant { @CacheField(() => [Asset], { nullable: true }) files?: Array + @CacheField(() => [Link], { nullable: true }) + supportLinks?: Array + @CacheField(() => [GenericTag], { nullable: true }) categoryTags?: Array @@ -85,7 +89,6 @@ export const mapGrant = ({ fields, sys }: IGrant): Grant => ({ applicationUrl: fields.granApplicationUrl?.fields ? mapReferenceLink(fields.granApplicationUrl) : undefined, - specialEmphasis: fields.grantSpecialEmphasis ? mapDocument(fields.grantSpecialEmphasis, sys.id + ':special-emphasis') : [], @@ -117,6 +120,8 @@ export const mapGrant = ({ fields, sys }: IGrant): Grant => ({ : undefined, fund: fields.grantFund ? mapFund(fields.grantFund) : undefined, files: (fields.grantFiles ?? []).map((file) => mapAsset(file)) ?? [], + supportLinks: + (fields.grantSupportLinks ?? []).map((link) => mapLink(link)) ?? [], categoryTags: fields.grantCategoryTags ? fields.grantCategoryTags.map((tag) => mapGenericTag(tag)) : undefined, diff --git a/libs/react-spa/bff/src/lib/BffPoller.tsx b/libs/react-spa/bff/src/lib/BffPoller.tsx index 974098093ea9..86f1330a1a12 100644 --- a/libs/react-spa/bff/src/lib/BffPoller.tsx +++ b/libs/react-spa/bff/src/lib/BffPoller.tsx @@ -46,6 +46,7 @@ export const BffPoller = ({ const { signIn, bffUrlGenerator } = useAuth() const userInfo = useUserInfo() const { postMessage } = useBffBroadcaster() + const bffBaseUrl = bffUrlGenerator() const url = useMemo( () => bffUrlGenerator('/user', { refresh: 'true' }), @@ -86,12 +87,13 @@ export const BffPoller = ({ postMessage({ type: BffBroadcastEvents.NEW_SESSION, userInfo: newUser, + bffBaseUrl, }) newSessionCb() } } - }, [newUser, error, userInfo, signIn, postMessage, newSessionCb]) + }, [newUser, error, userInfo, signIn, postMessage, newSessionCb, bffBaseUrl]) return children } diff --git a/libs/react-spa/bff/src/lib/BffProvider.tsx b/libs/react-spa/bff/src/lib/BffProvider.tsx index f10534fc550e..a8917228a07e 100644 --- a/libs/react-spa/bff/src/lib/BffProvider.tsx +++ b/libs/react-spa/bff/src/lib/BffProvider.tsx @@ -43,25 +43,37 @@ export const BffProvider = ({ authState === 'logging-out' const isLoggedIn = authState === 'logged-in' const oldLoginPath = `${applicationBasePath}/login` + const bffBaseUrl = bffUrlGenerator() const { postMessage } = useBffBroadcaster((event) => { - if ( - isLoggedIn && - event.data.type === BffBroadcastEvents.NEW_SESSION && - isNewUser(state.userInfo, event.data.userInfo) - ) { - setSessionExpiredScreen(true) - } else if (event.data.type === BffBroadcastEvents.LOGOUT) { - // We will wait 1 seconds before we dispatch logout action. - // The reason is that IDS will not log the user out immediately. - // Note! The bff poller may have triggered logout by that time anyways. - setTimeout(() => { - dispatch({ - type: ActionType.LOGGED_OUT, - }) - - signIn() - }, 1000) + /** + * Filter broadcast events by matching BFF base url + * + * Since the Broadcaster sends messages to all tabs/windows/iframes + * sharing the same origin (domain), we need to explicitly check if + * the message belongs to our specific BFF instance by comparing base urls. + * This prevents handling events meant for other applications/contexts + * running on the same domain. + */ + if (event.data.bffBaseUrl === bffBaseUrl) { + if ( + isLoggedIn && + event.data.type === BffBroadcastEvents.NEW_SESSION && + isNewUser(state.userInfo, event.data.userInfo) + ) { + setSessionExpiredScreen(true) + } else if (event.data.type === BffBroadcastEvents.LOGOUT) { + // We will wait 1 seconds before we dispatch logout action. + // The reason is that IDS will not log the user out immediately. + // Note! The bff poller may have triggered logout by that time anyways. + setTimeout(() => { + dispatch({ + type: ActionType.LOGGED_OUT, + }) + + signIn() + }, 1000) + } } }) @@ -71,9 +83,10 @@ export const BffProvider = ({ postMessage({ type: BffBroadcastEvents.NEW_SESSION, userInfo: state.userInfo, + bffBaseUrl, }) } - }, [postMessage, state.userInfo, isLoggedIn]) + }, [postMessage, state.userInfo, isLoggedIn, bffBaseUrl]) /** * Builds authentication query parameters for login redirection: @@ -175,12 +188,13 @@ export const BffProvider = ({ // Broadcast to all tabs/windows/iframes that the user is logging out postMessage({ type: BffBroadcastEvents.LOGOUT, + bffBaseUrl, }) window.location.href = bffUrlGenerator('/logout', { sid: state.userInfo.profile.sid, }) - }, [bffUrlGenerator, postMessage, state.userInfo]) + }, [bffUrlGenerator, postMessage, state.userInfo, bffBaseUrl]) const switchUser = useCallback( (nationalId?: string) => { diff --git a/libs/react-spa/bff/src/lib/bff.hooks.ts b/libs/react-spa/bff/src/lib/bff.hooks.ts index 72d4b52a6805..019258b759b4 100644 --- a/libs/react-spa/bff/src/lib/bff.hooks.ts +++ b/libs/react-spa/bff/src/lib/bff.hooks.ts @@ -64,10 +64,12 @@ export enum BffBroadcastEvents { type NewSessionEvent = { type: BffBroadcastEvents.NEW_SESSION userInfo: BffUser + bffBaseUrl: string } type LogoutEvent = { type: BffBroadcastEvents.LOGOUT + bffBaseUrl: string } export type BffBroadcastEvent = NewSessionEvent | LogoutEvent