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] 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": [