diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts index a969cf40351b..729645bb5f82 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts @@ -606,6 +606,7 @@ export class InternalCaseService { ? { name: theCase.prosecutor.name, nationalId: theCase.prosecutor.nationalId, + email: theCase.prosecutor.email, } : undefined, ) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts index 3236f05eb4de..c7b01baee552 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/internalCaseController/deliverIndictmentInfoToCourt.spec.ts @@ -54,7 +54,11 @@ describe('InternalCaseController - Deliver indictment info to court', () => { { eventType: EventType.INDICTMENT_CONFIRMED, created: indictmentDate }, ], defendants: [{ name: 'Test Ákærði', nationalId: '1234567890' }], - prosecutor: { name: 'Test Sækjandi', nationalId: '0101010101' }, + prosecutor: { + name: 'Test Sækjandi', + nationalId: '0101010101', + email: 'prosecutor@omnitrix.is', + }, } as Case let mockCourtService: CourtService diff --git a/apps/judicial-system/backend/src/app/modules/court/court.service.ts b/apps/judicial-system/backend/src/app/modules/court/court.service.ts index 7113dd6769b0..df13b35a90be 100644 --- a/apps/judicial-system/backend/src/app/modules/court/court.service.ts +++ b/apps/judicial-system/backend/src/app/modules/court/court.service.ts @@ -336,6 +336,9 @@ export class CourtService { ) const isIndictment = isIndictmentCase(type) + const policeCaseNumber = policeCaseNumbers[0] + ? policeCaseNumbers[0].replace(/-/g, '') + : '' return await this.courtClientService.createCase(courtId, { caseType: isIndictment ? 'S - Ákærumál' : 'R - Rannsóknarmál', @@ -344,7 +347,7 @@ export class CourtService { receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: isIndictment ? 'Sakamál' : 'Rannsóknarhagsmunir', // TODO: pass in all policeCaseNumbers when CourtService supports it - sourceNumber: policeCaseNumbers[0] ? policeCaseNumbers[0] : '', + sourceNumber: policeCaseNumber, }) } catch (reason) { if (reason instanceof ServiceUnavailableException) { @@ -569,14 +572,17 @@ export class CourtService { policeCaseNumber?: string, subtypes?: string[], defendants?: { name?: string; nationalId?: string }[], - prosecutor?: { name?: string; nationalId?: string }, + prosecutor?: { name?: string; nationalId?: string; email?: string }, ): Promise { try { const subject = `${courtName} - ${courtCaseNumber} - upplýsingar` + + const sanitizedPoliceCaseNumber = policeCaseNumber?.replace(/-/g, '') + const content = JSON.stringify({ receivedByCourtDate, indictmentDate, - policeCaseNumber, + sanitizedPoliceCaseNumber, subtypes, defendants, prosecutor, diff --git a/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts b/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts index a32798d80b4e..5b98cb354570 100644 --- a/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/court/test/createCourtCase.spec.ts @@ -105,7 +105,7 @@ describe('CourtService - Create court case', () => { status: 'Skráð', receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Rannsóknarhagsmunir', - sourceNumber: policeCaseNumbers[0], + sourceNumber: policeCaseNumbers[0].replace(/-/g, ''), }, ) }) @@ -146,7 +146,7 @@ describe('CourtService - Create court case', () => { status: 'Skráð', receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Sakamál', - sourceNumber: policeCaseNumbers[0], + sourceNumber: policeCaseNumbers[0].replace(/-/g, ''), }, ) }) @@ -183,7 +183,7 @@ describe('CourtService - Create court case', () => { status: 'Skráð', receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Rannsóknarhagsmunir', - sourceNumber: policeCaseNumbers[0], + sourceNumber: policeCaseNumbers[0].replace(/-/g, ''), }) }) }) @@ -218,7 +218,7 @@ describe('CourtService - Create court case', () => { status: 'Skráð', receivalDate: formatISO(receivalDate, { representation: 'date' }), basedOn: 'Rannsóknarhagsmunir', - sourceNumber: policeCaseNumbers[0], + sourceNumber: policeCaseNumbers[0].replace(/-/g, ''), }) }) }) diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.strings.ts b/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.strings.ts index 884252537f62..d231db70e78f 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.strings.ts +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.strings.ts @@ -36,6 +36,13 @@ const strings = defineMessages({ description: 'Notaður sem texti í valmöguleika fyrir það þegar ekki skal birta dómdfellda dóminn.', }, + serviceRequirementNotRequiredTooltip: { + id: 'judicial.system.core:court.indictments.completed.service_requirement_not_required_tooltip', + defaultMessage: + 'Ekki þarf að birta dóm þar sem sektarfjárhæð er lægri en sem nemur áfrýjunarfjárhæð í einkamáli kr. 1.355.762. Gildir frá 01.01.2024', + description: + 'Notað sem tooltip í valmöguleika fyrir það þegar ekki skal birta dómdfellda dóminn.', + }, serviceRequirementNotApplicable: { id: 'judicial.system.core:court.indictments.completed.service_requirement_not_applicable', defaultMessage: 'Dómfelldi var viðstaddur dómsuppkvaðningu', diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx index d988fde76856..88ac21c155da 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx @@ -291,6 +291,9 @@ const Completed: FC = () => { large backgroundColor="white" label={formatMessage(strings.serviceRequirementNotRequired)} + tooltip={formatMessage( + strings.serviceRequirementNotRequiredTooltip, + )} /> 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