From 72d681bdd380ec9d96e34a22cdd78117b24dd7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81sd=C3=ADs=20Erna=20Gu=C3=B0mundsd=C3=B3ttir?= Date: Thu, 31 Oct 2024 13:40:32 +0000 Subject: [PATCH 01/12] fix(my-pages): law and order breadcrumbs + button filter (#16668) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/service-portal/core/src/components/Filter/Filter.css.ts | 2 +- libs/service-portal/core/src/components/Filter/Filter.tsx | 1 + libs/service-portal/core/src/lib/messages.ts | 4 ++++ libs/service-portal/law-and-order/src/lib/navigation.ts | 3 +-- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/libs/service-portal/core/src/components/Filter/Filter.css.ts b/libs/service-portal/core/src/components/Filter/Filter.css.ts index 5b4792c2e7e0..fd90a32391f9 100644 --- a/libs/service-portal/core/src/components/Filter/Filter.css.ts +++ b/libs/service-portal/core/src/components/Filter/Filter.css.ts @@ -18,6 +18,6 @@ export const popoverContainer = style({ export const lgBtn = style({}) -globalStyle(`${lgBtn} span`, { +globalStyle(`${lgBtn} > span`, { height: '100%', }) diff --git a/libs/service-portal/core/src/components/Filter/Filter.tsx b/libs/service-portal/core/src/components/Filter/Filter.tsx index cbe6f6045117..a1c56538fabd 100644 --- a/libs/service-portal/core/src/components/Filter/Filter.tsx +++ b/libs/service-portal/core/src/components/Filter/Filter.tsx @@ -136,6 +136,7 @@ export const Filter: FC> = ({ icon="filter" fluid nowrap + size="small" > {popover.visible ? labelClose : labelOpen} diff --git a/libs/service-portal/core/src/lib/messages.ts b/libs/service-portal/core/src/lib/messages.ts index 7bb01507e4e2..eb534bda0c73 100644 --- a/libs/service-portal/core/src/lib/messages.ts +++ b/libs/service-portal/core/src/lib/messages.ts @@ -1656,6 +1656,10 @@ export const m = defineMessages({ defaultMessage: 'Hér eru upplýsingar og yfirlit yfir mál sem þú átt hjá dómskerfinu.', }, + myCourtCases: { + id: 'service.portal:my-court-cases', + defaultMessage: 'Mín dómsmál', + }, courtCases: { id: 'service.portal:court-cases', defaultMessage: 'Dómsmál', diff --git a/libs/service-portal/law-and-order/src/lib/navigation.ts b/libs/service-portal/law-and-order/src/lib/navigation.ts index e5b58bb4798e..f929f58a9c41 100644 --- a/libs/service-portal/law-and-order/src/lib/navigation.ts +++ b/libs/service-portal/law-and-order/src/lib/navigation.ts @@ -15,9 +15,8 @@ export const lawAndOrderNavigation: PortalNavigationItem = { path: LawAndOrderPaths.Overview, }, { - name: m.courtCases, + name: m.myCourtCases, path: LawAndOrderPaths.CourtCases, - breadcrumbHide: true, children: [ { name: m.courtCases, From 319f2bd4e4a8364e62122d1947490921186e2d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafn=20=C3=81rnason?= Date: Thu, 31 Oct 2024 14:53:53 +0000 Subject: [PATCH 02/12] feat(system-e2e): Endorsements tests refresh (#16651) * admin portal test fix * chore: nx format:write update dirty files * service portal test fix * chore: nx format:write update dirty files * service portal test fix --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../admin-portal/smoke/endorsements.spec.ts | 36 +++++++------------ .../service-portal/smoke/endorsements.spec.ts | 25 ++++++++----- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/apps/system-e2e/src/tests/islandis/admin-portal/smoke/endorsements.spec.ts b/apps/system-e2e/src/tests/islandis/admin-portal/smoke/endorsements.spec.ts index a65263282b98..fc267ff21f34 100644 --- a/apps/system-e2e/src/tests/islandis/admin-portal/smoke/endorsements.spec.ts +++ b/apps/system-e2e/src/tests/islandis/admin-portal/smoke/endorsements.spec.ts @@ -3,6 +3,7 @@ import { icelandicAndNoPopupUrl, urls } from '../../../../support/urls' import { session } from '../../../../support/session' const homeUrl = `${urls.islandisBaseUrl}/stjornbord` +const delegationTarget = 'text="65° ARTIC ehf."' test.use({ baseURL: urls.islandisBaseUrl }) test.describe('Admin portal (Endorsements)', () => { @@ -29,8 +30,8 @@ test.describe('Admin portal (Endorsements)', () => { test('Open old endorsement list and go back', async ({ page }) => { test.slow() await page.goto(icelandicAndNoPopupUrl(homeUrl)) - await page.getByTestId('active-module-name').click() - await page.getByRole('link', { name: 'Undirskriftalistar' }).click() + await page.click(delegationTarget) + await page.click('[title="Undirskriftalistar"]') await page.getByRole('tab', { name: 'Liðnir listar' }).click() await page.getByRole('button', { name: 'Skoða lista' }).first().click() @@ -41,6 +42,11 @@ test.describe('Admin portal (Endorsements)', () => { }) test('Update old endorsement list', async ({ page }) => { + test.slow() + await page.goto(icelandicAndNoPopupUrl(homeUrl)) + await page.click(delegationTarget) + await page.click('[title="Undirskriftalistar"]') + await page.getByRole('tab', { name: 'Liðnir listar' }).click() await page.getByRole('button', { name: 'Skoða lista' }).first().click() await page.getByRole('button', { name: 'Uppfæra lista' }).click() @@ -49,28 +55,12 @@ test.describe('Admin portal (Endorsements)', () => { ).toBeVisible() }) - test.skip('See locked lists are present and locked', async ({ page }) => { - await page.getByRole('tab', { name: 'Læstir listar' }).click() - const lockedLists = page.getByRole('button', { name: 'Skoða lista' }) - await expect(lockedLists).toHaveCountGreaterThan(1) - await lockedLists.first().click() - await expect( - page.getByRole('alert', { name: 'Listi er læstur' }), - ).toBeVisible() - }) - - test('Go back to overview', async ({ page }) => { - await page.getByTestId('active-module-name').click() - await page - .getByRole('menu', { name: 'Stjórnborðs valmynd' }) - .getByRole('link', { name: 'Yfirlit' }) - .click() - await expect( - page.getByRole('heading', { name: 'Stjórnborð Ísland.is' }), - ).toBeVisible() - }) - test('Access and edit a list', async ({ page }) => { + test.slow() + await page.goto(icelandicAndNoPopupUrl(homeUrl)) + await page.click(delegationTarget) + await page.click('[title="Undirskriftalistar"]') + // Setup await page.getByRole('tab', { name: 'Liðnir listar' }).click() diff --git a/apps/system-e2e/src/tests/islandis/service-portal/smoke/endorsements.spec.ts b/apps/system-e2e/src/tests/islandis/service-portal/smoke/endorsements.spec.ts index fa3940d1e428..a67b9e593bba 100644 --- a/apps/system-e2e/src/tests/islandis/service-portal/smoke/endorsements.spec.ts +++ b/apps/system-e2e/src/tests/islandis/service-portal/smoke/endorsements.spec.ts @@ -25,16 +25,23 @@ test.describe('Endorsements', () => { const page = await context.newPage() await disableI18n(page) - // Navigate to the specified page - await page.goto(icelandicAndNoPopupUrl('/minarsidur/min-gogn/listar')) + const timeout = 10000 - // Check for ui things - await expect( - page.locator('button:text("Stofna nýjan lista")'), - ).toBeVisible() + await page.goto(icelandicAndNoPopupUrl('/minarsidur/min-gogn/listar')) - // check for tabs - await expect(page.locator('button:text("Virkir listar")')).toBeVisible() - await expect(page.locator('button:text("Liðnir listar")')).toBeVisible() + await Promise.all([ + expect( + page.getByRole('button', { name: 'Stofna nýjan lista' }), + ).toBeVisible({ timeout }), + expect( + page.getByRole('link', { name: 'Almennir undirskriftalistar' }), + ).toBeVisible({ timeout }), + expect(page.getByRole('tab', { name: 'Virkir listar' })).toBeVisible({ + timeout, + }), + expect(page.getByRole('tab', { name: 'Liðnir listar' })).toBeVisible({ + timeout, + }), + ]) }) }) From 4023c21b10e2132683ecb6f44289fec3a526f139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81sd=C3=ADs=20Erna=20Gu=C3=B0mundsd=C3=B3ttir?= Date: Thu, 31 Oct 2024 14:59:17 +0000 Subject: [PATCH 03/12] fix(my-pages): wrap tag (#16675) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../core/src/components/ActionCard/ActionCard.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/service-portal/core/src/components/ActionCard/ActionCard.tsx b/libs/service-portal/core/src/components/ActionCard/ActionCard.tsx index 29c012e7cd48..236a75fd2269 100644 --- a/libs/service-portal/core/src/components/ActionCard/ActionCard.tsx +++ b/libs/service-portal/core/src/components/ActionCard/ActionCard.tsx @@ -11,11 +11,11 @@ import { TagVariant, Text, } from '@island.is/island-ui/core' +import cn from 'classnames' import * as React from 'react' import { CardLoader, isExternalLink } from '../..' -import * as styles from './ActionCard.css' import LinkResolver from '../LinkResolver/LinkResolver' -import cn from 'classnames' +import * as styles from './ActionCard.css' type ActionCardProps = { capitalizeHeading?: boolean @@ -349,6 +349,7 @@ export const ActionCard: React.FC> = ({ flexDirection="row" justifyContent="spaceBetween" alignItems={['flexStart', 'flexStart', 'flexEnd']} + flexWrap="wrap" > {heading && ( Date: Thu, 31 Oct 2024 15:13:46 +0000 Subject: [PATCH 04/12] fix: added missing grant to auth ids api for user notification (#16680) * fix: added missing grant to auth ids api for user notification * chore: nx format:write update dirty files --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/services/auth/ids-api/infra/ids-api.ts | 1 + charts/identity-server/values.dev.yaml | 15 +++++++++------ charts/identity-server/values.prod.yaml | 15 +++++++++------ charts/identity-server/values.staging.yaml | 15 +++++++++------ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/apps/services/auth/ids-api/infra/ids-api.ts b/apps/services/auth/ids-api/infra/ids-api.ts index 14ac4f6c60bf..470a1765ecd1 100644 --- a/apps/services/auth/ids-api/infra/ids-api.ts +++ b/apps/services/auth/ids-api/infra/ids-api.ts @@ -122,6 +122,7 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => { min: 2, max: 15, }) + .grantNamespaces('user-notification') } const cleanupId = 'services-auth-ids-api-cleanup' diff --git a/charts/identity-server/values.dev.yaml b/charts/identity-server/values.dev.yaml index c9f3dca4c65a..c7f2f34b4f1c 100644 --- a/charts/identity-server/values.dev.yaml +++ b/charts/identity-server/values.dev.yaml @@ -123,8 +123,9 @@ identity-server: SessionsApiSettings__BaseAddress: 'http://web-services-sessions.services-sessions.svc.cluster.local' files: - 'ids-signing.pfx' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -431,8 +432,9 @@ services-auth-ids-api: XROAD_TJODSKRA_MEMBER_CODE: '10001' XROAD_TLS_BASE_PATH: 'https://securityserver.dev01.devland.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.dev01.devland.is/r1/IS-DEV' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -533,8 +535,9 @@ services-auth-ids-api-cleanup: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=921 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml index 58e9d76a535b..39262957015d 100644 --- a/charts/identity-server/values.prod.yaml +++ b/charts/identity-server/values.prod.yaml @@ -121,8 +121,9 @@ identity-server: SessionsApiSettings__BaseAddress: 'https://sessions-api.internal.island.is' files: - 'ids-signing.pfx' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -428,8 +429,9 @@ services-auth-ids-api: XROAD_TJODSKRA_MEMBER_CODE: '6503760649' XROAD_TLS_BASE_PATH: 'https://securityserver.island.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.island.is/r1/IS' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -530,8 +532,9 @@ services-auth-ids-api-cleanup: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=921 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 diff --git a/charts/identity-server/values.staging.yaml b/charts/identity-server/values.staging.yaml index 07afb0776b4c..4ced0775ec0b 100644 --- a/charts/identity-server/values.staging.yaml +++ b/charts/identity-server/values.staging.yaml @@ -123,8 +123,9 @@ identity-server: SessionsApiSettings__BaseAddress: 'http://web-services-sessions.services-sessions.svc.cluster.local' files: - 'ids-signing.pfx' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -431,8 +432,9 @@ services-auth-ids-api: XROAD_TJODSKRA_MEMBER_CODE: '6503760649' XROAD_TLS_BASE_PATH: 'https://securityserver.staging01.devland.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.staging01.devland.is/r1/IS-TEST' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -533,8 +535,9 @@ services-auth-ids-api-cleanup: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=921 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 From 7adc95ab373562f9edca6fe46ea18ac5155ecdf7 Mon Sep 17 00:00:00 2001 From: birkirkristmunds <142495885+birkirkristmunds@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:30:29 +0000 Subject: [PATCH 05/12] fix(skilavottord): Improve error handling when creating access and companies (#16559) * TS-931 Fix error handling when creating a new access and companies * TS-931 Fix error handling when updating access * TS-931 Improve the error msg to the user --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../screens/AccessControl/AccessControl.tsx | 6 +++++ .../RecyclingCompanyCreate.tsx | 3 +++ .../accessControl/accessControl.resolver.ts | 25 ++++++++++++++----- .../recyclingPartner.resolver.ts | 2 +- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/skilavottord/web/screens/AccessControl/AccessControl.tsx b/apps/skilavottord/web/screens/AccessControl/AccessControl.tsx index aee25f9c033a..9f274e15eb52 100644 --- a/apps/skilavottord/web/screens/AccessControl/AccessControl.tsx +++ b/apps/skilavottord/web/screens/AccessControl/AccessControl.tsx @@ -135,6 +135,9 @@ const AccessControl: FC> = () => { const [createSkilavottordAccessControl] = useMutation( CreateSkilavottordAccessControlMutation, { + onError(_) { + // Hide Runtime error message. The error message is already shown to the user in toast. + }, refetchQueries: [ { query: SkilavottordAccessControlsQuery, @@ -145,6 +148,9 @@ const AccessControl: FC> = () => { const [updateSkilavottordAccessControl] = useMutation( UpdateSkilavottordAccessControlMutation, { + onError(_) { + // Hide Runtime error message. The error message is already shown to the user in toast. + }, refetchQueries: [ { query: SkilavottordAccessControlsQuery, diff --git a/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyCreate/RecyclingCompanyCreate.tsx b/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyCreate/RecyclingCompanyCreate.tsx index 20908b593766..a3d6c84b134e 100644 --- a/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyCreate/RecyclingCompanyCreate.tsx +++ b/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyCreate/RecyclingCompanyCreate.tsx @@ -50,6 +50,9 @@ const RecyclingCompanyCreate: FC> = () => { const [createSkilavottordRecyclingPartner] = useMutation( CreateSkilavottordRecyclingPartnerMutation, { + onError: (_) => { + // Hide Runtime error message. The error message is already shown to the user in toast. + }, refetchQueries: [ { query: SkilavottordAllRecyclingPartnersQuery, diff --git a/apps/skilavottord/ws/src/app/modules/accessControl/accessControl.resolver.ts b/apps/skilavottord/ws/src/app/modules/accessControl/accessControl.resolver.ts index 666861e944ca..61dffa84992a 100644 --- a/apps/skilavottord/ws/src/app/modules/accessControl/accessControl.resolver.ts +++ b/apps/skilavottord/ws/src/app/modules/accessControl/accessControl.resolver.ts @@ -1,16 +1,20 @@ -import { Query, Resolver, Args, Mutation } from '@nestjs/graphql' -import { NotFoundException, BadRequestException } from '@nestjs/common' +import { + BadRequestException, + ConflictException, + NotFoundException, +} from '@nestjs/common' +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' import { ApolloError } from 'apollo-server-express' -import { Authorize, CurrentUser, User, Role } from '../auth' +import { Authorize, CurrentUser, Role, User } from '../auth' -import { AccessControlModel } from './accessControl.model' -import { AccessControlService } from './accessControl.service' import { - UpdateAccessControlInput, CreateAccessControlInput, DeleteAccessControlInput, + UpdateAccessControlInput, } from './accessControl.input' +import { AccessControlModel } from './accessControl.model' +import { AccessControlService } from './accessControl.service' @Authorize({ roles: [Role.developer, Role.recyclingFund, Role.recyclingCompanyAdmin], @@ -77,6 +81,15 @@ export class AccessControlResolver { this.verifyDeveloperAccess(user, input.role) this.verifyRecyclingCompanyAdminInput(input.role, user) this.verifyRecyclingCompanyInput(input) + + const access = await this.accessControlService.findOne(input.nationalId) + + if (access) { + throw new ConflictException( + `Access with the national id ${input.nationalId} already exists`, + ) + } + return this.accessControlService.createAccess(input) } diff --git a/apps/skilavottord/ws/src/app/modules/recyclingPartner/recyclingPartner.resolver.ts b/apps/skilavottord/ws/src/app/modules/recyclingPartner/recyclingPartner.resolver.ts index 9efc48c9c865..66fb91a354a4 100644 --- a/apps/skilavottord/ws/src/app/modules/recyclingPartner/recyclingPartner.resolver.ts +++ b/apps/skilavottord/ws/src/app/modules/recyclingPartner/recyclingPartner.resolver.ts @@ -56,7 +56,7 @@ export class RecyclingPartnerResolver { if (recyclingPartner) { throw new ConflictException( - 'Recycling partner with that id already exists', + `Recycling partner with the id ${input.companyId} already exists`, ) } From d0c9471bef4360cf6134ae033378fd2d6ef8d760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sn=C3=A6r=20Seljan=20=C3=9E=C3=B3roddsson?= <112904566+snaerseljan@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:12:24 +0000 Subject: [PATCH 06/12] feat(services-bff): BFF (Backend for Frontend) (#15835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial bootstrap for bff * environment audit not optional * Add infra file for admin-portal * Auth login controller and service implemented * Updates to auth and user modules and services * Update project readme * Add secret * Remove unnecessary config * Fix env config for ids * Remove unused util isString * chore: nx format:write update dirty files * Rename dto to queries * Add logout flow * Finalize logout logic * Remove proxy * Move type from service to type file * chore: nx format:write update dirty files * Delete libs/auth/react/src/lib/bff/BFFProvider.tsx * Delete libs/auth/react/src/lib/bff/BFFContext.tsx * Small refactor in auth service * Small refactor in test * Small refactor * Fix esbuild * Add scope * chore: nx format:write update dirty files * Updates to bff service and client. WIP * chore: nx format:write update dirty files * Finishing proxy handling by the bff * Add scope to token response for backwards compatibility * Encrypted tokens, hooks update for admin portal, switch user, proxy updated * feat(proxy-api): Support for proxy api, hooks update, regulations download connection with bff * Better naming env * Rename secrets in infra * Refactor after self review * Fix test and env cleanup * Fix user menu test * Updates to environment and config * Update infra allowed external api urls to be hard coded * Simplify client urls with bff postfix in it * Add ingress to project and remove logout redirect path in favour of client base url * Add docker express to services bff * update config simpler syntax * chore: nx format:write update dirty files * Update config and redis dev setup * Update crypto service to include algorithm in the encryption, explain better in comments what encrypt/decrypt is doing and update crypto test to not use mock * Remove CORS entirely in favour of client proxy config * Update error handling in bff backend, refactor infra and handle error query param in client * When proxy service errors then handle as unauthorized. Update targetUrl to be defensive, i.e. no undefined possible. * Remove unnecessary Uint8Array conversion * Simplify the BFFUser object to not have dateOfBirth and remove double scope field which was due to backwards compatibility * Update cookies to share constants, update options to be more secure * access token expire time latency by 5 sec * remove omit * Update user profile cache ttl * update cache ttl again and rename baseUrl to issuerUrl in ids service * reaname var * remove params from cache attempt that where not used in the callback * Clean up old session in login callback if it exists * Fix login callback cache clean up and revoke refresh token * Update logout flow to clean up, revoke tokens and better validation. Also deletes the logout callback * remove unused import * Simplify error in favour of enhanced fetch * created enhanced fetch module, moved pkce service to services, updated proxy service and a little refactor * par support flag not optional * Fix typo * Add better validation to crypto decryption function * Update validate uri to be more secure, create test for validate uri. Update port range in environment * Remove state param from logout to ensure it will not be passed to redirect uri * Adding more tests and increasing security in the function * Refactor after reading comments from coderabbit * remove private from method for test * Move portal scopes to shareable location. * Remove unused import * Add no_refresh query to user endpoint in backend * Polling and broadcaster added to react spa bff library * Enhanced security in pkce service.and improve error handling to be more secure * Update usePolling to have better types and secure resumabiltiy. * Refactor useBroadcaster. * Add client logic to handle the case if bff server goes down * Fix tests and builds * Fix portal infra local vars * DX infra setup for services-bff * Remove error log from revokeRefreshToken since it is handled by enhancedFetch and update download service local url * Rename cached toke fields to be prefixed with encrypted and fix where encryption was missing. Also fix for revoking wrong token * Better handling on errors in auth service * Update api requests formatting and handling to handle exceptions and errors better. * Update apps/services/bff/src/app/bff.config.ts simpler redis config Co-authored-by: Eiríkur Heiðar Nilsson * cleanup after commit from github * Update after our pull request AI suggested the change * Remove broadcaster mocks * Remove redundant timeout in favour of poller * Fix portal config, fix redis cache module init, update bff provider to handle logout in before redirect * Remove timeout in logout broadcasting and throw the error in postRequest if not successful plain text response * Revert the timeout in the logout * chore: charts update dirty files * Rename queries to dto for consistency in monorepo and add log for logout callback * Fix cli error that got merged from main * Fix prettier formatting error * chore: nx format:write update dirty files * fix storybook build * ci: trigger from levy user * fix: use portals-admin, added portal-env test * Revert manual validation and use library * Use fetch instead of post in download url * Fix type errors and add forward get proxy api request * fix: main conflict * chore: charts update dirty files * fix: prettier issues * chore: prettify * chore: nx format:write update dirty files * ci: add services-bff to helm chart * Fix env vars for feature deploy * Fix health check to be excluded from prefix * update global prefix logic * update bff services options * Remove bff redis name env var * Update bff config again * Update portal env spec for feature branch * chore: charts update dirty files * Update validation error log * Remove database healthcheck * Revert globalprefix options and update liveness and readiness infra checks * chore: charts update dirty files * Add auth controller tests * Add logout log for testing in feature deploy * remove unused * clean up auth controller test * chore: nx format:write update dirty files * Add tests for proxy controller * Add ref to infra for api * update charts * add zed editor config to gitignore * Add support for mocks * chore: nx format:write update dirty files * Fix portal env spec * chore: charts update dirty files * Update mocking server logic for portals * update mock logic * fix: public envs (#16493) * fix: merge conflict * fix: improved zod schema generation * test: update portal-env test for service building * fix: generate feature deploy urls * fix: improve getEnvUrl func * feat: integrated bff to ServiceBuilder * fix: more abstraction to dsl * fix: simplify and cleanup * chore: remove unused file * chore: cleanup dupes * chore: nx format:write update dirty files * chore: more cleanup --------- Co-authored-by: andes-it * chore: remove nx-command impl (#16532) * chore: move nx runcommand cli to a new PR * chore: commit save point * chore: commit save point * Update infra setup * fix tests * chore: charts update dirty files * fix infra url * Removed un used import * fix: revert secret type changes * chore: nx format:write update dirty files * chore: cleanup * fix feature deployment url * fix tests * fix missing logger * chore: nx format:write update dirty files * update api graphql bff config env var * fix tests * fix tests * chore: charts update dirty files * chore: nx format:write update dirty files * grantnamespaces * chore: charts update dirty files * disable global auth on dev * chore: charts update dirty files * Update double negation query param * feat: Better error message when running infra cli without aws credentials. * Adding agent to proxy for for managing connections efficiently * Enable PAR support * update tests * chore: charts update dirty files --------- Co-authored-by: andes-it Co-authored-by: Eiríkur Heiðar Nilsson Co-authored-by: Jón Levy Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .gitignore | 2 + .prettierignore | 2 +- .prettierrc | 2 +- apps/api/infra/api.ts | 45 +- apps/api/src/app/environments/environment.ts | 2 + apps/portals/admin/project.json | 24 +- apps/portals/admin/proxy.config.json | 6 + apps/portals/admin/src/app/App.tsx | 28 +- apps/portals/admin/src/auth.ts | 47 -- apps/portals/admin/src/graphql.ts | 12 +- apps/portals/admin/src/main.tsx | 4 +- .../my-pages/src/components/Header/Header.tsx | 2 +- apps/services/bff/.eslintrc.json | 18 + apps/services/bff/README.md | 39 ++ apps/services/bff/docker-compose.yml | 14 + apps/services/bff/esbuild.json | 52 ++ apps/services/bff/infra/admin-portal.infra.ts | 74 +++ apps/services/bff/jest.config.ts | 18 + apps/services/bff/project.json | 61 ++ apps/services/bff/src/app/app.module.ts | 23 + apps/services/bff/src/app/bff.config.ts | 120 ++++ .../services/bff/src/app/constants/cookies.ts | 1 + apps/services/bff/src/app/constants/time.ts | 1 + .../app/modules/auth/auth.controller.spec.ts | 539 +++++++++++++++++ .../src/app/modules/auth/auth.controller.ts | 66 +++ .../bff/src/app/modules/auth/auth.module.ts | 13 + .../bff/src/app/modules/auth/auth.service.ts | 545 ++++++++++++++++++ .../bff/src/app/modules/auth/auth.types.ts | 23 + .../modules/auth/dto/callback-login.dto.ts | 25 + .../modules/auth/dto/callback-logout.dto.ts | 7 + .../bff/src/app/modules/auth/dto/login.dto.ts | 15 + .../src/app/modules/auth/dto/logout.dto.ts | 6 + .../bff/src/app/modules/cache/cache.module.ts | 38 ++ .../src/app/modules/cache/cache.service.ts | 65 +++ .../enhancedFetch/enhanced-fetch.module.ts | 9 + .../enhancedFetch/enhanced-fetch.provider.ts | 17 + .../bff/src/app/modules/ids/ids.service.ts | 202 +++++++ .../bff/src/app/modules/ids/ids.types.ts | 83 +++ .../app/modules/proxy/dto/api-proxy.dto.ts | 6 + .../modules/proxy/proxy.controller.spec.ts | 246 ++++++++ .../src/app/modules/proxy/proxy.controller.ts | 39 ++ .../bff/src/app/modules/proxy/proxy.module.ts | 13 + .../src/app/modules/proxy/proxy.service.ts | 219 +++++++ .../bff/src/app/modules/proxy/proxy.types.ts | 9 + .../src/app/modules/user/dto/get-user.dto.ts | 7 + .../src/app/modules/user/user.controller.ts | 25 + .../bff/src/app/modules/user/user.module.ts | 13 + .../bff/src/app/modules/user/user.service.ts | 82 +++ .../src/app/services/crypto.service.spec.ts | 92 +++ .../bff/src/app/services/crypto.service.ts | 107 ++++ .../bff/src/app/services/pkce.service.spec.ts | 74 +++ .../bff/src/app/services/pkce.service.ts | 81 +++ .../src/app/utils/create-error-query-str.ts | 17 + .../app/utils/has-timestamp-expired-in-ms.ts | 6 + .../bff/src/app/utils/qs-validation-pipe.ts | 7 + .../src/app/utils/remove-trailing-slash.ts | 13 + .../bff/src/app/utils/validate-uri.spec.ts | 84 +++ .../bff/src/app/utils/validate-uri.ts | 45 ++ .../bff/src/environment/environment.schema.ts | 27 + .../bff/src/environment/environment.ts | 12 + apps/services/bff/src/environment/index.ts | 1 + apps/services/bff/src/main.ts | 12 + apps/services/bff/test/setup.ts | 21 + apps/services/bff/test/setupTestServer.ts | 8 + apps/services/bff/test/sharedConstants.ts | 53 ++ apps/services/bff/tsconfig.app.json | 9 + apps/services/bff/tsconfig.json | 13 + apps/services/bff/tsconfig.spec.json | 9 + charts/islandis/values.dev.yaml | 89 ++- charts/islandis/values.prod.yaml | 92 ++- charts/islandis/values.staging.yaml | 90 ++- infra/helm/Chart.yaml | 4 + infra/package.json | 2 + infra/src/cli/cli.ts | 9 +- infra/src/cli/render-local-mocks.ts | 2 +- infra/src/dsl/adapters/get-ssm-params.ts | 12 +- infra/src/dsl/bff.ts | 83 +++ infra/src/dsl/dsl.ts | 14 +- infra/src/dsl/feature-values.spec.ts | 90 ++- infra/src/dsl/portal-env.spec.ts | 238 ++++++++ .../pre-process-service.ts | 1 - infra/src/dsl/types/input-types.ts | 15 + .../dsl/value-files-generators/local-setup.ts | 9 + infra/src/feature-env.ts | 2 +- infra/src/uber-charts/islandis.ts | 16 +- infra/yarn.lock | 294 +++++++++- libs/auth/scopes/src/index.ts | 2 + .../src/lib/clients/admin-portal-scopes.ts | 23 + .../src/lib/clients/service-portal-scopes.ts | 48 ++ libs/infra-nest-server/src/lib/bootstrap.ts | 10 +- libs/infra-nest-server/src/lib/types.ts | 6 + libs/island-ui/storybook/config/main.ts | 1 + .../admin/application-system/src/module.tsx | 4 +- .../screens/Overview/InstitutionOverview.tsx | 2 +- .../ids-admin/src/hooks/useSuperAdmin.tsx | 4 +- .../src/components/DownloadDraftButton.tsx | 70 +-- .../regulations-admin/src/state/reducer.ts | 19 +- .../service-desk/src/screens/Users/Users.tsx | 4 +- .../core/src/components/PortalProvider.tsx | 14 +- .../core/src/components/PortalRouter.tsx | 18 +- libs/portals/core/src/hooks/useModuleProps.ts | 4 +- libs/portals/core/src/hooks/useNavigation.ts | 4 +- libs/portals/core/src/index.ts | 3 + libs/portals/core/src/mocks/index.ts | 1 + .../portals/core/src/screens/AccessDenied.tsx | 4 +- libs/portals/core/src/screens/ModuleRoute.tsx | 4 +- libs/portals/core/src/types/portalCore.ts | 6 +- libs/portals/core/src/utils/modules.ts | 6 +- .../src/utils/router/prepareRouterData.ts | 6 +- .../components/access/AccessConfirmModal.tsx | 4 +- .../AccessDeleteModal/AccessDeleteModal.tsx | 18 +- .../delegations/src/screens/AccessControl.tsx | 11 +- .../src/screens/GrantAccess/GrantAccess.tsx | 24 +- libs/react-spa/bff/.babelrc | 12 + libs/react-spa/bff/.eslintrc.json | 18 + libs/react-spa/bff/README.md | 7 + libs/react-spa/bff/jest.config.ts | 11 + libs/react-spa/bff/project.json | 19 + libs/react-spa/bff/src/index.ts | 4 + libs/react-spa/bff/src/lib/BffContext.tsx | 15 + libs/react-spa/bff/src/lib/BffPoller.tsx | 97 ++++ libs/react-spa/bff/src/lib/BffProvider.tsx | 237 ++++++++ .../bff/src/lib/BffSessionExpiredModal.tsx | 40 ++ libs/react-spa/bff/src/lib/ErrorScreen.css.ts | 5 + libs/react-spa/bff/src/lib/ErrorScreen.tsx | 41 ++ libs/react-spa/bff/src/lib/bff.hooks.ts | 127 ++++ libs/react-spa/bff/src/lib/bff.mocks.ts | 19 + libs/react-spa/bff/src/lib/bff.state.ts | 116 ++++ libs/react-spa/bff/src/lib/bff.utils.ts | 48 ++ libs/react-spa/bff/tsconfig.json | 20 + libs/react-spa/bff/tsconfig.lib.json | 24 + libs/react-spa/bff/tsconfig.spec.json | 20 + .../shared/src/hooks/useBroadcaster.ts | 145 +++++ .../react-spa/shared/src/hooks/usePolling.tsx | 113 ++++ libs/react-spa/shared/src/index.ts | 2 + libs/react/feature-flags/src/lib/context.tsx | 10 +- .../UserOnboardingModal/components/Header.tsx | 2 +- .../service-portal/information/src/module.tsx | 13 +- .../src/auth/UserMenu/UserButton.tsx | 14 +- .../src/auth/UserMenu/UserDelegations.tsx | 15 +- .../src/auth/UserMenu/UserDropdown.tsx | 37 +- .../auth/UserMenu/UserLanguageSwitcher.tsx | 9 +- .../components/src/auth/UserMenu/UserMenu.tsx | 9 +- libs/shared/mocking/src/msw/startMocking.ts | 24 +- libs/shared/types/src/index.ts | 1 + libs/shared/types/src/lib/bff.ts | 24 + libs/shared/utils/src/lib/isDelegation.ts | 4 +- libs/testing/nest/src/lib/testServer.ts | 5 +- tsconfig.base.json | 1 + 149 files changed, 5961 insertions(+), 329 deletions(-) create mode 100644 apps/portals/admin/proxy.config.json delete mode 100644 apps/portals/admin/src/auth.ts create mode 100644 apps/services/bff/.eslintrc.json create mode 100644 apps/services/bff/README.md create mode 100644 apps/services/bff/docker-compose.yml create mode 100644 apps/services/bff/esbuild.json create mode 100644 apps/services/bff/infra/admin-portal.infra.ts create mode 100644 apps/services/bff/jest.config.ts create mode 100644 apps/services/bff/project.json create mode 100644 apps/services/bff/src/app/app.module.ts create mode 100644 apps/services/bff/src/app/bff.config.ts create mode 100644 apps/services/bff/src/app/constants/cookies.ts create mode 100644 apps/services/bff/src/app/constants/time.ts create mode 100644 apps/services/bff/src/app/modules/auth/auth.controller.spec.ts create mode 100644 apps/services/bff/src/app/modules/auth/auth.controller.ts create mode 100644 apps/services/bff/src/app/modules/auth/auth.module.ts create mode 100644 apps/services/bff/src/app/modules/auth/auth.service.ts create mode 100644 apps/services/bff/src/app/modules/auth/auth.types.ts create mode 100644 apps/services/bff/src/app/modules/auth/dto/callback-login.dto.ts create mode 100644 apps/services/bff/src/app/modules/auth/dto/callback-logout.dto.ts create mode 100644 apps/services/bff/src/app/modules/auth/dto/login.dto.ts create mode 100644 apps/services/bff/src/app/modules/auth/dto/logout.dto.ts create mode 100644 apps/services/bff/src/app/modules/cache/cache.module.ts create mode 100644 apps/services/bff/src/app/modules/cache/cache.service.ts create mode 100644 apps/services/bff/src/app/modules/enhancedFetch/enhanced-fetch.module.ts create mode 100644 apps/services/bff/src/app/modules/enhancedFetch/enhanced-fetch.provider.ts create mode 100644 apps/services/bff/src/app/modules/ids/ids.service.ts create mode 100644 apps/services/bff/src/app/modules/ids/ids.types.ts create mode 100644 apps/services/bff/src/app/modules/proxy/dto/api-proxy.dto.ts create mode 100644 apps/services/bff/src/app/modules/proxy/proxy.controller.spec.ts create mode 100644 apps/services/bff/src/app/modules/proxy/proxy.controller.ts create mode 100644 apps/services/bff/src/app/modules/proxy/proxy.module.ts create mode 100644 apps/services/bff/src/app/modules/proxy/proxy.service.ts create mode 100644 apps/services/bff/src/app/modules/proxy/proxy.types.ts create mode 100644 apps/services/bff/src/app/modules/user/dto/get-user.dto.ts create mode 100644 apps/services/bff/src/app/modules/user/user.controller.ts create mode 100644 apps/services/bff/src/app/modules/user/user.module.ts create mode 100644 apps/services/bff/src/app/modules/user/user.service.ts create mode 100644 apps/services/bff/src/app/services/crypto.service.spec.ts create mode 100644 apps/services/bff/src/app/services/crypto.service.ts create mode 100644 apps/services/bff/src/app/services/pkce.service.spec.ts create mode 100644 apps/services/bff/src/app/services/pkce.service.ts create mode 100644 apps/services/bff/src/app/utils/create-error-query-str.ts create mode 100644 apps/services/bff/src/app/utils/has-timestamp-expired-in-ms.ts create mode 100644 apps/services/bff/src/app/utils/qs-validation-pipe.ts create mode 100644 apps/services/bff/src/app/utils/remove-trailing-slash.ts create mode 100644 apps/services/bff/src/app/utils/validate-uri.spec.ts create mode 100644 apps/services/bff/src/app/utils/validate-uri.ts create mode 100644 apps/services/bff/src/environment/environment.schema.ts create mode 100644 apps/services/bff/src/environment/environment.ts create mode 100644 apps/services/bff/src/environment/index.ts create mode 100644 apps/services/bff/src/main.ts create mode 100644 apps/services/bff/test/setup.ts create mode 100644 apps/services/bff/test/setupTestServer.ts create mode 100644 apps/services/bff/test/sharedConstants.ts create mode 100644 apps/services/bff/tsconfig.app.json create mode 100644 apps/services/bff/tsconfig.json create mode 100644 apps/services/bff/tsconfig.spec.json create mode 100644 infra/src/dsl/bff.ts create mode 100644 infra/src/dsl/portal-env.spec.ts create mode 100644 libs/auth/scopes/src/lib/clients/admin-portal-scopes.ts create mode 100644 libs/auth/scopes/src/lib/clients/service-portal-scopes.ts create mode 100644 libs/portals/core/src/mocks/index.ts create mode 100644 libs/react-spa/bff/.babelrc create mode 100644 libs/react-spa/bff/.eslintrc.json create mode 100644 libs/react-spa/bff/README.md create mode 100644 libs/react-spa/bff/jest.config.ts create mode 100644 libs/react-spa/bff/project.json create mode 100644 libs/react-spa/bff/src/index.ts create mode 100644 libs/react-spa/bff/src/lib/BffContext.tsx create mode 100644 libs/react-spa/bff/src/lib/BffPoller.tsx create mode 100644 libs/react-spa/bff/src/lib/BffProvider.tsx create mode 100644 libs/react-spa/bff/src/lib/BffSessionExpiredModal.tsx create mode 100644 libs/react-spa/bff/src/lib/ErrorScreen.css.ts create mode 100644 libs/react-spa/bff/src/lib/ErrorScreen.tsx create mode 100644 libs/react-spa/bff/src/lib/bff.hooks.ts create mode 100644 libs/react-spa/bff/src/lib/bff.mocks.ts create mode 100644 libs/react-spa/bff/src/lib/bff.state.ts create mode 100644 libs/react-spa/bff/src/lib/bff.utils.ts create mode 100644 libs/react-spa/bff/tsconfig.json create mode 100644 libs/react-spa/bff/tsconfig.lib.json create mode 100644 libs/react-spa/bff/tsconfig.spec.json create mode 100644 libs/react-spa/shared/src/hooks/useBroadcaster.ts create mode 100644 libs/react-spa/shared/src/hooks/usePolling.tsx create mode 100644 libs/shared/types/src/lib/bff.ts diff --git a/.gitignore b/.gitignore index 160c3874cac9..a97e5eec68f8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ tmp/ out-tsc/ **/tsconfig.tsbuildinfo infra/mountebank-imposter-config.json +mountebank-imposter-config.json infra/helm/**/*.tgz # dependencies @@ -96,3 +97,4 @@ apps/**/index.html .next .nx/ +.zed/ diff --git a/.prettierignore b/.prettierignore index 2619b48cd441..7d2cdb83591b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,4 +14,4 @@ /infra/helm/ /.nx/cache /.nx/workspace-data -apps/web/public/assets/pdf.worker.min.mjs \ No newline at end of file +apps/web/public/assets/pdf.worker.min.mjs diff --git a/.prettierrc b/.prettierrc index 90779c7c514d..0dc4fc2525df 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,7 +3,7 @@ "semi": false, "trailingComma": "all", "arrowParens": "always", - "plugins": ["./scripts/prettier-plugins/sort-projects"], + "plugins": ["./scripts/prettier-plugins/sort-projects.js"], "endOfLine": "lf", "overrides": [ { diff --git a/apps/api/infra/api.ts b/apps/api/infra/api.ts index bf069dfb98ce..586dfec4a2b0 100644 --- a/apps/api/infra/api.ts +++ b/apps/api/infra/api.ts @@ -1,55 +1,55 @@ import { json, ref, service, ServiceBuilder } from '../../../infra/src/dsl/dsl' import { AdrAndMachine, + AircraftRegistry, Base, ChargeFjsV2, - EnergyFunds, Client, CriminalRecord, + DirectorateOfImmigration, Disability, + DistrictCommissionersLicenses, + DistrictCommissionersPCard, DrivingLicense, DrivingLicenseBook, Education, + EnergyFunds, Finance, Firearm, FishingLicense, + Frigg, + HealthDirectorateOrganDonation, + HealthDirectorateVaccination, HealthInsurance, + HousingBenefitCalculator, + Hunting, + IcelandicGovernmentInstitutionVacancies, + Inna, + IntellectualProperties, JudicialAdministration, + JudicialSystemServicePortal, Labor, MunicipalitiesFinancialAid, NationalRegistry, NationalRegistryB2C, + OccupationalLicenses, + OfficialJournalOfIceland, + OfficialJournalOfIcelandApplication, Passports, Payment, PaymentSchedule, Properties, RskCompanyInfo, - TransportAuthority, - Vehicles, - VehiclesMileage, - VehicleServiceFjsV1, - WorkMachines, - IcelandicGovernmentInstitutionVacancies, RskProcuring, - AircraftRegistry, - HousingBenefitCalculator, - OccupationalLicenses, ShipRegistry, - DistrictCommissionersPCard, - DistrictCommissionersLicenses, - DirectorateOfImmigration, - Hunting, SignatureCollection, SocialInsuranceAdministration, - IntellectualProperties, - Inna, + TransportAuthority, UniversityCareers, - OfficialJournalOfIceland, - OfficialJournalOfIcelandApplication, - JudicialSystemServicePortal, - Frigg, - HealthDirectorateOrganDonation, - HealthDirectorateVaccination, + Vehicles, + VehicleServiceFjsV1, + VehiclesMileage, + WorkMachines, } from '../../../infra/src/dsl/xroad' export const serviceSetup = (services: { @@ -469,5 +469,6 @@ export const serviceSetup = (services: { 'api-catalogue', 'application-system', 'consultation-portal', + 'services-bff-portals-admin', ) } diff --git a/apps/api/src/app/environments/environment.ts b/apps/api/src/app/environments/environment.ts index 588fbe19ccfe..2d5faa33b286 100644 --- a/apps/api/src/app/environments/environment.ts +++ b/apps/api/src/app/environments/environment.ts @@ -97,6 +97,7 @@ const prodConfig = () => ({ basePath: process.env.ISLYKILL_SERVICE_BASEPATH, }, }) + const devConfig = () => ({ production: false, xroad: { @@ -207,6 +208,7 @@ const devConfig = () => ({ basePath: process.env.ISLYKILL_SERVICE_BASEPATH, }, }) + export const getConfig = process.env.PROD_MODE === 'true' || process.env.NODE_ENV === 'production' ? prodConfig() diff --git a/apps/portals/admin/project.json b/apps/portals/admin/project.json index d7fc9572a5f0..53dda7697770 100644 --- a/apps/portals/admin/project.json +++ b/apps/portals/admin/project.json @@ -55,7 +55,8 @@ "executor": "@nx/webpack:dev-server", "options": { "buildTarget": "portals-admin:build", - "hmr": true + "hmr": true, + "proxyConfig": "apps/portals/admin/proxy.config.json" }, "configurations": { "production": { @@ -92,11 +93,28 @@ "parallel": false } }, + "start-bff": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "node -r esbuild-register src/cli/cli.ts run-local-env services-bff-portals-admin" + ], + "cwd": "infra" + } + }, "dev": { "executor": "nx:run-commands", "options": { - "commands": ["yarn start portals-admin"], - "parallel": true + "commands": [ + "yarn nx run portals-admin:start-bff", + "yarn start portals-admin" + ] + } + }, + "mockmode": { + "executor": "nx:run-commands", + "options": { + "commands": ["API_MOCKS=true yarn start portals-admin"] } }, "docker-static": { diff --git a/apps/portals/admin/proxy.config.json b/apps/portals/admin/proxy.config.json new file mode 100644 index 000000000000..8f1d53118e19 --- /dev/null +++ b/apps/portals/admin/proxy.config.json @@ -0,0 +1,6 @@ +{ + "/stjornbord/bff/*": { + "target": "http://localhost:3010", + "secure": false + } +} diff --git a/apps/portals/admin/src/app/App.tsx b/apps/portals/admin/src/app/App.tsx index 918f04a4307c..1627e3b6aa17 100644 --- a/apps/portals/admin/src/app/App.tsx +++ b/apps/portals/admin/src/app/App.tsx @@ -1,20 +1,34 @@ import { ApolloProvider } from '@apollo/client' -import { AuthProvider } from '@island.is/auth/react' import { LocaleProvider } from '@island.is/localization' -import { defaultLanguage } from '@island.is/shared/constants' +import { + ApplicationErrorBoundary, + PortalRouter, + isMockMode, +} from '@island.is/portals/core' +import { BffProvider, createMockedInitialState } from '@island.is/react-spa/bff' import { FeatureFlagProvider } from '@island.is/react/feature-flags' -import { ApplicationErrorBoundary, PortalRouter } from '@island.is/portals/core' -import { modules } from '../lib/modules' -import { client } from '../graphql' +import { defaultLanguage } from '@island.is/shared/constants' import environment from '../environments/environment' +import { client } from '../graphql' +import { modules } from '../lib/modules' import { AdminPortalPaths } from '../lib/paths' import { createRoutes } from '../lib/routes' +import { adminPortalScopes } from '@island.is/auth/scopes' + +const mockedInitialState = isMockMode + ? createMockedInitialState({ + scopes: adminPortalScopes, + }) + : undefined export const App = () => ( - + ( }} /> - + diff --git a/apps/portals/admin/src/auth.ts b/apps/portals/admin/src/auth.ts deleted file mode 100644 index 944cdc454551..000000000000 --- a/apps/portals/admin/src/auth.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { configure, configureMock } from '@island.is/auth/react' -import { AdminPortalScope } from '@island.is/auth/scopes' - -import environment from './environments/environment' - -const userMocked = process.env.API_MOCKS === 'true' - -if (userMocked) { - configureMock({ - profile: { name: 'Mock', locale: 'is', nationalId: '0000000000' }, - scopes: [], - }) -} else { - configure({ - baseUrl: `${window.location.origin}/stjornbord`, - redirectPath: '/signin-oidc', - redirectPathSilent: '/silent/signin-oidc', - switchUserRedirectUrl: '/', - authority: environment.identityServer.authority, - client_id: '@admin.island.is/web', - scope: [ - 'openid', - 'profile', - AdminPortalScope.delegations, - AdminPortalScope.airDiscountScheme, - AdminPortalScope.regulationAdmin, - AdminPortalScope.regulationAdminManage, - AdminPortalScope.icelandicNamesRegistry, - AdminPortalScope.applicationSystemAdmin, - AdminPortalScope.applicationSystemInstitution, - AdminPortalScope.documentProvider, - AdminPortalScope.idsAdmin, - AdminPortalScope.idsAdminSuperUser, - AdminPortalScope.petitionsAdmin, - AdminPortalScope.serviceDesk, - AdminPortalScope.explicitAirDiscountScheme, - AdminPortalScope.signatureCollectionManage, - AdminPortalScope.signatureCollectionProcess, - AdminPortalScope.formSystem, - AdminPortalScope.formSystemSuperUser, - AdminPortalScope.delegationSystem, - AdminPortalScope.delegationSystemAdmin, - ], - post_logout_redirect_uri: `${window.location.origin}`, - userStorePrefix: 'ap.', - }) -} diff --git a/apps/portals/admin/src/graphql.ts b/apps/portals/admin/src/graphql.ts index d20d4f3df080..9f31915d3166 100644 --- a/apps/portals/admin/src/graphql.ts +++ b/apps/portals/admin/src/graphql.ts @@ -8,16 +8,10 @@ import { import { onError } from '@apollo/client/link/error' import { RetryLink } from '@apollo/client/link/retry' -import { authLink } from '@island.is/auth/react' - -const uri = - process.env.NODE_ENV === 'development' - ? 'http://localhost:4444/api/graphql' - : '/api/graphql' - const httpLink = new HttpLink({ - uri: ({ operationName }) => `${uri}?op=${operationName}`, + uri: ({ operationName }) => `/stjornbord/bff/api/graphql?op=${operationName}`, fetch, + credentials: 'include', }) const retryLink = new RetryLink() @@ -34,7 +28,7 @@ const errorLink = onError(({ graphQLErrors, networkError }) => { }) export const client = new ApolloClient({ - link: ApolloLink.from([retryLink, errorLink, authLink, httpLink]), + link: ApolloLink.from([retryLink, errorLink, httpLink]), cache: new InMemoryCache({ typePolicies: { UserProfile: { diff --git a/apps/portals/admin/src/main.tsx b/apps/portals/admin/src/main.tsx index 0d8890a30cbb..4cca91a26a05 100644 --- a/apps/portals/admin/src/main.tsx +++ b/apps/portals/admin/src/main.tsx @@ -1,11 +1,11 @@ import '@island.is/api/mocks' + import { userMonitoring } from '@island.is/user-monitoring' -import React, { StrictMode } from 'react' +import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { isRunningOnEnvironment } from '@island.is/shared/utils' -import './auth' import environment from './environments/environment' import { App } from './app/App' diff --git a/apps/portals/my-pages/src/components/Header/Header.tsx b/apps/portals/my-pages/src/components/Header/Header.tsx index 21c913b99da0..f3d09e9f596d 100644 --- a/apps/portals/my-pages/src/components/Header/Header.tsx +++ b/apps/portals/my-pages/src/components/Header/Header.tsx @@ -130,7 +130,7 @@ export const Header = ({ position }: Props) => { /> )} - {user && } + {user && } ) diff --git a/libs/portals/admin/regulations-admin/src/state/reducer.ts b/libs/portals/admin/regulations-admin/src/state/reducer.ts index 3771f45b70f7..1b03eb072486 100644 --- a/libs/portals/admin/regulations-admin/src/state/reducer.ts +++ b/libs/portals/admin/regulations-admin/src/state/reducer.ts @@ -1,14 +1,14 @@ +import { AdminPortalScope } from '@island.is/auth/scopes' +import { useUserInfo } from '@island.is/react-spa/bff' +import { LawChapter, MinistryList } from '@island.is/regulations' +import { DraftImpactId, RegulationDraft } from '@island.is/regulations/admin' +import { produce, setAutoFreeze } from 'immer' import { Reducer, useReducer } from 'react' import { RegulationDraftTypes, Step } from '../types' -import { LawChapter, MinistryList } from '@island.is/regulations' +import { actionHandlers } from './actionHandlers' +import { makeDraftForm, stepsAmending, stepsBase } from './makeFields' import { Action, DraftingState, RegDraftFormSimpleProps } from './types' -import { produce, setAutoFreeze } from 'immer' -import { DraftImpactId, RegulationDraft } from '@island.is/regulations/admin' -import { useAuth } from '@island.is/auth/react' -import { AdminPortalScope } from '@island.is/auth/scopes' import { derivedUpdates, validateState } from './validations' -import { makeDraftForm, stepsAmending, stepsBase } from './makeFields' -import { actionHandlers } from './actionHandlers' const draftingStateReducer: Reducer = ( state, @@ -40,9 +40,8 @@ export const useEditDraftReducer = (inputs: StateInputs) => { const { regulationDraft, ministries, lawChapters, stepName } = inputs const isEditor = - useAuth().userInfo?.scopes?.includes( - AdminPortalScope.regulationAdminManage, - ) || false + useUserInfo()?.scopes?.includes(AdminPortalScope.regulationAdminManage) || + false const makeInitialState = () => { const draft = makeDraftForm(regulationDraft) diff --git a/libs/portals/admin/service-desk/src/screens/Users/Users.tsx b/libs/portals/admin/service-desk/src/screens/Users/Users.tsx index 6e66784fba2a..d78b0fa1b1cc 100644 --- a/libs/portals/admin/service-desk/src/screens/Users/Users.tsx +++ b/libs/portals/admin/service-desk/src/screens/Users/Users.tsx @@ -10,7 +10,7 @@ import { import { formatNationalId, IntroHeader } from '@island.is/portals/core' import { maskString } from '@island.is/shared/utils' import { useLocale } from '@island.is/localization' -import { useAuth } from '@island.is/auth/react' +import { useUserInfo } from '@island.is/react-spa/bff' import { replaceParams, useSubmitting } from '@island.is/react-spa/shared' import * as styles from '../Companies/Companies.css' @@ -26,7 +26,7 @@ const Users = () => { const actionData = useActionData() as GetUserProfilesResult const { formatMessage } = useLocale() const navigate = useNavigate() - const { userInfo } = useAuth() + const userInfo = useUserInfo() const { isSubmitting, isLoading } = useSubmitting() const users = actionData?.data?.data const [error, setError] = useState({ hasError: false, message: '' }) diff --git a/libs/portals/core/src/components/PortalProvider.tsx b/libs/portals/core/src/components/PortalProvider.tsx index 5aff6abb53a8..6c2159127d72 100644 --- a/libs/portals/core/src/components/PortalProvider.tsx +++ b/libs/portals/core/src/components/PortalProvider.tsx @@ -1,13 +1,13 @@ -import { useLocale } from '@island.is/localization' -import { createContext, useContext, useMemo } from 'react' -import { useLocation, matchPath, Outlet } from 'react-router-dom' -import { PortalModule, PortalRoute, PortalType } from '../types/portalCore' -import { useAuth } from '@island.is/auth/react' import { ApolloClient, - useApolloClient, NormalizedCacheObject, + useApolloClient, } from '@apollo/client' +import { useLocale } from '@island.is/localization' +import { useUserInfo } from '@island.is/react-spa/bff' +import { createContext, useContext, useMemo } from 'react' +import { Outlet, matchPath, useLocation } from 'react-router-dom' +import { PortalModule, PortalRoute, PortalType } from '../types/portalCore' export type PortalMeta = { portalType: PortalType @@ -44,7 +44,7 @@ export const PortalProvider = ({ routes, }: PortalProviderProps) => { const { pathname } = useLocation() - const { userInfo } = useAuth() + const userInfo = useUserInfo() const { formatMessage } = useLocale() const client = useApolloClient() as ApolloClient diff --git a/libs/portals/core/src/components/PortalRouter.tsx b/libs/portals/core/src/components/PortalRouter.tsx index 38038aa1a0c1..d85ec65d0837 100644 --- a/libs/portals/core/src/components/PortalRouter.tsx +++ b/libs/portals/core/src/components/PortalRouter.tsx @@ -1,24 +1,24 @@ -import React, { useEffect, useState, useRef } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { - createBrowserRouter, RouteObject, RouterProvider, + createBrowserRouter, } from 'react-router-dom' import { - useApolloClient, ApolloClient, NormalizedCacheObject, + useApolloClient, } from '@apollo/client' import { useLocale } from '@island.is/localization' -import { useFeatureFlagClient } from '@island.is/react/feature-flags' -import { useAuth } from '@island.is/auth/react' +import { useUserInfo } from '@island.is/react-spa/bff' import { LoadingScreen } from '@island.is/react/components' -import { createModuleRoutes } from '../utils/router/createModuleRoutes' +import { useFeatureFlagClient } from '@island.is/react/feature-flags' +import { m } from '../lib/messages' import { PortalModule, PortalRoute } from '../types/portalCore' -import { PortalMeta, PortalProvider } from './PortalProvider' +import { createModuleRoutes } from '../utils/router/createModuleRoutes' import { prepareRouterData } from '../utils/router/prepareRouterData' -import { m } from '../lib/messages' +import { PortalMeta, PortalProvider } from './PortalProvider' type PortalRouterProps = { modules: PortalModule[] @@ -37,7 +37,7 @@ export const PortalRouter = ({ const { formatMessage } = useLocale() const router = useRef>() const [error, setError] = useState(null) - const { userInfo } = useAuth() + const userInfo = useUserInfo() const featureFlagClient = useFeatureFlagClient() const [routerData, setRouterData] = useState<{ modules: PortalModule[] diff --git a/libs/portals/core/src/hooks/useModuleProps.ts b/libs/portals/core/src/hooks/useModuleProps.ts index a6e18220c078..895bc5744f57 100644 --- a/libs/portals/core/src/hooks/useModuleProps.ts +++ b/libs/portals/core/src/hooks/useModuleProps.ts @@ -3,10 +3,10 @@ import { ApolloClient, NormalizedCacheObject, } from '@apollo/client' -import { useAuth } from '@island.is/auth/react' +import { useUserInfo } from '@island.is/react-spa/bff' export const useModuleProps = () => { - const { userInfo } = useAuth() + const userInfo = useUserInfo() const client = useApolloClient() as ApolloClient if (userInfo === null) { diff --git a/libs/portals/core/src/hooks/useNavigation.ts b/libs/portals/core/src/hooks/useNavigation.ts index fe7156246abe..344ec5840b6c 100644 --- a/libs/portals/core/src/hooks/useNavigation.ts +++ b/libs/portals/core/src/hooks/useNavigation.ts @@ -1,6 +1,6 @@ +import { useUserInfo } from '@island.is/react-spa/bff' import { useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { useAuth } from '@island.is/auth/react' import { useRoutes } from '../components/PortalProvider' import { PortalNavigationItem } from '../types/portalCore' import { filterNavigationTree } from '../utils/filterNavigationTree/filterNavigationTree' @@ -9,7 +9,7 @@ export const useNavigation = ( navigation: PortalNavigationItem, dynamicRouteArray?: string[], ) => { - const { userInfo } = useAuth() + const userInfo = useUserInfo() const routes = useRoutes() const { pathname } = useLocation() diff --git a/libs/portals/core/src/index.ts b/libs/portals/core/src/index.ts index 398eba645449..228aa2fa7539 100644 --- a/libs/portals/core/src/index.ts +++ b/libs/portals/core/src/index.ts @@ -1,3 +1,6 @@ +// mocks +export * from './mocks' + // libs export * from './lib/paths' export * from './lib/messages' diff --git a/libs/portals/core/src/mocks/index.ts b/libs/portals/core/src/mocks/index.ts new file mode 100644 index 000000000000..2d1618cb1f70 --- /dev/null +++ b/libs/portals/core/src/mocks/index.ts @@ -0,0 +1 @@ +export const isMockMode = process.env.API_MOCKS === 'true' diff --git a/libs/portals/core/src/screens/AccessDenied.tsx b/libs/portals/core/src/screens/AccessDenied.tsx index ac7b5b5854c4..8ae9d5caa7dd 100644 --- a/libs/portals/core/src/screens/AccessDenied.tsx +++ b/libs/portals/core/src/screens/AccessDenied.tsx @@ -1,5 +1,5 @@ import { useLocale } from '@island.is/localization' -import { useAuth } from '@island.is/auth/react' +import { useUserInfo } from '@island.is/react-spa/bff' import { checkDelegation } from '@island.is/shared/utils' import { m } from '../lib/messages' @@ -7,7 +7,7 @@ import { Problem } from '@island.is/react-spa/shared' export const AccessDenied = () => { const { formatMessage } = useLocale() - const { userInfo: user } = useAuth() + const user = useUserInfo() const isDelegation = user && checkDelegation(user) return ( diff --git a/libs/portals/core/src/screens/ModuleRoute.tsx b/libs/portals/core/src/screens/ModuleRoute.tsx index ef37780576d7..156082b039f1 100644 --- a/libs/portals/core/src/screens/ModuleRoute.tsx +++ b/libs/portals/core/src/screens/ModuleRoute.tsx @@ -5,7 +5,7 @@ import { PortalRoute } from '../types/portalCore' import { usePortalMeta } from '../components/PortalProvider' import { plausiblePageviewDetail } from '../utils/plausible' import { Box } from '@island.is/island-ui/core' -import { useAuth } from '@island.is/auth/react' +import { useUserInfo } from '@island.is/react-spa/bff' type ModuleRouteProps = { route: PortalRoute @@ -14,7 +14,7 @@ type ModuleRouteProps = { export const ModuleRoute = React.memo(({ route }: ModuleRouteProps) => { const location = useLocation() const { basePath, portalTitle } = usePortalMeta() - const { userInfo } = useAuth() + const userInfo = useUserInfo() const { formatMessage } = useLocale() useEffect(() => { diff --git a/libs/portals/core/src/types/portalCore.ts b/libs/portals/core/src/types/portalCore.ts index 5dc5375c089d..fb426c2a1890 100644 --- a/libs/portals/core/src/types/portalCore.ts +++ b/libs/portals/core/src/types/portalCore.ts @@ -6,7 +6,7 @@ import { RouteObject } from 'react-router-dom' import type { Features } from '@island.is/react/feature-flags' import { IconProps } from '@island.is/island-ui/core' -import { User } from '@island.is/shared/types' +import { BffUser } from '@island.is/shared/types' import { OrganizationSlugType } from '@island.is/shared/constants' /** @@ -79,7 +79,7 @@ export interface PortalNavigationItem { * The props provided to a portal module */ export interface PortalModuleProps { - userInfo: User + userInfo: BffUser } export interface PortalModuleRoutesProps extends PortalModuleProps { @@ -166,7 +166,7 @@ export interface PortalModule { /** * Indicates if module is enabled or not */ - enabled?: (props: { userInfo: User; isCompany: boolean }) => boolean + enabled?: (props: { userInfo: BffUser; isCompany: boolean }) => boolean /** * The layout type of the module diff --git a/libs/portals/core/src/utils/modules.ts b/libs/portals/core/src/utils/modules.ts index 33c3b6449df2..09b85d297fb3 100644 --- a/libs/portals/core/src/utils/modules.ts +++ b/libs/portals/core/src/utils/modules.ts @@ -1,6 +1,6 @@ import { FormatMessage } from '@island.is/localization' import flatten from 'lodash/flatten' -import type { User } from '@island.is/shared/types' +import type { BffUser } from '@island.is/shared/types' import { FeatureFlagClient } from '@island.is/react/feature-flags' import type { PortalModule, PortalRoute } from '../types/portalCore' import { ApolloClient, NormalizedCacheObject } from '@apollo/client' @@ -8,7 +8,7 @@ import { ApolloClient, NormalizedCacheObject } from '@apollo/client' interface FilterEnabledModulesArgs { modules: PortalModule[] featureFlagClient: FeatureFlagClient - userInfo: User + userInfo: BffUser } export const filterEnabledModules = async ({ @@ -41,7 +41,7 @@ export const filterEnabledModules = async ({ } interface ArrangeRoutesArgs { - userInfo: User + userInfo: BffUser modules: PortalModule[] featureFlagClient: FeatureFlagClient client: ApolloClient diff --git a/libs/portals/core/src/utils/router/prepareRouterData.ts b/libs/portals/core/src/utils/router/prepareRouterData.ts index 7534f0de5233..d0959b3d9cb8 100644 --- a/libs/portals/core/src/utils/router/prepareRouterData.ts +++ b/libs/portals/core/src/utils/router/prepareRouterData.ts @@ -2,11 +2,11 @@ import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { FormatMessage } from '@island.is/localization' import { arrangeRoutes, filterEnabledModules } from '../modules' import { FeatureFlagClient } from '@island.is/feature-flags' -import { User } from '@island.is/shared/types' +import { BffUser } from '@island.is/shared/types' import { PortalModule, PortalRoute } from '../../types/portalCore' export type PrepareRouterDataProps = { - userInfo: User + userInfo: BffUser featureFlagClient: FeatureFlagClient modules: PortalModule[] client: ApolloClient @@ -16,7 +16,7 @@ export type PrepareRouterDataProps = { export type PrepareRouterDataReturnType = { modules: PortalModule[] routes: PortalRoute[] - userInfo: User + userInfo: BffUser formatMessage: FormatMessage } diff --git a/libs/portals/shared-modules/delegations/src/components/access/AccessConfirmModal.tsx b/libs/portals/shared-modules/delegations/src/components/access/AccessConfirmModal.tsx index 740c1f015906..dbb8f00a3c9e 100644 --- a/libs/portals/shared-modules/delegations/src/components/access/AccessConfirmModal.tsx +++ b/libs/portals/shared-modules/delegations/src/components/access/AccessConfirmModal.tsx @@ -1,6 +1,6 @@ import { isDefined } from '@island.is/shared/utils' import { AuthDelegationScope } from '@island.is/api/schema' -import { useAuth } from '@island.is/auth/react' +import { useUserInfo } from '@island.is/react-spa/bff' import { Box, useBreakpoint } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' import { formatNationalId, m as coreMessages } from '@island.is/portals/core' @@ -37,7 +37,7 @@ export const AccessConfirmModal = ({ ...rest }: AccessConfirmModalProps) => { const { formatMessage } = useLocale() - const { userInfo } = useAuth() + const userInfo = useUserInfo() const { md } = useBreakpoint() const [error, setError] = useState(formError ?? false) diff --git a/libs/portals/shared-modules/delegations/src/components/access/AccessDeleteModal/AccessDeleteModal.tsx b/libs/portals/shared-modules/delegations/src/components/access/AccessDeleteModal/AccessDeleteModal.tsx index 81903fff5335..1ae08d68f59a 100644 --- a/libs/portals/shared-modules/delegations/src/components/access/AccessDeleteModal/AccessDeleteModal.tsx +++ b/libs/portals/shared-modules/delegations/src/components/access/AccessDeleteModal/AccessDeleteModal.tsx @@ -1,24 +1,24 @@ import { useEffect, useState } from 'react' -import { useAuth } from '@island.is/auth/react' import { Box, toast, useBreakpoint } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' import { formatNationalId } from '@island.is/portals/core' +import { useUserInfo } from '@island.is/react-spa/bff' import { Problem } from '@island.is/react-spa/shared' import { Modal, ModalProps } from '@island.is/react/components' import { AuthDelegationType } from '@island.is/shared/types' -import { DelegationsFormFooter } from '../../delegations/DelegationsFormFooter' -import { IdentityCard } from '../../IdentityCard/IdentityCard' -import { AccessListContainer } from '../AccessList/AccessListContainer/AccessListContainer' -import { useAuthScopeTreeLazyQuery } from '../AccessList/AccessListContainer/AccessListContainer.generated' -import { useDeleteAuthDelegationMutation } from './AccessDeleteModal.generated' +import { useDynamicShadow } from '../../../hooks/useDynamicShadow' +import { m } from '../../../lib/messages' import { AuthCustomDelegation, AuthCustomDelegationOutgoing, } from '../../../types/customDelegation' -import { m } from '../../../lib/messages' -import { useDynamicShadow } from '../../../hooks/useDynamicShadow' +import { IdentityCard } from '../../IdentityCard/IdentityCard' +import { DelegationsFormFooter } from '../../delegations/DelegationsFormFooter' +import { AccessListContainer } from '../AccessList/AccessListContainer/AccessListContainer' +import { useAuthScopeTreeLazyQuery } from '../AccessList/AccessListContainer/AccessListContainer.generated' +import { useDeleteAuthDelegationMutation } from './AccessDeleteModal.generated' type AccessDeleteModalProps = Pick & { delegation?: AuthCustomDelegation @@ -32,7 +32,7 @@ export const AccessDeleteModal = ({ ...rest }: AccessDeleteModalProps) => { const { formatMessage, lang } = useLocale() - const { userInfo } = useAuth() + const userInfo = useUserInfo() const { md } = useBreakpoint() const [error, setError] = useState(false) const [deleteAuthDelegation, { loading }] = useDeleteAuthDelegationMutation() diff --git a/libs/portals/shared-modules/delegations/src/screens/AccessControl.tsx b/libs/portals/shared-modules/delegations/src/screens/AccessControl.tsx index bda0137d5844..ea32968da1da 100644 --- a/libs/portals/shared-modules/delegations/src/screens/AccessControl.tsx +++ b/libs/portals/shared-modules/delegations/src/screens/AccessControl.tsx @@ -1,11 +1,10 @@ -import React from 'react' -import { useNavigate } from 'react-router-dom' -import { useLocation } from 'react-use' import { Box, Button, GridColumn, Tabs } from '@island.is/island-ui/core' -import { IntroHeader, usePortalMeta } from '@island.is/portals/core' import { useLocale, useNamespaces } from '@island.is/localization' -import { useAuth } from '@island.is/auth/react' +import { IntroHeader, usePortalMeta } from '@island.is/portals/core' +import { useUserInfo } from '@island.is/react-spa/bff' import { isDefined } from '@island.is/shared/utils' +import { useNavigate } from 'react-router-dom' +import { useLocation } from 'react-use' import { DelegationsIncoming } from '../components/delegations/incoming/DelegationsIncoming' import { DelegationsOutgoing } from '../components/delegations/outgoing/DelegationsOutgoing' import { m } from '../lib/messages' @@ -18,7 +17,7 @@ const AccessControl = () => { useNamespaces(['sp.access-control-delegations']) const { formatMessage } = useLocale() - const { userInfo } = useAuth() + const userInfo = useUserInfo() const navigate = useNavigate() const location = useLocation() const { basePath } = usePortalMeta() diff --git a/libs/portals/shared-modules/delegations/src/screens/GrantAccess/GrantAccess.tsx b/libs/portals/shared-modules/delegations/src/screens/GrantAccess/GrantAccess.tsx index b9244ccbbabd..60b74e5bd3d0 100644 --- a/libs/portals/shared-modules/delegations/src/screens/GrantAccess/GrantAccess.tsx +++ b/libs/portals/shared-modules/delegations/src/screens/GrantAccess/GrantAccess.tsx @@ -1,36 +1,38 @@ import cn from 'classnames' import * as kennitala from 'kennitala' -import get from 'lodash/get' import React, { useEffect, useState } from 'react' -import { defineMessage } from 'react-intl' import { Control, FormProvider, useForm } from 'react-hook-form' +import { defineMessage } from 'react-intl' import { useNavigate } from 'react-router-dom' -import { useUserInfo } from '@island.is/auth/react' import { Box, - Input, Icon, - toast, - Text, + Input, SkeletonLoader, + Text, + toast, useBreakpoint, } from '@island.is/island-ui/core' import { useLocale, useNamespaces } from '@island.is/localization' -import { IntroHeader } from '@island.is/portals/core' -import { formatNationalId, m as coreMessages } from '@island.is/portals/core' +import { + IntroHeader, + m as coreMessages, + formatNationalId, +} from '@island.is/portals/core' +import { useUserInfo } from '@island.is/react-spa/bff' import { Problem } from '@island.is/react-spa/shared' import { InputController, SelectController, } from '@island.is/shared/form-fields' -import { DelegationsFormFooter } from '../../components/delegations/DelegationsFormFooter' import { IdentityCard } from '../../components/IdentityCard/IdentityCard' -import { DomainOption, useDomains } from '../../hooks/useDomains/useDomains' +import { DelegationsFormFooter } from '../../components/delegations/DelegationsFormFooter' import { ALL_DOMAINS } from '../../constants/domain' -import { DelegationPaths } from '../../lib/paths' +import { DomainOption, useDomains } from '../../hooks/useDomains/useDomains' import { m } from '../../lib/messages' +import { DelegationPaths } from '../../lib/paths' import { useCreateAuthDelegationMutation, useIdentityLazyQuery, diff --git a/libs/react-spa/bff/.babelrc b/libs/react-spa/bff/.babelrc new file mode 100644 index 000000000000..1ea870ead410 --- /dev/null +++ b/libs/react-spa/bff/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/react-spa/bff/.eslintrc.json b/libs/react-spa/bff/.eslintrc.json new file mode 100644 index 000000000000..75b85077debb --- /dev/null +++ b/libs/react-spa/bff/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/react-spa/bff/README.md b/libs/react-spa/bff/README.md new file mode 100644 index 000000000000..53a6d1e6ec85 --- /dev/null +++ b/libs/react-spa/bff/README.md @@ -0,0 +1,7 @@ +# React SPA BFF + +This library is intended to be used by a React SPA application. It handles authentication with a BFF(Backend For Frontend) server. + +## Running unit tests + +Run `nx test react-spa-bff` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/react-spa/bff/jest.config.ts b/libs/react-spa/bff/jest.config.ts new file mode 100644 index 000000000000..46183d6c70f0 --- /dev/null +++ b/libs/react-spa/bff/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'bff', + preset: '../../../jest.preset.js', + transform: { + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', + '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/libs/react-spa/bff', +} diff --git a/libs/react-spa/bff/project.json b/libs/react-spa/bff/project.json new file mode 100644 index 000000000000..252e6f2aab94 --- /dev/null +++ b/libs/react-spa/bff/project.json @@ -0,0 +1,19 @@ +{ + "name": "react-spa-bff", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/react-spa/bff/src", + "projectType": "library", + "tags": ["lib:react-spa", "scope:react-spa"], + "targets": { + "lint": { + "executor": "@nx/eslint:lint" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/react-spa/bff/jest.config.ts" + } + } + } +} diff --git a/libs/react-spa/bff/src/index.ts b/libs/react-spa/bff/src/index.ts new file mode 100644 index 000000000000..d535a9f34f45 --- /dev/null +++ b/libs/react-spa/bff/src/index.ts @@ -0,0 +1,4 @@ +export * from './lib/BffProvider' +export * from './lib/bff.hooks' +export * from './lib/bff.utils' +export * from './lib/bff.mocks' diff --git a/libs/react-spa/bff/src/lib/BffContext.tsx b/libs/react-spa/bff/src/lib/BffContext.tsx new file mode 100644 index 000000000000..dc8657b37e92 --- /dev/null +++ b/libs/react-spa/bff/src/lib/BffContext.tsx @@ -0,0 +1,15 @@ +import { createContext } from 'react' + +import { BffReducerState } from './bff.state' + +export type BffContextType = BffReducerState & { + signIn(): void + signOut(): void + switchUser(nationalId?: string): void + bffUrlGenerator( + relativePath?: string, + params?: Record, + ): string +} + +export const BffContext = createContext(undefined) diff --git a/libs/react-spa/bff/src/lib/BffPoller.tsx b/libs/react-spa/bff/src/lib/BffPoller.tsx new file mode 100644 index 000000000000..223640bcdaf8 --- /dev/null +++ b/libs/react-spa/bff/src/lib/BffPoller.tsx @@ -0,0 +1,97 @@ +import { usePolling } from '@island.is/react-spa/shared' +import { BffUser } from '@island.is/shared/types' +import { ReactNode, useCallback, useEffect, useMemo } from 'react' +import { + BffBroadcastEvents, + useBff, + useBffBroadcaster, + useUserInfo, +} from './bff.hooks' +import { isNewSession } from './bff.utils' + +type BffPollerProps = { + children: ReactNode + newSessionCb(): void + pollIntervalMS?: number +} + +/** + * BffPoller component continuously polls the user's session + * information from the backend and broadcasts session changes across tabs + * or windows using the BroadcastChannel API. It checks for changes in the + * user's session data and triggers appropriate actions like displaying a + * session expired modal when necessary. + * + * Features: + * - Polls the backend at a specified interval to fetch user session data. + * - If the user's session expires or the backend returns an error, it + * automatically triggers a sign-in process. + * - If a change in user session (e.g., a new session ID) is detected, it + * broadcasts a message to all open tabs/windows and triggers the provided + * `newSessionCb` callback to handle the current tab/window. + * + * @param newSessionCb - Callback function to be called when a new session is detected. + * @param pollIntervalMS - Polling interval in milliseconds. Default is 10000ms. + * + * @usage: + * Wrap your application's root component with BffPoller to continuously + * monitor the user's session and keep session state synchronized across + * multiple tabs/windows. + */ +export const BffPoller = ({ + children, + newSessionCb, + pollIntervalMS = 10000, +}: BffPollerProps) => { + const { signIn, bffUrlGenerator } = useBff() + const userInfo = useUserInfo() + const { postMessage } = useBffBroadcaster() + + const url = useMemo( + () => bffUrlGenerator('/user', { refresh: 'true' }), + [bffUrlGenerator], + ) + + const fetchUser = useCallback(async () => { + const res = await fetch(url, { + credentials: 'include', + }) + + if (!res.ok) { + signIn() + + return + } + + return res.json() as Promise + }, [url, signIn]) + + // Poll user data every 10 seconds + const { data: newUser, error } = usePolling({ + fetcher: fetchUser, + intervalMs: pollIntervalMS, + waitToStartMS: 5000, + }) + + useEffect(() => { + if (error) { + // If user polling fails, likely due to 401, then sign in. + signIn() + } else if (newUser) { + // If session has changed (e.g. delegation switch), then notifiy tabs/windows/iframes and execute the callback. + if (isNewSession(newUser, userInfo)) { + // Note! The tab, window, or iframe that sends this message will not receive it. + // This is because the BroadcastChannel API does not broadcast messages to the sender. + // Therefore we need to manually handle the new session in the current tab/window, by calling the newSessionCb(). + postMessage({ + type: BffBroadcastEvents.NEW_SESSION, + userInfo: newUser, + }) + + newSessionCb() + } + } + }, [newUser, error, userInfo, signIn, postMessage, newSessionCb]) + + return children +} diff --git a/libs/react-spa/bff/src/lib/BffProvider.tsx b/libs/react-spa/bff/src/lib/BffProvider.tsx new file mode 100644 index 000000000000..b04a1a6143be --- /dev/null +++ b/libs/react-spa/bff/src/lib/BffProvider.tsx @@ -0,0 +1,237 @@ +import { useEffectOnce } from '@island.is/react-spa/shared' +import { ReactNode, useCallback, useEffect, useReducer, useState } from 'react' + +import { LoadingScreen } from '@island.is/react/components' +import { BffContext } from './BffContext' +import { BffPoller } from './BffPoller' +import { BffSessionExpiredModal } from './BffSessionExpiredModal' +import { ErrorScreen } from './ErrorScreen' +import { BffBroadcastEvents, useBffBroadcaster } from './bff.hooks' +import { ActionType, LoggedInState, initialState, reducer } from './bff.state' +import { createBffUrlGenerator, isNewSession } from './bff.utils' + +const BFF_SERVER_UNAVAILABLE = 'BFF_SERVER_UNAVAILABLE' + +type BffProviderProps = { + children: ReactNode + /** + * The base path of the application. + */ + applicationBasePath: string + mockedInitialState?: LoggedInState +} + +export const BffProvider = ({ + children, + applicationBasePath, + mockedInitialState, +}: BffProviderProps) => { + const [showSessionExpiredScreen, setSessionExpiredScreen] = useState(false) + const bffUrlGenerator = createBffUrlGenerator(applicationBasePath) + const [state, dispatch] = useReducer( + reducer, + mockedInitialState ?? initialState, + ) + + const { authState } = state + const showErrorScreen = authState === 'error' + const showLoadingScreen = + authState === 'loading' || + authState === 'switching' || + authState === 'logging-out' + const isLoggedIn = authState === 'logged-in' + + const { postMessage } = useBffBroadcaster((event) => { + if ( + isLoggedIn && + event.data.type === BffBroadcastEvents.NEW_SESSION && + isNewSession(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) + } + }) + + useEffect(() => { + if (isLoggedIn) { + // Broadcast to all tabs/windows/iframes that a new session has started + postMessage({ + type: BffBroadcastEvents.NEW_SESSION, + userInfo: state.userInfo, + }) + } + }, [postMessage, state.userInfo, isLoggedIn]) + + const checkLogin = async (noRefresh = false) => { + dispatch({ + type: ActionType.SIGNIN_START, + }) + + try { + const url = bffUrlGenerator('/user', { + refresh: noRefresh.toString(), + }) + + const res = await fetch(url, { + credentials: 'include', + }) + + if (!res.ok) { + // Bff server is down + if (res.status >= 500) { + throw new Error(BFF_SERVER_UNAVAILABLE) + } + + // For other none ok responses, like 401/403, proceed with sign-in redirect. + signIn() + + return + } + + const user = await res.json() + + dispatch({ + type: ActionType.SIGNIN_SUCCESS, + payload: user, + }) + } catch (error) { + dispatch({ + type: ActionType.ERROR, + payload: error, + }) + } + } + + const signIn = useCallback(() => { + dispatch({ + type: ActionType.SIGNIN_START, + }) + + window.location.href = bffUrlGenerator('/login', { + target_link_uri: window.location.href, + }) + }, [bffUrlGenerator]) + + const signOut = useCallback(() => { + if (!state.userInfo) { + return + } + + dispatch({ + type: ActionType.LOGGING_OUT, + }) + + // Broadcast to all tabs/windows/iframes that the user is logging out + postMessage({ + type: BffBroadcastEvents.LOGOUT, + }) + + window.location.href = bffUrlGenerator('/logout', { + sid: state.userInfo.profile.sid, + }) + }, [bffUrlGenerator, postMessage, state.userInfo]) + + const switchUser = (nationalId?: string) => { + dispatch({ + type: ActionType.SWITCH_USER, + }) + + window.location.href = bffUrlGenerator( + '/login', + nationalId + ? { + login_hint: nationalId, + } + : { + prompt: 'select_account', + }, + ) + } + + const checkQueryStringError = () => { + const urlParams = new URLSearchParams(window.location.search) + const error = urlParams.get('bff_error_code') + const errorDescription = urlParams.get('bff_error_description') + + if (error) { + dispatch({ + type: ActionType.ERROR, + payload: new Error(`${error}: ${errorDescription}`), + }) + } + + // Returns true if there is an error + return !!error + } + + useEffectOnce(() => { + const hasError = checkQueryStringError() + + if (!hasError && !isLoggedIn) { + checkLogin() + } + }) + + const newSessionCb = useCallback(() => { + setSessionExpiredScreen(true) + }, []) + + const onRetry = () => { + window.location.href = applicationBasePath + } + + const renderContent = () => { + if (mockedInitialState) { + return children + } + + if (showErrorScreen) { + return ( + + ) + } + + if (showLoadingScreen) { + return + } + + if (showSessionExpiredScreen) { + return + } + + if (isLoggedIn) { + return {children} + } + + return null + } + + return ( + + {renderContent()} + + ) +} diff --git a/libs/react-spa/bff/src/lib/BffSessionExpiredModal.tsx b/libs/react-spa/bff/src/lib/BffSessionExpiredModal.tsx new file mode 100644 index 000000000000..0e7e5545568c --- /dev/null +++ b/libs/react-spa/bff/src/lib/BffSessionExpiredModal.tsx @@ -0,0 +1,40 @@ +import { Box, Button, ProblemTemplate } from '@island.is/island-ui/core' +import { fullScreen } from './ErrorScreen.css' + +type BffSessionExpiredModalProps = { + /** + * Login callback + */ + onLogin(): void +} + +/** + * This screen is unfortunately not translated because at this point we don't have a user locale. + */ +export const BffSessionExpiredModal = ({ + onLogin, +}: BffSessionExpiredModalProps) => ( + + + Þú hefur skráð þig inn í öðru umboði. Viltu{' '} + {' '} + aftur inn? + + } + /> + +) diff --git a/libs/react-spa/bff/src/lib/ErrorScreen.css.ts b/libs/react-spa/bff/src/lib/ErrorScreen.css.ts new file mode 100644 index 000000000000..62d600b0be1b --- /dev/null +++ b/libs/react-spa/bff/src/lib/ErrorScreen.css.ts @@ -0,0 +1,5 @@ +import { style } from '@vanilla-extract/css' + +export const fullScreen = style({ + height: '100vh', +}) diff --git a/libs/react-spa/bff/src/lib/ErrorScreen.tsx b/libs/react-spa/bff/src/lib/ErrorScreen.tsx new file mode 100644 index 000000000000..dbb6b48f1ed4 --- /dev/null +++ b/libs/react-spa/bff/src/lib/ErrorScreen.tsx @@ -0,0 +1,41 @@ +import { Box, Button, ProblemTemplate } from '@island.is/island-ui/core' +import { fullScreen } from './ErrorScreen.css' + +type ErrorScreenProps = { + title?: string + /** + * Retry callback + */ + onRetry(): void +} + +/** + * This screen is unfortunately not translated because at this point we don't have a user locale. + */ +export const ErrorScreen = ({ + title = 'Innskráning mistókst', + onRetry, +}: ErrorScreenProps) => ( + + + Vinsamlegast reyndu aftur síðar.{' '} + + + } + /> + +) diff --git a/libs/react-spa/bff/src/lib/bff.hooks.ts b/libs/react-spa/bff/src/lib/bff.hooks.ts new file mode 100644 index 000000000000..b1cf35fa4281 --- /dev/null +++ b/libs/react-spa/bff/src/lib/bff.hooks.ts @@ -0,0 +1,127 @@ +import { AuthContext } from '@island.is/auth/react' +import { BffUser, User } from '@island.is/shared/types' +import { useContext } from 'react' +import { BffContext, BffContextType } from './BffContext' +import { createBroadcasterHook } from '@island.is/react-spa/shared' + +/** + * Maps an object to a BffUser type. + */ +export const mapToBffUser = (input: User): BffUser => { + const { + profile: { + sid, + birthdate, + nationalId, + name, + idp, + actor, + subjectType, + delegationType, + locale, + }, + scopes, + } = input + + // Return a mapped BffUser object + return { + scopes: scopes || [], + profile: { + sid: sid || '', + birthdate, + nationalId, + name, + idp, + actor, + subjectType, + delegationType, + locale, + }, + } +} + +/** + * Dynamic hook to get the bff context. + */ +export const useDynamicBffHook = (hookName: string): BffContextType => { + const bffContext = useContext(BffContext) + + if (!bffContext) { + throw new Error(`${hookName} must be used within a BffProvider`) + } + + return bffContext +} + +/** + * This hook is used to get the BFF authentication context. + * It has backward compatibility with AuthContext. + */ +export const useAuth = () => { + const bffContext = useContext(BffContext) + const authContext = useContext(AuthContext) + + if (bffContext) { + return bffContext + } + + if (authContext) { + return authContext + } + + const errorMsg = (providerStr: string) => + `useAuth must be used within a ${providerStr}` + + if (!authContext) { + throw new Error(errorMsg('AuthProvider')) + } + + throw new Error(errorMsg('BffProvider')) +} + +/** + * This hook is used to get user information. + * It will determine what context to use based on the context that is available. + * We will remove support for AuthContext when other clients transition over to BFF. + * If AuthContext is being used then we will map the user info to the BffUser type. + */ +export const useUserInfo = (): BffUser => { + const bffContext = useContext(BffContext) + const authContext = useContext(AuthContext) + + if (bffContext?.userInfo) { + return bffContext.userInfo + } else if (authContext?.userInfo) { + return mapToBffUser(authContext.userInfo) + } + + throw new Error('User info is not available. Is the user authenticated?') +} + +/** + * This hook is used to get the bff url generator. + * The bff url generator is used to generate urls for the Bff in a conveinent way. + */ +export const useBffUrlGenerator = () => + useDynamicBffHook(useBffUrlGenerator.name).bffUrlGenerator + +export const useBff = () => useDynamicBffHook(useBff.name) + +export enum BffBroadcastEvents { + NEW_SESSION = 'NEW_SESSION', + LOGOUT = 'LOGOUT', +} + +type NewSessionEvent = { + type: BffBroadcastEvents.NEW_SESSION + userInfo: BffUser +} + +type LogoutEvent = { + type: BffBroadcastEvents.LOGOUT +} + +export type BffBroadcastEvent = NewSessionEvent | LogoutEvent + +export const useBffBroadcaster = + createBroadcasterHook('bff_auth_channel') diff --git a/libs/react-spa/bff/src/lib/bff.mocks.ts b/libs/react-spa/bff/src/lib/bff.mocks.ts new file mode 100644 index 000000000000..2af096c1b055 --- /dev/null +++ b/libs/react-spa/bff/src/lib/bff.mocks.ts @@ -0,0 +1,19 @@ +import { BffUser } from '@island.is/shared/types' +import { LoggedInState } from './bff.state' + +export const createMockedInitialState = ( + user?: Partial, +): LoggedInState => ({ + userInfo: { + profile: { + name: 'Mock', + locale: 'is', + nationalId: '0000000000', + ...user?.profile, + } as BffUser['profile'], + scopes: user?.scopes ?? [], + }, + authState: 'logged-in', + isAuthenticated: true, + error: null, +}) diff --git a/libs/react-spa/bff/src/lib/bff.state.ts b/libs/react-spa/bff/src/lib/bff.state.ts new file mode 100644 index 000000000000..d23dfd4b4f30 --- /dev/null +++ b/libs/react-spa/bff/src/lib/bff.state.ts @@ -0,0 +1,116 @@ +import { BffUser } from '@island.is/shared/types' + +// Defining the possible states for authentication +export type BffState = + | 'logged-out' + | 'loading' + | 'logged-in' + | 'switching' + | 'logging-out' + | 'error' + +export enum ActionType { + SIGNIN_START = 'SIGNIN_START', + SIGNIN_SUCCESS = 'SIGNIN_SUCCESS', + LOGGING_OUT = 'LOGGING_OUT', + LOGGED_OUT = 'LOGGED_OUT', + SWITCH_USER = 'SWITCH_USER', + ERROR = 'ERROR', +} + +type NonLoggedInAuthState = Exclude + +export interface BffReducerStateBase { + authState: BffState + isAuthenticated: boolean + error?: Error | null +} + +// State when the user is not logged in +export interface NonLoggedInState extends BffReducerStateBase { + authState: NonLoggedInAuthState + userInfo: null +} + +// State when the user is logged in +export interface LoggedInState extends BffReducerStateBase { + authState: 'logged-in' + userInfo: BffUser + isAuthenticated: true +} + +export type BffReducerState = NonLoggedInState | LoggedInState + +export const initialState: NonLoggedInState = { + userInfo: null, + authState: 'logged-out', + isAuthenticated: false, + error: null, +} + +export type Action = + | { + type: + | ActionType.SIGNIN_START + | ActionType.LOGGING_OUT + | ActionType.LOGGED_OUT + | ActionType.SWITCH_USER + } + | { type: ActionType.SIGNIN_SUCCESS; payload: BffUser } + | { type: ActionType.ERROR; payload: Error } + +/** + * Helper function to reset user-related state when switching users or logging out + */ +const resetState = (authState: NonLoggedInAuthState): NonLoggedInState => ({ + userInfo: null, + authState, + isAuthenticated: false, + error: null, +}) + +/** + * Reducer function to handle state transitions based on actions + */ +export const reducer = ( + state: BffReducerState, + action: Action, +): BffReducerState => { + switch (action.type) { + case ActionType.SIGNIN_START: + return { + ...state, + authState: 'loading', + userInfo: null, + } + + case ActionType.SIGNIN_SUCCESS: + return { + ...state, + userInfo: action.payload, + authState: 'logged-in', + isAuthenticated: true, + error: null, + } + + case ActionType.LOGGING_OUT: + return resetState('logging-out') + + case ActionType.SWITCH_USER: + return resetState('switching') + + case ActionType.ERROR: + return { + ...state, + error: action.payload, + authState: 'error', + userInfo: null, + } + + case ActionType.LOGGED_OUT: + return initialState + + default: + return state + } +} diff --git a/libs/react-spa/bff/src/lib/bff.utils.ts b/libs/react-spa/bff/src/lib/bff.utils.ts new file mode 100644 index 000000000000..1c7ce0f0e1d9 --- /dev/null +++ b/libs/react-spa/bff/src/lib/bff.utils.ts @@ -0,0 +1,48 @@ +import { BffUser } from '@island.is/shared/types' + +/** + * Creates a function that can generate a BFF URLs. + * + * @usage + * const bffBaseUrl = createBffUrlGenerator('/myapplication') + * const userUrl = bffBaseUrl('/user') // http://localhost:3010/myapplication/bff/user + * const userUrlWithParams = bffBaseUrl('/user', { id: '123' }) // http://localhost:3010/myapplication/bff/user?id=123 + */ +export const createBffUrlGenerator = (basePath: string) => { + const sanitizedBasePath = sanitizePath(basePath) + const baseUrl = `${window.location.origin}/${sanitizedBasePath}/bff` + + return (relativePath = '', params?: Record) => { + const url = `${baseUrl}${relativePath}` + + if (params) { + const qs = createQueryStr(params) + + return `${url}${qs ? `?${qs}` : ''}` + } + + return url + } +} + +/** + * Trim any leading and trailing slashes + */ +const sanitizePath = (path: string) => path.replace(/^\/+|\/+$/g, '') + +/** + * Creates a query string from an object + */ +export const createQueryStr = (params: Record) => { + return new URLSearchParams(params).toString() +} + +/** + * This method checks if the user has a new session + */ +export const isNewSession = (oldUser: BffUser, newUser: BffUser) => { + const oldSid = oldUser.profile.sid + const newSid = newUser.profile.sid + + return oldSid && newSid && oldSid !== newSid +} diff --git a/libs/react-spa/bff/tsconfig.json b/libs/react-spa/bff/tsconfig.json new file mode 100644 index 000000000000..4daaf45cd328 --- /dev/null +++ b/libs/react-spa/bff/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/react-spa/bff/tsconfig.lib.json b/libs/react-spa/bff/tsconfig.lib.json new file mode 100644 index 000000000000..21799b3e6ba3 --- /dev/null +++ b/libs/react-spa/bff/tsconfig.lib.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/react-spa/bff/tsconfig.spec.json b/libs/react-spa/bff/tsconfig.spec.json new file mode 100644 index 000000000000..25b7af8f6d00 --- /dev/null +++ b/libs/react-spa/bff/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/react-spa/shared/src/hooks/useBroadcaster.ts b/libs/react-spa/shared/src/hooks/useBroadcaster.ts new file mode 100644 index 000000000000..4f409c6bb2cf --- /dev/null +++ b/libs/react-spa/shared/src/hooks/useBroadcaster.ts @@ -0,0 +1,145 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +type UseBroadcasterArgs = { + channel: BroadcastChannel + onMessage?: (event: MessageEvent) => void +} + +type UseBroadcasterReturn = { + postMessage: (message: T) => void + error: Error | null +} + +/** + * Custom hook to manage communication via a BroadcastChannel. + * + * This hook: + * - Sets up a listener for incoming messages on the provided BroadcastChannel. + * - Provides a `postMessage` function to send messages through the BroadcastChannel. + * - Handles errors encountered while sending messages. + * + * @param channel - The BroadcastChannel instance to use for messaging. + * @param onMessage - Optional callback function to handle incoming messages. + * + * @returns An object containing the BroadcastChannel instance, the `postMessage` function, and any errors encountered. + */ +export const useBroadcaster = ({ + channel, + onMessage, +}: UseBroadcasterArgs): UseBroadcasterReturn => { + const [error, setError] = useState(null) + const onMessageRef = useRef(onMessage) + + useEffect(() => { + onMessageRef.current = onMessage + }, [onMessage]) + + const handleBroadcastMessage = useCallback((event: MessageEvent) => { + try { + onMessageRef.current?.(event) + } catch (err) { + setError(err as Error) + } + }, []) + + useEffect(() => { + channel.addEventListener('message', handleBroadcastMessage) + + return () => { + channel.removeEventListener('message', handleBroadcastMessage) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [channel]) + + const postMessage = useCallback( + (message: T) => { + try { + channel.postMessage(message) + } catch (err) { + console.error('Error posting message to BroadcastChannel:', err) + setError(err as Error) + } + }, + [channel], + ) + + return { + postMessage, + error, + } +} + +const isTestEnv = process.env.NODE_ENV === 'test' + +/** + * Factory function to create a custom hook for managing a BroadcastChannel. + * + * This factory function: + * - Creates a new BroadcastChannel instance with the specified channel name. + * - Passes the instance to `useBroadcaster` along with the `onMessage` handler if provided. + * + * @param channelName - The name of the BroadcastChannel to listen to. + * + * @returns A hook that can be used to manage the BroadcastChannel and handle messages. + * + * @example + * export enum AuthBroadcastEvents { + * NEW_SESSION = 'NEW_SESSION', + * LOGOUT = 'LOGOUT', + * } + * + * type NewSessionEvent = { + * type: AuthBroadcastEvents.NEW_SESSION + * userInfo: User + * } + * + * type LogoutEvent = { + * type: AuthBroadcastEvents.LOGOUT + * } + * + * export type AuthBroadcastEvent = NewSessionEvent | LogoutEvent + * + * export const useAuthBroadcaster = createBroadcasterHook('auth_channel') + * + * const MyComponent = () => { + * const { postMessage, error } = useAuthBroadcaster((event) => { + * if (event.data.type === AuthBroadcastEvents.NEW_SESSION) { + * console.log('New session started:', event.data.userInfo) + * } + * }) + * + * useEffect(() => { + * postMessage({ type: AuthBroadcastEvents.LOGOUT }) + * }, [postMessage]) + */ +export const createBroadcasterHook = (channelName: string) => { + let broadcastChannelInstance: BroadcastChannel | null = null + + // Skip BroadcastChannel initialization in test environment since it is not supported by Jest. + if (!isTestEnv) { + broadcastChannelInstance = new BroadcastChannel(channelName) + } + + return (onMessage?: (event: MessageEvent) => void) => { + if (isTestEnv) { + return { + postMessage: (message: Events) => { + console.warn( + 'postMessage called in test environment with message: ', + message, + ) + }, + error: null, + } as UseBroadcasterReturn + } else if (!broadcastChannelInstance) { + throw new Error( + 'BroadcastChannel is not supported in this environment. Ensure the environment supports BroadcastChannel before using this hook.', + ) + } + + return useBroadcaster({ + channel: broadcastChannelInstance, + onMessage, + }) + } +} diff --git a/libs/react-spa/shared/src/hooks/usePolling.tsx b/libs/react-spa/shared/src/hooks/usePolling.tsx new file mode 100644 index 000000000000..72d821a62eb9 --- /dev/null +++ b/libs/react-spa/shared/src/hooks/usePolling.tsx @@ -0,0 +1,113 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +interface PollingResult { + data?: T + loading: boolean + error: Error | null +} + +type UsePollingProps = { + /** + * A function that fetches data (returns a promise). + */ + fetcher(): Promise + /** + * The interval in milliseconds for how often to poll. + */ + intervalMs?: number + /** + * Optional prop for controlling polling externally. + */ + isCancelledProp?: boolean + /** + * The time in milliseconds to wait before starting the polling. + */ + waitToStartMS?: number +} + +/** + * usePolling is a custom hook for polling data at a specified interval. + */ +export const usePolling = ({ + fetcher, + intervalMs = 10000, + isCancelledProp, + waitToStartMS, +}: UsePollingProps): PollingResult => { + const [data, setData] = useState(undefined) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [shouldPoll, setShouldPoll] = useState(false) + + const intervalIdRef = useRef | null>(null) + const isCancelledRef = useRef(false) + + // Sync the external isCancelledProp with the ref to ensure real-time updates + useEffect(() => { + isCancelledRef.current = !!isCancelledProp + }, [isCancelledProp]) + + const poll = useCallback(async () => { + setLoading(true) + try { + const result = await fetcher() + + if (!isCancelledRef.current) { + setData(result) + setError(null) + } + } catch (err) { + if (!isCancelledRef.current) { + setError(err as Error) + } + } finally { + setLoading(false) + } + }, [fetcher]) + + useEffect(() => { + // The cleanup function sets isCancelled to true to stop polling + // If the polling resumes then we set it back to false + isCancelledRef.current = false + + if (!shouldPoll) { + return + } + + // Initial poll + poll() + + // Set up the interval for polling + intervalIdRef.current = setInterval(poll, intervalMs) + + // Cleanup on unmount or when polling should stop + return () => { + isCancelledRef.current = true + + if (intervalIdRef.current) { + clearInterval(intervalIdRef.current) + } + } + }, [fetcher, intervalMs, shouldPoll, poll]) + + useEffect(() => { + let timeoutId: ReturnType | null = null + + if (waitToStartMS) { + timeoutId = setTimeout(() => { + setShouldPoll(true) + }, waitToStartMS) + } else { + setShouldPoll(true) + } + + return () => { + // Clear the timeout if the component unmounts before the timeout is reached + if (timeoutId) { + clearTimeout(timeoutId) + } + } + }, [waitToStartMS]) + + return { data, loading, error } +} diff --git a/libs/react-spa/shared/src/index.ts b/libs/react-spa/shared/src/index.ts index 07a912e43525..d5e46c014376 100644 --- a/libs/react-spa/shared/src/index.ts +++ b/libs/react-spa/shared/src/index.ts @@ -10,6 +10,8 @@ export * from './lib/messages' // hooks export * from './hooks/useSubmitting' export * from './hooks/useEffectOnce' +export * from './hooks/usePolling' +export * from './hooks/useBroadcaster' // utils export * from './utils/getOrganizationSlugFromError' diff --git a/libs/react/feature-flags/src/lib/context.tsx b/libs/react/feature-flags/src/lib/context.tsx index c42ec62bc56a..8e5156cf44a3 100644 --- a/libs/react/feature-flags/src/lib/context.tsx +++ b/libs/react/feature-flags/src/lib/context.tsx @@ -1,13 +1,13 @@ -import React, { FC, createContext, useContext, useMemo } from 'react' -import * as ConfigCatJS from 'configcat-js' import { FeatureFlagClient, FeatureFlagUser, - SettingValue, SettingTypeOf, + SettingValue, createClientFactory, } from '@island.is/feature-flags' -import { useAuth } from '@island.is/auth/react' +import { useUserInfo } from '@island.is/react-spa/bff' +import * as ConfigCatJS from 'configcat-js' +import React, { FC, createContext, useContext, useMemo } from 'react' const createClient = createClientFactory(ConfigCatJS) @@ -26,7 +26,7 @@ export interface FeatureFlagContextProviderProps { export const FeatureFlagProvider: FC< React.PropsWithChildren > = ({ children, sdkKey, defaultUser: userProp }) => { - const { userInfo } = useAuth() + const userInfo = useUserInfo() const featureFlagClient = useMemo(() => { return createClient({ sdkKey }) }, [sdkKey]) diff --git a/libs/service-portal/information/src/components/PersonalInformation/UserOnboardingModal/components/Header.tsx b/libs/service-portal/information/src/components/PersonalInformation/UserOnboardingModal/components/Header.tsx index 5ac9e305fbe4..ce24187e7999 100644 --- a/libs/service-portal/information/src/components/PersonalInformation/UserOnboardingModal/components/Header.tsx +++ b/libs/service-portal/information/src/components/PersonalInformation/UserOnboardingModal/components/Header.tsx @@ -46,7 +46,7 @@ export const OnboardingHeader = ({ {!hideClose && ( - {user && } + {user && } import('./screens/UserInfoOverview/UserInfoOverview'), @@ -30,23 +29,23 @@ const UserNotificationsSettings = lazy(() => import('./screens/UserNotifications/UserNotifications'), ) -const sharedRoutes = (userInfo: User) => [ +const sharedRoutes = (scopes: string[]) => [ { name: m.mySettings, path: InformationPaths.SettingsOld, - enabled: userInfo.scopes.includes(UserProfileScope.write), + enabled: scopes.includes(UserProfileScope.write), element: , }, { name: m.mySettings, path: InformationPaths.Settings, - enabled: userInfo.scopes.includes(UserProfileScope.write), + enabled: scopes.includes(UserProfileScope.write), element: , }, { name: 'Notifications', path: InformationPaths.Notifications, - enabled: userInfo.scopes.includes(DocumentsScope.main), + enabled: scopes.includes(DocumentsScope.main), key: 'Notifications', element: , }, @@ -98,7 +97,7 @@ export const informationModule: PortalModule = { enabled: userInfo.scopes.includes(ApiScope.meDetails), element: , }, - ...sharedRoutes(userInfo), + ...sharedRoutes(userInfo.scopes), ], companyRoutes: ({ userInfo }) => [ { @@ -107,6 +106,6 @@ export const informationModule: PortalModule = { enabled: userInfo.scopes.includes(ApiScope.company), element: , }, - ...sharedRoutes(userInfo), + ...sharedRoutes(userInfo.scopes), ], } diff --git a/libs/shared/components/src/auth/UserMenu/UserButton.tsx b/libs/shared/components/src/auth/UserMenu/UserButton.tsx index 0a8b3909cac6..23b175d72d6b 100644 --- a/libs/shared/components/src/auth/UserMenu/UserButton.tsx +++ b/libs/shared/components/src/auth/UserMenu/UserButton.tsx @@ -1,19 +1,17 @@ -import React from 'react' import { + Box, Button, Hidden, Inline, UserAvatar, - Box, } from '@island.is/island-ui/core' -import { User } from '@island.is/shared/types' import { useLocale } from '@island.is/localization' +import { useUserInfo } from '@island.is/react-spa/bff' import { userMessages } from '@island.is/shared/translations' -import * as styles from './UserMenu.css' import { checkDelegation } from '@island.is/shared/utils' +import * as styles from './UserMenu.css' interface UserButtonProps { - user: User small: boolean onClick(): void iconOnlyMobile?: boolean @@ -22,11 +20,11 @@ interface UserButtonProps { export const UserButton = ({ onClick, - user, small, iconOnlyMobile = false, userMenuOpen, }: UserButtonProps) => { + const user = useUserInfo() const isDelegation = checkDelegation(user) const { profile } = user const { formatMessage } = useLocale() @@ -77,7 +75,9 @@ export const UserButton = ({ {isDelegation ? ( <>
{profile.name}
-
{profile.actor!.name}
+ {profile?.actor?.name && ( +
{profile.actor.name}
+ )} ) : ( profile.name diff --git a/libs/shared/components/src/auth/UserMenu/UserDelegations.tsx b/libs/shared/components/src/auth/UserMenu/UserDelegations.tsx index 229260989e8f..b4db29d4d10f 100644 --- a/libs/shared/components/src/auth/UserMenu/UserDelegations.tsx +++ b/libs/shared/components/src/auth/UserMenu/UserDelegations.tsx @@ -1,23 +1,20 @@ -import React from 'react' -import { Stack, Text, SkeletonLoader, Box } from '@island.is/island-ui/core' -import { User } from '@island.is/shared/types' +import { Box, Stack } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' +import { useAuth, useUserInfo } from '@island.is/react-spa/bff' import { userMessages } from '@island.is/shared/translations' -import { UserTopicCard } from './UserTopicCard' import { UserDropdownItem } from './UserDropdownItem' -import { useAuth } from '@island.is/auth/react' +import { UserTopicCard } from './UserTopicCard' interface UserDelegationsProps { - user: User showActorButton: boolean onSwitchUser: (nationalId: string) => void } export const UserDelegations = ({ - user, showActorButton, onSwitchUser, }: UserDelegationsProps) => { + const user = useUserInfo() const { formatMessage } = useLocale() const { switchUser } = useAuth() const actor = user.profile.actor @@ -28,9 +25,9 @@ export const UserDelegations = ({ {showActorButton && !!actor && ( onSwitchUser(actor?.nationalId)} + onClick={() => onSwitchUser(actor.nationalId)} > - {actor?.name} + {actor.name} )} > onLogout?: () => void @@ -34,7 +34,6 @@ interface UserDropdownProps { } export const UserDropdown = ({ - user, dropdownState, setDropdownState, onSwitchUser, @@ -43,6 +42,7 @@ export const UserDropdown = ({ showActorButton, showDropdownLanguage, }: UserDropdownProps) => { + const user = useUserInfo() const { formatMessage } = useLocale() const isVisible = dropdownState === 'open' const onClose = () => { @@ -127,16 +127,13 @@ export const UserDropdown = ({
{showDropdownLanguage && ( - - {} - + {} )} diff --git a/libs/shared/components/src/auth/UserMenu/UserLanguageSwitcher.tsx b/libs/shared/components/src/auth/UserMenu/UserLanguageSwitcher.tsx index 9f3eada03a13..3ed67d33e9ec 100644 --- a/libs/shared/components/src/auth/UserMenu/UserLanguageSwitcher.tsx +++ b/libs/shared/components/src/auth/UserMenu/UserLanguageSwitcher.tsx @@ -1,18 +1,17 @@ -import React from 'react' import { Box, Button, Select } from '@island.is/island-ui/core' -import { User, Locale } from '@island.is/shared/types' import { useLocale } from '@island.is/localization' -import { useUpdateUserProfileMutation } from '../../../gen/schema' +import { useUserInfo } from '@island.is/react-spa/bff' import { sharedMessages } from '@island.is/shared/translations' +import { Locale } from '@island.is/shared/types' import { checkDelegation } from '@island.is/shared/utils' +import { useUpdateUserProfileMutation } from '../../../gen/schema' export const UserLanguageSwitcher = ({ - user, dropdown = false, }: { - user: User dropdown?: boolean }) => { + const user = useUserInfo() const { lang, formatMessage, changeLanguage } = useLocale() const [updateUserProfileMutation] = useUpdateUserProfileMutation() diff --git a/libs/shared/components/src/auth/UserMenu/UserMenu.tsx b/libs/shared/components/src/auth/UserMenu/UserMenu.tsx index b831fbdf4b32..875b16169a25 100644 --- a/libs/shared/components/src/auth/UserMenu/UserMenu.tsx +++ b/libs/shared/components/src/auth/UserMenu/UserMenu.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from 'react' import { Box, Hidden } from '@island.is/island-ui/core' -import { useAuth } from '@island.is/auth/react' +import { useAuth } from '@island.is/react-spa/bff' +import { useEffect, useState } from 'react' import { UserButton } from './UserButton' import { UserDropdown } from './UserDropdown' import { UserLanguageSwitcher } from './UserLanguageSwitcher' @@ -52,19 +52,16 @@ export const UserMenu = ({ {showLanguageSwitcher && ( - + )} - { diff --git a/libs/shared/mocking/src/msw/startMocking.ts b/libs/shared/mocking/src/msw/startMocking.ts index 27baa3ce6c20..9c9b225997bb 100644 --- a/libs/shared/mocking/src/msw/startMocking.ts +++ b/libs/shared/mocking/src/msw/startMocking.ts @@ -3,6 +3,22 @@ import { RequestHandler } from 'msw' // eslint-disable-next-line @typescript-eslint/no-explicit-any export declare type RequestHandlersList = RequestHandler[] +const allowedKeyPaths = ['stjornbord', 'minarsidur'] + +const extractUniqueKeyPath = (url: string) => { + try { + const parsedUrl = new URL(url) + const pathSegments = parsedUrl.pathname + .replace(/\/$/, '') + .split('/') + .filter(Boolean) + return pathSegments.length > 0 ? pathSegments[0] : null + } catch (error) { + // noop + return null + } +} + export const startMocking = (requestHandlers: RequestHandlersList) => { if (typeof window === 'undefined') { // https://github.com/webpack/webpack/issues/8826 @@ -15,10 +31,14 @@ export const startMocking = (requestHandlers: RequestHandlersList) => { // eslint-disable-next-line @typescript-eslint/no-var-requires const { setupWorker } = require('msw') const worker = setupWorker(...requestHandlers) - if (location.pathname.split('/')[1] === 'minarsidur') { + const keyPath = extractUniqueKeyPath(location.href) + + if (keyPath && allowedKeyPaths.includes(keyPath)) { + const normalizedPath = keyPath.endsWith('/') ? keyPath : `${keyPath}/` + worker.start({ serviceWorker: { - url: '/minarsidur/mockServiceWorker.js', + url: `/${normalizedPath}mockServiceWorker.js`, }, }) } else { diff --git a/libs/shared/types/src/index.ts b/libs/shared/types/src/index.ts index cb35f1030dd4..66ea2586686e 100644 --- a/libs/shared/types/src/index.ts +++ b/libs/shared/types/src/index.ts @@ -10,3 +10,4 @@ export * from './lib/delegation' export * from './lib/environment' export * from './lib/searchable-content-types' export * from './lib/PersonalRepresentativeDelegationType' +export * from './lib/bff' diff --git a/libs/shared/types/src/lib/bff.ts b/libs/shared/types/src/lib/bff.ts new file mode 100644 index 000000000000..436aa0ae8907 --- /dev/null +++ b/libs/shared/types/src/lib/bff.ts @@ -0,0 +1,24 @@ +import { AuthDelegationType } from './delegation' + +export interface IdTokenClaims { + // Session ID + sid: string + // Birthdate in the format YYYY-MM-DD + birthdate?: string + nationalId: string + name: string + // Identity provider + idp: string + actor?: { + nationalId: string + name: string + } + subjectType: 'person' | 'legalEntity' + delegationType?: AuthDelegationType[] + locale?: string +} + +export type BffUser = { + scopes: string[] + profile: IdTokenClaims +} diff --git a/libs/shared/utils/src/lib/isDelegation.ts b/libs/shared/utils/src/lib/isDelegation.ts index 9708f075015b..fb36417a6f5d 100644 --- a/libs/shared/utils/src/lib/isDelegation.ts +++ b/libs/shared/utils/src/lib/isDelegation.ts @@ -1,5 +1,5 @@ -import { User } from '@island.is/shared/types' +import { BffUser, User } from '@island.is/shared/types' -export const checkDelegation = (user: User) => { +export const checkDelegation = (user: User | BffUser) => { return Boolean(user?.profile.actor) } diff --git a/libs/testing/nest/src/lib/testServer.ts b/libs/testing/nest/src/lib/testServer.ts index 00ae8af89a00..fefd302a8214 100644 --- a/libs/testing/nest/src/lib/testServer.ts +++ b/libs/testing/nest/src/lib/testServer.ts @@ -3,7 +3,8 @@ import { Test } from '@nestjs/testing' import { TestingModuleBuilder } from '@nestjs/testing/testing-module.builder' import { InfraModule, HealthCheckOptions } from '@island.is/infra-nest-server' -import bodyParser from 'body-parser' + +import cookieParser from 'cookie-parser' type CleanUp = () => Promise | undefined @@ -74,6 +75,8 @@ export const testServer = async ({ await beforeServerStart(app) } + app.use(cookieParser()) + await app.init() const hookCleanups = await Promise.all( diff --git a/tsconfig.base.json b/tsconfig.base.json index 79b510cb730e..1d7accc38f8c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -981,6 +981,7 @@ "@island.is/portals/shared-modules/delegations/messages": [ "libs/portals/shared-modules/delegations/src/lib/messages.ts" ], + "@island.is/react-spa/bff": ["libs/react-spa/bff/src/index.ts"], "@island.is/react-spa/shared": ["libs/react-spa/shared/src/index.ts"], "@island.is/react/components": ["libs/react/components/src/index.ts"], "@island.is/react/feature-flags": [ From 82fde9094f5a3bc88b6f2b6d0258f8234ecf4819 Mon Sep 17 00:00:00 2001 From: Kristofer Date: Fri, 1 Nov 2024 11:44:07 +0000 Subject: [PATCH 07/12] revert: "fix: added missing grant to auth ids api for user notification (#16680)" (#16693) This reverts commit f0dd1e433238a746ed78ad1dbf89ef9339075347. --- apps/services/auth/ids-api/infra/ids-api.ts | 1 - charts/identity-server/values.dev.yaml | 15 ++++++--------- charts/identity-server/values.prod.yaml | 15 ++++++--------- charts/identity-server/values.staging.yaml | 15 ++++++--------- 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/apps/services/auth/ids-api/infra/ids-api.ts b/apps/services/auth/ids-api/infra/ids-api.ts index 470a1765ecd1..14ac4f6c60bf 100644 --- a/apps/services/auth/ids-api/infra/ids-api.ts +++ b/apps/services/auth/ids-api/infra/ids-api.ts @@ -122,7 +122,6 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => { min: 2, max: 15, }) - .grantNamespaces('user-notification') } const cleanupId = 'services-auth-ids-api-cleanup' diff --git a/charts/identity-server/values.dev.yaml b/charts/identity-server/values.dev.yaml index c7f2f34b4f1c..c9f3dca4c65a 100644 --- a/charts/identity-server/values.dev.yaml +++ b/charts/identity-server/values.dev.yaml @@ -123,9 +123,8 @@ identity-server: SessionsApiSettings__BaseAddress: 'http://web-services-sessions.services-sessions.svc.cluster.local' files: - 'ids-signing.pfx' - grantNamespaces: - - 'user-notification' - grantNamespacesEnabled: true + grantNamespaces: [] + grantNamespacesEnabled: false healthCheck: liveness: initialDelaySeconds: 3 @@ -432,9 +431,8 @@ services-auth-ids-api: XROAD_TJODSKRA_MEMBER_CODE: '10001' XROAD_TLS_BASE_PATH: 'https://securityserver.dev01.devland.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.dev01.devland.is/r1/IS-DEV' - grantNamespaces: - - 'user-notification' - grantNamespacesEnabled: true + grantNamespaces: [] + grantNamespacesEnabled: false healthCheck: liveness: initialDelaySeconds: 3 @@ -535,9 +533,8 @@ services-auth-ids-api-cleanup: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=921 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' - grantNamespaces: - - 'user-notification' - grantNamespacesEnabled: true + grantNamespaces: [] + grantNamespacesEnabled: false healthCheck: liveness: initialDelaySeconds: 3 diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml index 39262957015d..58e9d76a535b 100644 --- a/charts/identity-server/values.prod.yaml +++ b/charts/identity-server/values.prod.yaml @@ -121,9 +121,8 @@ identity-server: SessionsApiSettings__BaseAddress: 'https://sessions-api.internal.island.is' files: - 'ids-signing.pfx' - grantNamespaces: - - 'user-notification' - grantNamespacesEnabled: true + grantNamespaces: [] + grantNamespacesEnabled: false healthCheck: liveness: initialDelaySeconds: 3 @@ -429,9 +428,8 @@ services-auth-ids-api: XROAD_TJODSKRA_MEMBER_CODE: '6503760649' XROAD_TLS_BASE_PATH: 'https://securityserver.island.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.island.is/r1/IS' - grantNamespaces: - - 'user-notification' - grantNamespacesEnabled: true + grantNamespaces: [] + grantNamespacesEnabled: false healthCheck: liveness: initialDelaySeconds: 3 @@ -532,9 +530,8 @@ services-auth-ids-api-cleanup: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=921 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' - grantNamespaces: - - 'user-notification' - grantNamespacesEnabled: true + grantNamespaces: [] + grantNamespacesEnabled: false healthCheck: liveness: initialDelaySeconds: 3 diff --git a/charts/identity-server/values.staging.yaml b/charts/identity-server/values.staging.yaml index 4ced0775ec0b..07afb0776b4c 100644 --- a/charts/identity-server/values.staging.yaml +++ b/charts/identity-server/values.staging.yaml @@ -123,9 +123,8 @@ identity-server: SessionsApiSettings__BaseAddress: 'http://web-services-sessions.services-sessions.svc.cluster.local' files: - 'ids-signing.pfx' - grantNamespaces: - - 'user-notification' - grantNamespacesEnabled: true + grantNamespaces: [] + grantNamespacesEnabled: false healthCheck: liveness: initialDelaySeconds: 3 @@ -432,9 +431,8 @@ services-auth-ids-api: XROAD_TJODSKRA_MEMBER_CODE: '6503760649' XROAD_TLS_BASE_PATH: 'https://securityserver.staging01.devland.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.staging01.devland.is/r1/IS-TEST' - grantNamespaces: - - 'user-notification' - grantNamespacesEnabled: true + grantNamespaces: [] + grantNamespacesEnabled: false healthCheck: liveness: initialDelaySeconds: 3 @@ -535,9 +533,8 @@ services-auth-ids-api-cleanup: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=921 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' - grantNamespaces: - - 'user-notification' - grantNamespacesEnabled: true + grantNamespaces: [] + grantNamespacesEnabled: false healthCheck: liveness: initialDelaySeconds: 3 From b267ed0cdf0bcd1f963df7284b1fa4050769fc73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dvar=20Oddsson?= Date: Fri, 1 Nov 2024 12:31:48 +0000 Subject: [PATCH 08/12] feat(j-s): Send service update emails to prosecutors (#16671) * Send service failed and success notification to prosecutors * Add overview urls for prosecutors and judges * Checkpoint * Get case in component * Remove console.log * Refactor * Rename Notification to routehandler * Cleanup * Refactor * Remove unused code --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../subpoenaNotification.service.ts | 89 +++++++++----- apps/judicial-system/web/pages/beinir/[id].ts | 3 + .../Shared/RouteHandler/RouteHandler.css.ts | 12 ++ .../Shared/RouteHandler/RouteHandler.tsx | 113 ++++++++++++++++++ libs/judicial-system/consts/src/lib/consts.ts | 1 + 5 files changed, 188 insertions(+), 30 deletions(-) create mode 100644 apps/judicial-system/web/pages/beinir/[id].ts create mode 100644 apps/judicial-system/web/src/routes/Shared/RouteHandler/RouteHandler.css.ts create mode 100644 apps/judicial-system/web/src/routes/Shared/RouteHandler/RouteHandler.tsx diff --git a/apps/judicial-system/backend/src/app/modules/notification/subpoenaNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/subpoenaNotification.service.ts index 61b9e9a86a00..13fad8b63ff5 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/subpoenaNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/subpoenaNotification.service.ts @@ -12,7 +12,7 @@ import { EmailService } from '@island.is/email-service' import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' import { type ConfigType } from '@island.is/nest/config' -import { INDICTMENTS_COURT_OVERVIEW_ROUTE } from '@island.is/judicial-system/consts' +import { ROUTE_HANDLER_ROUTE } from '@island.is/judicial-system/consts' import { SubpoenaNotificationType } from '@island.is/judicial-system/types' import { Case } from '../case' @@ -50,42 +50,33 @@ export class SubpoenaNotificationService extends BaseNotificationService { notificationType: SubpoenaNotificationType, subject: MessageDescriptor, body: MessageDescriptor, + to: { name?: string; email?: string }[], ) { const formattedSubject = this.formatMessage(subject, { courtCaseNumber: theCase.courtCaseNumber, }) - const formattedBody = this.formatMessage(body, { - courtCaseNumber: theCase.courtCaseNumber, - linkStart: ``, - linkEnd: '', - }) const promises: Promise[] = [] - if (theCase.judge?.email) { - promises.push( - this.sendEmail( - formattedSubject, - formattedBody, - theCase.judge.name, - theCase.judge.email, - undefined, - true, - ), - ) - } - - if (theCase.registrar?.email) { - promises.push( - this.sendEmail( - formattedSubject, - formattedBody, - theCase.registrar.name, - theCase.registrar.email, - undefined, - true, - ), - ) + for (const recipient of to) { + if (recipient.email && recipient.name) { + const formattedBody = this.formatMessage(body, { + courtCaseNumber: theCase.courtCaseNumber, + linkStart: ``, + linkEnd: '', + }) + + promises.push( + this.sendEmail( + formattedSubject, + formattedBody, + recipient.name, + recipient.email, + undefined, + true, + ), + ) + } } const recipients = await Promise.all(promises) @@ -101,6 +92,20 @@ export class SubpoenaNotificationService extends BaseNotificationService { SubpoenaNotificationType.SERVICE_SUCCESSFUL, strings.serviceSuccessfulSubject, strings.serviceSuccessfulBody, + [ + { + name: theCase.judge?.name, + email: theCase.judge?.email, + }, + { + name: theCase.registrar?.name, + email: theCase.registrar?.email, + }, + { + name: theCase.prosecutor?.name, + email: theCase.prosecutor?.email, + }, + ], ) } @@ -112,6 +117,20 @@ export class SubpoenaNotificationService extends BaseNotificationService { SubpoenaNotificationType.SERVICE_FAILED, strings.serviceFailedSubject, strings.serviceFailedBody, + [ + { + name: theCase.judge?.name, + email: theCase.judge?.email, + }, + { + name: theCase.registrar?.name, + email: theCase.registrar?.email, + }, + { + name: theCase.prosecutor?.name, + email: theCase.prosecutor?.email, + }, + ], ) } @@ -123,6 +142,16 @@ export class SubpoenaNotificationService extends BaseNotificationService { SubpoenaNotificationType.DEFENDANT_SELECTED_DEFENDER, strings.defendantSelectedDefenderSubject, strings.defendantSelectedDefenderBody, + [ + { + name: theCase.judge?.name, + email: theCase.judge?.email, + }, + { + name: theCase.registrar?.name, + email: theCase.registrar?.email, + }, + ], ) } diff --git a/apps/judicial-system/web/pages/beinir/[id].ts b/apps/judicial-system/web/pages/beinir/[id].ts new file mode 100644 index 000000000000..588f0ae54455 --- /dev/null +++ b/apps/judicial-system/web/pages/beinir/[id].ts @@ -0,0 +1,3 @@ +import RouteHandler from '@island.is/judicial-system-web/src/routes/Shared/RouteHandler/RouteHandler' + +export default RouteHandler diff --git a/apps/judicial-system/web/src/routes/Shared/RouteHandler/RouteHandler.css.ts b/apps/judicial-system/web/src/routes/Shared/RouteHandler/RouteHandler.css.ts new file mode 100644 index 000000000000..1aff285efe15 --- /dev/null +++ b/apps/judicial-system/web/src/routes/Shared/RouteHandler/RouteHandler.css.ts @@ -0,0 +1,12 @@ +import { style } from '@vanilla-extract/css' + +export const loadingContainer = style({ + position: 'absolute', + top: 0, + background: 'white', + width: '100vw', + height: '100vh', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}) diff --git a/apps/judicial-system/web/src/routes/Shared/RouteHandler/RouteHandler.tsx b/apps/judicial-system/web/src/routes/Shared/RouteHandler/RouteHandler.tsx new file mode 100644 index 000000000000..67f1f3b3a0d4 --- /dev/null +++ b/apps/judicial-system/web/src/routes/Shared/RouteHandler/RouteHandler.tsx @@ -0,0 +1,113 @@ +import { useCallback, useContext, useEffect, useState } from 'react' +import { useRouter } from 'next/router' + +import { Box, LoadingDots } from '@island.is/island-ui/core' +import { + CLOSED_INDICTMENT_OVERVIEW_ROUTE, + INDICTMENTS_COMPLETED_ROUTE, + INDICTMENTS_COURT_OVERVIEW_ROUTE, + INDICTMENTS_OVERVIEW_ROUTE, +} from '@island.is/judicial-system/consts' +import { + isCompletedCase, + isDistrictCourtUser, + isProsecutionUser, +} from '@island.is/judicial-system/types' +import { + FormContext, + UserContext, +} from '@island.is/judicial-system-web/src/components' +import { + Case, + CaseState, + CaseType, + User, +} from '@island.is/judicial-system-web/src/graphql/schema' + +import * as styles from './RouteHandler.css' + +type UserType = 'prosecution' | 'districtCourt' +type CaseStatus = 'completed' | 'ongoing' + +const routes: Partial< + Record>> +> = { + [CaseType.INDICTMENT]: { + prosecution: { + completed: CLOSED_INDICTMENT_OVERVIEW_ROUTE, + ongoing: INDICTMENTS_OVERVIEW_ROUTE, + }, + districtCourt: { + completed: INDICTMENTS_COMPLETED_ROUTE, + ongoing: INDICTMENTS_COURT_OVERVIEW_ROUTE, + }, + }, +} + +const getCaseStatus = (state?: CaseState | null): CaseStatus => + isCompletedCase(state) ? 'completed' : 'ongoing' + +const getRoute = (caseToOpen?: Case, user?: User): string => { + if (!caseToOpen || !user) { + return '/' + } + + const userType: UserType | null = isProsecutionUser(user) + ? 'prosecution' + : isDistrictCourtUser(user) + ? 'districtCourt' + : null + const caseStatus = getCaseStatus(caseToOpen.state) + + const route = + caseToOpen.type && + userType && + routes[caseToOpen.type]?.[userType]?.[caseStatus] + + return route ? `${route}/${caseToOpen.id}` : '/' +} + +const RouteHandler: React.FC = () => { + const router = useRouter() + const { user } = useContext(UserContext) + const { getCase } = useContext(FormContext) + const [caseToOpen, setCaseToOpen] = useState() + + const handleGetCase = useCallback( + (caseId?: string) => { + if (!caseId) { + router.push('/') + return + } + + if (!caseToOpen) { + getCase( + caseId, + (caseData) => setCaseToOpen(caseData), + () => router.push('/'), + ) + } + }, + [caseToOpen, getCase, router], + ) + + useEffect(() => { + handleGetCase(router.query.id?.toString()) + }, [handleGetCase, router.query.id]) + + useEffect(() => { + if (!caseToOpen || !user) { + return + } + + router.push(getRoute(caseToOpen, user)) + }, [caseToOpen, router, user]) + + return ( + + + + ) +} + +export default RouteHandler diff --git a/libs/judicial-system/consts/src/lib/consts.ts b/libs/judicial-system/consts/src/lib/consts.ts index 162b36cb1d1b..04ed47326b0c 100644 --- a/libs/judicial-system/consts/src/lib/consts.ts +++ b/libs/judicial-system/consts/src/lib/consts.ts @@ -202,6 +202,7 @@ export const CHANGE_USER_ROUTE = '/notendur/breyta' //#region Shared routes export const CASES_ROUTE = '/krofur' export const SIGNED_VERDICT_OVERVIEW_ROUTE = '/krafa/yfirlit' +export const ROUTE_HANDLER_ROUTE = '/beinir' //#endregion Shared routes export const prosecutorRestrictionCasesRoutes = [ From d6f2ed1a36fd6736ad8253a5818b051b1a228e95 Mon Sep 17 00:00:00 2001 From: unakb Date: Fri, 1 Nov 2024 12:56:04 +0000 Subject: [PATCH 09/12] fix(j-s): Handling of wrong appeal info (#16643) * fix(j-s): Handling of wrong appeal info * Update case.transformer.spec.ts * Update case.transformer.spec.ts * Update caseList.interceptor.ts --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../interceptors/caseList.interceptor.ts | 24 ++++++++++++++++++- .../interceptors/case.transformer.spec.ts | 23 ++++++++++++++++++ .../case/interceptors/case.transformer.ts | 21 ++++++++++------ 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/apps/judicial-system/api/src/app/modules/case-list/interceptors/caseList.interceptor.ts b/apps/judicial-system/api/src/app/modules/case-list/interceptors/caseList.interceptor.ts index 3a61a3ae891a..2511c32ac5e3 100644 --- a/apps/judicial-system/api/src/app/modules/case-list/interceptors/caseList.interceptor.ts +++ b/apps/judicial-system/api/src/app/modules/case-list/interceptors/caseList.interceptor.ts @@ -8,7 +8,10 @@ import { NestInterceptor, } from '@nestjs/common' -import { isRequestCase } from '@island.is/judicial-system/types' +import { + CaseAppealDecision, + isRequestCase, +} from '@island.is/judicial-system/types' import { getIndictmentInfo } from '../../case/interceptors/case.transformer' import { CaseListEntry } from '../models/caseList.model' @@ -20,6 +23,16 @@ const getAppealedDate = ( return prosecutorPostponedAppealDate ?? accusedPostponedAppealDate } +const wasAcceptedInCourt = ( + prosecutorAppealDecision?: CaseAppealDecision, + accusedAppealDecision?: CaseAppealDecision, +): boolean => { + return ( + prosecutorAppealDecision === CaseAppealDecision.ACCEPT && + accusedAppealDecision === CaseAppealDecision.ACCEPT + ) +} + @Injectable() export class CaseListInterceptor implements NestInterceptor { intercept( @@ -39,6 +52,15 @@ export class CaseListInterceptor implements NestInterceptor { theCase.prosecutorPostponedAppealDate, theCase.accusedPostponedAppealDate, ), + // This state overwrite is added in at least temporarily to handle strange + // behaviour when a case is reopened after being appealed and then closed + // again with everyone having accepted the ruling in court + appealState: !wasAcceptedInCourt( + theCase.prosecutorAppealDecision, + theCase.accusedAppealDecision, + ) + ? theCase.appealState + : undefined, } } diff --git a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts index 5250d791c866..5c76205ba8d7 100644 --- a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts +++ b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts @@ -593,6 +593,29 @@ describe('getAppealInfo', () => { }) }) + it('should transform appeal state and dates if appeal data does not match up', () => { + const theCase = { + type: CaseType.CUSTODY, + rulingDate: '2022-06-15T19:50:08.033Z', + appealState: CaseAppealState.APPEALED, + accusedAppealDecision: CaseAppealDecision.ACCEPT, + prosecutorAppealDecision: CaseAppealDecision.ACCEPT, + accusedPostponedAppealDate: '2022-06-15T19:50:08.033Z', + prosecutorPostponedAppealDate: '2022-06-15T19:50:08.033Z', + } as Case + + const appealInfo = getAppealInfo(theCase) + + expect(appealInfo).toEqual({ + canBeAppealed: false, + hasBeenAppealed: false, + appealedDate: undefined, + appealDeadline: '2022-06-18T19:50:08.033Z', + canDefenderAppeal: false, + canProsecutorAppeal: false, + }) + }) + const rulingDate = new Date().toISOString() Object.values(CaseAppealDecision).forEach((decision) => { diff --git a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts index b132f761d493..bf8139e14014 100644 --- a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts +++ b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts @@ -61,16 +61,23 @@ export const getAppealInfo = (theCase: Case): AppealInfo => { return appealInfo } - const hasBeenAppealed = Boolean(appealState) - + const didProsecutorAcceptInCourt = + prosecutorAppealDecision === CaseAppealDecision.ACCEPT + const didAccusedAcceptInCourt = + accusedAppealDecision === CaseAppealDecision.ACCEPT + const didAllAcceptInCourt = + didProsecutorAcceptInCourt && didAccusedAcceptInCourt + + const hasBeenAppealed = Boolean(appealState) && !didAllAcceptInCourt appealInfo.hasBeenAppealed = hasBeenAppealed if (hasBeenAppealed) { - appealInfo.appealedByRole = prosecutorPostponedAppealDate - ? UserRole.PROSECUTOR - : accusedPostponedAppealDate - ? UserRole.DEFENDER - : undefined + appealInfo.appealedByRole = + prosecutorPostponedAppealDate && !didProsecutorAcceptInCourt + ? UserRole.PROSECUTOR + : accusedPostponedAppealDate && !didAccusedAcceptInCourt + ? UserRole.DEFENDER + : undefined appealInfo.appealedDate = appealInfo.appealedByRole === UserRole.PROSECUTOR From 25dc082129f6abdebed3fc38c77b394baf649102 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:00:14 +0000 Subject: [PATCH 10/12] fix(portals-admin): allow delete list & double tag (#16686) * fix(portals-admin): allow delete list & double tag * rabbit --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../signature-collection/src/lib/messages.ts | 5 + .../Constituency/index.tsx | 182 ++++++++++-------- 2 files changed, 105 insertions(+), 82 deletions(-) diff --git a/libs/portals/admin/signature-collection/src/lib/messages.ts b/libs/portals/admin/signature-collection/src/lib/messages.ts index 62e473363e04..a00e5df07947 100644 --- a/libs/portals/admin/signature-collection/src/lib/messages.ts +++ b/libs/portals/admin/signature-collection/src/lib/messages.ts @@ -511,6 +511,11 @@ export const m = defineMessages({ defaultMessage: 'Læsa söfnun', description: '', }, + listOpen: { + id: 'admin-portal.signature-collection:listOpened', + defaultMessage: 'Söfnun í gangi', + description: '', + }, listLocked: { id: 'admin-portal.signature-collection:listLocked', defaultMessage: 'Lista læst', diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/Constituency/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/Constituency/index.tsx index 419a9c6bd199..20fee97cfe68 100644 --- a/libs/portals/admin/signature-collection/src/screens-parliamentary/Constituency/index.tsx +++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/Constituency/index.tsx @@ -15,6 +15,7 @@ import { Stack, Text, toast, + TagVariant, } from '@island.is/island-ui/core' import { useLoaderData, @@ -30,10 +31,7 @@ import electionsCommitteeLogo from '../../../assets/electionsCommittee.svg' import nationalRegistryLogo from '../../../assets/nationalRegistry.svg' import { useSignatureCollectionAdminRemoveListMutation } from './removeList.generated' import { useSignatureCollectionAdminRemoveCandidateMutation } from './removeCandidate.generated' -import { - CollectionStatus, - SignatureCollectionList, -} from '@island.is/api/schema' +import { SignatureCollectionList } from '@island.is/api/schema' export const Constituency = ({ allowedToProcess, @@ -44,8 +42,7 @@ export const Constituency = ({ const navigate = useNavigate() const { revalidate } = useRevalidator() - const { collection, collectionStatus, allLists } = - useLoaderData() as ListsLoaderReturn + const { collection, allLists } = useLoaderData() as ListsLoaderReturn const { constituencyName } = useParams() as { constituencyName: string } const constituencyLists = allLists.filter( @@ -75,6 +72,33 @@ export const Constituency = ({ const [removeCandidate] = useSignatureCollectionAdminRemoveCandidateMutation() + const getTagConfig = (list: SignatureCollectionList) => { + // Lista læst + if (!list.active && !list.reviewed) { + return { + label: formatMessage(m.listLocked), + variant: 'blueberry' as TagVariant, + outlined: false, + } + } + + // Úrvinnslu lokið + if (!list.active && list.reviewed) { + return { + label: formatMessage(m.confirmListReviewed), + variant: 'mint' as TagVariant, + outlined: false, + } + } + + // Söfnun í gangi + return { + label: formatMessage(m.listOpen), + variant: 'blue' as TagVariant, + outlined: false, + } + } + return ( @@ -160,92 +184,86 @@ export const Constituency = ({ }, }} tag={ - allowedToProcess && - list.active && - collectionStatus === CollectionStatus.InitialActive + allowedToProcess ? { - label: 'Cancel collection', - renderTag: () => ( - - - - - - } - onConfirm={() => { - removeList({ - variables: { - input: { - listId: list.id, - }, - }, - }) - - if ( + ...getTagConfig(list), + renderTag: (cld) => ( + + {cld} + + + + + + } + onConfirm={() => { + removeList({ variables: { input: { - candidateId: list.candidate.id, + listId: list.id, }, }, }) + + if ( + candidatesListCount[list.candidate.id] === + 1 + ) { + removeCandidate({ + variables: { + input: { + candidateId: list.candidate.id, + }, + }, + }) + } + }} + buttonTextConfirm={ + candidatesListCount[list.candidate.id] === 1 + ? formatMessage( + m.cancelCollectionAndCandidateModalConfirmButton, + ) + : formatMessage( + m.cancelCollectionModalConfirmButton, + ) } - }} - buttonTextConfirm={ - candidatesListCount[list.candidate.id] === 1 - ? formatMessage( - m.cancelCollectionAndCandidateModalConfirmButton, - ) - : formatMessage( - m.cancelCollectionModalConfirmButton, - ) - } - buttonPropsConfirm={{ - variant: 'primary', - colorScheme: 'destructive', - }} - buttonTextCancel={formatMessage( - m.cancelCollectionModalCancelButton, - )} - /> + buttonPropsConfirm={{ + variant: 'primary', + colorScheme: 'destructive', + }} + buttonTextCancel={formatMessage( + m.cancelCollectionModalCancelButton, + )} + /> + ), } - : !list.active && !list.reviewed - ? { - label: formatMessage(m.listLocked), - variant: 'blueberry', - outlined: false, - } - : !list.active && list.reviewed - ? { - label: formatMessage(m.confirmListReviewed), - variant: 'mint', - outlined: false, - } : undefined } /> From 63a12a709868deb5522021d2e7baf9ca644f92a6 Mon Sep 17 00:00:00 2001 From: Kristofer Date: Fri, 1 Nov 2024 13:04:15 +0000 Subject: [PATCH 11/12] revert: "revert: "fix: added missing grant to auth ids api for user notification (#16680)" (#16693)" (#16695) This reverts commit 82fde9094f5a3bc88b6f2b6d0258f8234ecf4819. Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/services/auth/ids-api/infra/ids-api.ts | 1 + charts/identity-server/values.dev.yaml | 15 +++++++++------ charts/identity-server/values.prod.yaml | 15 +++++++++------ charts/identity-server/values.staging.yaml | 15 +++++++++------ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/apps/services/auth/ids-api/infra/ids-api.ts b/apps/services/auth/ids-api/infra/ids-api.ts index 14ac4f6c60bf..470a1765ecd1 100644 --- a/apps/services/auth/ids-api/infra/ids-api.ts +++ b/apps/services/auth/ids-api/infra/ids-api.ts @@ -122,6 +122,7 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => { min: 2, max: 15, }) + .grantNamespaces('user-notification') } const cleanupId = 'services-auth-ids-api-cleanup' diff --git a/charts/identity-server/values.dev.yaml b/charts/identity-server/values.dev.yaml index c9f3dca4c65a..c7f2f34b4f1c 100644 --- a/charts/identity-server/values.dev.yaml +++ b/charts/identity-server/values.dev.yaml @@ -123,8 +123,9 @@ identity-server: SessionsApiSettings__BaseAddress: 'http://web-services-sessions.services-sessions.svc.cluster.local' files: - 'ids-signing.pfx' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -431,8 +432,9 @@ services-auth-ids-api: XROAD_TJODSKRA_MEMBER_CODE: '10001' XROAD_TLS_BASE_PATH: 'https://securityserver.dev01.devland.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.dev01.devland.is/r1/IS-DEV' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -533,8 +535,9 @@ services-auth-ids-api-cleanup: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=921 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml index 58e9d76a535b..39262957015d 100644 --- a/charts/identity-server/values.prod.yaml +++ b/charts/identity-server/values.prod.yaml @@ -121,8 +121,9 @@ identity-server: SessionsApiSettings__BaseAddress: 'https://sessions-api.internal.island.is' files: - 'ids-signing.pfx' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -428,8 +429,9 @@ services-auth-ids-api: XROAD_TJODSKRA_MEMBER_CODE: '6503760649' XROAD_TLS_BASE_PATH: 'https://securityserver.island.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.island.is/r1/IS' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -530,8 +532,9 @@ services-auth-ids-api-cleanup: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=921 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 diff --git a/charts/identity-server/values.staging.yaml b/charts/identity-server/values.staging.yaml index 07afb0776b4c..4ced0775ec0b 100644 --- a/charts/identity-server/values.staging.yaml +++ b/charts/identity-server/values.staging.yaml @@ -123,8 +123,9 @@ identity-server: SessionsApiSettings__BaseAddress: 'http://web-services-sessions.services-sessions.svc.cluster.local' files: - 'ids-signing.pfx' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -431,8 +432,9 @@ services-auth-ids-api: XROAD_TJODSKRA_MEMBER_CODE: '6503760649' XROAD_TLS_BASE_PATH: 'https://securityserver.staging01.devland.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.staging01.devland.is/r1/IS-TEST' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 @@ -533,8 +535,9 @@ services-auth-ids-api-cleanup: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=921 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' - grantNamespaces: [] - grantNamespacesEnabled: false + grantNamespaces: + - 'user-notification' + grantNamespacesEnabled: true healthCheck: liveness: initialDelaySeconds: 3 From dc89848622705122360cb0d16ee44673413d35e9 Mon Sep 17 00:00:00 2001 From: norda-gunni <161026627+norda-gunni@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:20:46 +0000 Subject: [PATCH 12/12] feat(application-system): Make required a dynamic field (#16691) * feat(application-system): Make required a dynamic field * Added tests --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../core/src/lib/fieldBuilders.spec.ts | 72 +++++++++++++++++++ .../application/core/src/lib/fieldBuilders.ts | 12 +++- libs/application/types/src/lib/Fields.ts | 36 +++++----- .../AsyncSelectFormField.tsx | 4 +- .../CompanySearchFormField.tsx | 8 ++- .../src/lib/DateFormField/DateFormField.tsx | 8 ++- .../NationalIdWithNameFormField.tsx | 3 +- .../src/lib/PhoneFormField/PhoneFormField.tsx | 4 +- .../lib/SelectFormField/SelectFormField.tsx | 3 +- .../src/lib/TextFormField/TextFormField.tsx | 4 +- 10 files changed, 121 insertions(+), 33 deletions(-) create mode 100644 libs/application/core/src/lib/fieldBuilders.spec.ts diff --git a/libs/application/core/src/lib/fieldBuilders.spec.ts b/libs/application/core/src/lib/fieldBuilders.spec.ts new file mode 100644 index 000000000000..b3614c8c0855 --- /dev/null +++ b/libs/application/core/src/lib/fieldBuilders.spec.ts @@ -0,0 +1,72 @@ +import { Field } from '@island.is/application/types' + +import { FieldComponents, FieldTypes } from '@island.is/application/types' + +import { Application } from '@island.is/application/types' +import { buildFieldOptions, buildFieldRequired } from './fieldBuilders' + +describe('buildFieldOptions', () => { + const mockApplication = { + id: 'test-app', + state: 'draft', + answers: {}, + } as Application + + const mockField = { + id: 'test-field', + type: FieldTypes.SELECT, + component: FieldComponents.SELECT, + } as Field + + it('should return options array when passed static options', () => { + const staticOptions = [ + { label: 'Option 1', value: '1' }, + { label: 'Option 2', value: '2' }, + ] + + const result = buildFieldOptions(staticOptions, mockApplication, mockField) + + expect(result).toEqual(staticOptions) + }) + + it('should call function with application and field when passed function', () => { + const dynamicOptions = jest.fn().mockReturnValue([ + { label: 'Dynamic 1', value: 'd1' }, + { label: 'Dynamic 2', value: 'd2' }, + ]) + + const result = buildFieldOptions(dynamicOptions, mockApplication, mockField) + + expect(dynamicOptions).toHaveBeenCalledWith(mockApplication, mockField) + expect(result).toEqual([ + { label: 'Dynamic 1', value: 'd1' }, + { label: 'Dynamic 2', value: 'd2' }, + ]) + }) +}) + +describe('buildFieldRequired', () => { + const mockApplication = { + id: 'test-app', + state: 'draft', + answers: {}, + } as Application + + it('should return boolean value when passed static boolean', () => { + expect(buildFieldRequired(mockApplication, true)).toBe(true) + expect(buildFieldRequired(mockApplication, false)).toBe(false) + }) + + it('should return undefined when passed undefined', () => { + expect(buildFieldRequired(mockApplication, undefined)).toBeUndefined() + }) + + it('should call function with application when passed function', () => { + const dynamicRequired = jest.fn().mockReturnValue(true) + + const result = buildFieldRequired(mockApplication, dynamicRequired) + + expect(dynamicRequired).toHaveBeenCalledWith(mockApplication) + expect(result).toBe(true) + }) +}) diff --git a/libs/application/core/src/lib/fieldBuilders.ts b/libs/application/core/src/lib/fieldBuilders.ts index 15ed082dedce..8f510a73a6c1 100644 --- a/libs/application/core/src/lib/fieldBuilders.ts +++ b/libs/application/core/src/lib/fieldBuilders.ts @@ -44,6 +44,7 @@ import { AccordionField, BankAccountField, SliderField, + MaybeWithApplication, } from '@island.is/application/types' import { Colors } from '@island.is/island-ui/theme' @@ -532,10 +533,19 @@ export const buildFieldOptions = ( if (typeof maybeOptions === 'function') { return maybeOptions(application, field) } - return maybeOptions } +export const buildFieldRequired = ( + application: Application, + maybeRequired?: MaybeWithApplication, +) => { + if (typeof maybeRequired === 'function') { + return maybeRequired(application) + } + return maybeRequired +} + export const buildRedirectToServicePortalField = (data: { id: string title: FormText diff --git a/libs/application/types/src/lib/Fields.ts b/libs/application/types/src/lib/Fields.ts index a5ed41802863..688c0903a0bd 100644 --- a/libs/application/types/src/lib/Fields.ts +++ b/libs/application/types/src/lib/Fields.ts @@ -23,6 +23,7 @@ import { Locale } from '@island.is/shared/types' type Space = keyof typeof theme.spacing export type RecordObject = Record +export type MaybeWithApplication = T | ((application: Application) => T) export type MaybeWithApplicationAndField = | T | ((application: Application, field: Field) => T) @@ -196,10 +197,15 @@ export interface BaseField extends FormItem { isPartOfRepeater?: boolean defaultValue?: MaybeWithApplicationAndField doesNotRequireAnswer?: boolean + // TODO use something like this for non-schema validation? // validate?: (formValue: FormValue, context?: object) => boolean } +export interface InputField extends BaseField { + required?: MaybeWithApplication +} + export enum FieldTypes { CHECKBOX = 'CHECKBOX', CUSTOM = 'CUSTOM', @@ -271,19 +277,18 @@ export enum FieldComponents { SLIDER = 'SliderFormField', } -export interface CheckboxField extends BaseField { +export interface CheckboxField extends InputField { readonly type: FieldTypes.CHECKBOX component: FieldComponents.CHECKBOX options: MaybeWithApplicationAndField large?: boolean strong?: boolean - required?: boolean backgroundColor?: InputBackgroundColor onSelect?: ((s: string[]) => void) | undefined spacing?: 0 | 1 | 2 } -export interface DateField extends BaseField { +export interface DateField extends InputField { readonly type: FieldTypes.DATE placeholder?: FormText component: FieldComponents.DATE @@ -292,7 +297,6 @@ export interface DateField extends BaseField { excludeDates?: MaybeWithApplicationAndField backgroundColor?: DatePickerBackgroundColor onChange?(date: string): void - required?: boolean readOnly?: boolean } @@ -308,41 +312,38 @@ export interface DescriptionField extends BaseField { titleVariant?: TitleVariants } -export interface RadioField extends BaseField { +export interface RadioField extends InputField { readonly type: FieldTypes.RADIO component: FieldComponents.RADIO options: MaybeWithApplicationAndField backgroundColor?: InputBackgroundColor largeButtons?: boolean - required?: boolean space?: BoxProps['paddingTop'] hasIllustration?: boolean widthWithIllustration?: '1/1' | '1/2' | '1/3' onSelect?(s: string): void } -export interface SelectField extends BaseField { +export interface SelectField extends InputField { readonly type: FieldTypes.SELECT component: FieldComponents.SELECT options: MaybeWithApplicationAndField onSelect?(s: SelectOption, cb: (t: unknown) => void): void placeholder?: FormText backgroundColor?: InputBackgroundColor - required?: boolean isMulti?: boolean } -export interface CompanySearchField extends BaseField { +export interface CompanySearchField extends InputField { readonly type: FieldTypes.COMPANY_SEARCH component: FieldComponents.COMPANY_SEARCH placeholder?: FormText setLabelToDataSchema?: boolean shouldIncludeIsatNumber?: boolean checkIfEmployerIsOnForbiddenList?: boolean - required?: boolean } -export interface AsyncSelectField extends BaseField { +export interface AsyncSelectField extends InputField { readonly type: FieldTypes.ASYNC_SELECT component: FieldComponents.ASYNC_SELECT placeholder?: FormText @@ -351,11 +352,10 @@ export interface AsyncSelectField extends BaseField { loadingError?: FormText backgroundColor?: InputBackgroundColor isSearchable?: boolean - required?: boolean isMulti?: boolean } -export interface TextField extends BaseField { +export interface TextField extends InputField { readonly type: FieldTypes.TEXT component: FieldComponents.TEXT disabled?: boolean @@ -372,11 +372,10 @@ export interface TextField extends BaseField { format?: string | FormatInputValueFunction suffix?: string rows?: number - required?: boolean onChange?: (...event: any[]) => void } -export interface PhoneField extends BaseField { +export interface PhoneField extends InputField { readonly type: FieldTypes.PHONE component: FieldComponents.PHONE disabled?: boolean @@ -386,7 +385,6 @@ export interface PhoneField extends BaseField { backgroundColor?: InputBackgroundColor allowedCountryCodes?: string[] enableCountrySelector?: boolean - required?: boolean onChange?: (...event: any[]) => void } @@ -559,11 +557,10 @@ export interface PdfLinkButtonField extends BaseField { downloadButtonTitle?: StaticText } -export interface NationalIdWithNameField extends BaseField { +export interface NationalIdWithNameField extends InputField { readonly type: FieldTypes.NATIONAL_ID_WITH_NAME component: FieldComponents.NATIONAL_ID_WITH_NAME disabled?: boolean - required?: boolean customNationalIdLabel?: StaticText customNameLabel?: StaticText onNationalIdChange?: (s: string) => void @@ -636,11 +633,10 @@ export type TableRepeaterField = BaseField & { format?: Record string | StaticText> } } -export interface FindVehicleField extends BaseField { +export interface FindVehicleField extends InputField { readonly type: FieldTypes.FIND_VEHICLE component: FieldComponents.FIND_VEHICLE disabled?: boolean - required?: boolean additionalErrors: boolean getDetails?: (plate: string) => Promise findVehicleButtonText?: FormText diff --git a/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx b/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx index 22f97bc59d4a..49388c7171b5 100644 --- a/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx +++ b/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx @@ -1,6 +1,6 @@ import React, { FC, useEffect, useState } from 'react' -import { formatText } from '@island.is/application/core' +import { buildFieldRequired, formatText } from '@island.is/application/core' import { AsyncSelectField, FieldBaseProps } from '@island.is/application/types' import { Box } from '@island.is/island-ui/core' import { @@ -65,7 +65,7 @@ export const AsyncSelectFormField: FC> = ({ > = ({ return ( > = ({ id={id} name={id} locale={lang} - required={required} + required={buildFieldRequired(application, required)} excludeDates={finalExcludeDates} minDate={finalMinDate} maxDate={finalMaxDate} diff --git a/libs/application/ui-fields/src/lib/NationalIdWithNameFormField/NationalIdWithNameFormField.tsx b/libs/application/ui-fields/src/lib/NationalIdWithNameFormField/NationalIdWithNameFormField.tsx index 88124ccc8cfa..1caf9fd5be81 100644 --- a/libs/application/ui-fields/src/lib/NationalIdWithNameFormField/NationalIdWithNameFormField.tsx +++ b/libs/application/ui-fields/src/lib/NationalIdWithNameFormField/NationalIdWithNameFormField.tsx @@ -1,3 +1,4 @@ +import { buildFieldRequired } from '@island.is/application/core' import { FieldBaseProps, NationalIdWithNameField, @@ -17,7 +18,7 @@ export const NationalIdWithNameFormField: FC< id={field.id} application={application} disabled={field.disabled} - required={field.required} + required={buildFieldRequired(application, field.required)} customNationalIdLabel={field.customNationalIdLabel} customNameLabel={field.customNameLabel} onNationalIdChange={field.onNationalIdChange} diff --git a/libs/application/ui-fields/src/lib/PhoneFormField/PhoneFormField.tsx b/libs/application/ui-fields/src/lib/PhoneFormField/PhoneFormField.tsx index 0fbcbb72c30e..067aed313ec3 100644 --- a/libs/application/ui-fields/src/lib/PhoneFormField/PhoneFormField.tsx +++ b/libs/application/ui-fields/src/lib/PhoneFormField/PhoneFormField.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react' import { useFormContext } from 'react-hook-form' -import { formatText } from '@island.is/application/core' +import { buildFieldRequired, formatText } from '@island.is/application/core' import { FieldBaseProps, PhoneField } from '@island.is/application/types' import { Box } from '@island.is/island-ui/core' import { @@ -75,7 +75,7 @@ export const PhoneFormField: FC> = ({ }} defaultValue={getDefaultValue(field, application)} backgroundColor={backgroundColor} - required={required} + required={buildFieldRequired(application, required)} /> diff --git a/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx b/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx index 1c9ae5f3ecfd..fa9bd3e182c8 100644 --- a/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx +++ b/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx @@ -4,6 +4,7 @@ import { formatText, buildFieldOptions, getValueViaPath, + buildFieldRequired, } from '@island.is/application/core' import { FieldBaseProps, SelectField } from '@island.is/application/types' import { Box } from '@island.is/island-ui/core' @@ -53,7 +54,7 @@ export const SelectFormField: FC> = ({ > = ({ defaultValue={getDefaultValue(field, application)} backgroundColor={backgroundColor} rows={rows} - required={required} + required={buildFieldRequired(application, required)} rightAlign={rightAlign} max={max} min={min}