diff --git a/.github/actions/get-cache/action.yml b/.github/actions/get-cache/action.yml
new file mode 100644
index 000000000000..a4f1184142d6
--- /dev/null
+++ b/.github/actions/get-cache/action.yml
@@ -0,0 +1,41 @@
+name: 'Get Cache Keys'
+description: 'Action to get cache'
+inputs:
+ github-token:
+ description: 'GitHub token'
+ required: true
+ enable-cache:
+ description: 'Enable cache'
+ default: ''
+ keys:
+ description: 'Keys'
+ default: 'false'
+outputs:
+ keys:
+ description: 'Keys'
+ value: ${{ steps.prepare.outputs._CACHE_KEYS }}
+runs:
+ using: 'composite'
+ steps:
+ - name: Adding required env vars
+ uses: actions/github-script@v7
+ env:
+ github-token: ${{ inputs.GITHUB_TOKEN }}
+ with:
+ script: |
+ core.exportVariable('ACTIONS_CACHE_URL', 'https://cache.dev01.devland.is/')
+ core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN'])
+ core.exportVariable('ACTIONS_RUNTIME_URL', process.env['ACTIONS_RUNTIME_URL'])
+ - name: Get cache
+ id: prepare
+ shell: bash
+ env:
+ _CACHE_KEYS: ${{ inputs.keys }}
+ ENABLE_CACHE: ${{ inputs.enable-cache }}
+ NODE_OPTIONS: --max-old-space-size=8192
+ run: |
+ echo $_CACHE_KEYS
+ cd scripts/ci/cache
+ yarn install --immutable
+ node cache-action.mjs
+ echo $_CACHE_KEYS
diff --git a/.github/actions/unit-test/action.yml b/.github/actions/unit-test/action.yml
index dcea4d884e94..5a311af15f13 100644
--- a/.github/actions/unit-test/action.yml
+++ b/.github/actions/unit-test/action.yml
@@ -53,7 +53,7 @@ runs:
- name: Cached codecov uploader
id: codecov-cache
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: codecov
key: ${{ runner.os }}-codeconv-${{ env.CODECOV_REV }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index bee2fbfacdf6..d9e874289fdc 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
diff --git a/.github/workflows/config-values.yaml b/.github/workflows/config-values.yaml
index af314b1368dd..ec68201b40a4 100644
--- a/.github/workflows/config-values.yaml
+++ b/.github/workflows/config-values.yaml
@@ -52,13 +52,13 @@ jobs:
image: public.ecr.aws/m3u4c4h9/island-is/actions-runner-public:latest
timeout-minutes: 5
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
if: ${{ github.event_name == 'pull_request' }}
with:
ref: ${{ github.event.pull_request.head.ref }}
token: ${{ secrets.DIRTY_FIX_BOT_TOKEN }}
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
if: ${{ github.event_name != 'pull_request' }}
- uses: actions/setup-node@v4
@@ -108,7 +108,7 @@ jobs:
fail-fast: false
matrix: ${{ fromJson(needs.prepare.outputs.ENVS) }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
diff --git a/.github/workflows/external-checks.yml b/.github/workflows/external-checks.yml
index f6bc4c684d56..5b35b5239f39 100644
--- a/.github/workflows/external-checks.yml
+++ b/.github/workflows/external-checks.yml
@@ -18,7 +18,7 @@ jobs:
image: public.ecr.aws/m3u4c4h9/island-is/actions-runner-public:latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if codeowners file changed
diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml
index 9c5dcaa47d2c..ff461afdff3f 100644
--- a/.github/workflows/pullrequest.yml
+++ b/.github/workflows/pullrequest.yml
@@ -15,7 +15,6 @@ concurrency:
env:
COMPOSE_HTTP_TIMEOUT: 180
- GITHUB_ACTIONS_CACHE_URL: https://cache.dev01.devland.is/
SKIP_GENERATED_CACHE: ${{ contains(github.event.pull_request.labels.*.name, 'skip-generated-cache') }}
NX_AFFECTED_ALL: ${{ contains(github.event.pull_request.labels.*.name, 'nx-affected-all') }}
@@ -30,19 +29,18 @@ jobs:
AFFECTED_ALL: ${{ secrets.AFFECTED_ALL }}
CHUNK_SIZE: 7
SERVERSIDE_FEATURES_ON: ''
+ DOCKER_REGISTRY: 821090935708.dkr.ecr.eu-west-1.amazonaws.com/
+ DOCKER_BASE_IMAGE_REGISTRY: 821090935708.dkr.ecr.eu-west-1.amazonaws.com/ecr-public
outputs:
TEST_CHUNKS: ${{ steps.test_projects.outputs.CHUNKS }}
E2E_CHUNKS: ${{ steps.e2e_projects.outputs.CHUNKS }}
E2E_BUILD_ID: ${{ steps.e2e_projects.outputs.BUILD_ID }}
LINT_CHUNKS: ${{ steps.lint_projects.outputs.CHUNKS }}
- UNAFFECTED: ${{ steps.unaffected.outputs.UNAFFECTED }}
BUILD_CHUNKS: ${{ steps.build_projects.outputs.CHUNKS }}
- BUILD_MAP: ${{ steps.build_map.outputs.BUILD_MAP }}
- node-modules-hash: ${{ steps.calculate_node_modules_hash.outputs.node-modules-hash }}
- generated-files-cache-key: ${{ steps.calculate_generated_files_cache_key.outputs.generated-files-cache-key }}
+ CACHE_KEYS: ${{ steps.get-cache.outputs.keys }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
@@ -56,7 +54,6 @@ jobs:
run: |
node -v
ls -l `which node`
-
- name: Checking out relevant branches
run: |
git checkout $GITHUB_HEAD_REF
@@ -76,36 +73,12 @@ jobs:
name: pr-event
path: event.json
retention-days: 60
-
- - name: Calculate cache key for node modules
- id: calculate_node_modules_hash
- run: |
- HASH="$(./scripts/ci/get-node-modules-hash.mjs)"
- echo "node-modules-hash: ${HASH}"
- echo "node-modules-hash=${HASH}" >> $GITHUB_OUTPUT
-
- - name: Calculate cache keys for generated files
- id: calculate_generated_files_cache_key
- run: |
- export HASH=$(./scripts/_hash-generated-files.sh)
- export GENERATED_FILES_KEY=${{ runner.os }}-$HASH-files-generated-03
- echo "GENERATED_FILES_KEY: $GENERATED_FILES_KEY"
- echo "generated-files-cache-key=$GENERATED_FILES_KEY" >> $GITHUB_OUTPUT
-
- - name: Cache for NodeJS dependencies - host OS
- id: node-modules
- continue-on-error: true
- uses: ./.github/actions/cache
+ - name: Get cache
+ id: get-cache
+ uses: ./.github/actions/get-cache
with:
- path: node_modules
- key: ${{ steps.calculate_node_modules_hash.outputs.node-modules-hash }}-yarn
-
- - name: Check node-modules cache success
- run: '[[ "${{ steps.node-modules.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Building NodeJS dependencies
- if: steps.node-modules.outputs.cache-hit != 'true'
- run: ./scripts/ci/10_prepare-host-deps.sh
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ enable-cache: 'node_modules,cypress,generated-files'
- run: |
echo "HEAD=$GITHUB_SHA" >> $GITHUB_ENV
@@ -123,47 +96,6 @@ jobs:
ISSUE_REPORTING_SLACK_WEBHOOK_URL: ${{ secrets.SLACK_BUILD_ISSUES_REPORTING_WEBHOOK_URL }}
name: Preparing HEAD and BASE tags
- - name: Cache for cypress
- id: cypress
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: /github/home/.cache/Cypress
- key: cypress-cache-${{ steps.calculate_node_modules_hash.outputs.node-modules-hash }}
-
- - name: Check cypress cache success
- run: '[[ "${{ steps.cypress.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Verify cypress
- id: cypress-check
- run: npx cypress verify
- continue-on-error: true
-
- - name: Install cypress
- if: steps.cypress-check.outcome != 'success'
- run: npx cypress install
-
- - name: Cache for generated files
- id: generated-files-cache
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: generated_files.tar.gz
- key: ${{ steps.calculate_generated_files_cache_key.outputs.generated-files-cache-key }}
- # force-cache-save: ${{ env.SKIP_GENERATED_CACHE }}
-
- - name: Check generated files cache success
- run: '[[ "${{ steps.generated-files-cache.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Run codegen
- if: steps.generated-files-cache.outputs.cache-hit != 'true'
- run: |
- node --version
- tar zcvf generated_files.tar.gz $(./scripts/ci/get-files-touched-by.sh yarn codegen --skip-cache | xargs realpath --relative-to $(pwd))
-
- # - name: Security audit Node modules
- # run: ./scripts/ci/20_security-audit.sh
-
- name: License audit Node modules
run: ./scripts/ci/20_license-audit.sh
@@ -240,7 +172,7 @@ jobs:
fail-fast: false
matrix: ${{ fromJson(needs.prepare.outputs.TEST_CHUNKS) }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -251,30 +183,13 @@ jobs:
- name: Setup yarn
run: npm install -g yarn
- - name: Cache for NodeJS dependencies - host OS
- id: node-modules
- continue-on-error: true
- uses: ./.github/actions/cache
+ - name: Get cache
+ id: get-cache
+ uses: ./.github/actions/get-cache
with:
- path: node_modules
- key: ${{ needs.prepare.outputs.node-modules-hash }}-yarn
-
- - name: Check node-modules cache success
- run: '[[ "${{ steps.node-modules.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Cache for generated files
- id: generated-files-cache
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: generated_files.tar.gz
- key: ${{ needs.prepare.outputs.generated-files-cache-key }}
-
- - name: Check generated-files cache success
- run: '[[ "${{ steps.generated-files-cache.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Untar generated files
- run: tar zxvf generated_files.tar.gz
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ keys: ${{ needs.prepare.outputs.CACHE_KEYS }}
+ enable-cache: 'node_modules,cypress,generated-files'
- uses: ./.github/actions/unit-test
with:
@@ -303,7 +218,7 @@ jobs:
fail-fast: false
matrix: ${{ fromJson(needs.prepare.outputs.E2E_CHUNKS) }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
@@ -312,41 +227,13 @@ jobs:
- name: Setup yarn
run: npm install -g yarn
- - name: Cache for NodeJS dependencies - host OS
- id: node-modules
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: node_modules
- key: ${{ needs.prepare.outputs.node-modules-hash }}-yarn
-
- - name: Check node-modules cache success
- run: '[[ "${{ steps.node-modules.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Cache for cypress
- id: cypress
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: /github/home/.cache/Cypress
- key: cypress-cache-${{ needs.prepare.outputs.node-modules-hash }}
-
- - name: Check cypress cache success
- run: '[[ "${{ steps.cypress.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Cache for generated files
- id: generated-files-cache
- continue-on-error: true
- uses: ./.github/actions/cache
+ - name: Get cache
+ id: get-cache
+ uses: ./.github/actions/get-cache
with:
- path: generated_files.tar.gz
- key: ${{ needs.prepare.outputs.generated-files-cache-key }}
-
- - name: Check generated-files cache success
- run: '[[ "${{ steps.generated-files-cache.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Untar generated files
- run: tar zxvf generated_files.tar.gz
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ keys: ${{ needs.prepare.outputs.CACHE_KEYS }}
+ enable-cache: 'node_modules,cypress,generated-files'
- name: Running e2e tests
run: ./scripts/ci/40_e2e.sh ${AFFECTED_PROJECT}
@@ -359,23 +246,20 @@ jobs:
image: public.ecr.aws/m3u4c4h9/island-is/actions-runner-public:latest
timeout-minutes: 5
steps:
- - uses: actions/checkout@v3
- - name: Cache for NodeJS dependencies - host OS
- id: node-modules
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: node_modules
- key: ${{ needs.prepare.outputs.node-modules-hash }}-yarn
-
+ - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
-
- name: Setup yarn
run: npm install -g yarn
- - name: Check node-modules cache success
- run: '[[ "${{ steps.node-modules.outputs.success }}" != "false" ]] || exit 1'
+ - name: Get cache
+ id: get-cache
+ uses: ./.github/actions/get-cache
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ keys: ${{ needs.prepare.outputs.CACHE_KEYS }}
+ enable-cache: 'node_modules,generated-files'
+
- name: Linting workspace
run: ./scripts/ci/20_lint-workspace.sh
@@ -385,7 +269,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@2.0.0
with:
@@ -402,13 +286,13 @@ jobs:
image: public.ecr.aws/m3u4c4h9/island-is/actions-runner-public:latest
timeout-minutes: 5
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
if: ${{ github.event_name == 'pull_request' }}
with:
token: ${{ secrets.DIRTY_FIX_BOT_TOKEN }}
ref: ${{ github.event.pull_request.head.ref }}
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
if: ${{ github.ref == 'ref/heads/main' }}
- uses: actions/setup-node@v4
@@ -417,16 +301,13 @@ jobs:
- name: Setup yarn
run: npm install -g yarn
- - name: Cache for NodeJS dependencies - host OS
- id: node-modules
- continue-on-error: true
- uses: ./.github/actions/cache
+ - name: Get cache
+ id: get-cache
+ uses: ./.github/actions/get-cache
with:
- path: node_modules
- key: ${{ needs.prepare.outputs.node-modules-hash }}-yarn
-
- - name: Check node-modules cache success
- run: '[[ "${{ steps.node-modules.outputs.success }}" != "false" ]] || exit 1'
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ keys: ${{ needs.prepare.outputs.CACHE_KEYS }}
+ enable-cache: 'node_modules'
- name: NX format:check
if: ${{ github.ref == 'ref/heads/main' }}
@@ -454,37 +335,19 @@ jobs:
fail-fast: false
matrix: ${{ fromJson(needs.prepare.outputs.LINT_CHUNKS) }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
- name: Setup yarn
run: npm install -g yarn
- - name: Cache for NodeJS dependencies - host OS
- id: node-modules
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: node_modules
- key: ${{ needs.prepare.outputs.node-modules-hash }}-yarn
-
- - name: Check node-modules cache success
- run: '[[ "${{ steps.node-modules.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Cache for generated files
- id: generated-files-cache
- continue-on-error: true
- uses: ./.github/actions/cache
+ - name: Get cache
+ id: get-cache
+ uses: ./.github/actions/get-cache
with:
- path: generated_files.tar.gz
- key: ${{ needs.prepare.outputs.generated-files-cache-key }}
-
- - name: Check generated-files cache success
- run: '[[ "${{ steps.generated-files-cache.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Untar generated files
- run: tar zxvf generated_files.tar.gz
-
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ keys: ${{ needs.prepare.outputs.CACHE_KEYS }}
+ enable-cache: 'node_modules,generated-files'
- name: Linting
run: ./scripts/ci/run-in-parallel-native.sh lint
@@ -503,36 +366,19 @@ jobs:
matrix: ${{ fromJson(needs.prepare.outputs.BUILD_CHUNKS) }}
if: needs.prepare.outputs.BUILD_CHUNKS
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
- name: Setup yarn
run: npm install -g yarn
- - name: Cache for NodeJS dependencies - host OS
- id: node-modules
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: node_modules
- key: ${{ needs.prepare.outputs.node-modules-hash }}-yarn
-
- - name: Check node-modules cache success
- run: '[[ "${{ steps.node-modules.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Cache for generated files
- id: generated-files-cache
- continue-on-error: true
- uses: ./.github/actions/cache
+ - name: Get cache
+ id: get-cache
+ uses: ./.github/actions/get-cache
with:
- path: generated_files.tar.gz
- key: ${{ needs.prepare.outputs.generated-files-cache-key }}
-
- - name: Check generated-files cache success
- run: '[[ "${{ steps.generated-files-cache.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Untar generated files
- run: tar zxvf generated_files.tar.gz
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ keys: ${{ needs.prepare.outputs.CACHE_KEYS }}
+ enable-cache: 'node_modules,generated-files'
- name: Building
run: ./scripts/ci/run-in-parallel-native.sh build
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index ed33617bba7a..3ea89d74cc04 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -156,6 +156,7 @@ jobs:
SERVERSIDE_FEATURES_ON: ''
outputs:
+ CACHE_KEYS: ${{ steps.get-cache.outputs.keys }}
TEST_CHUNKS: ${{ steps.test_projects.outputs.CHUNKS }}
DOCKER_TAG: ${{ steps.docker_tags.outputs.DOCKER_TAG }}
NODE_IMAGE_TAG: ${{ steps.nodejs_image.outputs.NODE_IMAGE_TAG }}
@@ -164,7 +165,6 @@ jobs:
BUILD_CHUNKS: ${{ steps.build_map.outputs.BUILD_CHUNKS }}
IMAGES: ${{ steps.deploy_map.outputs.IMAGES }}
node-modules-hash: ${{ steps.calculate_node_modules_hash.outputs.node-modules-hash }}
- generated-files-cache-key: ${{ steps.calculate_generated_files_cache_key.outputs.generated-files-cache-key }}
steps:
- uses: actions/checkout@v3
with:
@@ -284,35 +284,12 @@ jobs:
id: git_nx_head
name: Preparing HEAD tag
- - name: Calculate cache key for node modules
- id: calculate_node_modules_hash
- run: |
- HASH="$(./scripts/ci/get-node-modules-hash.mjs)"
- echo "node-modules-hash: ${HASH}"
- echo "node-modules-hash=${HASH}" >> $GITHUB_OUTPUT
-
- - name: Calculate cache keys for generated files
- id: calculate_generated_files_cache_key
- run: |
- export HASH=$(./scripts/_hash-generated-files.sh)
- export GENERATED_FILES_KEY=${{ runner.os }}-$HASH-files-generated
- echo "GENERATED_FILES_KEY: $GENERATED_FILES_KEY"
- echo "generated-files-cache-key=$GENERATED_FILES_KEY" >> $GITHUB_OUTPUT
-
- - name: Cache for NodeJS dependencies - host OS
- id: node-modules
- continue-on-error: true
- uses: ./.github/actions/cache
+ - name: Get cache
+ id: get-cache
+ uses: ./.github/actions/get-cache
with:
- path: node_modules
- key: ${{ steps.calculate_node_modules_hash.outputs.node-modules-hash }}-yarn
-
- - name: Check cache success
- run: '[[ "${{ steps.node-modules.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Building NodeJS dependencies
- if: steps.node-modules.outputs.cache-hit != 'true'
- run: ./scripts/ci/10_prepare-host-deps.sh
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ enable-cache: 'node_modules,generated-files'
- name: Set Test Everything true
run: |
@@ -357,7 +334,15 @@ jobs:
image: '${{ env.DOCKER_BASE_IMAGE_REGISTRY }}/eks-distro-build-tooling/binfmt-misc:qemu-v6.1.0'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
+ - name: Get node hash
+ uses: actions/github-script@v7
+ id: calculate_node_modules_hash
+ env:
+ _CACHE_KEY: ${{ steps.get-cache.outputs.keys }}
+ with:
+ script: |
+ const CACHE_KEYS = JSON.parse(process.env._CACHE_KEY)
+ core.setOutput('node-modules-hash', CACHE_KEYS["node_modules"])
- name: Cache for NodeJS dependencies - Docker layer
id: cache-deps
continue-on-error: true
@@ -380,31 +365,9 @@ jobs:
- name: Check cache success
run: '[[ "${{ steps.cache-deps-base.outputs.success }}" != "false" ]] || exit 1'
- - name: Building NodeJS dependencies
- if: steps.cache-deps.outputs.cache-hit != 'true' || steps.cache-deps-base.outputs.cache-hit != 'true'
- run: |
- ./scripts/ci/10_prepare-docker-deps.sh
-
- name: set BRANCH env var
run: echo "BRANCH=$GIT_BRANCH" >> $GITHUB_ENV
- - name: Cache for generated files
- id: generated-files-cache
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: generated_files.tar.gz
- key: ${{ steps.calculate_generated_files_cache_key.outputs.generated-files-cache-key }}
-
- - name: Check cache success
- run: '[[ "${{ steps.generated-files-cache.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Run codegen
- if: steps.generated-files-cache.outputs.cache-hit != 'true' || env.SKIP_GENERATED_CACHE == 'true'
- run: |
- node --version
- tar zcvf generated_files.tar.gz $(./scripts/ci/get-files-touched-by.sh yarn codegen --skip-cache | xargs realpath --relative-to $(pwd))
-
- name: Prepare test targets
id: test_projects
run: |
@@ -472,30 +435,13 @@ jobs:
- name: Setup yarn
run: npm install -g yarn
- - name: Cache for NodeJS dependencies - host OS
- id: node-modules
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: node_modules
- key: ${{ needs.prepare.outputs.node-modules-hash }}-yarn
-
- - name: Check cache success
- run: '[[ "${{ steps.node-modules.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Cache for generated files
- id: generated-files-cache
- continue-on-error: true
- uses: ./.github/actions/cache
+ - name: Get cache
+ id: get-cache
+ uses: ./.github/actions/get-cache
with:
- path: generated_files.tar.gz
- key: ${{ needs.prepare.outputs.generated-files-cache-key }}
-
- - name: Check cache success
- run: '[[ "${{ steps.generated-files-cache.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Untar generated files
- run: tar zxvf generated_files.tar.gz
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ keys: ${{ needs.prepare.outputs.CACHE_KEYS }}
+ enable-cache: 'node_modules,generated-files'
- uses: ./.github/actions/unit-test
with:
@@ -544,21 +490,13 @@ jobs:
run: npm install -g yarn
if: steps.gather.outcome == 'success'
- - name: Cache for generated files
- id: generated-files-cache
- if: steps.gather.outcome == 'success'
- continue-on-error: true
- uses: ./.github/actions/cache
+ - name: Get cache
+ id: get-cache
+ uses: ./.github/actions/get-cache
with:
- path: generated_files.tar.gz
- key: ${{ needs.prepare.outputs.generated-files-cache-key }}
-
- - name: Check cache success
- run: '[[ "${{ steps.generated-files-cache.outputs.success }}" != "false" ]] || exit 1'
-
- - name: Untar generated files
- if: steps.gather.outcome == 'success'
- run: tar zxvf generated_files.tar.gz
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ keys: ${{ needs.prepare.outputs.CACHE_KEYS }}
+ enable-cache: 'node_modules,generated-files'
- name: Cache for dependencies Docker layer
if: steps.gather.outcome == 'success'
@@ -584,18 +522,6 @@ jobs:
- name: Check cache success
run: '[[ "${{ steps.cache-deps.outputs.success }}" != "false" ]] || exit 1'
- # needed to run `nx show project` in scripts/ci/_docker.sh - should be refactored.
- - name: Cache for NodeJS dependencies - host OS
- id: node-modules
- continue-on-error: true
- uses: ./.github/actions/cache
- with:
- path: node_modules
- key: ${{ needs.prepare.outputs.node-modules-hash }}-yarn
-
- - name: Check cache success
- run: '[[ "${{ steps.node-modules.outputs.success }}" != "false" ]] || exit 1'
-
- name: Docker login to ECR repo
if: steps.gather.outcome == 'success'
run: ./scripts/ci/docker-login-ecr.sh
diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts
index 3fbf0f5b2c94..42f08b552804 100644
--- a/apps/api/src/app/app.module.ts
+++ b/apps/api/src/app/app.module.ts
@@ -185,6 +185,9 @@ import {
import { HousingBenefitsConfig } from '@island.is/clients/hms-housing-benefits'
import { UserProfileClientConfig } from '@island.is/clients/user-profile'
import { UltravioletRadiationClientConfig } from '@island.is/clients/ultraviolet-radiation'
+import { CriminalRecordClientConfig } from '@island.is/clients/criminal-record'
+import { HealthInsuranceV2ClientConfig } from '@island.is/clients/icelandic-health-insurance/health-insurance'
+import { VmstClientConfig } from '@island.is/clients/vmst'
const environment = getConfig
@@ -223,7 +226,7 @@ const environment = getConfig
baseApiUrl: environment.applicationSystem.baseApiUrl!,
}),
LicenseServiceModule,
- DirectorateOfLabourModule.register(),
+ DirectorateOfLabourModule,
FileUploadModule,
DocumentModule,
DocumentProviderModule.register({
@@ -246,15 +249,7 @@ const environment = getConfig
}),
CmsTranslationsModule,
TerminusModule,
- HealthInsuranceModule.register({
- clientV2Config: {
- xRoadBaseUrl: environment.healthInsuranceV2.xRoadBaseUrl!,
- xRoadProviderId: environment.healthInsuranceV2.xRoadProviderId!,
- xRoadClientId: environment.healthInsuranceV2.xRoadClientId!,
- username: environment.healthInsuranceV2.username!,
- password: environment.healthInsuranceV2.password!,
- },
- }),
+ HealthInsuranceModule,
UserProfileModule.register({
islykill: {
cert: environment.islykill.cert!,
@@ -309,13 +304,7 @@ const environment = getConfig
ApiDomainsPaymentModule,
PaymentScheduleModule,
ProblemModule,
- CriminalRecordModule.register({
- clientConfig: {
- xroadBaseUrl: environment.xroad.baseUrl!,
- xroadClientId: environment.xroad.clientId!,
- xroadPath: environment.criminalRecord.xroadPath!,
- },
- }),
+ CriminalRecordModule,
MunicipalitiesFinancialAidModule,
FishingLicenseModule,
MortgageCertificateModule,
@@ -414,6 +403,9 @@ const environment = getConfig
LicenseConfig,
UserProfileClientConfig,
UltravioletRadiationClientConfig,
+ VmstClientConfig,
+ HealthInsuranceV2ClientConfig,
+ CriminalRecordClientConfig,
],
}),
],
diff --git a/apps/api/src/app/environments/environment.ts b/apps/api/src/app/environments/environment.ts
index dcf935a98db5..588fbe19ccfe 100644
--- a/apps/api/src/app/environments/environment.ts
+++ b/apps/api/src/app/environments/environment.ts
@@ -23,9 +23,6 @@ const prodConfig = () => ({
: process.env.XROAD_DRIVING_LICENSE_V2_PATH,
},
},
- criminalRecord: {
- xroadPath: process.env.XROAD_CRIMINAL_RECORD_PATH,
- },
education: {
xroadLicenseServiceId: process.env.XROAD_MMS_LICENSE_SERVICE_ID,
xroadGradeServiceId: process.env.XROAD_MMS_GRADE_SERVICE_ID,
@@ -42,13 +39,6 @@ const prodConfig = () => ({
clientID: process.env.XROAD_CLIENT_ID,
xroadID: process.env.XROAD_HEALTH_INSURANCE_ID,
},
- healthInsuranceV2: {
- xRoadBaseUrl: process.env.XROAD_BASE_PATH,
- xRoadClientId: process.env.XROAD_CLIENT_ID,
- xRoadProviderId: process.env.XROAD_HEALTH_INSURANCE_ID,
- username: process.env.XROAD_HEALTH_INSURANCE_V2_XROAD_USERNAME,
- password: process.env.XROAD_HEALTH_INSURANCE_V2_XROAD_PASSWORD,
- },
auth: {
issuer: process.env.IDENTITY_SERVER_ISSUER_URL,
audience: ['@island.is', '@admin.island.is'],
@@ -129,11 +119,6 @@ const devConfig = () => ({
'r1/IS-DEV/GOV/10005/Logreglan-Protected/RafraentOkuskirteini-v2',
},
},
- criminalRecord: {
- xroadPath:
- process.env.XROAD_CRIMINAL_RECORD_PATH ??
- 'r1/IS-DEV/GOV/10005/Logreglan-Protected/Sakavottord-PDF-v2',
- },
education: {
xroadLicenseServiceId: 'IS-DEV/GOV/10066/MMS-Protected/license-api-v1',
xroadGradeServiceId: 'IS-DEV/GOV/10066/MMS-Protected/grade-api-v1',
@@ -152,16 +137,6 @@ const devConfig = () => ({
clientID: process.env.XROAD_CLIENT_ID ?? '',
xroadID: process.env.XROAD_HEALTH_INSURANCE_ID ?? '',
},
- healthInsuranceV2: {
- xRoadBaseUrl: process.env.XROAD_BASE_PATH ?? 'http://localhost:8080',
- xRoadClientId:
- process.env.XROAD_CLIENT_ID ?? 'IS-DEV/GOV/10000/island-is-client',
- xRoadProviderId:
- process.env.XROAD_HEALTH_INSURANCE_ID ??
- 'IS-DEV/GOV/10007/SJUKRA-Protected',
- username: process.env.XROAD_HEALTH_INSURANCE_V2_XROAD_USERNAME ?? '',
- password: process.env.XROAD_HEALTH_INSURANCE_V2_XROAD_PASSWORD ?? '',
- },
auth: {
issuer: 'https://identity-server.dev01.devland.is',
audience: ['@island.is', '@admin.island.is'],
diff --git a/apps/application-system/api/src/app/app.module.ts b/apps/application-system/api/src/app/app.module.ts
index d81eb4cafd50..f410c2afc1cc 100644
--- a/apps/application-system/api/src/app/app.module.ts
+++ b/apps/application-system/api/src/app/app.module.ts
@@ -38,6 +38,10 @@ import { SignatureCollectionClientConfig } from '@island.is/clients/signature-co
import { InnaClientConfig } from '@island.is/clients/inna'
import { OfficialJournalOfIcelandClientConfig } from '@island.is/clients/official-journal-of-iceland'
import { OfficialJournalOfIcelandApplicationClientConfig } from '@island.is/clients/official-journal-of-iceland/application'
+import { DataProtectionComplaintClientConfig } from '@island.is/clients/data-protection-complaint'
+import { CriminalRecordClientConfig } from '@island.is/clients/criminal-record'
+import { HealthInsuranceV2ClientConfig } from '@island.is/clients/icelandic-health-insurance/health-insurance'
+import { VmstClientConfig } from '@island.is/clients/vmst'
@Module({
imports: [
@@ -79,6 +83,10 @@ import { OfficialJournalOfIcelandApplicationClientConfig } from '@island.is/clie
InnaClientConfig,
OfficialJournalOfIcelandClientConfig,
OfficialJournalOfIcelandApplicationClientConfig,
+ DataProtectionComplaintClientConfig,
+ CriminalRecordClientConfig,
+ HealthInsuranceV2ClientConfig,
+ VmstClientConfig,
],
}),
],
diff --git a/apps/application-system/api/src/environments/environment.ts b/apps/application-system/api/src/environments/environment.ts
index 6de3ad51ee9a..ebf09a8ed504 100644
--- a/apps/application-system/api/src/environments/environment.ts
+++ b/apps/application-system/api/src/environments/environment.ts
@@ -37,38 +37,11 @@ const devConfig = {
password: process.env.NOVA_PASSWORD,
acceptUnauthorized: true,
},
- criminalRecord: {
- clientConfig: {
- xroadClientId:
- process.env.XROAD_CLIENT_ID ?? 'IS-DEV/GOV/10000/island-is-client',
- xroadBaseUrl: process.env.XROAD_BASE_PATH ?? 'http://localhost:8081',
- xroadPath:
- process.env.XROAD_CRIMINAL_RECORD_PATH ??
- 'r1/IS-DEV/GOV/10005/Logreglan-Protected/Sakavottord-PDF-v2',
- },
- },
presignBucket: process.env.FILE_SERVICE_PRESIGN_BUCKET,
attachmentBucket: process.env.APPLICATION_ATTACHMENT_BUCKET,
generalPetition: {
endorsementsApiBasePath: 'http://localhost:4246',
},
- healthInsuranceV2: {
- xRoadBaseUrl: process.env.XROAD_BASE_PATH ?? 'http://localhost:8080',
- xRoadProviderId:
- process.env.XROAD_HEALTH_INSURANCE_ID ??
- 'IS-DEV/GOV/10007/SJUKRA-Protected',
- xRoadClientId:
- process.env.XROAD_CLIENT_ID ?? 'IS-DEV/GOV/10000/island-is-client',
- username: process.env.XROAD_HEALTH_INSURANCE_V2_XROAD_USERNAME ?? '',
- password: process.env.XROAD_HEALTH_INSURANCE_V2_XROAD_PASSWORD ?? '',
- },
- dataProtectionComplaint: {
- password: process.env.DATA_PROTECTION_COMPLAINT_API_PASSWORD,
- username: process.env.DATA_PROTECTION_COMPLAINT_API_USERNAME,
- XRoadProviderId: process.env.DATA_PROTECTION_COMPLAINT_XROAD_PROVIDER_ID,
- xRoadClientId: process.env.XROAD_CLIENT_ID,
- xRoadBaseUrl: process.env.XROAD_BASE_PATH ?? 'http://localhost:8080',
- },
userProfile: {
serviceBasePath: 'http://localhost:3366',
},
@@ -123,30 +96,9 @@ const prodConfig = {
},
presignBucket: process.env.FILE_SERVICE_PRESIGN_BUCKET,
attachmentBucket: process.env.APPLICATION_ATTACHMENT_BUCKET,
- criminalRecord: {
- clientConfig: {
- xroadClientId: process.env.XROAD_CLIENT_ID,
- xroadBaseUrl: process.env.XROAD_BASE_PATH,
- xroadPath: process.env.XROAD_CRIMINAL_RECORD_PATH,
- },
- },
generalPetition: {
endorsementsApiBasePath: process.env.ENDORSEMENTS_API_BASE_PATH,
},
- healthInsuranceV2: {
- xRoadBaseUrl: process.env.XROAD_BASE_PATH,
- xRoadProviderId: process.env.XROAD_HEALTH_INSURANCE_ID,
- xRoadClientId: process.env.XROAD_CLIENT_ID,
- username: process.env.XROAD_HEALTH_INSURANCE_V2_XROAD_USERNAME,
- password: process.env.XROAD_HEALTH_INSURANCE_V2_XROAD_PASSWORD,
- },
- dataProtectionComplaint: {
- password: process.env.DATA_PROTECTION_COMPLAINT_API_PASSWORD,
- username: process.env.DATA_PROTECTION_COMPLAINT_API_USERNAME,
- XRoadProviderId: process.env.DATA_PROTECTION_COMPLAINT_XROAD_PROVIDER_ID,
- xRoadClientId: process.env.XROAD_CLIENT_ID,
- xRoadBaseUrl: process.env.XROAD_BASE_PATH,
- },
userProfile: {
serviceBasePath: process.env.SERVICE_USER_PROFILE_URL,
},
diff --git a/apps/consultation-portal/components/Layout/Layout.json b/apps/consultation-portal/components/Layout/Layout.json
index b49cfe4a6045..963248ac7337 100644
--- a/apps/consultation-portal/components/Layout/Layout.json
+++ b/apps/consultation-portal/components/Layout/Layout.json
@@ -16,6 +16,10 @@
"label": "Áskriftir",
"href": "/askriftir"
},
+ {
+ "label": "Tölfræði",
+ "href": "/tolfraedi"
+ },
{
"label": "Mínar umsagnir",
"href": "/umsagnir"
diff --git a/apps/consultation-portal/components/Layout/components/Menu/components/MenuItems/MenuItems.ts b/apps/consultation-portal/components/Layout/components/Menu/components/MenuItems/MenuItems.ts
index 9b2b6c77bcfc..04db232a20fb 100644
--- a/apps/consultation-portal/components/Layout/components/Menu/components/MenuItems/MenuItems.ts
+++ b/apps/consultation-portal/components/Layout/components/Menu/components/MenuItems/MenuItems.ts
@@ -13,14 +13,14 @@ export const menuItems = [
href: loc[1].href,
testId: 'subscriptions-btn',
},
- // Tölfræði is hidden until PowerBI
- // {
- // label: 'Tölfræði',
- // href: '/tolfraedi',
- // },
{
label: loc[2].label,
href: loc[2].href,
+ testId: 'statistics-btn',
+ },
+ {
+ label: loc[3].label,
+ href: loc[3].href,
testId: 'advices-btn',
},
]
diff --git a/apps/consultation-portal/components/PowerBI/PowerBI.tsx b/apps/consultation-portal/components/PowerBI/PowerBI.tsx
new file mode 100644
index 000000000000..7abb922eada5
--- /dev/null
+++ b/apps/consultation-portal/components/PowerBI/PowerBI.tsx
@@ -0,0 +1,31 @@
+import { useRef } from 'react'
+import { PowerBIEmbed } from 'powerbi-client-react'
+import { type Embed, models } from 'powerbi-client'
+
+export const PowerBI = () => {
+ const embedRef = useRef(null)
+
+ const getEmbeddedComponent = (embed: Embed) => {
+ embed.element.style.height = '700px'
+ embed.element.style.width = '100%'
+ embed.iframe.style.border = 'none'
+ embedRef.current = embed
+ }
+
+ return (
+
+ )
+}
+
+export default PowerBI
diff --git a/apps/consultation-portal/components/PowerBI/index.ts b/apps/consultation-portal/components/PowerBI/index.ts
new file mode 100644
index 000000000000..a8748281fdc6
--- /dev/null
+++ b/apps/consultation-portal/components/PowerBI/index.ts
@@ -0,0 +1,5 @@
+import dynamic from 'next/dynamic'
+
+export const PowerBIComponent = dynamic(() => import('./PowerBI'), {
+ ssr: false,
+})
diff --git a/apps/consultation-portal/pages/tolfraedi.tsx b/apps/consultation-portal/pages/tolfraedi.tsx
deleted file mode 100644
index 9bb43ab068c4..000000000000
--- a/apps/consultation-portal/pages/tolfraedi.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Layout } from '../components'
-import { Box, GridColumn, GridRow } from '@island.is/island-ui/core'
-const Statistics = () => {
- return (
-
-
-
-
- {/* */}
-
-
-
-
- )
-}
-
-export default Statistics
diff --git a/apps/consultation-portal/pages/tolfraedi/index.tsx b/apps/consultation-portal/pages/tolfraedi/index.tsx
new file mode 100644
index 000000000000..1ec2bae64598
--- /dev/null
+++ b/apps/consultation-portal/pages/tolfraedi/index.tsx
@@ -0,0 +1,5 @@
+import { StatisticsScreen } from '../../screens/Statistics/Statistics'
+
+export const Index = () =>
+
+export default Index
diff --git a/apps/consultation-portal/pages/user/link-expired/index.tsx b/apps/consultation-portal/pages/user/link-expired/index.tsx
new file mode 100644
index 000000000000..43b9a6aeba0d
--- /dev/null
+++ b/apps/consultation-portal/pages/user/link-expired/index.tsx
@@ -0,0 +1,7 @@
+import LinkExpiredScreen from '../../../screens/EmailVerified/LinkExpired'
+
+export const Index = () => {
+ return
+}
+
+export default Index
diff --git a/apps/consultation-portal/pages/user/not-found/index.tsx b/apps/consultation-portal/pages/user/not-found/index.tsx
new file mode 100644
index 000000000000..e8fb87a89a4a
--- /dev/null
+++ b/apps/consultation-portal/pages/user/not-found/index.tsx
@@ -0,0 +1,7 @@
+import NotFoundScreen from '../../../screens/EmailVerified/NotFound'
+
+export const Index = () => {
+ return
+}
+
+export default Index
diff --git a/apps/consultation-portal/screens/EmailVerified/EmailVerified.json b/apps/consultation-portal/screens/EmailVerified/EmailVerified.json
index bec3b37792d6..d1525f11c973 100644
--- a/apps/consultation-portal/screens/EmailVerified/EmailVerified.json
+++ b/apps/consultation-portal/screens/EmailVerified/EmailVerified.json
@@ -8,5 +8,25 @@
"emailVerifiedText": "Netfang staðfest",
"text": "Nú geturðu skráð þig í áskrift að málum, hægt er að velja um nokkrar leiðir.",
"arrowLinkText": "Aftur á forsíðu"
+ },
+ "linkExpired": {
+ "seo": {
+ "title": "Hlekkur útrunninn",
+ "url": "/user/link-expired"
+ },
+ "failText": "Aðgerð tókst ekki",
+ "linkExpiredText": "Hlekkur útrunninn",
+ "text": "Hlekkur er útrunninn, vinsamlegast athugaðu að hlekkur sé ekki eldri en 24 klukkustundir. Vinsamlegast sláðu inn netfang aftur og fáðu nýjan hlekk.",
+ "arrowLinkText": "Aftur á forsíðu"
+ },
+ "notFound": {
+ "seo": {
+ "title": "Eitthvað fór úrskeiðis",
+ "url": "/user/not-found"
+ },
+ "failText": "Aðgerð tókst ekki",
+ "notFoundText": "Eitthvað fór úrskeiðis",
+ "text": "Reyndu aftur eða hafðu samband.",
+ "arrowLinkText": "Aftur á forsíðu"
}
}
diff --git a/apps/consultation-portal/screens/EmailVerified/LinkExpired.tsx b/apps/consultation-portal/screens/EmailVerified/LinkExpired.tsx
new file mode 100644
index 000000000000..f1033d5ee92d
--- /dev/null
+++ b/apps/consultation-portal/screens/EmailVerified/LinkExpired.tsx
@@ -0,0 +1,46 @@
+import {
+ ArrowLink,
+ Box,
+ GridColumn,
+ GridContainer,
+ GridRow,
+ Text,
+} from '@island.is/island-ui/core'
+import { Layout } from '../../components'
+import localization from './EmailVerified.json'
+
+export const LinkExpiredScreen = () => {
+ const loc = localization['linkExpired']
+ return (
+
+
+
+
+
+
+ {loc.failText}
+
+
+
+ {loc.linkExpiredText}
+
+ {loc.text}
+ {loc.arrowLinkText}
+
+
+
+
+
+ )
+}
+export default LinkExpiredScreen
diff --git a/apps/consultation-portal/screens/EmailVerified/NotFound.tsx b/apps/consultation-portal/screens/EmailVerified/NotFound.tsx
new file mode 100644
index 000000000000..64682a2edb1b
--- /dev/null
+++ b/apps/consultation-portal/screens/EmailVerified/NotFound.tsx
@@ -0,0 +1,46 @@
+import {
+ ArrowLink,
+ Box,
+ GridColumn,
+ GridContainer,
+ GridRow,
+ Text,
+} from '@island.is/island-ui/core'
+import { Layout } from '../../components'
+import localization from './EmailVerified.json'
+
+export const NotFoundScreen = () => {
+ const loc = localization['notFound']
+ return (
+
+
+
+
+
+
+ {loc.failText}
+
+
+
+ {loc.notFoundText}
+
+ {loc.text}
+ {loc.arrowLinkText}
+
+
+
+
+
+ )
+}
+export default NotFoundScreen
diff --git a/apps/consultation-portal/screens/Statistics/Statistics.json b/apps/consultation-portal/screens/Statistics/Statistics.json
new file mode 100644
index 000000000000..d94712e8309b
--- /dev/null
+++ b/apps/consultation-portal/screens/Statistics/Statistics.json
@@ -0,0 +1,19 @@
+{
+ "statistics": {
+ "seo": {
+ "title": "Tölfræði",
+ "url": "/tolfraedi",
+ "description": "Tölfræði um samráðsgátt",
+ "keywords": ""
+ },
+ "breadcrumbs": [
+ {
+ "title": "Samráðsgátt",
+ "href": "/samradsgatt"
+ },
+ {
+ "title": "Tölfræði"
+ }
+ ]
+ }
+}
diff --git a/apps/consultation-portal/screens/Statistics/Statistics.tsx b/apps/consultation-portal/screens/Statistics/Statistics.tsx
new file mode 100644
index 000000000000..7d8468d9c4bc
--- /dev/null
+++ b/apps/consultation-portal/screens/Statistics/Statistics.tsx
@@ -0,0 +1,32 @@
+import { GridContainer } from '@island.is/island-ui/core'
+
+import { Breadcrumbs, Layout } from '../../components'
+import localization from './Statistics.json'
+import { PowerBIComponent } from '../../components/PowerBI'
+
+export const StatisticsScreen = () => {
+ const loc = localization['statistics']
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default StatisticsScreen
diff --git a/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts b/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts
index 76047360dc5d..5e4e35bf0a9f 100644
--- a/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts
+++ b/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts
@@ -28,15 +28,16 @@ const setFont = (doc: PDFKit.PDFDocument, font?: string) => {
}
}
-const addCenteredText = (
+const addAlignedText = (
doc: PDFKit.PDFDocument,
fontSize: number,
heading: string,
+ alignment: 'center' | 'left' | 'right' | 'justify',
font?: string,
) => {
setFont(doc, font)
- doc.fontSize(fontSize).text(heading, { align: 'center', paragraphGap: 1 })
+ doc.fontSize(fontSize).text(heading, { align: alignment, paragraphGap: 1 })
}
const addText = (
@@ -51,17 +52,6 @@ const addText = (
doc.fontSize(fontSize).text(text, { continued, paragraphGap: 1 })
}
-const addJustifiedText = (
- doc: PDFKit.PDFDocument,
- fontSize: number,
- text: string,
- font?: string,
-) => {
- setFont(doc, font)
-
- doc.fontSize(fontSize).text(text, { align: 'justify', paragraphGap: 1 })
-}
-
export const setTitle = (doc: PDFKit.PDFDocument, title: string) => {
if (doc.info) {
doc.info['Title'] = title
@@ -333,7 +323,7 @@ export const addGiganticHeading = (
heading: string,
font?: string,
) => {
- addCenteredText(doc, giganticFontSize, heading, font)
+ addAlignedText(doc, giganticFontSize, heading, 'center', font)
}
export const addHugeHeading = (
@@ -341,7 +331,7 @@ export const addHugeHeading = (
heading: string,
font?: string,
) => {
- addCenteredText(doc, hugeFontSize, heading, font)
+ addAlignedText(doc, hugeFontSize, heading, 'center', font)
}
export const addLargeHeading = (
@@ -349,7 +339,7 @@ export const addLargeHeading = (
heading: string,
font?: string,
) => {
- addCenteredText(doc, largeFontSize, heading, font)
+ addAlignedText(doc, largeFontSize, heading, 'center', font)
}
export const addMediumPlusHeading = (
@@ -357,7 +347,7 @@ export const addMediumPlusHeading = (
heading: string,
font?: string,
) => {
- addCenteredText(doc, mediumPlusFontSize, heading, font)
+ addAlignedText(doc, mediumPlusFontSize, heading, 'center', font)
}
export const addMediumHeading = (
@@ -365,7 +355,7 @@ export const addMediumHeading = (
heading: string,
font?: string,
) => {
- addCenteredText(doc, mediumFontSize, heading, font)
+ addAlignedText(doc, mediumFontSize, heading, 'center', font)
}
export const addLargeText = (
@@ -398,7 +388,7 @@ export const addNormalPlusCenteredText = (
text: string,
font?: string,
) => {
- addCenteredText(doc, basePlusFontSize, text, font)
+ addAlignedText(doc, basePlusFontSize, text, 'center', font)
}
export const addNormalText = (
@@ -415,7 +405,7 @@ export const addNormalJustifiedText = (
text: string,
font?: string,
) => {
- addJustifiedText(doc, baseFontSize, text, font)
+ addAlignedText(doc, baseFontSize, text, 'justify', font)
}
export const addNormalPlusJustifiedText = (
@@ -423,7 +413,7 @@ export const addNormalPlusJustifiedText = (
text: string,
font?: string,
) => {
- addJustifiedText(doc, basePlusFontSize, text, font)
+ addAlignedText(doc, basePlusFontSize, text, 'justify', font)
}
export const addNormalCenteredText = (
@@ -431,7 +421,15 @@ export const addNormalCenteredText = (
text: string,
font?: string,
) => {
- addCenteredText(doc, baseFontSize, text, font)
+ addAlignedText(doc, baseFontSize, text, 'center', font)
+}
+
+export const addNormalRightAlignedText = (
+ doc: PDFKit.PDFDocument,
+ text: string,
+ font?: string,
+) => {
+ addAlignedText(doc, baseFontSize, text, 'right', font)
}
export const addNumberedList = (
diff --git a/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts b/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts
new file mode 100644
index 000000000000..7b9fc2da66b9
--- /dev/null
+++ b/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts
@@ -0,0 +1,151 @@
+import PDFDocument from 'pdfkit'
+
+import { FormatMessage } from '@island.is/cms-translations'
+
+import { formatDate, formatDOB } from '@island.is/judicial-system/formatters'
+import {
+ DateType,
+ DistrictCourtLocation,
+ DistrictCourts,
+ SubpoenaType,
+} from '@island.is/judicial-system/types'
+
+import { subpoena as strings } from '../messages'
+import { Case } from '../modules/case'
+import { Defendant } from '../modules/defendant'
+import {
+ addEmptyLines,
+ addFooter,
+ addHugeHeading,
+ addMediumText,
+ addNormalRightAlignedText,
+ addNormalText,
+ setTitle,
+} from './pdfHelpers'
+
+export const createSubpoenaPDF = (
+ theCase: Case,
+ formatMessage: FormatMessage,
+ defendant?: Defendant,
+): Promise => {
+ const doc = new PDFDocument({
+ size: 'A4',
+ margins: {
+ top: 40,
+ bottom: 60,
+ left: 50,
+ right: 50,
+ },
+ bufferPages: true,
+ })
+
+ if (!defendant) {
+ return Promise.reject('Defendant is missing')
+ }
+
+ const sinc: Buffer[] = []
+ const arraignmentDate = theCase.dateLogs?.find(
+ (d) => d.dateType === DateType.ARRAIGNMENT_DATE,
+ )
+
+ doc.on('data', (chunk) => sinc.push(chunk))
+
+ setTitle(doc, formatMessage(strings.title))
+ addNormalText(doc, `${theCase.court?.name}`, 'Times-Bold', true)
+
+ if (arraignmentDate) {
+ addNormalRightAlignedText(
+ doc,
+ `${formatDate(new Date(arraignmentDate.created), 'PPP')}`,
+ 'Times-Roman',
+ )
+ }
+
+ if (theCase.court?.name) {
+ addNormalText(
+ doc,
+ DistrictCourtLocation[theCase.court.name as DistrictCourts],
+ 'Times-Roman',
+ )
+ }
+
+ addEmptyLines(doc)
+ addNormalText(
+ doc,
+ defendant.name
+ ? `${defendant.name}, ${formatDOB(
+ defendant.nationalId,
+ defendant.noNationalId,
+ )}`
+ : 'Nafn ekki skráð',
+ )
+ addNormalText(doc, defendant.address || 'Heimili ekki skráð')
+ addEmptyLines(doc)
+ addHugeHeading(doc, formatMessage(strings.title).toUpperCase(), 'Times-Bold')
+ addEmptyLines(doc)
+ addMediumText(doc, `Mál nr. ${theCase.courtCaseNumber}`, 'Times-Bold')
+ addEmptyLines(doc)
+ addNormalText(doc, 'Ákærandi: ', 'Times-Bold', true)
+ addNormalText(
+ doc,
+ theCase.prosecutor?.institution
+ ? theCase.prosecutor.institution.name
+ : 'Ekki skráður',
+ 'Times-Roman',
+ )
+ addNormalText(
+ doc,
+ theCase.prosecutor
+ ? ` (${theCase.prosecutor.name} ${theCase.prosecutor.title})`
+ : 'Ekki skráður',
+ 'Times-Roman',
+ )
+ addEmptyLines(doc)
+ addNormalText(doc, 'Ákærði: ', 'Times-Bold', true)
+ addNormalText(doc, defendant.name || 'Nafn ekki skráð', 'Times-Roman')
+ addEmptyLines(doc, 2)
+
+ if (arraignmentDate?.date) {
+ addNormalText(
+ doc,
+ formatMessage(strings.arraignmentDate, {
+ arraignmentDate: formatDate(new Date(arraignmentDate.date), 'PPP'),
+ }),
+ 'Times-Bold',
+ )
+ }
+
+ if (arraignmentDate?.location) {
+ addNormalText(
+ doc,
+ formatMessage(strings.courtRoom, {
+ courtRoom: arraignmentDate.location,
+ }),
+ 'Times-Roman',
+ )
+ }
+
+ addNormalText(doc, formatMessage(strings.type), 'Times-Roman')
+ addEmptyLines(doc)
+ addNormalText(doc, formatMessage(strings.intro), 'Times-Bold')
+ addNormalText(
+ doc,
+ formatMessage(
+ defendant.subpoenaType === SubpoenaType.ABSENCE
+ ? strings.absenceIntro
+ : strings.arrestIntro,
+ ),
+ 'Times-Bold',
+ )
+
+ addEmptyLines(doc)
+ addNormalText(doc, formatMessage(strings.deadline), 'Times-Roman')
+
+ addFooter(doc)
+
+ doc.end()
+
+ return new Promise((resolve) =>
+ doc.on('end', () => resolve(Buffer.concat(sinc))),
+ )
+}
diff --git a/apps/judicial-system/backend/src/app/messages/index.ts b/apps/judicial-system/backend/src/app/messages/index.ts
index c27d0d4aa3ed..7c74f43383d6 100644
--- a/apps/judicial-system/backend/src/app/messages/index.ts
+++ b/apps/judicial-system/backend/src/app/messages/index.ts
@@ -7,3 +7,4 @@ export { notifications } from './notifications'
export { courtUpload } from './courtUpload'
export { caseFilesRecord } from './pdfCaseFilesRecord'
export { indictment } from './pdfIndictment'
+export { subpoena } from './pdfSubpoena'
diff --git a/apps/judicial-system/backend/src/app/messages/pdfSubpoena.ts b/apps/judicial-system/backend/src/app/messages/pdfSubpoena.ts
new file mode 100644
index 000000000000..90cf3dbcaaf9
--- /dev/null
+++ b/apps/judicial-system/backend/src/app/messages/pdfSubpoena.ts
@@ -0,0 +1,47 @@
+import { defineMessage } from '@formatjs/intl'
+
+export const subpoena = {
+ title: defineMessage({
+ id: 'judicial.system.backend:pdf.subpoena.title',
+ defaultMessage: 'Fyrirkall',
+ description: 'Notaður sem titill á fyrirkalli.',
+ }),
+ arraignmentDate: defineMessage({
+ id: 'judicial.system.backend:pdf.subpoena.arraignment_date',
+ defaultMessage: 'verður tekið fyrir á dómþingi {arraignmentDate}',
+ description: 'Notaður sem texti fyrir dagsetningu dómþings.',
+ }),
+ courtRoom: defineMessage({
+ id: 'judicial.system.backend:pdf.subpoena.court_room',
+ defaultMessage: 'Staður: {courtRoom}',
+ description: 'Notaður sem texti fyrir stað.',
+ }),
+ type: defineMessage({
+ id: 'judicial.system.backend:pdf.subpoena.type',
+ defaultMessage: 'Dómsathöfn: Þingfesting',
+ description: 'Notaður sem texti fyrir tegund.',
+ }),
+ intro: defineMessage({
+ id: 'judicial.system.backend:pdf.subpoena.intro',
+ defaultMessage:
+ 'Ákærði er kvaddur til að koma fyrir dóm, hlýða á ákæru, halda uppi vörnum og sæta dómi.',
+ description: 'Notaður sem inngangur.',
+ }),
+ arrestIntro: defineMessage({
+ id: 'judicial.system.backend:pdf.subpoena.type_intro',
+ defaultMessage:
+ 'Sæki ákærði ekki þing má hann búast við því að verða handtekinn og færður fyrir dóm.',
+ description: 'Notaður sem inngangur fyrir handtökufyrirkall.',
+ }),
+ absenceIntro: defineMessage({
+ id: 'judicial.system.backend:pdf.subpoena.absence_intro',
+ defaultMessage:
+ 'Sæki ákærði ekki þing má hann búast við því að fjarvist hans verði metin til jafns við það að hann viðurkenni að hafa framið brot það sem hann er ákærður fyrir og dómur verði lagður á málið að honum fjarstöddum.',
+ description: 'Notaður sem inngangur fyrir útivistarfyrirkall.',
+ }),
+ deadline: defineMessage({
+ id: 'judicial.system.backend:pdf.subpoena.deadline',
+ defaultMessage: 'Birtingarfrestur er þrír sólarhringar.',
+ description: 'Notaður sem texti fyrir frest.',
+ }),
+}
diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts
index 117bcc062f09..10271556cc3c 100644
--- a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts
@@ -164,6 +164,7 @@ export class InternalCaseService {
private readonly defendantService: DefendantService,
@Inject(forwardRef(() => EventLogService))
private readonly eventLogService: EventLogService,
+ @Inject(forwardRef(() => PDFService))
private readonly pdfService: PDFService,
@Inject(LOGGER_PROVIDER) private readonly logger: Logger,
) {}
diff --git a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts
index 63fb173ca122..5f84913ff8e5 100644
--- a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts
@@ -17,6 +17,7 @@ import {
EventType,
hasIndictmentCaseBeenSubmittedToCourt,
isTrafficViolationCase,
+ SubpoenaType,
type User as TUser,
} from '@island.is/judicial-system/types'
@@ -29,6 +30,7 @@ import {
getRulingPdfAsBuffer,
IndictmentConfirmation,
} from '../../formatters'
+import { createSubpoenaPDF } from '../../formatters/subpoenaPdf'
import { AwsS3Service } from '../aws-s3'
import { UserService } from '../user'
import { Case } from './models/case.model'
diff --git a/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx b/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx
index eb3d2a2fcd9d..75dd469bc821 100644
--- a/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx
+++ b/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx
@@ -15,13 +15,14 @@ import { strings } from './InfoCardIndictment.strings'
export interface Props {
defendantInfoActionButton?: DefendantInfoActionButton
+ displayAppealExpirationInfo?: boolean
}
const InfoCardClosedIndictment: React.FC = (props) => {
const { workingCase } = useContext(FormContext)
const { formatMessage } = useIntl()
- const { defendantInfoActionButton } = props
+ const { defendantInfoActionButton, displayAppealExpirationInfo } = props
return (
= (props) => {
),
items: workingCase.defendants,
defendantInfoActionButton: defendantInfoActionButton,
- displayAppealExpirationInfo: true,
+ displayAppealExpirationInfo,
}
: undefined
}
diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Cases/PublicProsecutorCases.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Cases/PublicProsecutorCases.tsx
index 30b105d27406..ee4612bb5f1f 100644
--- a/apps/judicial-system/web/src/routes/PublicProsecutor/Cases/PublicProsecutorCases.tsx
+++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Cases/PublicProsecutorCases.tsx
@@ -9,7 +9,10 @@ import {
PageHeader,
SharedPageLayout,
} from '@island.is/judicial-system-web/src/components'
-import { CaseListEntry } from '@island.is/judicial-system-web/src/graphql/schema'
+import {
+ CaseIndictmentRulingDecision,
+ CaseListEntry,
+} from '@island.is/judicial-system-web/src/graphql/schema'
import { useCasesQuery } from '../../Shared/Cases/cases.generated'
import CasesForReview from '../Tables/CasesForReview'
@@ -32,7 +35,12 @@ export const PublicProsecutorCases: React.FC = () => {
if (
c.state &&
isCompletedCase(c.state) &&
- !c.indictmentReviewDecision
+ !c.indictmentReviewDecision &&
+ c.indictmentRulingDecision &&
+ [
+ CaseIndictmentRulingDecision.RULING,
+ CaseIndictmentRulingDecision.FINE,
+ ].includes(c.indictmentRulingDecision)
) {
acc.casesForReview.push(c)
} else if (c.indictmentReviewDecision) {
diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx
index eed2faf3d792..64fde6470844 100644
--- a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx
+++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx
@@ -143,6 +143,7 @@ export const Overview = () => {
}
: undefined
}
+ displayAppealExpirationInfo={true}
/>
{lawsBroken.size > 0 && (
diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx b/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx
index cde1b2b43836..53f9be1328b2 100644
--- a/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx
+++ b/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx
@@ -277,19 +277,23 @@ export const Cases: React.FC = () => {
) : (
<>
- {isProsecutionUser(user) && filter.value !== 'INVESTIGATION' && (
+ {isProsecutionUser(user) && (
<>
-
- {isPublicProsecutor(user) && (
-
+ {filter.value !== 'INVESTIGATION' && (
+ <>
+
+ {isPublicProsecutor(user) && (
+
+ )}
+ >
)}
@@ -317,13 +321,15 @@ export const Cases: React.FC = () => {
>
)}
- {isDistrictCourtUser(user) && filter.value !== 'INVESTIGATION' && (
+ {isDistrictCourtUser(user) && (
<>
-
+ {filter.value !== 'INVESTIGATION' && (
+
+ )}
{
)}
{caseIsClosed ? (
-
+
) : (
)}
diff --git a/apps/judicial-system/web/src/types/index.ts b/apps/judicial-system/web/src/types/index.ts
index 7ff58f6efd25..214127f7c17a 100644
--- a/apps/judicial-system/web/src/types/index.ts
+++ b/apps/judicial-system/web/src/types/index.ts
@@ -174,7 +174,6 @@ export interface NationalRegistryResponseBusiness {
* We use this type so that we don't have to migrate all the code
* at once and this type will be removed when we are done.
*/
-
export interface TempIndictmentCount
extends Omit {
substances?: SubstanceMap | null
diff --git a/apps/native/app/android/app/build.gradle b/apps/native/app/android/app/build.gradle
index bf84ce0109fb..82992c91f025 100644
--- a/apps/native/app/android/app/build.gradle
+++ b/apps/native/app/android/app/build.gradle
@@ -119,7 +119,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode getMyVersionCode(143)
- versionName "1.2.6"
+ versionName "1.3.0"
manifestPlaceholders = [
appAuthRedirectScheme: "is.island.app" // project.config.get("BUNDLE_ID_ANDROID")
]
diff --git a/apps/native/app/ios/IslandApp/Info.plist b/apps/native/app/ios/IslandApp/Info.plist
index 844ab4e5baf2..ba2175dcf006 100644
--- a/apps/native/app/ios/IslandApp/Info.plist
+++ b/apps/native/app/ios/IslandApp/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.2.6
+ 1.3.0
CFBundleSignature
????
CFBundleURLTypes
diff --git a/apps/native/app/ios/Podfile.lock b/apps/native/app/ios/Podfile.lock
index a418cf1515f6..3d819f02d122 100644
--- a/apps/native/app/ios/Podfile.lock
+++ b/apps/native/app/ios/Podfile.lock
@@ -1,9 +1,10 @@
PODS:
- - AppAuth (1.4.0):
- - AppAuth/Core (= 1.4.0)
- - AppAuth/ExternalUserAgent (= 1.4.0)
- - AppAuth/Core (1.4.0)
- - AppAuth/ExternalUserAgent (1.4.0)
+ - AppAuth (1.7.5):
+ - AppAuth/Core (= 1.7.5)
+ - AppAuth/ExternalUserAgent (= 1.7.5)
+ - AppAuth/Core (1.7.5)
+ - AppAuth/ExternalUserAgent (1.7.5):
+ - AppAuth/Core
- Base64 (1.1.2)
- boost (1.76.0)
- CocoaAsyncSocket (7.6.5)
@@ -503,8 +504,8 @@ PODS:
- React-jsinspector (0.71.1)
- React-logger (0.71.1):
- glog
- - react-native-app-auth (6.4.3):
- - AppAuth (= 1.4.0)
+ - react-native-app-auth (7.2.0):
+ - AppAuth (>= 1.7.3)
- React-Core
- react-native-blob-util (0.16.4):
- React-Core
@@ -970,7 +971,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
- AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7
+ AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa
Base64: cecfb41a004124895a7bcee567a89bae5a89d49b
boost: 57d2868c099736d80fcd648bf211b4431e51a558
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
@@ -1044,7 +1045,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: 60cf272aababc5212410e4249d17cea14fc36caa
React-jsinspector: ff56004b0c974b688a6548c156d5830ad751ae07
React-logger: 60a0b5f8bed667ecf9e24fecca1f30d125de6d75
- react-native-app-auth: fd1eaa667c0bc014199456d14a6440cb74de814e
+ react-native-app-auth: 63fa4e58c5bd29aeb974d3a06a23c5858322d533
react-native-blob-util: 60453b777610c87a22b3524032c0214e8db555db
react-native-cookies: f54fcded06bb0cda05c11d86788020b43528a26c
react-native-mmkv-storage: cfb6854594cfdc5f7383a9e464bb025417d1721c
diff --git a/apps/native/app/package.json b/apps/native/app/package.json
index aa43702e1444..0908a4c6a63a 100644
--- a/apps/native/app/package.json
+++ b/apps/native/app/package.json
@@ -63,7 +63,7 @@
"react": "18.2.0",
"react-intl": "5.20.12",
"react-native": "0.71.1",
- "react-native-app-auth": "6.4.3",
+ "react-native-app-auth": "7.2.0",
"react-native-blob-util": "0.16.4",
"react-native-code-push": "7.1.0",
"react-native-device-info": "10.3.0",
diff --git a/apps/native/app/src/components/offline/offline-banner.tsx b/apps/native/app/src/components/offline/offline-banner.tsx
index f17c66a33140..60f01b78f504 100644
--- a/apps/native/app/src/components/offline/offline-banner.tsx
+++ b/apps/native/app/src/components/offline/offline-banner.tsx
@@ -4,7 +4,7 @@ import { Animated, Easing, SafeAreaView } from 'react-native'
import { Navigation } from 'react-native-navigation'
import styled from 'styled-components/native'
import { getIntl } from '../../contexts/i18n-provider'
-import { useOfflineActions, useOfflineStore } from '../../stores/offline-store'
+import { useOfflineActions } from '../../stores/offline-store'
import { ComponentRegistry as CR } from '../../utils/component-registry'
const TranslateYValue = 200
diff --git a/apps/native/app/src/graphql/client.ts b/apps/native/app/src/graphql/client.ts
index 97b98e2d2f51..4149a0091e62 100644
--- a/apps/native/app/src/graphql/client.ts
+++ b/apps/native/app/src/graphql/client.ts
@@ -161,7 +161,7 @@ const cache = new InMemoryCache({
},
},
},
- Document: {
+ DocumentV2: {
fields: {
archived: {
read(_value, { readField, variables }) {
diff --git a/apps/native/app/src/graphql/fragments/document.fragment.graphql b/apps/native/app/src/graphql/fragments/document.fragment.graphql
index ede2164f6c57..e074389e3a53 100644
--- a/apps/native/app/src/graphql/fragments/document.fragment.graphql
+++ b/apps/native/app/src/graphql/fragments/document.fragment.graphql
@@ -1,15 +1,16 @@
directive @client on FIELD
-fragment ListDocument on Document {
+fragment ListDocument on DocumentV2 {
id
subject
- senderName
- senderNatReg
- date
- fileType
- url
+ publicationDate
+ downloadUrl
opened
categoryId
bookmarked
+ sender {
+ id
+ name
+ }
archived @client
}
diff --git a/apps/native/app/src/graphql/queries/inbox.graphql b/apps/native/app/src/graphql/queries/inbox.graphql
index 36b29b11ba8d..2ecdf2458686 100644
--- a/apps/native/app/src/graphql/queries/inbox.graphql
+++ b/apps/native/app/src/graphql/queries/inbox.graphql
@@ -12,28 +12,28 @@ query ListOrganizations {
}
}
-query ListDocuments($input: GetDocumentListInput!) {
- listDocumentsV2(input: $input) {
+query ListDocuments($input: DocumentsV2DocumentsInput!) {
+ documentsV2(input: $input) {
data {
...ListDocument
}
totalCount
+ unreadCount
}
}
-query GetDocument($input: GetDocumentInput!) {
- getDocument(input: $input) {
- fileType
- content
- url
- html
+query GetDocument($input: DocumentInput!) {
+ documentV2(input: $input) {
+ ...ListDocument
+ content {
+ type
+ value
+ }
}
}
-mutation PostMailActionMutation($input: PostMailActionResolverInput!) {
- postMailAction(input: $input) {
+mutation PostMailActionMutation($input: DocumentsV2MailActionInput!) {
+ postMailActionV2(input: $input) {
success
- messageId
- action
}
}
diff --git a/apps/native/app/src/hoc/offline-hoc.tsx b/apps/native/app/src/hoc/offline-hoc.tsx
index 24bbe519b1d5..12ac92c7bf24 100644
--- a/apps/native/app/src/hoc/offline-hoc.tsx
+++ b/apps/native/app/src/hoc/offline-hoc.tsx
@@ -20,15 +20,13 @@ export const OfflineHoc = ({ children }: OfflineHocProps) => {
const lockScreenActivatedAt = useAuthStore(
({ lockScreenActivatedAt }) => lockScreenActivatedAt,
)
- const userInfo = useAuthStore(({ userInfo }) => userInfo)
useEffect(() => {
if (
bannerVisible &&
!bannerHasBeenShown &&
!isConnected &&
- !lockScreenActivatedAt &&
- userInfo
+ lockScreenActivatedAt === null
) {
void impactAsync(ImpactFeedbackStyle.Heavy)
void Navigation.showOverlay({
@@ -39,13 +37,7 @@ export const OfflineHoc = ({ children }: OfflineHocProps) => {
})
setBannerHasBeenShown(true)
}
- }, [
- bannerVisible,
- bannerHasBeenShown,
- isConnected,
- lockScreenActivatedAt,
- userInfo,
- ])
+ }, [bannerVisible, bannerHasBeenShown, isConnected, lockScreenActivatedAt])
return <>{children}>
}
diff --git a/apps/native/app/src/lib/post-mail-action.ts b/apps/native/app/src/lib/post-mail-action.ts
index 58cf25c2cf35..166f406bf6ae 100644
--- a/apps/native/app/src/lib/post-mail-action.ts
+++ b/apps/native/app/src/lib/post-mail-action.ts
@@ -21,17 +21,18 @@ export async function toggleAction(
mutation: PostMailActionMutationDocument,
variables: {
input: {
- messageId,
+ documentIds: [messageId],
action,
},
},
refetchQueries: refetch ? [ListDocumentsDocument] : undefined,
update(cache, { data }) {
const id = cache.identify({
- __typename: 'Document',
+ __typename: 'DocumentV2',
id: messageId,
})
- const success = data?.postMailAction?.success
+
+ const success = data?.postMailActionV2?.success
if (!success) {
return
}
diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts
index 2490a8017358..6792e2614ac4 100644
--- a/apps/native/app/src/messages/en.ts
+++ b/apps/native/app/src/messages/en.ts
@@ -281,6 +281,9 @@ export const en: TranslatedMessages = {
'notifications.markAllAsRead': 'Mark all as read',
'notifications.settings': 'My settings',
'notifications.errorUnknown': 'Error occurred while loading notifications',
+ 'notifications.emptyListTitle': 'No notifications',
+ 'notifications.emptyListDescription':
+ 'When you receive notifications, they will appear here.',
// profile
'profile.screenTitle': 'More',
diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts
index f6754c762e4c..c3bcdb5d8bf4 100644
--- a/apps/native/app/src/messages/is.ts
+++ b/apps/native/app/src/messages/is.ts
@@ -415,6 +415,9 @@ export const is = {
'notifications.markAllAsRead': 'Merkja allt lesið',
'notifications.settings': 'Mínar stillingar',
'notifications.errorUnknown': 'Villa kom upp við að sækja tilkynningar',
+ 'notifications.emptyListTitle': 'Engar tilkynningar',
+ 'notifications.emptyListDescription':
+ 'Þegar þú færð sendar tilkynningar þá birtast þær hér.',
// applications screen
'applications.title': 'Umsóknir',
diff --git a/apps/native/app/src/screens/app-lock/app-lock.tsx b/apps/native/app/src/screens/app-lock/app-lock.tsx
index 79fab7333bd7..ae72cf99b3be 100644
--- a/apps/native/app/src/screens/app-lock/app-lock.tsx
+++ b/apps/native/app/src/screens/app-lock/app-lock.tsx
@@ -89,15 +89,18 @@ export const AppLockScreen: NavigationFunctionComponent<{
lockScreenComponentId: undefined,
}))
}, [])
-
const unlockApp = useCallback(() => {
Animated.spring(av, {
toValue: 0,
useNativeDriver: true,
delay: 100,
}).start(() => {
- resetLockScreen()
- void Navigation.dismissAllOverlays()
+ // We want to reset lockScreenActivatedAt to null here to trigger offline banner if offline
+ authStore.setState(() => ({
+ lockScreenActivatedAt: null,
+ lockScreenComponentId: undefined,
+ }))
+ void Navigation.dismissOverlay(componentId)
av.setValue(1)
})
}, [componentId])
diff --git a/apps/native/app/src/screens/document-detail/document-detail.tsx b/apps/native/app/src/screens/document-detail/document-detail.tsx
index ae696f46eeed..635f4a2907e1 100644
--- a/apps/native/app/src/screens/document-detail/document-detail.tsx
+++ b/apps/native/app/src/screens/document-detail/document-detail.tsx
@@ -19,10 +19,9 @@ import Share from 'react-native-share'
import WebView from 'react-native-webview'
import styled from 'styled-components/native'
import {
- Document,
+ DocumentV2,
ListDocumentFragmentDoc,
useGetDocumentQuery,
- useListDocumentsLazyQuery,
} from '../../graphql/types/schema'
import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks'
import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator'
@@ -178,48 +177,44 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
const [accessToken, setAccessToken] = useState()
const [error, setError] = useState(false)
- const doc = useFragment_experimental({
+ // Check if we have the document in the cache
+ const doc = useFragment_experimental({
fragment: ListDocumentFragmentDoc,
from: {
- __typename: 'Document',
+ __typename: 'DocumentV2',
id: docId,
},
returnPartialData: true,
})
- const [getListDocuments] = useListDocumentsLazyQuery()
+ // Fetch the document to get the content information
const docRes = useGetDocumentQuery({
variables: {
input: {
id: docId,
},
},
+ fetchPolicy: 'no-cache',
})
const Document = {
- ...(docRes.data?.getDocument || {}),
- ...doc.data,
+ ...(doc?.data || {}),
+ ...(docRes.data?.documentV2 || {}),
}
- useEffect(() => {
- if (doc.missing) {
- void getListDocuments({
- variables: {
- input: {
- page: 1,
- pageSize: 50,
- },
- },
- })
- }
- }, [doc])
-
const [visible, setVisible] = useState(false)
const [loaded, setLoaded] = useState(false)
const [pdfUrl, setPdfUrl] = useState('')
const [touched, setTouched] = useState(false)
- const hasPdf = Document.fileType?.toLocaleLowerCase() === 'pdf'
- const isHtml = typeof Document.html === 'string' && Document.html !== ''
+
+ const loading = docRes.loading || !accessToken
+ const fileTypeLoaded = !!Document?.content?.type
+ const hasError = error || docRes.error
+
+ const hasPdf = Document?.content?.type.toLocaleLowerCase() === 'pdf'
+ const isHtml =
+ Document?.content?.type.toLocaleLowerCase() === 'html' &&
+ Document.content?.value !== ''
useConnectivityIndicator({
componentId,
@@ -247,16 +242,16 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
)
setTouched(true)
}
- if (buttonId === ButtonRegistry.ShareButton) {
+ if (buttonId === ButtonRegistry.ShareButton && loaded) {
if (Platform.OS === 'android') {
authStore.setState({ noLockScreenUntilNextAppStateActive: true })
}
Share.open({
title: Document.subject!,
subject: Document.subject!,
- message: `${Document.senderName!} \n ${Document.subject!}`,
+ message: `${Document.sender!.name!} \n ${Document.subject!}`,
type: hasPdf ? 'application/pdf' : undefined,
- url: hasPdf ? `file://${pdfUrl}` : Document.url!,
+ url: hasPdf ? `file://${pdfUrl}` : Document.downloadUrl!,
})
}
}, componentId)
@@ -281,7 +276,7 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
// Lets mark the document as read
client.cache.modify({
id: client.cache.identify({
- __typename: 'Document',
+ __typename: 'DocumentV2',
id: Document.id,
}),
fields: {
@@ -305,8 +300,6 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
}
}, [])
- const loading = docRes.loading || !accessToken
-
const fadeAnim = useRef(new Animated.Value(0)).current
React.useEffect(() => {
@@ -323,12 +316,16 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
<>
}
+ title={Document.sender?.name ?? ''}
+ date={
+ Document.publicationDate ? (
+
+ ) : undefined
+ }
message={Document.subject}
- isLoading={loading}
+ isLoading={loading && !Document.subject}
hasBorder={false}
- logo={getOrganizationLogoUrl(Document.senderName!, 75)}
+ logo={getOrganizationLogoUrl(Document.sender?.name ?? '', 75)}
/>
@@ -343,42 +340,48 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
opacity: fadeAnim,
}}
>
- {isHtml ? (
- {
- setLoaded(true)
- }}
- />
- ) : hasPdf ? (
-
- {visible && accessToken && (
- {
- setPdfUrl(filePath)
- setLoaded(true)
- }}
- onError={() => {
- setLoaded(true)
- setError(true)
- }}
- />
- )}
-
- ) : (
- {
- setLoaded(true)
- }}
- />
- )}
+ {fileTypeLoaded &&
+ !error &&
+ (isHtml ? (
+ {
+ setLoaded(true)
+ }}
+ />
+ ) : hasPdf ? (
+
+ {visible && accessToken && (
+ {
+ setPdfUrl(filePath)
+ setLoaded(true)
+ }}
+ onError={() => {
+ setLoaded(true)
+ setError(true)
+ }}
+ />
+ )}
+
+ ) : (
+ {
+ setLoaded(true)
+ }}
+ onError={() => {
+ setLoaded(true)
+ setError(true)
+ }}
+ />
+ ))}
- {(!loaded || !accessToken || error) && (
+ {(!loaded || !accessToken || hasError) && (
- {error ? (
+ {hasError ? (
) : (
{
+ ({ item, listParams }: { item: DocumentV2; listParams: any }) => {
const { getOrganizationLogoUrl } = useOrganizationsStore()
const [starred, setStarred] = useState(!!item.bookmarked)
useEffect(() => setStarred(!!item.bookmarked), [item.bookmarked])
@@ -105,22 +103,26 @@ const PressableListItem = React.memo(
highlightColor={theme.shade.shade400}
onPress={() =>
navigateTo(`/inbox/${item.id}`, {
- title: item.senderName,
+ title: item.sender.name,
listParams,
})
}
>
{
toggleAction(!item.bookmarked ? 'bookmark' : 'unbookmark', item.id)
setStarred(!item.bookmarked)
}}
- icon={getOrganizationLogoUrl(item.senderName, 75)}
+ icon={
+ item.sender.name && getOrganizationLogoUrl(item.sender.name, 75)
+ }
/>
)
@@ -139,23 +141,6 @@ function useThrottleState(state: string, delay = 500) {
return throttledState
}
-const useUnreadCount = () => {
- const res = useListDocumentsQuery({
- fetchPolicy: 'cache-first',
- variables: {
- input: {
- page: 1,
- pageSize: 50,
- opened: false,
- },
- },
- })
- const unopened = res?.data?.listDocumentsV2?.data?.filter(
- (item) => item.opened === false,
- )
- return unopened?.length ?? 0
-}
-
type Filters = {
opened?: boolean
archived?: boolean
@@ -175,7 +160,7 @@ function applyFilters(filters?: Filters) {
function useInboxQuery(incomingFilters?: Filters) {
const [filters, setFilters] = useState(applyFilters(incomingFilters))
const [page, setPage] = useState(1)
- const [data, setData] = useState()
+ const [data, setData] = useState()
const [refetching, setRefetching] = useState(true)
const [loading, setLoading] = useState(false)
const [refetcher, setRefetcher] = useState(0)
@@ -229,12 +214,13 @@ function useInboxQuery(incomingFilters?: Filters) {
setData((prevData) => ({
data: [
...(prevData?.data ?? []),
- ...(data.listDocumentsV2?.data ?? []),
+ ...(data.documentsV2?.data ?? []),
],
- totalCount: data.listDocumentsV2?.totalCount,
+ totalCount: data.documentsV2?.totalCount ?? 0,
+ unreadCount: data.documentsV2?.unreadCount ?? 0,
}))
} else {
- setData(data.listDocumentsV2)
+ setData(data.documentsV2)
}
}
})
@@ -289,7 +275,6 @@ export const InboxScreen: NavigationFunctionComponent<{
const [query, setQuery] = useState('')
const queryString = useThrottleState(query)
const theme = useTheme()
- const unreadCount = useUnreadCount()
const [visible, setVisible] = useState(false)
const [refetching, setRefetching] = useState(false)
@@ -308,6 +293,7 @@ export const InboxScreen: NavigationFunctionComponent<{
}
},
})
+ const unreadCount = res?.data?.unreadCount ?? 0
useConnectivityIndicator({
componentId,
@@ -324,7 +310,7 @@ export const InboxScreen: NavigationFunctionComponent<{
res.refetch()
}, [refresh])
- const items = res.data?.data ?? []
+ const items = useMemo(() => res.data?.data ?? [], [res.data])
const isSearch = ui.inboxQuery.length > 2
useActiveTabItemPress(0, () => {
@@ -383,7 +369,7 @@ export const InboxScreen: NavigationFunctionComponent<{
}
return (
theme.spacing[3]}px;
@@ -47,10 +55,14 @@ const ButtonWrapper = styled.View`
flex-direction: row;
margin-horizontal: ${({ theme }) => theme.spacing[2]}px;
margin-top: ${({ theme }) => theme.spacing[2]}px;
+ margin-bottom: ${({ theme }) => theme.spacing[2]}px;
`
const DEFAULT_PAGE_SIZE = 50
+const FALLBACK_ICON_URL =
+ 'https://images.ctfassets.net/8k0h54kbe6bj/6XhCz5Ss17OVLxpXNVDxAO/d3d6716bdb9ecdc5041e6baf68b92ba6/coat_of_arms.svg'
+
const { getNavigationOptions, useNavigationOptions } =
createNavigationOptionHooks(() => ({
topBar: {
@@ -62,7 +74,12 @@ type NotificationItem = NonNullable<
NonNullable['data']
>[0]
-type ListItem = SkeletonItem | NotificationItem
+export type EmptyItem = {
+ id: string
+ __typename: 'Empty'
+}
+
+type ListItem = SkeletonItem | NotificationItem | EmptyItem
export const NotificationsScreen: NavigationFunctionComponent = ({
componentId,
@@ -123,6 +140,10 @@ export const NotificationsScreen: NavigationFunctionComponent = ({
return createSkeletonArr(9)
}
+ if (data?.userNotifications?.data?.length === 0) {
+ return [{ id: '0', __typename: 'Empty' }]
+ }
+
return data?.userNotifications?.data || []
}, [data, loading])
@@ -185,6 +206,26 @@ export const NotificationsScreen: NavigationFunctionComponent = ({
return
}
+ if (item.__typename === 'Empty') {
+ return (
+
+
+ }
+ />
+
+ )
+ }
+
return (
onNotificationPress(item)}
testID={testIDs.NOTIFICATION_CARD_BUTTON}
@@ -219,40 +260,6 @@ export const NotificationsScreen: NavigationFunctionComponent = ({
showLoading={loading && !!data}
/>
-
- markAllUserNotificationsAsRead()}
- />
- navigateTo('/settings', componentId)}
- icon={settings}
- style={{
- maxWidth: 145,
- }}
- iconStyle={{
- tintColor: theme.color.blue400,
- resizeMode: 'contain',
- }}
- />
-
{showError ? (
) : (
@@ -265,6 +272,42 @@ export const NotificationsScreen: NavigationFunctionComponent = ({
onEndReached={handleEndReached}
scrollEventThrottle={16}
scrollToOverflowEnabled
+ ListHeaderComponent={
+
+ markAllUserNotificationsAsRead()}
+ />
+ navigateTo('/settings', componentId)}
+ icon={settings}
+ style={{
+ maxWidth: 145,
+ }}
+ iconStyle={{
+ tintColor: theme.color.blue400,
+ resizeMode: 'contain',
+ }}
+ />
+
+ }
ListFooterComponent={
loadingMore && !error ? (
diff --git a/apps/native/app/src/stores/auth-store.ts b/apps/native/app/src/stores/auth-store.ts
index 6352406e78a5..b55f738d1b4e 100644
--- a/apps/native/app/src/stores/auth-store.ts
+++ b/apps/native/app/src/stores/auth-store.ts
@@ -1,25 +1,28 @@
+import AsyncStorage from '@react-native-community/async-storage'
import { Alert } from 'react-native'
import {
authorize,
AuthorizeResult,
- refresh,
+ refresh as authRefresh,
RefreshResult,
revoke,
} from 'react-native-app-auth'
import Keychain from 'react-native-keychain'
-import { Navigation } from 'react-native-navigation'
import createUse from 'zustand'
import create, { State } from 'zustand/vanilla'
+import { persist } from 'zustand/middleware'
import { bundleId, getConfig } from '../config'
import { getIntl } from '../contexts/i18n-provider'
import { getApolloClientAsync } from '../graphql/client'
import { isAndroid } from '../utils/devices'
-import { getAppRoot } from '../utils/lifecycle/get-app-root'
+import { offlineStore } from './offline-store'
import { preferencesStore } from './preferences-store'
import { clearAllStorages } from '../stores/mmkv'
import { notificationsStore } from './notifications-store'
const KEYCHAIN_AUTH_KEY = `@islandis_${bundleId}`
+const INVALID_REFRESH_TOKEN_ERROR = 'invalid_grant'
+const UNAUTHORIZED_USER_INFO = 'Got 401 when fetching user info'
// Optional scopes (not required for all users so we do not want to force a logout)
const OPTIONAL_SCOPES = ['@island.is/licenses:barcode']
@@ -33,15 +36,15 @@ interface UserInfo {
interface AuthStore extends State {
authorizeResult: AuthorizeResult | RefreshResult | undefined
userInfo: UserInfo | undefined
- lockScreenActivatedAt: number | undefined
+ lockScreenActivatedAt?: number | null
lockScreenComponentId: string | undefined
noLockScreenUntilNextAppStateActive: boolean
isCogitoAuth: boolean
cognitoDismissCount: number
cognitoAuthUrl?: string
cookies: string
- fetchUserInfo(_refresh?: boolean): Promise
- refresh(): Promise
+ fetchUserInfo(skipRefresh?: boolean): Promise
+ refresh(): Promise
login(): Promise
logout(): Promise
}
@@ -49,6 +52,7 @@ interface AuthStore extends State {
const getAppAuthConfig = () => {
const config = getConfig()
const android = isAndroid && !config.isTestingApp ? '.auth' : ''
+
return {
issuer: config.idsIssuer,
clientId: config.idsClientId,
@@ -57,151 +61,165 @@ const getAppAuthConfig = () => {
}
}
-export const authStore = create((set, get) => ({
- authorizeResult: undefined,
- userInfo: undefined,
- lockScreenActivatedAt: undefined,
- lockScreenComponentId: undefined,
- noLockScreenUntilNextAppStateActive: false,
- isCogitoAuth: false,
- cognitoDismissCount: 0,
- cognitoAuthUrl: undefined,
- cookies: '',
- async fetchUserInfo(_refresh = false) {
- const appAuthConfig = getAppAuthConfig()
- // Detect expired token
- const expiresAt = get().authorizeResult?.accessTokenExpirationDate ?? 0
- if (new Date(expiresAt) < new Date()) {
- await get().refresh()
- }
- return fetch(
- `${appAuthConfig.issuer.replace(/\/$/, '')}/connect/userinfo`,
- {
- headers: {
- Authorization: `Bearer ${get().authorizeResult?.accessToken}`,
- },
+export const authStore = create(
+ persist(
+ (set, get) => ({
+ authorizeResult: undefined,
+ userInfo: undefined,
+ lockScreenActivatedAt: undefined,
+ lockScreenComponentId: undefined,
+ noLockScreenUntilNextAppStateActive: false,
+ isCogitoAuth: false,
+ cognitoDismissCount: 0,
+ cognitoAuthUrl: undefined,
+ cookies: '',
+ async fetchUserInfo(skipRefresh = false) {
+ const appAuthConfig = getAppAuthConfig()
+ // Detect expired token
+ const expiresAt = get().authorizeResult?.accessTokenExpirationDate ?? 0
+
+ if (!skipRefresh && new Date(expiresAt) < new Date()) {
+ await get().refresh()
+ }
+
+ const res = await fetch(
+ `${appAuthConfig.issuer.replace(/\/$/, '')}/connect/userinfo`,
+ {
+ headers: {
+ Authorization: `Bearer ${get().authorizeResult?.accessToken}`,
+ },
+ },
+ )
+
+ if (res.status === 401) {
+ // Attempt to refresh the access token
+ if (!skipRefresh) {
+ await get().refresh()
+ // Retry the userInfo call
+ return get().fetchUserInfo(true)
+ }
+ throw new Error(UNAUTHORIZED_USER_INFO)
+ } else if (res.status === 200) {
+ const userInfo = await res.json()
+ set({ userInfo })
+
+ return userInfo
+ }
},
- ).then(async (res) => {
- if (res.status === 401) {
- // Attempt to refresh the access token
- if (!_refresh && (await get().refresh())) {
- // Retry the userInfo call
- return get().fetchUserInfo(true)
+ async refresh() {
+ const appAuthConfig = getAppAuthConfig()
+ const refreshToken = get().authorizeResult?.refreshToken
+
+ if (!refreshToken) {
+ return
}
- throw new Error('Unauthorized')
- } else if (res.status === 200) {
- const userInfo = await res.json()
- set({ userInfo })
- return userInfo
- }
- return undefined
- })
- },
- async refresh() {
- const appAuthConfig = getAppAuthConfig()
- const authorizeResult = {
- ...get().authorizeResult,
- ...(await refresh(appAuthConfig, {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- refreshToken: get().authorizeResult!.refreshToken!,
- })),
- }
- if (authorizeResult) {
- await Keychain.setGenericPassword(
- KEYCHAIN_AUTH_KEY,
- JSON.stringify(authorizeResult),
- { service: KEYCHAIN_AUTH_KEY },
- )
- set({ authorizeResult })
- return true
- }
- return false
- },
- async login() {
- const appAuthConfig = getAppAuthConfig()
- const authorizeResult = await authorize({
- ...appAuthConfig,
- additionalParameters: {
- prompt: 'login',
- prompt_delegations: 'true',
- ui_locales: preferencesStore.getState().locale,
- externalUserAgent: 'yes',
+
+ const newAuthorizeResult = await authRefresh(appAuthConfig, {
+ refreshToken,
+ })
+
+ const authorizeResult = {
+ ...get().authorizeResult,
+ ...newAuthorizeResult,
+ }
+
+ await Keychain.setGenericPassword(
+ KEYCHAIN_AUTH_KEY,
+ JSON.stringify(authorizeResult),
+ { service: KEYCHAIN_AUTH_KEY },
+ )
+ set({ authorizeResult })
},
- })
+ async login() {
+ const appAuthConfig = getAppAuthConfig()
+ const authorizeResult = await authorize({
+ ...appAuthConfig,
+ additionalParameters: {
+ prompt: 'login',
+ prompt_delegations: 'true',
+ ui_locales: preferencesStore.getState().locale,
+ externalUserAgent: 'yes',
+ },
+ })
- if (authorizeResult) {
- await Keychain.setGenericPassword(
- KEYCHAIN_AUTH_KEY,
- JSON.stringify(authorizeResult),
- { service: KEYCHAIN_AUTH_KEY },
- )
- set({ authorizeResult })
- return true
- }
- return false
- },
- async logout() {
- // Clear all MMKV storages
- clearAllStorages()
-
- // Clear push token if exists
- const pushToken = notificationsStore.getState().pushToken
- if (pushToken) {
- notificationsStore.getState().deletePushToken(pushToken)
- }
- notificationsStore.getState().reset()
-
- const appAuthConfig = getAppAuthConfig()
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const tokenToRevoke = get().authorizeResult!.accessToken!
- try {
- await revoke(appAuthConfig, {
- tokenToRevoke,
- includeBasicAuth: true,
- sendClientId: true,
- })
- } catch (e) {
- // NOOP
- }
+ if (authorizeResult) {
+ await Keychain.setGenericPassword(
+ KEYCHAIN_AUTH_KEY,
+ JSON.stringify(authorizeResult),
+ { service: KEYCHAIN_AUTH_KEY },
+ )
+ set({ authorizeResult })
+ return true
+ }
+ return false
+ },
+ async logout() {
+ // Clear all MMKV storages
+ clearAllStorages()
- const client = await getApolloClientAsync()
- await client.cache.reset()
- await Keychain.resetGenericPassword({ service: KEYCHAIN_AUTH_KEY })
- set(
- (state) => ({
- ...state,
- authorizeResult: undefined,
- userInfo: undefined,
- }),
- true,
- )
- return true
- },
-}))
+ // Clear push token if exists
+ const pushToken = notificationsStore.getState().pushToken
+ if (pushToken) {
+ notificationsStore.getState().deletePushToken(pushToken)
+ }
+ notificationsStore.getState().reset()
+
+ const appAuthConfig = getAppAuthConfig()
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const tokenToRevoke = get().authorizeResult!.accessToken!
+ try {
+ await revoke(appAuthConfig, {
+ tokenToRevoke,
+ includeBasicAuth: true,
+ sendClientId: true,
+ })
+ } catch (e) {
+ // NOOP
+ }
+
+ const client = await getApolloClientAsync()
+ await client.cache.reset()
+ await Keychain.resetGenericPassword({ service: KEYCHAIN_AUTH_KEY })
+ set(
+ (state) => ({
+ ...state,
+ authorizeResult: undefined,
+ userInfo: undefined,
+ }),
+ true,
+ )
+ return true
+ },
+ }),
+ {
+ name: 'auth_01',
+ getStorage: () => AsyncStorage,
+ whitelist: ['userInfo'],
+ },
+ ),
+)
export const useAuthStore = createUse(authStore)
-export async function readAuthorizeResult(): Promise {
+export async function readAuthorizeResult(): Promise {
const { authorizeResult } = authStore.getState()
if (authorizeResult) {
- return authorizeResult as AuthorizeResult
+ return
}
try {
const res = await Keychain.getGenericPassword({
service: KEYCHAIN_AUTH_KEY,
})
+
if (res) {
const authRes = JSON.parse(res.password)
authStore.setState({ authorizeResult: authRes })
- return authRes
}
} catch (err) {
console.log('Unable to read from keystore: ', err)
}
-
- return null
}
export async function checkIsAuthenticated() {
@@ -225,23 +243,37 @@ export async function checkIsAuthenticated() {
intl.formatMessage({ id: 'login.expiredTitle' }),
intl.formatMessage({ id: 'login.expiredScopesMessage' }),
)
+
await logout()
+
return false
}
}
- fetchUserInfo().catch(async (err) => {
+ if (!offlineStore.getState().isConnected) {
+ return true
+ }
+
+ try {
+ await fetchUserInfo()
+
+ return true
+ } catch (e) {
+ const err = e as Error & { code?: string }
+ const shouldLogout =
+ err.code === INVALID_REFRESH_TOKEN_ERROR ||
+ err.message === UNAUTHORIZED_USER_INFO
+
+ if (!shouldLogout) {
+ return true
+ }
+
Alert.alert(
intl.formatMessage({ id: 'login.expiredTitle' }),
intl.formatMessage({ id: 'login.expiredMissingUserMessage' }),
)
await logout()
- await Navigation.dismissAllModals()
- await Navigation.dismissAllOverlays()
- await Navigation.setRoot({
- root: await getAppRoot(),
- })
- })
- return true
+ return false
+ }
}
diff --git a/apps/native/app/src/stores/environment-store.ts b/apps/native/app/src/stores/environment-store.ts
index 6e73355e38e4..36ef645bca08 100644
--- a/apps/native/app/src/stores/environment-store.ts
+++ b/apps/native/app/src/stores/environment-store.ts
@@ -48,7 +48,7 @@ export interface EnvironmentStore extends State {
export const environmentStore = create(
persist(
(set, get) => ({
- environment: environments.prod,
+ environment: config.isTestingApp ? environments.dev : environments.prod,
result: [],
fetchedAt: 0,
cognito: null,
@@ -113,9 +113,11 @@ export const environmentStore = create(
const { state, version } = JSON.parse(str)
delete state.actions
delete state.loading
+
if (!config.isTestingApp) {
state.environment = environments.prod
}
+
return { state, version }
},
},
diff --git a/apps/native/app/src/ui/lib/detail/header.tsx b/apps/native/app/src/ui/lib/detail/header.tsx
index c2062d815e90..3265d46c3030 100644
--- a/apps/native/app/src/ui/lib/detail/header.tsx
+++ b/apps/native/app/src/ui/lib/detail/header.tsx
@@ -93,7 +93,7 @@ export function Header({
)}
- {date}
+ {date && {date} }
>
)}
diff --git a/apps/native/app/src/ui/lib/empty-state/empty-list.tsx b/apps/native/app/src/ui/lib/empty-state/empty-list.tsx
index 90f950d57ce4..f55706c3abf9 100644
--- a/apps/native/app/src/ui/lib/empty-state/empty-list.tsx
+++ b/apps/native/app/src/ui/lib/empty-state/empty-list.tsx
@@ -2,8 +2,8 @@ import React from 'react'
import { View } from 'react-native'
import styled from 'styled-components/native'
-import { font } from '../../utils/font'
import { dynamicColor } from '@ui/utils'
+import { Typography } from '../typography/typography'
const Host = styled.View`
display: flex;
@@ -12,6 +12,7 @@ const Host = styled.View`
justify-content: center;
align-items: center;
padding: 0 53px;
+ margin-top: ${({ theme }) => theme.spacing[3]}px;
`
const HostWithBorder = styled.View`
@@ -36,21 +37,12 @@ const ImageWrap = styled.View`
margin-bottom: 50px;
`
-const Title = styled.Text`
- margin-bottom: ${({ theme }) => theme.spacing[2]}px;
-
- ${font({
- fontWeight: '600',
- })}
-
+const Title = styled(Typography)`
+ margin-bottom: ${({ theme }) => theme.spacing[1]}px;
text-align: center;
`
-const Description = styled.Text`
- ${font({
- fontWeight: '300',
- lineHeight: 24,
- })}
+const Description = styled(Typography)`
text-align: center;
`
@@ -74,7 +66,7 @@ export function EmptyList({ title, description, image, small }: HeadingProps) {
return (
{image}
- {title}
+ {title}
{description}
)
diff --git a/apps/native/app/src/ui/lib/list/list-item.tsx b/apps/native/app/src/ui/lib/list/list-item.tsx
index 9efc8168365b..8f77cd93bc13 100644
--- a/apps/native/app/src/ui/lib/list/list-item.tsx
+++ b/apps/native/app/src/ui/lib/list/list-item.tsx
@@ -79,7 +79,7 @@ interface ListItemAction {
interface ListItemProps {
title: string
- date: Date | string
+ date?: Date | string
subtitle: string
unread?: boolean
actions?: ListItemAction[]
@@ -122,9 +122,11 @@ export function ListItem({
{title}
-
-
-
+ {date && (
+
+
+
+ )}
Date.now()
) {
- hideAppLockOverlay()
+ hideAppLockOverlay(lockScreenComponentId)
} else {
Navigation.updateProps(lockScreenComponentId, { status })
}
diff --git a/apps/service-portal/src/components/Notifications/NotificationLine.tsx b/apps/service-portal/src/components/Notifications/NotificationLine.tsx
index 2e8a14b5aa80..add475d4efff 100644
--- a/apps/service-portal/src/components/Notifications/NotificationLine.tsx
+++ b/apps/service-portal/src/components/Notifications/NotificationLine.tsx
@@ -51,6 +51,8 @@ export const NotificationLine = ({ data, onClickCallback }: Props) => {
) : undefined}
span`, {
boxShadow: 'none',
})
diff --git a/apps/services/auth/ids-api/infra/ids-api.ts b/apps/services/auth/ids-api/infra/ids-api.ts
index e61297eef4f7..f865a7dfc0b4 100644
--- a/apps/services/auth/ids-api/infra/ids-api.ts
+++ b/apps/services/auth/ids-api/infra/ids-api.ts
@@ -11,6 +11,18 @@ import { UserProfileScope } from '../../../../../libs/auth/scopes/src/lib/userPr
const namespace = 'identity-server'
const imageName = 'services-auth-ids-api'
+const REDIS_NODE_CONFIG = {
+ dev: json([
+ 'clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379',
+ ]),
+ staging: json([
+ 'clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379',
+ ]),
+ prod: json([
+ 'clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379',
+ ]),
+}
+
export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => {
return service('services-auth-ids-api')
.namespace(namespace)
@@ -39,39 +51,9 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => {
staging: 'IS-TEST/GOV/6503760649/SKRA-Protected/Einstaklingar-v1',
prod: 'IS/GOV/6503760649/SKRA-Protected/Einstaklingar-v1',
},
- XROAD_NATIONAL_REGISTRY_REDIS_NODES: {
- dev: json([
- 'clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379',
- ]),
- staging: json([
- 'clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379',
- ]),
- prod: json([
- 'clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379',
- ]),
- },
- COMPANY_REGISTRY_REDIS_NODES: {
- dev: json([
- 'clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379',
- ]),
- staging: json([
- 'clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379',
- ]),
- prod: json([
- 'clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379',
- ]),
- },
- XROAD_RSK_PROCURING_REDIS_NODES: {
- dev: json([
- 'clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379',
- ]),
- staging: json([
- 'clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379',
- ]),
- prod: json([
- 'clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379',
- ]),
- },
+ XROAD_NATIONAL_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG,
+ COMPANY_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG,
+ XROAD_RSK_PROCURING_REDIS_NODES: REDIS_NODE_CONFIG,
COMPANY_REGISTRY_XROAD_PROVIDER_ID: {
dev: 'IS-DEV/GOV/10006/Skatturinn/ft-v1',
staging: 'IS-TEST/GOV/5402696029/Skatturinn/ft-v1',
@@ -88,6 +70,10 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => {
staging: 'false',
prod: 'false',
},
+ PASSKEY_CORE_RP_ID: 'island.is',
+ PASSKEY_CORE_RP_NAME: 'Island.is',
+ PASSKEY_CORE_CHALLENGE_TTL_MS: '120000',
+ REDIS_NODES: REDIS_NODE_CONFIG,
})
.secrets({
IDENTITY_SERVER_CLIENT_SECRET:
@@ -95,6 +81,9 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => {
NOVA_URL: '/k8s/services-auth/NOVA_URL',
NOVA_USERNAME: '/k8s/services-auth/NOVA_USERNAME',
NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD',
+ PASSKEY_CORE_ALLOWED_ORIGINS:
+ '/k8s/services-auth/PASSKEY_CORE_ALLOWED_ORIGINS',
+ PASSKEY_CORE_MAX_AGE_DAYS: '/k8s/services-auth/PASSKEY_CORE_MAX_AGE_DAYS',
NATIONAL_REGISTRY_B2C_CLIENT_SECRET:
'/k8s/services-auth/NATIONAL_REGISTRY_B2C_CLIENT_SECRET',
})
diff --git a/apps/services/auth/ids-api/src/app/app.module.ts b/apps/services/auth/ids-api/src/app/app.module.ts
index 4ded032e8d59..f98deb7af1a1 100644
--- a/apps/services/auth/ids-api/src/app/app.module.ts
+++ b/apps/services/auth/ids-api/src/app/app.module.ts
@@ -3,6 +3,7 @@ import { SequelizeModule } from '@nestjs/sequelize'
import {
DelegationConfig,
+ PasskeysCoreConfig,
SequelizeConfigService,
} from '@island.is/auth-api-lib'
import { AuthModule } from '@island.is/auth-nest-tools'
@@ -31,6 +32,7 @@ import { ResourcesModule } from './resources/resources.module'
import { TranslationModule } from './translation/translation.module'
import { UserProfileModule } from './user-profile/user-profile.module'
import { UsersModule } from './users/users.module'
+import { PasskeysModule } from './passkeys/passkeys.module'
@Module({
imports: [
@@ -50,6 +52,7 @@ import { UsersModule } from './users/users.module'
UserProfileModule,
NotificationsModule,
LoginRestrictionsModule,
+ PasskeysModule,
ConfigModule.forRoot({
isGlobal: true,
load: [
@@ -61,6 +64,7 @@ import { UsersModule } from './users/users.module'
RskRelationshipsClientConfig,
UserProfileClientConfig,
XRoadConfig,
+ PasskeysCoreConfig,
NationalRegistryV3ClientConfig,
],
}),
diff --git a/apps/services/auth/ids-api/src/app/passkeys/dto/authenticationOptions.dto.ts b/apps/services/auth/ids-api/src/app/passkeys/dto/authenticationOptions.dto.ts
new file mode 100644
index 000000000000..b79ccb2d1c3b
--- /dev/null
+++ b/apps/services/auth/ids-api/src/app/passkeys/dto/authenticationOptions.dto.ts
@@ -0,0 +1,23 @@
+import { ApiProperty } from '@nestjs/swagger'
+
+import { IsBoolean, IsOptional, IsString } from 'class-validator'
+
+export class AuthenticationOptions {
+ @IsString()
+ @ApiProperty()
+ passkey: string
+}
+
+export class AuthenticationResult {
+ @IsBoolean()
+ @ApiProperty()
+ verified!: boolean
+
+ @IsString()
+ @ApiProperty()
+ idp!: string
+
+ @IsString()
+ @ApiProperty()
+ sub!: string
+}
diff --git a/apps/services/auth/ids-api/src/app/passkeys/passkeys.controller.ts b/apps/services/auth/ids-api/src/app/passkeys/passkeys.controller.ts
new file mode 100644
index 000000000000..886b29d3c30f
--- /dev/null
+++ b/apps/services/auth/ids-api/src/app/passkeys/passkeys.controller.ts
@@ -0,0 +1,55 @@
+import {
+ BadRequestException,
+ Body,
+ Controller,
+ Post,
+ UseGuards,
+ VERSION_NEUTRAL,
+} from '@nestjs/common'
+import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'
+
+import { PasskeysCoreService } from '@island.is/auth-api-lib'
+import { Documentation } from '@island.is/nest/swagger'
+import {
+ AuthenticationOptions,
+ AuthenticationResult,
+} from './dto/authenticationOptions.dto'
+import { IdsAuthGuard, ScopesGuard } from '@island.is/auth-nest-tools'
+import { Audit } from '@island.is/nest/audit'
+import {
+ FeatureFlag,
+ FeatureFlagGuard,
+ Features,
+} from '@island.is/nest/feature-flags'
+
+const namespace = '@island.is/auth/ids-api/passkeys'
+
+@ApiTags('passkeys')
+@Controller({
+ path: 'passkeys',
+ version: ['1', VERSION_NEUTRAL],
+})
+@UseGuards(IdsAuthGuard, ScopesGuard, FeatureFlagGuard)
+@Audit({ namespace })
+export class PasskeysController {
+ constructor(private readonly passkeysCoreService: PasskeysCoreService) {}
+
+ @Post('authenticate')
+ @Documentation({
+ summary:
+ 'Validates passkey authentication based on input from authenticated user.',
+ description: 'Verifies authenticated user passkey authentication response.',
+ response: { status: 200, type: AuthenticationResult },
+ })
+ @ApiCreatedResponse({ type: AuthenticationResult })
+ @Audit({
+ resources: (authenticationResult) =>
+ authenticationResult.verified.toString(),
+ })
+ @FeatureFlag(Features.isPasskeyAuthEnabled)
+ async verifyAuthentication(
+ @Body() body: AuthenticationOptions,
+ ): Promise {
+ return this.passkeysCoreService.verifyAuthenticationString(body.passkey)
+ }
+}
diff --git a/apps/services/auth/ids-api/src/app/passkeys/passkeys.module.ts b/apps/services/auth/ids-api/src/app/passkeys/passkeys.module.ts
new file mode 100644
index 000000000000..27dd56624b47
--- /dev/null
+++ b/apps/services/auth/ids-api/src/app/passkeys/passkeys.module.ts
@@ -0,0 +1,16 @@
+import { Module } from '@nestjs/common'
+import { PasskeysController } from './passkeys.controller'
+import { PasskeysCoreConfig, PasskeysCoreModule } from '@island.is/auth-api-lib'
+
+import { FeatureFlagModule } from '@island.is/nest/feature-flags'
+
+@Module({
+ imports: [
+ PasskeysCoreConfig.registerOptional(),
+ PasskeysCoreModule,
+ FeatureFlagModule,
+ ],
+ controllers: [PasskeysController],
+ providers: [],
+})
+export class PasskeysModule {}
diff --git a/apps/services/auth/public-api/infra/auth-public-api.ts b/apps/services/auth/public-api/infra/auth-public-api.ts
index 7f073834968f..aa9377e08af8 100644
--- a/apps/services/auth/public-api/infra/auth-public-api.ts
+++ b/apps/services/auth/public-api/infra/auth-public-api.ts
@@ -2,6 +2,18 @@ import { service, ServiceBuilder } from '../../../../../infra/src/dsl/dsl'
import { json } from '../../../../../infra/src/dsl/dsl'
import { Base, Client, RskProcuring } from '../../../../../infra/src/dsl/xroad'
+const REDIS_NODE_CONFIG = {
+ dev: json([
+ 'clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379',
+ ]),
+ staging: json([
+ 'clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379',
+ ]),
+ prod: json([
+ 'clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379',
+ ]),
+}
+
export const serviceSetup = (): ServiceBuilder<'services-auth-public-api'> => {
return service('services-auth-public-api')
.namespace('identity-server-admin')
@@ -27,39 +39,25 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-public-api'> => {
staging: 'IS-TEST/GOV/6503760649/SKRA-Protected/Einstaklingar-v1',
prod: 'IS/GOV/6503760649/SKRA-Protected/Einstaklingar-v1',
},
- XROAD_NATIONAL_REGISTRY_REDIS_NODES: {
- dev: json([
- 'clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379',
- ]),
- staging: json([
- 'clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379',
- ]),
- prod: json([
- 'clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379',
- ]),
- },
- XROAD_RSK_PROCURING_REDIS_NODES: {
- dev: json([
- 'clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379',
- ]),
- staging: json([
- 'clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379',
- ]),
- prod: json([
- 'clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379',
- ]),
- },
+ XROAD_NATIONAL_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG,
+ XROAD_RSK_PROCURING_REDIS_NODES: REDIS_NODE_CONFIG,
XROAD_TJODSKRA_MEMBER_CODE: {
prod: '6503760649',
dev: '10001',
staging: '6503760649',
},
+ PASSKEY_CORE_RP_ID: 'island.is',
+ PASSKEY_CORE_RP_NAME: 'Island.is',
+ PASSKEY_CORE_CHALLENGE_TTL_MS: '120000',
+ REDIS_NODES: REDIS_NODE_CONFIG,
})
.secrets({
IDENTITY_SERVER_CLIENT_SECRET:
'/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET',
NATIONAL_REGISTRY_IDS_CLIENT_SECRET:
'/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET',
+ PASSKEY_CORE_ALLOWED_ORIGINS:
+ '/k8s/services-auth/PASSKEY_CORE_ALLOWED_ORIGINS',
})
.xroad(Base, Client, RskProcuring)
.ingress({
diff --git a/apps/services/auth/public-api/src/app/app.module.ts b/apps/services/auth/public-api/src/app/app.module.ts
index e7f9e900e806..1f539e8f75b6 100644
--- a/apps/services/auth/public-api/src/app/app.module.ts
+++ b/apps/services/auth/public-api/src/app/app.module.ts
@@ -4,6 +4,7 @@ import { SequelizeModule } from '@nestjs/sequelize'
import {
SequelizeConfigService,
DelegationConfig,
+ PasskeysCoreConfig,
} from '@island.is/auth-api-lib'
import { AuthModule } from '@island.is/auth-nest-tools'
import { AuditModule } from '@island.is/nest/audit'
@@ -19,6 +20,7 @@ import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationshi
import { environment } from '../environments'
import { DelegationsModule } from './modules/delegations/delegations.module'
+import { PasskeysModule } from './modules/passkeys/passkeys.module'
@Module({
imports: [
@@ -29,6 +31,7 @@ import { DelegationsModule } from './modules/delegations/delegations.module'
}),
ProblemModule,
DelegationsModule,
+ PasskeysModule,
ConfigModule.forRoot({
isGlobal: true,
load: [
@@ -38,6 +41,7 @@ import { DelegationsModule } from './modules/delegations/delegations.module'
NationalRegistryClientConfig,
RskRelationshipsClientConfig,
XRoadConfig,
+ PasskeysCoreConfig,
],
}),
],
diff --git a/apps/services/auth/public-api/src/app/modules/passkeys/dto/authenticationOptions.dto.ts b/apps/services/auth/public-api/src/app/modules/passkeys/dto/authenticationOptions.dto.ts
new file mode 100644
index 000000000000..afb7e893b1d0
--- /dev/null
+++ b/apps/services/auth/public-api/src/app/modules/passkeys/dto/authenticationOptions.dto.ts
@@ -0,0 +1,94 @@
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
+import { Type } from 'class-transformer'
+import {
+ IsArray,
+ IsBoolean,
+ IsNumber,
+ IsObject,
+ IsOptional,
+ IsString,
+} from 'class-validator'
+
+export class AuthenticationOptionsPublicKeyCredentialDescriptor {
+ @IsString()
+ @ApiProperty()
+ id!: string
+
+ @IsString()
+ @ApiProperty()
+ type!: string
+
+ @IsArray()
+ @Type(() => String)
+ @ApiProperty({ type: [String] })
+ transports!: string[]
+}
+
+export class AuthenticationOptionsExtensions {
+ @IsOptional()
+ @IsString()
+ @ApiPropertyOptional()
+ appid?: string
+
+ @IsOptional()
+ @IsBoolean()
+ @ApiPropertyOptional()
+ credProps?: boolean
+
+ @IsOptional()
+ @IsBoolean()
+ @ApiProperty()
+ hmacCreateSecret?: boolean
+}
+
+export class AuthenticationOptions {
+ @IsString()
+ @ApiProperty()
+ challenge!: string
+
+ @IsOptional()
+ @IsNumber()
+ @ApiPropertyOptional()
+ timeout?: number
+
+ @IsOptional()
+ @IsString()
+ @ApiPropertyOptional()
+ rpId?: string
+
+ @IsOptional()
+ @IsArray()
+ @Type(() => AuthenticationOptionsPublicKeyCredentialDescriptor)
+ @ApiPropertyOptional({
+ type: [AuthenticationOptionsPublicKeyCredentialDescriptor],
+ })
+ allowCredentials?: AuthenticationOptionsPublicKeyCredentialDescriptor[]
+
+ @IsOptional()
+ @IsString()
+ @ApiPropertyOptional()
+ userVerification?: string
+
+ @IsOptional()
+ @IsObject()
+ @ApiPropertyOptional({
+ type: AuthenticationOptionsExtensions,
+ })
+ extensions?: AuthenticationOptionsExtensions
+}
+
+// TODO remove before merging into main
+// should only be possible to verify authentication through auth-ids-api
+export class AuthenticationResult {
+ @IsBoolean()
+ @ApiProperty()
+ verified!: boolean
+
+ @IsString()
+ @ApiProperty()
+ idp!: string
+
+ @IsString()
+ @ApiProperty()
+ sub!: string
+}
diff --git a/apps/services/auth/public-api/src/app/modules/passkeys/dto/authenticationResponse.dto.ts b/apps/services/auth/public-api/src/app/modules/passkeys/dto/authenticationResponse.dto.ts
new file mode 100644
index 000000000000..9c920ba23735
--- /dev/null
+++ b/apps/services/auth/public-api/src/app/modules/passkeys/dto/authenticationResponse.dto.ts
@@ -0,0 +1,11 @@
+import { ApiProperty } from '@nestjs/swagger'
+
+import { IsString } from 'class-validator'
+
+// TODO remove before merging into main
+// should only be possible to verify authentication through auth-ids-api
+export class AuthenticationResponse {
+ @IsString()
+ @ApiProperty()
+ passkey!: string
+}
diff --git a/apps/services/auth/public-api/src/app/modules/passkeys/dto/handleChallenge.dto.ts b/apps/services/auth/public-api/src/app/modules/passkeys/dto/handleChallenge.dto.ts
new file mode 100644
index 000000000000..f2cb4ef18a57
--- /dev/null
+++ b/apps/services/auth/public-api/src/app/modules/passkeys/dto/handleChallenge.dto.ts
@@ -0,0 +1,13 @@
+import { ApiProperty } from '@nestjs/swagger'
+import { IsEnum } from 'class-validator'
+
+export enum PasskeyAction {
+ CREATE = 'webauthn.create',
+ GET = 'webauthn.get',
+ APPATTEST_CREATE = 'appattest.create',
+}
+
+export class HandleChallengeDTO {
+ @IsEnum(PasskeyAction)
+ type!: PasskeyAction
+}
diff --git a/apps/services/auth/public-api/src/app/modules/passkeys/dto/registrationOptions.dto.ts b/apps/services/auth/public-api/src/app/modules/passkeys/dto/registrationOptions.dto.ts
new file mode 100644
index 000000000000..a40bec4f0899
--- /dev/null
+++ b/apps/services/auth/public-api/src/app/modules/passkeys/dto/registrationOptions.dto.ts
@@ -0,0 +1,165 @@
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
+
+import { Type } from 'class-transformer'
+import {
+ IsArray,
+ IsBoolean,
+ IsNumber,
+ IsObject,
+ IsOptional,
+ IsString,
+} from 'class-validator'
+
+class RegistrationOptionsRp {
+ @IsString()
+ @IsOptional()
+ @ApiPropertyOptional()
+ id?: string
+
+ @IsString()
+ @ApiProperty()
+ name!: string
+}
+
+class RegistrationOptionsUser {
+ @IsString()
+ @ApiProperty()
+ id!: string
+
+ @IsString()
+ @ApiProperty()
+ name!: string
+
+ @IsString()
+ @ApiProperty()
+ displayName!: string
+}
+
+class RegistrationOptionsPublicKeyCredentialOption {
+ @IsNumber()
+ @ApiProperty()
+ alg!: number
+
+ @IsString()
+ @ApiProperty()
+ type!: string
+}
+
+class RegistrationOptionsAuthenticatorSelection {
+ @IsString()
+ @IsOptional()
+ @ApiPropertyOptional()
+ authenticatorAttachment?: string
+
+ @IsBoolean()
+ @IsOptional()
+ @ApiPropertyOptional()
+ requireResidentKey?: boolean
+
+ @IsString()
+ @ApiProperty()
+ @ApiPropertyOptional()
+ residentKey?: string
+
+ @IsString()
+ @IsOptional()
+ @ApiPropertyOptional()
+ userVerification?: string
+}
+
+class RegistrationOptionsPublicKeyCredentialDescriptorJSON {
+ @IsString()
+ @ApiProperty()
+ id!: string
+
+ @IsString()
+ @ApiProperty()
+ type!: string
+
+ @IsArray()
+ @Type(() => String)
+ @IsOptional()
+ @ApiPropertyOptional({
+ type: [String],
+ })
+ transports!: string[]
+}
+
+class RegistrationOptionsExtensions {
+ @IsString()
+ @IsOptional()
+ @ApiPropertyOptional()
+ appid?: string
+
+ @IsBoolean()
+ @IsOptional()
+ @ApiPropertyOptional()
+ credProps?: boolean
+
+ @IsBoolean()
+ @IsOptional()
+ @ApiPropertyOptional()
+ hmacCreateSecret?: boolean
+}
+
+export class RegistrationOptions {
+ @IsString()
+ @ApiProperty()
+ challenge!: string
+
+ @IsObject()
+ @ApiProperty({
+ type: RegistrationOptionsRp,
+ })
+ rp!: RegistrationOptionsRp
+
+ @IsObject()
+ @ApiProperty({
+ type: RegistrationOptionsUser,
+ })
+ user!: RegistrationOptionsUser
+
+ @IsArray()
+ @Type(() => RegistrationOptionsPublicKeyCredentialOption)
+ @ApiProperty({
+ type: [RegistrationOptionsPublicKeyCredentialOption],
+ })
+ pubKeyCredParams!: RegistrationOptionsPublicKeyCredentialOption[]
+
+ @IsNumber()
+ @IsOptional()
+ @ApiPropertyOptional()
+ timeout?: number
+
+ @IsString()
+ @IsOptional()
+ @ApiPropertyOptional()
+ attestation?: string
+
+ @IsArray()
+ @Type(() => RegistrationOptionsPublicKeyCredentialDescriptorJSON)
+ @ApiPropertyOptional({
+ type: [RegistrationOptionsPublicKeyCredentialDescriptorJSON],
+ })
+ excludeCredentials?: RegistrationOptionsPublicKeyCredentialDescriptorJSON[]
+
+ @IsObject()
+ @IsOptional()
+ @ApiPropertyOptional({
+ type: RegistrationOptionsAuthenticatorSelection,
+ })
+ authenticatorSelection?: RegistrationOptionsAuthenticatorSelection
+
+ @IsObject()
+ @IsOptional()
+ @ApiPropertyOptional({
+ type: RegistrationOptionsExtensions,
+ })
+ extensions?: RegistrationOptionsExtensions
+}
+
+export class RegistrationResult {
+ @IsBoolean()
+ @ApiProperty()
+ verified!: boolean
+}
diff --git a/apps/services/auth/public-api/src/app/modules/passkeys/dto/registrationResponse.dto.ts b/apps/services/auth/public-api/src/app/modules/passkeys/dto/registrationResponse.dto.ts
new file mode 100644
index 000000000000..467b56583912
--- /dev/null
+++ b/apps/services/auth/public-api/src/app/modules/passkeys/dto/registrationResponse.dto.ts
@@ -0,0 +1,99 @@
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
+
+import type {
+ AuthenticatorAttachment,
+ PublicKeyCredentialType,
+ AuthenticatorTransportFuture,
+ COSEAlgorithmIdentifier,
+} from '@simplewebauthn/types'
+
+import { IsBoolean, IsObject, IsOptional, IsString } from 'class-validator'
+
+class ClientExtensionResultsCredProps {
+ @IsBoolean()
+ @IsOptional()
+ @ApiPropertyOptional()
+ rk?: boolean
+}
+
+class RegistrationResponseClientExtensionResults {
+ @IsBoolean()
+ @IsOptional()
+ @ApiPropertyOptional()
+ appid?: boolean
+
+ @IsObject()
+ @IsOptional()
+ @ApiPropertyOptional({
+ type: ClientExtensionResultsCredProps,
+ })
+ credProps?: ClientExtensionResultsCredProps
+
+ @IsBoolean()
+ @IsOptional()
+ @ApiPropertyOptional()
+ hmacCreateSecret?: boolean
+}
+
+class RegistrationResponseResponse {
+ @IsString()
+ @ApiProperty()
+ attestationObject!: string
+
+ @IsString()
+ @ApiProperty()
+ clientDataJSON!: string
+
+ @IsString({ each: true })
+ @IsOptional()
+ @ApiPropertyOptional({
+ type: [String],
+ })
+ transports?: AuthenticatorTransportFuture[]
+
+ @IsString()
+ @IsOptional()
+ @ApiPropertyOptional()
+ publicKeyAlgorithm?: COSEAlgorithmIdentifier
+
+ @IsString()
+ @IsOptional()
+ @ApiPropertyOptional()
+ publicKey?: string
+
+ @IsString()
+ @IsOptional()
+ @ApiPropertyOptional()
+ authenticatorData?: string
+}
+
+export class RegistrationResponse {
+ @IsString()
+ @ApiProperty()
+ id!: string
+
+ @IsString()
+ @ApiProperty()
+ rawId!: string
+
+ @IsObject()
+ @ApiProperty({
+ type: RegistrationResponseResponse,
+ })
+ response!: RegistrationResponseResponse
+
+ @IsString()
+ @IsOptional()
+ @ApiPropertyOptional()
+ authenticatorAttachment?: AuthenticatorAttachment
+
+ @IsObject()
+ @ApiProperty({
+ type: RegistrationResponseClientExtensionResults,
+ })
+ clientExtensionResults!: RegistrationResponseClientExtensionResults
+
+ @IsString()
+ @ApiProperty()
+ type!: PublicKeyCredentialType
+}
diff --git a/apps/services/auth/public-api/src/app/modules/passkeys/dto/validateRegistration.dto.ts b/apps/services/auth/public-api/src/app/modules/passkeys/dto/validateRegistration.dto.ts
new file mode 100644
index 000000000000..d38db31d6655
--- /dev/null
+++ b/apps/services/auth/public-api/src/app/modules/passkeys/dto/validateRegistration.dto.ts
@@ -0,0 +1,14 @@
+import { IsOptional, IsString } from 'class-validator'
+
+export class RegistrationOptions {
+ @IsString()
+ data: string
+
+ @IsOptional()
+ @IsString()
+ appAttest?: string
+
+ @IsOptional()
+ @IsString()
+ playIntegrity?: string
+}
diff --git a/apps/services/auth/public-api/src/app/modules/passkeys/passkeys.controller.ts b/apps/services/auth/public-api/src/app/modules/passkeys/passkeys.controller.ts
new file mode 100644
index 000000000000..74934099e133
--- /dev/null
+++ b/apps/services/auth/public-api/src/app/modules/passkeys/passkeys.controller.ts
@@ -0,0 +1,138 @@
+import {
+ CurrentActor,
+ IdsUserGuard,
+ ScopesGuard,
+} from '@island.is/auth-nest-tools'
+import {
+ Body,
+ Controller,
+ Delete,
+ Get,
+ Post,
+ UseGuards,
+ VERSION_NEUTRAL,
+} from '@nestjs/common'
+import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'
+import type { User } from '@island.is/auth-nest-tools'
+
+import { PasskeysCoreService } from '@island.is/auth-api-lib'
+import { Audit } from '@island.is/nest/audit'
+import { Documentation } from '@island.is/nest/swagger'
+import {
+ FeatureFlag,
+ FeatureFlagGuard,
+ Features,
+} from '@island.is/nest/feature-flags'
+
+import {
+ RegistrationOptions,
+ RegistrationResult,
+} from './dto/registrationOptions.dto'
+
+import { RegistrationResponse } from './dto/registrationResponse.dto'
+import {
+ AuthenticationOptions,
+ AuthenticationResult,
+} from './dto/authenticationOptions.dto'
+import { AuthenticationResponse } from './dto/authenticationResponse.dto'
+
+const namespace = '@island.is/auth/public-api/passkeys'
+
+@ApiTags('passkeys')
+@UseGuards(IdsUserGuard, ScopesGuard, FeatureFlagGuard)
+@Controller({
+ path: 'passkeys',
+ version: ['1', VERSION_NEUTRAL],
+})
+@Audit({ namespace })
+export class PasskeysController {
+ constructor(private readonly passkeysCoreService: PasskeysCoreService) {}
+
+ @Delete('')
+ @Documentation({
+ summary: 'Deletes passkey for the authenticated user.',
+ description: 'Deletes passkey for the authenticated user.',
+ response: { status: 204 },
+ })
+ @Audit()
+ async deletePasskey(@CurrentActor() actor: User): Promise {
+ await this.passkeysCoreService.deletePasskeyByUser(actor)
+ }
+
+ @Get('register')
+ @Documentation({
+ summary: 'Gets passkey registration options for the authenicated user.',
+ description: 'Passkey registration options for the authenticated user.',
+ response: { status: 200, type: RegistrationOptions },
+ })
+ @Audit()
+ @ApiCreatedResponse({ type: RegistrationOptions })
+ @FeatureFlag(Features.isPasskeyRegistrationEnabled)
+ async getPasskeyRegistrationOptions(
+ @CurrentActor() actor: User,
+ ): Promise {
+ const response = await this.passkeysCoreService.generateRegistrationOptions(
+ actor,
+ )
+
+ return response as RegistrationOptions
+ }
+
+ @Post('register')
+ @Documentation({
+ summary: 'Validates registration based on input from authenicated user.',
+ description: 'Verifies authenticated user passkey registration response.',
+ response: { status: 200, type: RegistrationResult },
+ })
+ @ApiCreatedResponse({ type: RegistrationResult })
+ @Audit({
+ resources: (result) => result.verified.toString(),
+ })
+ @FeatureFlag(Features.isPasskeyRegistrationEnabled)
+ async verifyRegistration(
+ @CurrentActor() actor: User,
+ @Body() body: RegistrationResponse,
+ ): Promise {
+ const response = await this.passkeysCoreService.verifyRegistration(
+ actor,
+ body,
+ )
+
+ return response
+ }
+
+ @Get('authenticate')
+ @Documentation({
+ summary: 'Gets passkey authentication options for the authenticated user.',
+ description: 'Passkey authenticate options for the authenticated user.',
+ response: { status: 200, type: AuthenticationOptions },
+ })
+ @ApiCreatedResponse({ type: AuthenticationOptions })
+ @Audit()
+ @FeatureFlag(Features.isPasskeyAuthEnabled)
+ async getPasskeyAuthenticationOptions(
+ @CurrentActor() actor: User,
+ ): Promise {
+ const response =
+ await this.passkeysCoreService.generateAuthenticationOptions(actor)
+
+ return response as AuthenticationOptions
+ }
+
+ // TODO remove before merging into main
+ // should only be possible to verify authentication through auth-ids-api
+ @Post('authenticate')
+ @Documentation({
+ summary:
+ 'Validates passkey authentication based on input from authenticated user.',
+ description: 'Verifies authenticated user passkey authentication response.',
+ response: { status: 200, type: AuthenticationResult },
+ })
+ @ApiCreatedResponse({ type: AuthenticationResult })
+ @FeatureFlag(Features.isPasskeyAuthEnabled)
+ async verifyAuthentication(
+ @Body() body: AuthenticationResponse,
+ ): Promise {
+ return this.passkeysCoreService.verifyAuthenticationString(body.passkey)
+ }
+}
diff --git a/apps/services/auth/public-api/src/app/modules/passkeys/passkeys.module.ts b/apps/services/auth/public-api/src/app/modules/passkeys/passkeys.module.ts
new file mode 100644
index 000000000000..720d0622076f
--- /dev/null
+++ b/apps/services/auth/public-api/src/app/modules/passkeys/passkeys.module.ts
@@ -0,0 +1,12 @@
+import { Module } from '@nestjs/common'
+import { PasskeysController } from './passkeys.controller'
+import { PasskeysCoreConfig, PasskeysCoreModule } from '@island.is/auth-api-lib'
+
+import { FeatureFlagModule } from '@island.is/nest/feature-flags'
+
+@Module({
+ imports: [PasskeysCoreModule, FeatureFlagModule],
+ controllers: [PasskeysController],
+ providers: [],
+})
+export class PasskeysModule {}
diff --git a/apps/web/hooks/useLinkResolver/useLinkResolver.ts b/apps/web/hooks/useLinkResolver/useLinkResolver.ts
index 338bbb0a8bb4..2e8796315fc5 100644
--- a/apps/web/hooks/useLinkResolver/useLinkResolver.ts
+++ b/apps/web/hooks/useLinkResolver/useLinkResolver.ts
@@ -39,14 +39,6 @@ export const routesTemplate = {
is: '/s/[organization]/vidburdir',
en: '/en/o/[organization]/events',
},
- organizationsubpagelistitem: {
- is: '/s/[organization]/[slug]/[listItemSlug]',
- en: '/en/o/[organization]/[slug]/[listItemSlug]',
- },
- projectsubpagelistitem: {
- is: '/v/[project]/[slug]/[listItemSlug]',
- en: '/en/p/[project]/[slug]/[listItemSlug]',
- },
aboutsubpage: {
is: '/s/stafraent-island/[slug]',
en: '',
@@ -195,6 +187,14 @@ export const routesTemplate = {
is: '/v/[slug]',
en: '/en/p/[slug]',
},
+ organizationsubpagelistitem: {
+ is: '/s/[organization]/[slug]/[listItemSlug]',
+ en: '/en/o/[organization]/[slug]/[listItemSlug]',
+ },
+ projectsubpagelistitem: {
+ is: '/v/[project]/[slug]/[listItemSlug]',
+ en: '/en/p/[project]/[slug]/[listItemSlug]',
+ },
lifeevents: {
is: '/lifsvidburdir',
en: '/en/life-events',
diff --git a/apps/web/screens/Home/Home.tsx b/apps/web/screens/Home/Home.tsx
index 3472044bce1d..e555f8effa73 100644
--- a/apps/web/screens/Home/Home.tsx
+++ b/apps/web/screens/Home/Home.tsx
@@ -109,27 +109,6 @@ const Home: Screen = ({ categories, news, page, locale }) => {
items={news}
/>
-
-
- x.date)
- .map(({ date, text, url }) => {
- return {
- text,
- date: new Date(date),
- href: url,
- }
- })}
- />
-
-
{watsonConfig[locale] && (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore make web strict
diff --git a/charts/identity-server/values.dev.yaml b/charts/identity-server/values.dev.yaml
index 9f9bb6685b83..7e66c9694b7f 100644
--- a/charts/identity-server/values.dev.yaml
+++ b/charts/identity-server/values.dev.yaml
@@ -377,7 +377,11 @@ services-auth-ids-api:
NATIONAL_REGISTRY_B2C_SCOPE: 'https://skraidentitydev.onmicrosoft.com/midlun/.default'
NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init'
NOVA_ACCEPT_UNAUTHORIZED: 'true'
+ PASSKEY_CORE_CHALLENGE_TTL_MS: '120000'
+ PASSKEY_CORE_RP_ID: 'island.is'
+ PASSKEY_CORE_RP_NAME: 'Island.is'
PUBLIC_URL: 'https://identity-server.dev01.devland.is/api'
+ REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: ''
USER_PROFILE_CLIENT_SCOPE: '["@island.is/user-profile:read"]'
USER_PROFILE_CLIENT_URL: 'http://web-service-portal-api.service-portal.svc.cluster.local'
@@ -473,6 +477,8 @@ services-auth-ids-api:
NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD'
NOVA_URL: '/k8s/services-auth/NOVA_URL'
NOVA_USERNAME: '/k8s/services-auth/NOVA_USERNAME'
+ PASSKEY_CORE_ALLOWED_ORIGINS: '/k8s/services-auth/PASSKEY_CORE_ALLOWED_ORIGINS'
+ PASSKEY_CORE_MAX_AGE_DAYS: '/k8s/services-auth/PASSKEY_CORE_MAX_AGE_DAYS'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -699,7 +705,11 @@ services-auth-public-api:
IDENTITY_SERVER_ISSUER_URL: 'https://identity-server.dev01.devland.is'
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=345 -r dd-trace/init'
+ PASSKEY_CORE_CHALLENGE_TTL_MS: '120000'
+ PASSKEY_CORE_RP_ID: 'island.is'
+ PASSKEY_CORE_RP_NAME: 'Island.is'
PUBLIC_URL: 'https://identity-server.dev01.devland.is/api'
+ REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: ''
XROAD_BASE_PATH: 'http://securityserver.dev01.devland.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.dev01.devland.is/r1/IS-DEV'
@@ -771,6 +781,7 @@ services-auth-public-api:
DB_PASS: '/k8s/servicesauth/DB_PASSWORD'
IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET'
NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET'
+ PASSKEY_CORE_ALLOWED_ORIGINS: '/k8s/services-auth/PASSKEY_CORE_ALLOWED_ORIGINS'
securityContext:
allowPrivilegeEscalation: false
privileged: false
diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml
index dd073e729778..7909bb08c385 100644
--- a/charts/identity-server/values.prod.yaml
+++ b/charts/identity-server/values.prod.yaml
@@ -374,7 +374,11 @@ services-auth-ids-api:
NATIONAL_REGISTRY_B2C_SCOPE: 'https://skraidentity.onmicrosoft.com/midlun/.default'
NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init'
NOVA_ACCEPT_UNAUTHORIZED: 'false'
+ PASSKEY_CORE_CHALLENGE_TTL_MS: '120000'
+ PASSKEY_CORE_RP_ID: 'island.is'
+ PASSKEY_CORE_RP_NAME: 'Island.is'
PUBLIC_URL: 'https://innskra.island.is/api'
+ REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms'
USER_PROFILE_CLIENT_SCOPE: '["@island.is/user-profile:read"]'
USER_PROFILE_CLIENT_URL: 'https://service-portal-api.internal.island.is'
@@ -470,6 +474,8 @@ services-auth-ids-api:
NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD'
NOVA_URL: '/k8s/services-auth/NOVA_URL'
NOVA_USERNAME: '/k8s/services-auth/NOVA_USERNAME'
+ PASSKEY_CORE_ALLOWED_ORIGINS: '/k8s/services-auth/PASSKEY_CORE_ALLOWED_ORIGINS'
+ PASSKEY_CORE_MAX_AGE_DAYS: '/k8s/services-auth/PASSKEY_CORE_MAX_AGE_DAYS'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -688,7 +694,11 @@ services-auth-public-api:
IDENTITY_SERVER_ISSUER_URL: 'https://innskra.island.is'
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=345 -r dd-trace/init'
+ PASSKEY_CORE_CHALLENGE_TTL_MS: '120000'
+ PASSKEY_CORE_RP_ID: 'island.is'
+ PASSKEY_CORE_RP_NAME: 'Island.is'
PUBLIC_URL: 'https://innskra.island.is/api'
+ REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms'
XROAD_BASE_PATH: 'http://securityserver.island.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.island.is/r1/IS'
@@ -760,6 +770,7 @@ services-auth-public-api:
DB_PASS: '/k8s/servicesauth/DB_PASSWORD'
IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET'
NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET'
+ PASSKEY_CORE_ALLOWED_ORIGINS: '/k8s/services-auth/PASSKEY_CORE_ALLOWED_ORIGINS'
securityContext:
allowPrivilegeEscalation: false
privileged: false
diff --git a/charts/identity-server/values.staging.yaml b/charts/identity-server/values.staging.yaml
index c15725814d96..41650b2612b5 100644
--- a/charts/identity-server/values.staging.yaml
+++ b/charts/identity-server/values.staging.yaml
@@ -371,13 +371,17 @@ services-auth-ids-api:
IDENTITY_SERVER_CLIENT_ID: '@island.is/clients/auth-api'
IDENTITY_SERVER_ISSUER_URL: 'https://identity-server.staging01.devland.is'
LOG_LEVEL: 'info'
- NATIONAL_REGISTRY_B2C_CLIENT_ID: 'a84b70d3-5a75-470f-a137-71d25f2e8794'
- NATIONAL_REGISTRY_B2C_ENDPOINT: 'https://skraidentitystaging.b2clogin.com/skraidentitystaging.onmicrosoft.com/b2c_1_midlun_flow/oauth2/v2.0/token'
- NATIONAL_REGISTRY_B2C_PATH: 'IS-TEST/GOV/6503760649/SKRA-Cloud-Protected/Midlun-v1'
- NATIONAL_REGISTRY_B2C_SCOPE: 'https://skraidentitystaging.onmicrosoft.com/midlun/.default'
+ NATIONAL_REGISTRY_B2C_CLIENT_ID: '6cf94113-d326-4e4d-b97c-1fea12d2f5e1'
+ NATIONAL_REGISTRY_B2C_ENDPOINT: 'https://skraidentitydev.b2clogin.com/skraidentitydev.onmicrosoft.com/b2c_1_midlun_flow/oauth2/v2.0/token'
+ NATIONAL_REGISTRY_B2C_PATH: 'IS-DEV/GOV/10001/SKRA-Cloud-Protected/Midlun-v1'
+ NATIONAL_REGISTRY_B2C_SCOPE: 'https://skraidentitydev.onmicrosoft.com/midlun/.default'
NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init'
NOVA_ACCEPT_UNAUTHORIZED: 'false'
+ PASSKEY_CORE_CHALLENGE_TTL_MS: '120000'
+ PASSKEY_CORE_RP_ID: 'island.is'
+ PASSKEY_CORE_RP_NAME: 'Island.is'
PUBLIC_URL: 'https://identity-server.staging01.devland.is/api'
+ REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: ''
USER_PROFILE_CLIENT_SCOPE: '["@island.is/user-profile:read"]'
USER_PROFILE_CLIENT_URL: 'http://web-service-portal-api.service-portal.svc.cluster.local'
@@ -473,6 +477,8 @@ services-auth-ids-api:
NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD'
NOVA_URL: '/k8s/services-auth/NOVA_URL'
NOVA_USERNAME: '/k8s/services-auth/NOVA_USERNAME'
+ PASSKEY_CORE_ALLOWED_ORIGINS: '/k8s/services-auth/PASSKEY_CORE_ALLOWED_ORIGINS'
+ PASSKEY_CORE_MAX_AGE_DAYS: '/k8s/services-auth/PASSKEY_CORE_MAX_AGE_DAYS'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -691,7 +697,11 @@ services-auth-public-api:
IDENTITY_SERVER_ISSUER_URL: 'https://identity-server.staging01.devland.is'
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=345 -r dd-trace/init'
+ PASSKEY_CORE_CHALLENGE_TTL_MS: '120000'
+ PASSKEY_CORE_RP_ID: 'island.is'
+ PASSKEY_CORE_RP_NAME: 'Island.is'
PUBLIC_URL: 'https://identity-server.staging01.devland.is/api'
+ REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: ''
XROAD_BASE_PATH: 'http://securityserver.staging01.devland.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.staging01.devland.is/r1/IS-TEST'
@@ -763,6 +773,7 @@ services-auth-public-api:
DB_PASS: '/k8s/servicesauth/DB_PASSWORD'
IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET'
NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET'
+ PASSKEY_CORE_ALLOWED_ORIGINS: '/k8s/services-auth/PASSKEY_CORE_ALLOWED_ORIGINS'
securityContext:
allowPrivilegeEscalation: false
privileged: false
diff --git a/infra/src/dsl/xroad.ts b/infra/src/dsl/xroad.ts
index f93b00b58356..e8784bc45c31 100644
--- a/infra/src/dsl/xroad.ts
+++ b/infra/src/dsl/xroad.ts
@@ -447,23 +447,23 @@ export const NationalRegistryAuthB2C = new XroadConf({
env: {
NATIONAL_REGISTRY_B2C_CLIENT_ID: {
dev: '6cf94113-d326-4e4d-b97c-1fea12d2f5e1',
- staging: 'a84b70d3-5a75-470f-a137-71d25f2e8794',
+ staging: '6cf94113-d326-4e4d-b97c-1fea12d2f5e1',
prod: '8271bbc2-d8de-480f-8540-ea43fc40b7ae',
},
NATIONAL_REGISTRY_B2C_ENDPOINT: {
dev: 'https://skraidentitydev.b2clogin.com/skraidentitydev.onmicrosoft.com/b2c_1_midlun_flow/oauth2/v2.0/token',
staging:
- 'https://skraidentitystaging.b2clogin.com/skraidentitystaging.onmicrosoft.com/b2c_1_midlun_flow/oauth2/v2.0/token',
+ 'https://skraidentitydev.b2clogin.com/skraidentitydev.onmicrosoft.com/b2c_1_midlun_flow/oauth2/v2.0/token',
prod: 'https://skraidentity.b2clogin.com/skraidentity.onmicrosoft.com/b2c_1_midlun_flow/oauth2/v2.0/token',
},
NATIONAL_REGISTRY_B2C_SCOPE: {
dev: 'https://skraidentitydev.onmicrosoft.com/midlun/.default',
- staging: 'https://skraidentitystaging.onmicrosoft.com/midlun/.default',
+ staging: 'https://skraidentitydev.onmicrosoft.com/midlun/.default',
prod: 'https://skraidentity.onmicrosoft.com/midlun/.default',
},
NATIONAL_REGISTRY_B2C_PATH: {
dev: 'IS-DEV/GOV/10001/SKRA-Cloud-Protected/Midlun-v1',
- staging: 'IS-TEST/GOV/6503760649/SKRA-Cloud-Protected/Midlun-v1',
+ staging: 'IS-DEV/GOV/10001/SKRA-Cloud-Protected/Midlun-v1',
prod: 'IS/GOV/6503760649/SKRA-Cloud-Protected/Midlun-v1',
},
},
diff --git a/libs/api/domains/auth/src/lib/auth.module.ts b/libs/api/domains/auth/src/lib/auth.module.ts
index 0f79094ec04f..7e28da430596 100644
--- a/libs/api/domains/auth/src/lib/auth.module.ts
+++ b/libs/api/domains/auth/src/lib/auth.module.ts
@@ -29,6 +29,8 @@ import { ApiScopeResolver } from './resolvers/apiScope.resolver'
import { DomainResolver } from './resolvers/domain.resolver'
import { ClientResolver } from './resolvers/client.resolver'
import { ConsentResolver } from './resolvers/consent.resolver'
+import { PasskeyResolver } from './resolvers/passkey.resolver'
+import { PasskeyService } from './services/passkey.service'
@Module({
providers: [
@@ -54,6 +56,8 @@ import { ConsentResolver } from './resolvers/consent.resolver'
ConsentTenantsService,
LoginRestrictionResolver,
LoginRestrictionService,
+ PasskeyService,
+ PasskeyResolver,
],
imports: [
AuthPublicApiClientModule,
diff --git a/libs/api/domains/auth/src/lib/dto/authenticationObject.input.ts b/libs/api/domains/auth/src/lib/dto/authenticationObject.input.ts
new file mode 100644
index 000000000000..33a0efb7ed2b
--- /dev/null
+++ b/libs/api/domains/auth/src/lib/dto/authenticationObject.input.ts
@@ -0,0 +1,42 @@
+import { Field, InputType } from '@nestjs/graphql'
+
+@InputType('AuthPasskeyAuthenticationObjectResponse')
+export class PasskeyAuthenticationObjectResponse {
+ @Field(() => String)
+ authenticatorData!: string
+
+ @Field(() => String)
+ clientDataJSON!: string
+
+ @Field(() => String)
+ signature!: string
+
+ @Field(() => String, { nullable: true })
+ userHandle?: string
+}
+
+@InputType('AuthPasskeyAuthenticationObjectCredentialPropertiesOutput')
+export class PasskeyAuthenticationObjectCredentialPropertiesOutput {
+ @Field(() => Boolean)
+ rk!: boolean
+}
+
+@InputType('AuthPasskeyAuthenticationObjectExtensionsClientOutputs')
+export class PasskeyAuthenticationObjectExtensionsClientOutputs {
+ @Field(() => Boolean, { nullable: true })
+ appid?: boolean
+
+ @Field(() => PasskeyAuthenticationObjectCredentialPropertiesOutput, {
+ nullable: true,
+ })
+ credProps?: PasskeyAuthenticationObjectCredentialPropertiesOutput
+
+ @Field(() => Boolean, { nullable: true })
+ hmacCreateSecret?: boolean
+}
+
+@InputType('AuthPasskeyAuthenticationObject')
+export class PasskeyAuthenticationObject {
+ @Field(() => String)
+ passkey!: string
+}
diff --git a/libs/api/domains/auth/src/lib/dto/registrationObject.input.ts b/libs/api/domains/auth/src/lib/dto/registrationObject.input.ts
new file mode 100644
index 000000000000..500fa7fb8a8b
--- /dev/null
+++ b/libs/api/domains/auth/src/lib/dto/registrationObject.input.ts
@@ -0,0 +1,61 @@
+import { Field, InputType } from '@nestjs/graphql'
+
+@InputType('AuthExtensionCredProps')
+export class ExtensionCredProps {
+ @Field(() => Boolean, { nullable: true })
+ rk?: boolean
+}
+
+@InputType('AuthPasskeyRegistrationObjectClientExtensionResults')
+export class PasskeyRegistrationObjectClientExtensionResults {
+ @Field(() => Boolean, { nullable: true })
+ appid?: boolean
+
+ @Field(() => ExtensionCredProps, { nullable: true })
+ credProps?: ExtensionCredProps
+
+ @Field(() => Boolean, { nullable: true })
+ hmacCreateSecret?: boolean
+}
+
+@InputType('AuthPasskeyRegistrationObjectResponse')
+export class PasskeyRegistrationObjectResponse {
+ @Field()
+ attestationObject!: string
+
+ @Field(() => String)
+ clientDataJSON!: string
+
+ @Field(() => [String], { nullable: true })
+ transports?: string[]
+
+ @Field(() => Number, { nullable: true })
+ publicKeyAlgorithm?: number
+
+ @Field(() => String, { nullable: true })
+ publicKey?: string
+
+ @Field(() => String, { nullable: true })
+ authenticatorData?: string
+}
+
+@InputType('AuthPasskeyRegistrationObject')
+export class PasskeyRegistrationObject {
+ @Field(() => String)
+ id!: string
+
+ @Field(() => String)
+ rawId!: string
+
+ @Field(() => PasskeyRegistrationObjectResponse)
+ response!: PasskeyRegistrationObjectResponse
+
+ @Field(() => String)
+ type!: string
+
+ @Field(() => PasskeyRegistrationObjectClientExtensionResults)
+ clientExtensionResults!: PasskeyRegistrationObjectClientExtensionResults
+
+ @Field(() => String, { nullable: true })
+ authenticatorAttachment?: string
+}
diff --git a/libs/api/domains/auth/src/lib/models/authenticationOptions.model.ts b/libs/api/domains/auth/src/lib/models/authenticationOptions.model.ts
new file mode 100644
index 000000000000..86e01cb5ccaf
--- /dev/null
+++ b/libs/api/domains/auth/src/lib/models/authenticationOptions.model.ts
@@ -0,0 +1,31 @@
+import { Field, ObjectType } from '@nestjs/graphql'
+
+@ObjectType('AuthPasskeyAuthenticationOptionsCredentials')
+export class PasskeyAuthenticationOptionsCredentials {
+ @Field(() => String)
+ id!: string
+
+ @Field(() => String)
+ type!: string
+
+ @Field(() => [String])
+ transports!: string[]
+}
+
+@ObjectType('AuthPasskeyAuthenticationOptions')
+export class PasskeyAuthenticationOptions {
+ @Field(() => String)
+ rpId!: string
+
+ @Field(() => String)
+ challenge!: string
+
+ @Field(() => [PasskeyAuthenticationOptionsCredentials])
+ allowCredentials!: PasskeyAuthenticationOptionsCredentials[]
+
+ @Field(() => Number)
+ timeout!: number
+
+ @Field(() => String)
+ userVerification!: string
+}
diff --git a/libs/api/domains/auth/src/lib/models/registrationOptions.model.ts b/libs/api/domains/auth/src/lib/models/registrationOptions.model.ts
new file mode 100644
index 000000000000..8cc2a9db624d
--- /dev/null
+++ b/libs/api/domains/auth/src/lib/models/registrationOptions.model.ts
@@ -0,0 +1,102 @@
+import { Field, ObjectType } from '@nestjs/graphql'
+
+@ObjectType('AuthRegistrationOptionsRp')
+export class RegistrationOptionsRp {
+ @Field(() => String, { nullable: true })
+ id?: string
+
+ @Field(() => String)
+ name!: string
+}
+
+@ObjectType('AuthRegistrationOptionsUser')
+export class RegistrationOptionsUser {
+ @Field(() => String)
+ id!: string
+
+ @Field(() => String)
+ name!: string
+
+ @Field(() => String)
+ displayName!: string
+}
+
+@ObjectType('AuthRegistrationOptionsPublicKeyCredentialOption')
+export class RegistrationOptionsPublicKeyCredentialOption {
+ @Field(() => Number)
+ alg!: number
+
+ @Field(() => String)
+ type!: string
+}
+
+@ObjectType('AuthRegistrationOptionsAuthenticatorSelection')
+export class RegistrationOptionsAuthenticatorSelection {
+ @Field(() => String, { nullable: true })
+ authenticatorAttachment?: string
+
+ @Field(() => Boolean, { nullable: true })
+ requireResidentKey?: boolean
+
+ @Field(() => String, { nullable: true })
+ residentKey?: string
+
+ @Field(() => String, { nullable: true })
+ userVerification?: string
+}
+
+@ObjectType('AuthRegistrationOptionsExtensions')
+export class RegistrationOptionsExtensions {
+ @Field(() => String, { nullable: true })
+ appid?: string
+
+ @Field(() => Boolean, { nullable: true })
+ credProps?: boolean
+
+ @Field(() => Boolean, { nullable: true })
+ hmacCreateSecret?: boolean
+}
+
+@ObjectType('AuthRegistrationOptionsPublicKeyCredentialDescriptorJSON')
+export class RegistrationOptionsPublicKeyCredentialDescriptorJSON {
+ @Field(() => String)
+ id!: string
+
+ @Field(() => String)
+ type!: 'public-key'
+
+ @Field(() => [String])
+ transports!: string[]
+}
+
+@ObjectType('AuthPasskeyRegistrationOptions')
+export class PasskeyRegistrationOptions {
+ @Field(() => String)
+ challenge!: string
+
+ @Field(() => RegistrationOptionsRp)
+ rp!: RegistrationOptionsRp
+
+ @Field(() => RegistrationOptionsUser)
+ user!: RegistrationOptionsUser
+
+ @Field(() => [RegistrationOptionsPublicKeyCredentialOption])
+ pubKeyCredParams!: RegistrationOptionsPublicKeyCredentialOption[]
+
+ @Field(() => Number, { nullable: true })
+ timeout?: number
+
+ @Field(() => String, { nullable: true })
+ attestation?: string
+
+ @Field(() => [RegistrationOptionsPublicKeyCredentialDescriptorJSON], {
+ nullable: true,
+ })
+ excludeCredentials?: RegistrationOptionsPublicKeyCredentialDescriptorJSON[]
+
+ @Field(() => RegistrationOptionsAuthenticatorSelection, { nullable: true })
+ authenticatorSelection?: RegistrationOptionsAuthenticatorSelection
+
+ @Field(() => RegistrationOptionsExtensions, { nullable: true })
+ extensions?: RegistrationOptionsExtensions
+}
diff --git a/libs/api/domains/auth/src/lib/models/verifyRegistration.model.ts b/libs/api/domains/auth/src/lib/models/verifyRegistration.model.ts
new file mode 100644
index 000000000000..b0a899b62930
--- /dev/null
+++ b/libs/api/domains/auth/src/lib/models/verifyRegistration.model.ts
@@ -0,0 +1,7 @@
+import { Field, ObjectType } from '@nestjs/graphql'
+
+@ObjectType('AuthPasskeyRegistrationVerification')
+export class PasskeyRegistrationVerification {
+ @Field(() => Boolean)
+ verified!: boolean
+}
diff --git a/libs/api/domains/auth/src/lib/resolvers/passkey.resolver.ts b/libs/api/domains/auth/src/lib/resolvers/passkey.resolver.ts
new file mode 100644
index 000000000000..3c842ccaad76
--- /dev/null
+++ b/libs/api/domains/auth/src/lib/resolvers/passkey.resolver.ts
@@ -0,0 +1,66 @@
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
+import { UseGuards } from '@nestjs/common'
+
+import type { User } from '@island.is/auth-nest-tools'
+import { CurrentUser, IdsUserGuard } from '@island.is/auth-nest-tools'
+
+import { PasskeyRegistrationOptions } from '../models/registrationOptions.model'
+import { PasskeyService } from '../services/passkey.service'
+import { PasskeyRegistrationObject } from '../dto/registrationObject.input'
+import { PasskeyRegistrationVerification } from '../models/verifyRegistration.model'
+import { PasskeyAuthenticationOptions } from '../models/authenticationOptions.model'
+import { PasskeyAuthenticationObject } from '../dto/authenticationObject.input'
+
+@UseGuards(IdsUserGuard)
+@Resolver(() => PasskeyRegistrationOptions)
+export class PasskeyResolver {
+ constructor(private passkey: PasskeyService) {}
+
+ @Query(() => PasskeyRegistrationOptions, {
+ name: 'authPasskeyRegistrationOptions',
+ })
+ getRegistrationOptions(
+ @CurrentUser() user: User,
+ ): Promise {
+ return this.passkey.getRegistrationOptions(user)
+ }
+
+ @Mutation(() => PasskeyRegistrationVerification, {
+ name: 'authPasskeyVerifyRegistration',
+ })
+ async verifyRegistration(
+ @CurrentUser() user: User,
+ @Args('input', { type: () => PasskeyRegistrationObject })
+ input: PasskeyRegistrationObject,
+ ): Promise {
+ return this.passkey.verifyRegistration(user, input)
+ }
+
+ @Query(() => PasskeyAuthenticationOptions, {
+ name: 'authPasskeyAuthenticationOptions',
+ })
+ getAuthenticationOptions(
+ @CurrentUser() user: User,
+ ): Promise {
+ return this.passkey.getAuthenticationOptions(user)
+ }
+
+ @Mutation(() => PasskeyRegistrationVerification, {
+ name: 'authPasskeyVerifyAuthentication',
+ })
+ async verifyAuthentication(
+ @CurrentUser() user: User,
+ @Args('input', { type: () => PasskeyAuthenticationObject })
+ input: PasskeyAuthenticationObject,
+ ): Promise {
+ return this.passkey.verifyAuthentication(user, input)
+ }
+
+ @Mutation(() => Boolean, {
+ name: 'authDeletePasskey',
+ })
+ async deletePasskey(@CurrentUser() user: User): Promise {
+ await this.passkey.deletePasskey(user)
+ return true
+ }
+}
diff --git a/libs/api/domains/auth/src/lib/services/passkey.service.ts b/libs/api/domains/auth/src/lib/services/passkey.service.ts
new file mode 100644
index 000000000000..c49cc4dc53ac
--- /dev/null
+++ b/libs/api/domains/auth/src/lib/services/passkey.service.ts
@@ -0,0 +1,63 @@
+import { Injectable } from '@nestjs/common'
+
+import { Auth, AuthMiddleware, User } from '@island.is/auth-nest-tools'
+import { PasskeysApi } from '@island.is/clients/auth/public-api'
+
+import { PasskeyRegistrationOptions } from '../models/registrationOptions.model'
+import { PasskeyRegistrationVerification } from '../models/verifyRegistration.model'
+import { PasskeyRegistrationObject } from '../dto/registrationObject.input'
+import { PasskeyAuthenticationOptions } from '../models/authenticationOptions.model'
+import { PasskeyAuthenticationObject } from '../dto/authenticationObject.input'
+
+@Injectable()
+export class PasskeyService {
+ constructor(private passkeysApi: PasskeysApi) {}
+
+ passkeysApiWithAuth(auth: Auth): PasskeysApi {
+ return this.passkeysApi.withMiddleware(new AuthMiddleware(auth))
+ }
+
+ async getRegistrationOptions(
+ user: User,
+ ): Promise {
+ const options = await this.passkeysApiWithAuth(
+ user,
+ ).passkeysControllerGetPasskeyRegistrationOptions()
+
+ return options as PasskeyRegistrationOptions
+ }
+
+ verifyRegistration(
+ user: User,
+ input: PasskeyRegistrationObject,
+ ): Promise {
+ return this.passkeysApiWithAuth(user).passkeysControllerVerifyRegistration({
+ registrationResponse: input,
+ })
+ }
+
+ async getAuthenticationOptions(
+ user: User,
+ ): Promise {
+ const options = await this.passkeysApiWithAuth(
+ user,
+ ).passkeysControllerGetPasskeyAuthenticationOptions()
+
+ return options as PasskeyAuthenticationOptions
+ }
+
+ verifyAuthentication(
+ user: User,
+ input: PasskeyAuthenticationObject,
+ ): Promise {
+ return this.passkeysApiWithAuth(
+ user,
+ ).passkeysControllerVerifyAuthentication({
+ authenticationResponse: input,
+ })
+ }
+
+ deletePasskey(user: User): Promise {
+ return this.passkeysApiWithAuth(user).passkeysControllerDeletePasskey()
+ }
+}
diff --git a/libs/api/domains/criminal-record/src/lib/criminalRecord.module.ts b/libs/api/domains/criminal-record/src/lib/criminalRecord.module.ts
index e4b23a534f13..95bb0706eb7f 100644
--- a/libs/api/domains/criminal-record/src/lib/criminalRecord.module.ts
+++ b/libs/api/domains/criminal-record/src/lib/criminalRecord.module.ts
@@ -1,23 +1,11 @@
-import { Module, DynamicModule } from '@nestjs/common'
+import { Module } from '@nestjs/common'
import { CriminalRecordService } from './criminalRecord.service'
-import {
- CriminalRecordApiModule,
- CriminalRecordApiConfig,
-} from '@island.is/clients/criminal-record'
+import { CriminalRecordApiModule } from '@island.is/clients/criminal-record'
-export interface Config {
- clientConfig: CriminalRecordApiConfig
-}
-
-@Module({})
-export class CriminalRecordModule {
- static register(config: Config): DynamicModule {
- return {
- module: CriminalRecordModule,
- providers: [CriminalRecordService],
- imports: [CriminalRecordApiModule.register(config.clientConfig)],
- exports: [CriminalRecordService],
- }
- }
-}
+@Module({
+ providers: [CriminalRecordService],
+ exports: [CriminalRecordService],
+ imports: [CriminalRecordApiModule],
+})
+export class CriminalRecordModule {}
diff --git a/libs/api/domains/criminal-record/src/lib/criminalRecord.spec.ts b/libs/api/domains/criminal-record/src/lib/criminalRecord.spec.ts
index 1c2da333cbe2..611134764e53 100644
--- a/libs/api/domains/criminal-record/src/lib/criminalRecord.spec.ts
+++ b/libs/api/domains/criminal-record/src/lib/criminalRecord.spec.ts
@@ -7,7 +7,8 @@ import {
requestHandlers,
} from './__mock-data__/requestHandlers'
import { startMocking } from '@island.is/shared/mocking'
-import { createLogger } from 'winston'
+import { ConfigModule } from '@nestjs/config'
+import { defineConfig } from '@island.is/nest/config'
startMocking(requestHandlers)
@@ -17,15 +18,24 @@ describe('CriminalRecordService', () => {
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [
- CriminalRecordApiModule.register({
- xroadBaseUrl: 'http://localhost',
- xroadClientId: '',
- xroadPath: 'v2',
- fetchOptions: {
- logger: createLogger({
- silent: true,
+ CriminalRecordApiModule,
+ ConfigModule.forRoot({
+ isGlobal: true,
+ load: [
+ defineConfig({
+ name: 'CriminalRecordClient',
+ load: () => ({
+ xRoadServicePath: 'v2',
+ }),
}),
- },
+ defineConfig({
+ name: 'XRoadConfig',
+ load: () => ({
+ xRoadBasePath: 'http://localhost',
+ xRoadClient: '',
+ }),
+ }),
+ ],
}),
],
providers: [CriminalRecordService, { provide: 'CONFIG', useValue: {} }],
diff --git a/libs/api/domains/directorate-of-labour/src/lib/directorate-of-labour.module.ts b/libs/api/domains/directorate-of-labour/src/lib/directorate-of-labour.module.ts
index 1b0f07bea649..c34b7061029d 100644
--- a/libs/api/domains/directorate-of-labour/src/lib/directorate-of-labour.module.ts
+++ b/libs/api/domains/directorate-of-labour/src/lib/directorate-of-labour.module.ts
@@ -1,45 +1,15 @@
-import { DynamicModule, Module } from '@nestjs/common'
-
+import { Module } from '@nestjs/common'
import { VMSTModule } from '@island.is/clients/vmst'
-import {
- createXRoadAPIPath,
- XRoadMemberClass,
-} from '@island.is/shared/utils/server'
-
import { DirectorateOfLabourRepository } from './directorate-of-labour.repository'
import { DirectorateOfLabourResolver } from './directorate-of-labour.resolver'
import { DirectorateOfLabourService } from './directorate-of-labour.service'
-const XROAD_BASE_PATH_WITH_ENV = process.env.XROAD_BASE_PATH_WITH_ENV ?? ''
-const XROAD_VMST_MEMBER_CODE = process.env.XROAD_VMST_MEMBER_CODE ?? ''
-const XROAD_VMST_API_PATH = process.env.XROAD_VMST_API_PATH ?? ''
-const XROAD_VMST_API_KEY = process.env.XROAD_VMST_API_KEY ?? ''
-const XROAD_CLIENT_ID = process.env.XROAD_CLIENT_ID ?? ''
-const XROAD_VMST_MEMBER_CLASS = XRoadMemberClass.GovernmentInstitution
-
-@Module({})
-export class DirectorateOfLabourModule {
- static register(): DynamicModule {
- return {
- module: DirectorateOfLabourModule,
- providers: [
- DirectorateOfLabourResolver,
- DirectorateOfLabourService,
- DirectorateOfLabourRepository,
- ],
- imports: [
- VMSTModule.register({
- xRoadPath: createXRoadAPIPath(
- XROAD_BASE_PATH_WITH_ENV,
- XROAD_VMST_MEMBER_CLASS,
- XROAD_VMST_MEMBER_CODE,
- XROAD_VMST_API_PATH,
- ),
- xRoadClient: XROAD_CLIENT_ID,
- apiKey: XROAD_VMST_API_KEY,
- }),
- ],
- exports: [],
- }
- }
-}
+@Module({
+ providers: [
+ DirectorateOfLabourResolver,
+ DirectorateOfLabourService,
+ DirectorateOfLabourRepository,
+ ],
+ imports: [VMSTModule],
+})
+export class DirectorateOfLabourModule {}
diff --git a/libs/api/domains/health-insurance/src/lib/api-domains-health-insurance.spec.ts b/libs/api/domains/health-insurance/src/lib/api-domains-health-insurance.spec.ts
index 039c22883181..3d5e03e428a9 100644
--- a/libs/api/domains/health-insurance/src/lib/api-domains-health-insurance.spec.ts
+++ b/libs/api/domains/health-insurance/src/lib/api-domains-health-insurance.spec.ts
@@ -2,10 +2,12 @@ import { apiDomainsHealthInsurance } from './api-domains-health-insurance'
import { Test } from '@nestjs/testing'
import { HealthInsuranceService } from './healthInsurance.service'
import {
- HealthInsuranceV2Client,
- HealthInsuranceV2Options,
+ HealthInsuranceV2ClientConfig,
+ HealthInsuranceV2ClientModule,
} from '@island.is/clients/icelandic-health-insurance/health-insurance'
import { logger, LOGGER_PROVIDER } from '@island.is/logging'
+import { ConfigModule } from '@nestjs/config'
+import { XRoadConfig } from '@island.is/nest/config'
describe('apiDomainsHealthInsurance', () => {
it('should work', () => {
@@ -14,22 +16,16 @@ describe('apiDomainsHealthInsurance', () => {
})
describe('healthInsuranceTest', () => {
- interface HealthInsuranceOptions {
- clientV2Config: HealthInsuranceV2Options
- }
- const options: HealthInsuranceOptions = {
- clientV2Config: {
- xRoadBaseUrl: 'http://localhost:8080',
- password: '',
- username: '',
- xRoadClientId: '',
- xRoadProviderId: '',
- },
- }
let service: HealthInsuranceService
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
- imports: [HealthInsuranceV2Client.register(options.clientV2Config)],
+ imports: [
+ HealthInsuranceV2ClientModule,
+ ConfigModule.forRoot({
+ isGlobal: true,
+ load: [HealthInsuranceV2ClientConfig, XRoadConfig],
+ }),
+ ],
providers: [
{
provide: LOGGER_PROVIDER,
diff --git a/libs/api/domains/health-insurance/src/lib/healthInsurance.module.ts b/libs/api/domains/health-insurance/src/lib/healthInsurance.module.ts
index aeb20d6905de..504e527e4261 100644
--- a/libs/api/domains/health-insurance/src/lib/healthInsurance.module.ts
+++ b/libs/api/domains/health-insurance/src/lib/healthInsurance.module.ts
@@ -1,31 +1,20 @@
-import { DynamicModule } from '@nestjs/common'
+import { Module } from '@nestjs/common'
import {
HealthInsuranceAccidentNotificationResolver,
HealthInsuranceResolver,
} from './graphql'
import { HealthInsuranceService } from './healthInsurance.service'
-import {
- HealthInsuranceV2Client,
- HealthInsuranceV2Options,
-} from '@island.is/clients/icelandic-health-insurance/health-insurance'
+import { HealthInsuranceV2ClientModule } from '@island.is/clients/icelandic-health-insurance/health-insurance'
import { AccidentNotificationService } from './accident-notification.service'
-export interface HealthInsuranceOptions {
- clientV2Config: HealthInsuranceV2Options
-}
-
-export class HealthInsuranceModule {
- static register(options: HealthInsuranceOptions): DynamicModule {
- return {
- module: HealthInsuranceModule,
- imports: [HealthInsuranceV2Client.register(options.clientV2Config)],
- providers: [
- HealthInsuranceService,
- AccidentNotificationService,
- HealthInsuranceResolver,
- HealthInsuranceAccidentNotificationResolver,
- ],
- exports: [],
- }
- }
-}
+@Module({
+ imports: [HealthInsuranceV2ClientModule],
+ providers: [
+ HealthInsuranceService,
+ AccidentNotificationService,
+ HealthInsuranceResolver,
+ HealthInsuranceAccidentNotificationResolver,
+ ],
+ exports: [],
+})
+export class HealthInsuranceModule {}
diff --git a/libs/application/core/src/lib/fieldBuilders.ts b/libs/application/core/src/lib/fieldBuilders.ts
index 575115e45a09..ee364f300030 100644
--- a/libs/application/core/src/lib/fieldBuilders.ts
+++ b/libs/application/core/src/lib/fieldBuilders.ts
@@ -194,6 +194,7 @@ export function buildSelectField(
placeholder,
onSelect,
backgroundColor = 'blue',
+ isMulti,
required,
} = data
return {
@@ -205,6 +206,7 @@ export function buildSelectField(
type: FieldTypes.SELECT,
component: FieldComponents.SELECT,
onSelect,
+ isMulti,
backgroundColor,
}
}
@@ -219,6 +221,7 @@ export function buildAsyncSelectField(
onSelect,
backgroundColor = 'blue',
isSearchable,
+ isMulti,
} = data
return {
@@ -232,6 +235,7 @@ export function buildAsyncSelectField(
onSelect,
backgroundColor,
isSearchable,
+ isMulti,
}
}
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/accident-notification/accident-notification.module.ts b/libs/application/template-api-modules/src/lib/modules/templates/accident-notification/accident-notification.module.ts
index 7b993b3b280f..c3d21f0fbc8f 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/accident-notification/accident-notification.module.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/accident-notification/accident-notification.module.ts
@@ -3,7 +3,7 @@ import { SharedTemplateAPIModule } from '../../shared'
import { BaseTemplateAPIModuleConfig } from '../../../types'
import { ACCIDENT_NOTIFICATION_CONFIG } from './config'
import { AccidentNotificationService } from './accident-notification.service'
-import { HealthInsuranceV2Client } from '@island.is/clients/icelandic-health-insurance/health-insurance'
+import { HealthInsuranceV2ClientModule } from '@island.is/clients/icelandic-health-insurance/health-insurance'
import { ApplicationAttachmentService } from './attachments/applicationAttachment.service'
import { AccidentNotificationAttachmentProvider } from './attachments/applicationAttachmentProvider'
import { S3 } from 'aws-sdk'
@@ -26,7 +26,7 @@ export class AccidentNotificationModule {
module: AccidentNotificationModule,
imports: [
SharedTemplateAPIModule.register(config),
- HealthInsuranceV2Client.register(config.healthInsuranceV2),
+ HealthInsuranceV2ClientModule,
],
providers: [
{
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/criminal-record-submission/criminal-record-submission.module.ts b/libs/application/template-api-modules/src/lib/modules/templates/criminal-record-submission/criminal-record-submission.module.ts
index 83a5179096b7..a9734b91c4e4 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/criminal-record-submission/criminal-record-submission.module.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/criminal-record-submission/criminal-record-submission.module.ts
@@ -14,7 +14,7 @@ export class CriminalRecordSubmissionModule {
module: CriminalRecordSubmissionModule,
imports: [
SharedTemplateAPIModule.register(baseConfig),
- CriminalRecordModule.register(baseConfig.criminalRecord),
+ CriminalRecordModule,
SyslumennClientModule,
],
providers: [CriminalRecordSubmissionService],
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/data-protection-complaint.module.ts b/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/data-protection-complaint.module.ts
index 01cb4d43d2e2..631204c5000c 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/data-protection-complaint.module.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/data-protection-complaint.module.ts
@@ -22,9 +22,7 @@ export class DataProtectionComplaintModule {
imports: [
SharedTemplateAPIModule.register(config),
FileStorageModule,
- ClientsDataProtectionComplaintModule.register(
- config.dataProtectionComplaint,
- ),
+ ClientsDataProtectionComplaintModule,
],
providers: [
ApplicationAttachmentProvider,
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/health-insurance/health-insurance.module.ts b/libs/application/template-api-modules/src/lib/modules/templates/health-insurance/health-insurance.module.ts
index 2465965400d7..a6bdf74fd03a 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/health-insurance/health-insurance.module.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/health-insurance/health-insurance.module.ts
@@ -9,7 +9,7 @@ import { BaseTemplateAPIModuleConfig } from '../../../types'
// Here you import your module service
import { HealthInsuranceService } from './health-insurance.service'
-import { HealthInsuranceV2Client } from '@island.is/clients/icelandic-health-insurance/health-insurance'
+import { HealthInsuranceV2ClientModule } from '@island.is/clients/icelandic-health-insurance/health-insurance'
import { BucketService } from './bucket/bucket.service'
export class HealthInsuranceModule {
@@ -17,7 +17,7 @@ export class HealthInsuranceModule {
return {
module: HealthInsuranceModule,
imports: [
- HealthInsuranceV2Client.register(config.healthInsuranceV2),
+ HealthInsuranceV2ClientModule,
SharedTemplateAPIModule.register(config),
],
providers: [HealthInsuranceService, BucketService],
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/operating-license/operatingLicense.module.ts b/libs/application/template-api-modules/src/lib/modules/templates/operating-license/operatingLicense.module.ts
index 843b4f90ae8f..4610e3de6d46 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/operating-license/operatingLicense.module.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/operating-license/operatingLicense.module.ts
@@ -14,7 +14,7 @@ export class OperatingLicenseModule {
imports: [
SyslumennClientModule,
SharedTemplateAPIModule.register(config),
- CriminalRecordModule.register(config.criminalRecord),
+ CriminalRecordModule,
FinanceClientModule,
JudicialAdministrationClientModule,
],
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.module.ts b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.module.ts
index 371ab3284a7a..bc370c831460 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.module.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.module.ts
@@ -1,11 +1,5 @@
import { DynamicModule } from '@nestjs/common'
-
import { VMSTModule } from '@island.is/clients/vmst'
-import {
- createXRoadAPIPath,
- XRoadMemberClass,
-} from '@island.is/shared/utils/server'
-
import { BaseTemplateAPIModuleConfig } from '../../../types'
import { SharedTemplateAPIModule } from '../../shared'
import { ParentalLeaveService } from './parental-leave.service'
@@ -18,26 +12,12 @@ import {
} from '@island.is/clients/national-registry-v2'
import { APPLICATION_ATTACHMENT_BUCKET } from './constants'
-const XROAD_VMST_MEMBER_CODE = process.env.XROAD_VMST_MEMBER_CODE ?? ''
-const XROAD_VMST_API_PATH = process.env.XROAD_VMST_API_PATH ?? ''
-const XROAD_CLIENT_ID = process.env.XROAD_CLIENT_ID ?? ''
-const XROAD_VMST_API_KEY = process.env.XROAD_VMST_API_KEY ?? ''
-
export class ParentalLeaveModule {
static register(config: BaseTemplateAPIModuleConfig): DynamicModule {
return {
module: ParentalLeaveModule,
imports: [
- VMSTModule.register({
- xRoadPath: createXRoadAPIPath(
- config.xRoadBasePathWithEnv,
- XRoadMemberClass.GovernmentInstitution,
- XROAD_VMST_MEMBER_CODE,
- XROAD_VMST_API_PATH,
- ),
- xRoadClient: XROAD_CLIENT_ID,
- apiKey: XROAD_VMST_API_KEY,
- }),
+ VMSTModule,
SharedTemplateAPIModule.register(config),
SmsModule.register(config.smsOptions),
ApplicationApiCoreModule,
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.ts
index 7b42943ddea5..7e84e1d6cff8 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.ts
@@ -127,7 +127,9 @@ export class ParentalLeaveService extends BaseTemplateApiService {
}
return {
- message: Object.entries(e.errors).map(([, values]) => values.join(', ')),
+ message: e.errors
+ ? Object.entries(e.errors).map(([, values]) => values.join(', '))
+ : e.status,
}
}
@@ -1527,7 +1529,7 @@ export class ParentalLeaveService extends BaseTemplateApiService {
return
} catch (e) {
- this.logger.error('Failed to validate the parental leave application', e)
+ this.logger.warn('Failed to validate the parental leave application', e)
throw this.parseErrors(e as VMSTError)
}
}
diff --git a/libs/application/template-api-modules/src/lib/types/index.ts b/libs/application/template-api-modules/src/lib/types/index.ts
index ce2afb90cd13..9d3696b512a2 100644
--- a/libs/application/template-api-modules/src/lib/types/index.ts
+++ b/libs/application/template-api-modules/src/lib/types/index.ts
@@ -1,15 +1,11 @@
import { Injectable, Type } from '@nestjs/common'
-import { Config as CriminalRecordConfig } from '@island.is/api/domains/criminal-record'
import {
Application,
ApplicationWithAttachments,
} from '@island.is/application/types'
import { User } from '@island.is/auth-nest-tools'
-import { DataProtectionComplaintClientConfig } from '@island.is/clients/data-protection-complaint'
-import { HealthInsuranceV2Options } from '@island.is/clients/icelandic-health-insurance/health-insurance'
import { IslykillApiModuleConfig } from '@island.is/clients/islykill'
-import { PaymentScheduleServiceOptions } from '@island.is/clients/payment-schedule'
import { Message } from '@island.is/email-service'
import type { Locale } from '@island.is/shared/types'
@@ -36,14 +32,11 @@ export interface BaseTemplateAPIModuleConfig {
password: string
acceptUnauthorized?: boolean
}
- criminalRecord: CriminalRecordConfig
attachmentBucket: string
presignBucket: string
generalPetition: {
endorsementsApiBasePath: string
}
- healthInsuranceV2: HealthInsuranceV2Options
- dataProtectionComplaint: DataProtectionComplaintClientConfig
applicationService: Type
userProfile: {
serviceBasePath: string
diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/accidentDetailSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/accidentDetailSubSection.ts
index dfe52a39a6d8..a09af95d16c0 100644
--- a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/accidentDetailSubSection.ts
+++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/accidentDetailSubSection.ts
@@ -7,7 +7,7 @@ import {
buildTextField,
} from '@island.is/application/core'
import { accidentDetails } from '../../../lib/messages'
-import { isDateOlderThanAYear } from '../../../utils'
+import { isDateOlderThanAYear, isHomeActivitiesAccident } from '../../../utils'
import { isHealthInsured } from '../../../utils/isHealthInsured'
// Details of the accident
@@ -50,7 +50,8 @@ export const accidentDetailsSubSection = buildSubSection({
message: accidentDetails.general.insuranceAlertText,
width: 'full',
alertType: 'warning',
- condition: (formValue) => !isHealthInsured(formValue),
+ condition: (formValue) =>
+ !isHealthInsured(formValue) && isHomeActivitiesAccident(formValue),
marginBottom: 0,
}),
buildTextField({
diff --git a/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/FormerIcelanderSubSection.ts b/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/FormerIcelanderSubSection.ts
index 09453213a773..3f1fc9dcfbfd 100644
--- a/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/FormerIcelanderSubSection.ts
+++ b/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/FormerIcelanderSubSection.ts
@@ -14,40 +14,41 @@ import { ApplicantInformation, ParentsToApplicant } from '../../../shared'
export const FormerIcelanderSubSection = buildSubSection({
id: 'formerIcelander',
title: information.labels.formerIcelander.subSectionTitle,
- condition: (formValue: FormValue, externalData) => {
- const residenceConditionInfo = getValueViaPath(
- externalData,
- 'applicantInformation.data.residenceConditionInfo',
- {},
- ) as ApplicantInformation
+ // TODO REVERT WHEN FIXED WITH UTL
+ // condition: (formValue: FormValue, externalData) => {
+ // const residenceConditionInfo = getValueViaPath(
+ // externalData,
+ // 'applicantInformation.data.residenceConditionInfo',
+ // {},
+ // ) as ApplicantInformation
- const parentAnswer = getValueViaPath(
- formValue,
- 'parentInformation.parents',
- [],
- ) as Array
+ // const parentAnswer = getValueViaPath(
+ // formValue,
+ // 'parentInformation.parents',
+ // [],
+ // ) as Array
- const totalParentsInAnswer = parentAnswer.filter(
- (x) => x.wasRemoved === 'false',
- )
- const hasResConMaritalStatus =
- residenceConditionInfo.cohabitationISCitizen5YearDomicile ||
- residenceConditionInfo.cohabitationISCitizen5YrsDomicileMissingDate ||
- residenceConditionInfo.marriedISCitizenDomicile4Years ||
- residenceConditionInfo.marriedISCitizenDomicile4YrsMissingDate
+ // const totalParentsInAnswer = parentAnswer.filter(
+ // (x) => x.wasRemoved === 'false',
+ // )
+ // const hasResConMaritalStatus =
+ // residenceConditionInfo.cohabitationISCitizen5YearDomicile ||
+ // residenceConditionInfo.cohabitationISCitizen5YrsDomicileMissingDate ||
+ // residenceConditionInfo.marriedISCitizenDomicile4Years ||
+ // residenceConditionInfo.marriedISCitizenDomicile4YrsMissingDate
- const hasOtherValidResidenceConditions =
- residenceConditionInfo.domicileResidence7Years ||
- residenceConditionInfo.asylumSeekerOrHumanitarianResPerm5year ||
- residenceConditionInfo.noNationalityAnd5YearsDomicile ||
- residenceConditionInfo.nordicCitizenship4YearDomicile
+ // const hasOtherValidResidenceConditions =
+ // residenceConditionInfo.domicileResidence7Years ||
+ // residenceConditionInfo.asylumSeekerOrHumanitarianResPerm5year ||
+ // residenceConditionInfo.noNationalityAnd5YearsDomicile ||
+ // residenceConditionInfo.nordicCitizenship4YearDomicile
- return (
- !hasResConMaritalStatus &&
- !hasOtherValidResidenceConditions &&
- totalParentsInAnswer.length === 0
- )
- },
+ // return (
+ // !hasResConMaritalStatus &&
+ // !hasOtherValidResidenceConditions &&
+ // totalParentsInAnswer.length === 0
+ // )
+ // },
children: [
buildMultiField({
id: 'formerIcelanderMultiField',
@@ -68,23 +69,24 @@ export const FormerIcelanderSubSection = buildSubSection({
{ value: NO, label: information.labels.radioButtons.radioOptionNo },
],
}),
- buildAlertMessageField({
- id: 'formerIcelanderAlert',
- title: information.labels.formerIcelander.alertTitle,
- alertType: 'error',
- message: information.labels.formerIcelander.alertDescription,
- condition: (answer: Answer) => {
- const answers = answer as Citizenship
- return answers?.formerIcelander === NO
- },
- links: [
- {
- title: information.labels.formerIcelander.alertLinkTitle,
- url: information.labels.formerIcelander.alertLinkUrl,
- isExternal: true,
- },
- ],
- }),
+ // TODO REVERT WHEN UTL FIXES SERVICES
+ // buildAlertMessageField({
+ // id: 'formerIcelanderAlert',
+ // title: information.labels.formerIcelander.alertTitle,
+ // alertType: 'error',
+ // message: information.labels.formerIcelander.alertDescription,
+ // condition: (answer: Answer) => {
+ // const answers = answer as Citizenship
+ // return answers?.formerIcelander && answers?.formerIcelander !== YES
+ // },
+ // links: [
+ // {
+ // title: information.labels.formerIcelander.alertLinkTitle,
+ // url: information.labels.formerIcelander.alertLinkUrl,
+ // isExternal: true,
+ // },
+ // ],
+ // }),
],
}),
],
diff --git a/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/MaritalStatusSubSection.ts b/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/MaritalStatusSubSection.ts
index b2aed0f500ab..6225bdd1051a 100644
--- a/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/MaritalStatusSubSection.ts
+++ b/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/MaritalStatusSubSection.ts
@@ -19,33 +19,48 @@ export const MaritalStatusSubSection = buildSubSection({
id: Routes.MARITALSTATUS,
title: information.labels.maritalStatus.subSectionTitle,
condition: (_, externalData) => {
+ // TODO REVERT THIS WHEN UTL FIXES SERVICES
// Check if the only residence condition that the applicant can apply for, is related to marital status
- const residenceConditionInfo = getValueViaPath(
- externalData,
- 'applicantInformation.data.residenceConditionInfo',
- {},
- ) as ApplicantInformation
+ // const residenceConditionInfo = getValueViaPath(
+ // externalData,
+ // 'applicantInformation.data.residenceConditionInfo',
+ // {},
+ // ) as ApplicantInformation
+
+ // const hasResConMaritalStatus =
+ // residenceConditionInfo.cohabitationISCitizen5YearDomicile ||
+ // residenceConditionInfo.cohabitationISCitizen5YrsDomicileMissingDate ||
+ // residenceConditionInfo.marriedISCitizenDomicile4Years ||
+ // residenceConditionInfo.marriedISCitizenDomicile4YrsMissingDate
+
+ // const hasOtherValidResidenceConditions =
+ // residenceConditionInfo.domicileResidence7Years ||
+ // residenceConditionInfo.asylumSeekerOrHumanitarianResPerm5year ||
+ // residenceConditionInfo.noNationalityAnd5YearsDomicile ||
+ // residenceConditionInfo.nordicCitizenship4YearDomicile
- const hasResConMaritalStatus =
- residenceConditionInfo.cohabitationISCitizen5YearDomicile ||
- residenceConditionInfo.cohabitationISCitizen5YrsDomicileMissingDate ||
- residenceConditionInfo.marriedISCitizenDomicile4Years ||
- residenceConditionInfo.marriedISCitizenDomicile4YrsMissingDate
+ // const spouseIsCitizen = residenceConditionInfo.spouseIsCitizen
+ // const eesResidenceCondition = residenceConditionInfo.eesResidenceCondition
+ // const showThisPage = spouseIsCitizen && !eesResidenceCondition
- const hasOtherValidResidenceConditions =
- residenceConditionInfo.domicileResidence7Years ||
- residenceConditionInfo.asylumSeekerOrHumanitarianResPerm5year ||
- residenceConditionInfo.noNationalityAnd5YearsDomicile ||
- residenceConditionInfo.nordicCitizenship4YearDomicile
+ // return (
+ // (!!hasResConMaritalStatus && !hasOtherValidResidenceConditions) ||
+ // !!showThisPage
+ // )
+
+ // TODO REMOVE THIS WHEN UTL FIXES SERVICES
+ const spouseDetails = getValueViaPath(
+ externalData,
+ 'spouseDetails.data',
+ undefined,
+ ) as NationalRegistrySpouse | undefined
- const spouseIsCitizen = residenceConditionInfo.spouseIsCitizen
- const eesResidenceCondition = residenceConditionInfo.eesResidenceCondition
- const showThisPage = spouseIsCitizen && !eesResidenceCondition
+ const maritalStatus = spouseDetails?.maritalStatus
+ const hasSpouse = !!spouseDetails?.nationalId
+ const isMarriedOrCohabitation =
+ maritalStatus === '3' || (maritalStatus === '1' && hasSpouse)
- return (
- (!!hasResConMaritalStatus && !hasOtherValidResidenceConditions) ||
- !!showThisPage
- )
+ return isMarriedOrCohabitation
},
children: [
buildMultiField({
diff --git a/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/ParentsSubSection.ts b/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/ParentsSubSection.ts
index 40699c1e0d20..1adc55979a55 100644
--- a/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/ParentsSubSection.ts
+++ b/libs/application/templates/directorate-of-immigration/citizenship/src/forms/CitizenshipForm/InformationSection/ParentsSubSection.ts
@@ -11,32 +11,33 @@ import { ApplicantInformation } from '../../../shared'
export const ParentsSubSection = buildSubSection({
id: Routes.PARENTINFORMATION,
title: information.labels.parents.subSectionTitle,
- condition: (_, externalData) => {
- const residenceConditionInfo = getValueViaPath(
- externalData,
- 'applicantInformation.data.residenceConditionInfo',
- {},
- ) as ApplicantInformation
+ // TODO REVERT WHEN FIXED WITH UTL
+ // condition: (_, externalData) => {
+ // const residenceConditionInfo = getValueViaPath(
+ // externalData,
+ // 'applicantInformation.data.residenceConditionInfo',
+ // {},
+ // ) as ApplicantInformation
- const hasResConMaritalStatus =
- residenceConditionInfo.cohabitationISCitizen5YearDomicile ||
- residenceConditionInfo.cohabitationISCitizen5YrsDomicileMissingDate ||
- residenceConditionInfo.marriedISCitizenDomicile4Years ||
- residenceConditionInfo.marriedISCitizenDomicile4YrsMissingDate
+ // const hasResConMaritalStatus =
+ // residenceConditionInfo.cohabitationISCitizen5YearDomicile ||
+ // residenceConditionInfo.cohabitationISCitizen5YrsDomicileMissingDate ||
+ // residenceConditionInfo.marriedISCitizenDomicile4Years ||
+ // residenceConditionInfo.marriedISCitizenDomicile4YrsMissingDate
- const hasOtherValidResidenceConditions =
- residenceConditionInfo.domicileResidence7Years ||
- residenceConditionInfo.asylumSeekerOrHumanitarianResPerm5year ||
- residenceConditionInfo.noNationalityAnd5YearsDomicile ||
- residenceConditionInfo.nordicCitizenship4YearDomicile
+ // const hasOtherValidResidenceConditions =
+ // residenceConditionInfo.domicileResidence7Years ||
+ // residenceConditionInfo.asylumSeekerOrHumanitarianResPerm5year ||
+ // residenceConditionInfo.noNationalityAnd5YearsDomicile ||
+ // residenceConditionInfo.nordicCitizenship4YearDomicile
- const eesResidenceCondition = residenceConditionInfo.eesResidenceCondition
+ // const eesResidenceCondition = residenceConditionInfo.eesResidenceCondition
- return (
- (!hasResConMaritalStatus && !hasOtherValidResidenceConditions) ||
- !eesResidenceCondition
- ) //only show this screen if cohabitation is not a reason for applying and no other conditions are met
- },
+ // return (
+ // (!hasResConMaritalStatus && !hasOtherValidResidenceConditions) ||
+ // !eesResidenceCondition
+ // ) //only show this screen if cohabitation is not a reason for applying and no other conditions are met
+ // },
children: [
buildMultiField({
id: Routes.PARENTINFORMATION,
diff --git a/libs/application/templates/directorate-of-immigration/citizenship/src/lib/dataSchema.ts b/libs/application/templates/directorate-of-immigration/citizenship/src/lib/dataSchema.ts
index 7895eda6905d..6f1ae3897236 100644
--- a/libs/application/templates/directorate-of-immigration/citizenship/src/lib/dataSchema.ts
+++ b/libs/application/templates/directorate-of-immigration/citizenship/src/lib/dataSchema.ts
@@ -294,7 +294,7 @@ export const CitizenshipSchema = z.object({
passport: PassportSchema,
childrenPassport: z.array(ChildrenPassportSchema).optional(),
maritalStatus: MaritalStatusSchema,
- formerIcelander: z.enum([YES, NO]).refine((v) => v === YES),
+ formerIcelander: z.enum([YES, NO]), //.refine((v) => v === YES) // TODO REVERT WHEN UTL FIXED SERVICES
supportingDocuments: SupportingDocumentsSchema,
childrenSupportingDocuments: z
.array(ChildrenSupportingDocumentsSchema)
diff --git a/libs/application/templates/directorate-of-immigration/citizenship/src/lib/messages/supportingDocuments.ts b/libs/application/templates/directorate-of-immigration/citizenship/src/lib/messages/supportingDocuments.ts
index e06fefb0563b..f13810639064 100644
--- a/libs/application/templates/directorate-of-immigration/citizenship/src/lib/messages/supportingDocuments.ts
+++ b/libs/application/templates/directorate-of-immigration/citizenship/src/lib/messages/supportingDocuments.ts
@@ -104,7 +104,7 @@ export const supportingDocuments = {
description: 'Other documents description',
},
title: {
- id: 'doi.cs.application:supportingDocuments.labels.otherDocuments.title',
+ id: 'doi.cs.application:supportingDocuments.labels.otherDocuments.title#markdown',
defaultMessage: 'Vinsamlegast hlaðið inn eftirfarandi fylgigögnum.',
description: 'Other documents title',
},
diff --git a/libs/application/templates/driving-instructor-registrations/src/forms/instructorRegistrations.ts b/libs/application/templates/driving-instructor-registrations/src/forms/instructorRegistrations.ts
index 5cbbfa0b01c8..e44b66439423 100644
--- a/libs/application/templates/driving-instructor-registrations/src/forms/instructorRegistrations.ts
+++ b/libs/application/templates/driving-instructor-registrations/src/forms/instructorRegistrations.ts
@@ -42,7 +42,7 @@ export const getInstructorRegistrations = (allowBELicense = false): Form => {
subTitle: '',
}),
buildDataProviderItem({
- provider: GetTeacherRightsApi,
+ provider: allowBELicense ? GetTeacherRightsApi : undefined,
title: '',
subTitle: '',
}),
diff --git a/libs/application/templates/estate/src/fields/EstateMembersRepeater/AdditionalEstateMember.tsx b/libs/application/templates/estate/src/fields/EstateMembersRepeater/AdditionalEstateMember.tsx
index ebdf90c25669..8c887f79d2b0 100644
--- a/libs/application/templates/estate/src/fields/EstateMembersRepeater/AdditionalEstateMember.tsx
+++ b/libs/application/templates/estate/src/fields/EstateMembersRepeater/AdditionalEstateMember.tsx
@@ -181,7 +181,7 @@ export const AdditionalEstateMember = ({
options={relationOptions}
error={error?.relation}
backgroundColor="blue"
- required
+ required={!field.initial}
/>
{application.answers.selectedEstate ===
diff --git a/libs/application/templates/general-petition/src/forms/form.ts b/libs/application/templates/general-petition/src/forms/form.ts
index ddb7c3c3275b..5b4b56b9a3ab 100644
--- a/libs/application/templates/general-petition/src/forms/form.ts
+++ b/libs/application/templates/general-petition/src/forms/form.ts
@@ -11,6 +11,7 @@ import {
buildDividerField,
buildDescriptionField,
buildPhoneField,
+ getValueViaPath,
} from '@island.is/application/core'
import {
DefaultEvents,
@@ -27,6 +28,7 @@ import { UserProfile } from '@island.is/api/schema'
import { formatPhoneNumber } from '@island.is/application/ui-components'
import { parse } from 'libphonenumber-js'
import { getExcludedDates } from '../lib/generalPetitionUtils'
+import addMonths from 'date-fns/addMonths'
export const form: Form = buildForm({
id: 'GeneralPetitionForm',
@@ -94,6 +96,13 @@ export const form: Form = buildForm({
maxLength: 1000,
defaultValue: () => '',
}),
+ //fake field to trigger rerender on date switch
+ buildDescriptionField({
+ id: 'fake_helper_field',
+ title: '',
+ condition: (answers) =>
+ !!getValueViaPath(answers, 'dates.dateFrom'),
+ }),
buildDateField({
id: 'dates.dateFrom',
title: m.dateTitle,
@@ -110,9 +119,13 @@ export const form: Form = buildForm({
width: 'half',
backgroundColor: 'white',
minDate: new Date(),
- maxDate: new Date(
- new Date().setFullYear(new Date().getFullYear() + 1),
- ),
+ maxDate: ({ answers }) => {
+ const dateFrom = getValueViaPath(
+ answers,
+ 'dates.dateFrom',
+ ) as string
+ return addMonths(new Date(dateFrom), 3)
+ },
excludeDates: getExcludedDates(),
}),
buildDescriptionField({
diff --git a/libs/application/templates/inheritance-report/src/fields/AssetsRepeater/index.tsx b/libs/application/templates/inheritance-report/src/fields/AssetsRepeater/index.tsx
index 7cbe11193827..5a7921d3e94d 100644
--- a/libs/application/templates/inheritance-report/src/fields/AssetsRepeater/index.tsx
+++ b/libs/application/templates/inheritance-report/src/fields/AssetsRepeater/index.tsx
@@ -413,29 +413,31 @@ const RealEstateNumberField = ({
}, [queryLoading])
useEffect(() => {
- const propertyNumber = propertyNumberInput
- .trim()
- .toUpperCase()
- .replace('-', '')
+ if (!props.readOnly) {
+ const propertyNumber = propertyNumberInput
+ .trim()
+ .toUpperCase()
+ .replace('-', '')
- setValue(descriptionFieldName, '')
+ setValue(descriptionFieldName, '')
- if (isValidRealEstate(propertyNumber)) {
- clearErrors(fieldName)
+ if (isValidRealEstate(propertyNumber)) {
+ clearErrors(fieldName)
- getProperty({
- variables: {
- input: {
- propertyNumber,
+ getProperty({
+ variables: {
+ input: {
+ propertyNumber,
+ },
},
- },
- })
- } else {
- if (propertyNumber.length !== 0) {
- setError(fieldName, {
- message: formatMessage(m.errorPropertyNumber),
- type: 'validate',
})
+ } else {
+ if (propertyNumber.length !== 0) {
+ setError(fieldName, {
+ message: formatMessage(m.errorPropertyNumber),
+ type: 'validate',
+ })
+ }
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/libs/application/templates/inheritance-report/src/lib/utils/helpers.ts b/libs/application/templates/inheritance-report/src/lib/utils/helpers.ts
index c5769bedce58..638218839390 100644
--- a/libs/application/templates/inheritance-report/src/lib/utils/helpers.ts
+++ b/libs/application/templates/inheritance-report/src/lib/utils/helpers.ts
@@ -21,8 +21,9 @@ export const currencyStringToNumber = (str: string) => {
return parseInt(cleanString, 10)
}
-export const isValidString = (string: string | undefined) =>
- string && /\S/.test(string)
+export const isValidString = (string: string | undefined) => {
+ return string && /\S/.test(string)
+}
export const getEstateDataFromApplication = (
application: Application,
diff --git a/libs/application/types/src/lib/Fields.ts b/libs/application/types/src/lib/Fields.ts
index 945b9202707d..39c4379dd173 100644
--- a/libs/application/types/src/lib/Fields.ts
+++ b/libs/application/types/src/lib/Fields.ts
@@ -239,7 +239,7 @@ export interface DateField extends BaseField {
readonly type: FieldTypes.DATE
placeholder?: FormText
component: FieldComponents.DATE
- maxDate?: Date
+ maxDate?: MaybeWithApplicationAndField
minDate?: MaybeWithApplicationAndField
excludeDates?: MaybeWithApplicationAndField
backgroundColor?: DatePickerBackgroundColor
@@ -279,6 +279,7 @@ export interface SelectField extends BaseField {
placeholder?: FormText
backgroundColor?: InputBackgroundColor
required?: boolean
+ isMulti?: boolean
}
export interface CompanySearchField extends BaseField {
@@ -301,6 +302,7 @@ export interface AsyncSelectField extends BaseField {
backgroundColor?: InputBackgroundColor
isSearchable?: boolean
required?: boolean
+ isMulti?: boolean
}
export interface TextField extends BaseField {
diff --git a/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx b/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx
index abf9cf91e0ef..791e983a3518 100644
--- a/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx
+++ b/libs/application/ui-fields/src/lib/AsyncSelectFormField/AsyncSelectFormField.tsx
@@ -33,6 +33,7 @@ export const AsyncSelectFormField: FC> = ({
onSelect,
backgroundColor,
isSearchable,
+ isMulti,
required = false,
} = field
const { formatMessage } = useLocale()
@@ -89,9 +90,12 @@ export const AsyncSelectFormField: FC> = ({
? formatText(placeholder as string, application, formatMessage)
: undefined
}
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore make web strict
onSelect={onSelect}
backgroundColor={backgroundColor}
isSearchable={isSearchable}
+ isMulti={isMulti}
/>
diff --git a/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.stories.mdx b/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.stories.mdx
index 3724a5e6595d..75bdeaaa2559 100644
--- a/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.stories.mdx
+++ b/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.stories.mdx
@@ -91,6 +91,74 @@ The previous configuration object will result in the following component:
+# SelectFormField, multi select
+
+### Usage in a template
+
+You can create a SelectFormField using the following function `buildSelectField`.
+
+
+
+The previous configuration object will result in the following component:
+
+
+
+
+
+
+
You can also use this field into a custom component by using ` ` with the configuration object defined above.
# Props
diff --git a/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx b/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx
index bd0238a90b09..1c9ae5f3ecfd 100644
--- a/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx
+++ b/libs/application/ui-fields/src/lib/SelectFormField/SelectFormField.tsx
@@ -34,6 +34,7 @@ export const SelectFormField: FC> = ({
onSelect,
backgroundColor,
required = false,
+ isMulti,
} = field
const { formatMessage } = useLocale()
@@ -64,6 +65,7 @@ export const SelectFormField: FC> = ({
error={error}
id={id}
dataTestId={field.dataTestId}
+ isMulti={isMulti}
backgroundColor={backgroundColor}
options={finalOptions?.map(({ label, tooltip, ...o }) => ({
...o,
@@ -77,6 +79,8 @@ export const SelectFormField: FC> = ({
? formatText(placeholder as string, application, formatMessage)
: undefined
}
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore make web strict
onSelect={onSelect}
/>
diff --git a/libs/auth-api-lib/migrations/20240422100000-create-passkeys.js b/libs/auth-api-lib/migrations/20240422100000-create-passkeys.js
new file mode 100644
index 000000000000..bd770e5250ae
--- /dev/null
+++ b/libs/auth-api-lib/migrations/20240422100000-create-passkeys.js
@@ -0,0 +1,70 @@
+'use strict'
+
+module.exports = {
+ async up(queryInterface, Sequelize) {
+ return queryInterface.sequelize.transaction(async (transaction) => {
+ await queryInterface.createTable(
+ 'passkey',
+ {
+ passkey_id: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ public_key: {
+ type: Sequelize.BLOB,
+ allowNull: false,
+ },
+ user_sub: {
+ type: Sequelize.STRING,
+ primaryKey: true,
+ allowNull: false,
+ },
+ type: {
+ type: Sequelize.STRING,
+ primaryKey: true,
+ allowNull: false,
+ },
+ idp: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ name: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ },
+ audkenni_sim_number: {
+ type: Sequelize.STRING,
+ allowNull: true,
+ },
+ created: {
+ type: Sequelize.DATE,
+ allowNull: false,
+ defaultValue: Sequelize.fn('now'),
+ },
+ modified: {
+ type: Sequelize.DATE,
+ },
+ },
+ { transaction },
+ )
+
+ await queryInterface.addConstraint('passkey', {
+ fields: ['user_sub', 'type'], // Only possible to have one passkey of each type per user
+ type: 'unique',
+ name: 'passkey_primary_key',
+ transaction,
+ })
+
+ await queryInterface.addIndex('passkey', ['user_sub', 'type'], {
+ name: 'passkey_user_sub_type_idx',
+ transaction,
+ })
+ })
+ },
+
+ async down(queryInterface) {
+ return queryInterface.sequelize.transaction(async (transaction) => {
+ await queryInterface.dropTable('passkey', { transaction })
+ })
+ },
+}
diff --git a/libs/auth-api-lib/seeders/data/scope-application-system-admin.ts b/libs/auth-api-lib/seeders/data/scope-application-system-admin.ts
index b19aad01bfb0..7e8f2e5c72c3 100644
--- a/libs/auth-api-lib/seeders/data/scope-application-system-admin.ts
+++ b/libs/auth-api-lib/seeders/data/scope-application-system-admin.ts
@@ -3,7 +3,7 @@ import { compose, createScope } from './helpers'
export const up = compose(
createScope({
name: '@admin.island.is/application-system:institution',
- displayName: 'Umsóknarkerfi Ísland.is',
+ displayName: 'Umsóknarkerfi',
description: 'Umsjón með umsóknir fyrir stofnanir',
accessControlled: true,
addToClients: ['@admin.island.is/web'],
@@ -15,7 +15,7 @@ export const up = compose(
}),
createScope({
name: '@admin.island.is/application-system',
- displayName: 'Umsóknarkerfi Ísland.is',
+ displayName: 'Umsóknarkerfi',
description: 'Umsjón með umsóknir',
accessControlled: true,
addToClients: ['@admin.island.is/web'],
diff --git a/libs/auth-api-lib/src/index.ts b/libs/auth-api-lib/src/index.ts
index 6e320235aa40..34305b1979e7 100644
--- a/libs/auth-api-lib/src/index.ts
+++ b/libs/auth-api-lib/src/index.ts
@@ -179,3 +179,8 @@ export * from './lib/personal-representative/dto/paginated-personal-representati
export * from './lib/personal-representative/dto/personal-representative-scope-permission.dto'
export * from './lib/clients/admin/dto/admin-create-client.dto'
export * from './lib/clients/admin/dto/admin-client.dto'
+
+// Passkeys core module
+export * from './lib/passkeys-core/passkeys-core.module'
+export * from './lib/passkeys-core/passkeys-core.service'
+export * from './lib/passkeys-core/passkeys-core.config'
diff --git a/libs/auth-api-lib/src/lib/clients/admin/admin-clients.service.ts b/libs/auth-api-lib/src/lib/clients/admin/admin-clients.service.ts
index 2db2dccbf30e..9022d004b352 100644
--- a/libs/auth-api-lib/src/lib/clients/admin/admin-clients.service.ts
+++ b/libs/auth-api-lib/src/lib/clients/admin/admin-clients.service.ts
@@ -414,17 +414,19 @@ export class AdminClientsService {
supportsProcuringHolders,
supportsPersonalRepresentatives,
supportsLegalGuardians,
- ...rest
+ ...clientAttributes
} = data.clientAttributes
- if (Object.keys(data.clientAttributes as object).length > 0) {
+ if (Object.keys(clientAttributes as object).length > 0) {
// Update includes client base attributes
+ const refreshTokenExpiration = data.refreshTokenExpiration
+ ? translateRefreshTokenExpiration(data.refreshTokenExpiration)
+ : undefined
+
await this.clientModel.update(
{
- ...rest,
- refreshTokenExpiration: translateRefreshTokenExpiration(
- data.refreshTokenExpiration,
- ),
+ ...clientAttributes,
+ refreshTokenExpiration,
},
{
where: {
diff --git a/libs/auth-api-lib/src/lib/passkeys-core/models/passkey.model.ts b/libs/auth-api-lib/src/lib/passkeys-core/models/passkey.model.ts
new file mode 100644
index 000000000000..c10d149d3607
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/passkeys-core/models/passkey.model.ts
@@ -0,0 +1,88 @@
+import {
+ Column,
+ CreatedAt,
+ DataType,
+ Table,
+ UpdatedAt,
+ PrimaryKey,
+ Model,
+} from 'sequelize-typescript'
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
+import type {
+ CreationOptional,
+ InferAttributes,
+ InferCreationAttributes,
+} from 'sequelize'
+
+@Table({
+ tableName: 'passkey',
+ indexes: [
+ {
+ unique: true,
+ fields: ['user_sub', 'type'],
+ },
+ ],
+})
+export class PasskeyModel extends Model<
+ InferAttributes,
+ InferCreationAttributes
+> {
+ @PrimaryKey
+ @Column({
+ type: DataType.STRING,
+ primaryKey: true,
+ allowNull: false,
+ })
+ @ApiProperty()
+ passkey_id!: string
+
+ @Column({
+ type: DataType.BLOB,
+ allowNull: false,
+ })
+ @ApiProperty()
+ public_key!: Uint8Array
+
+ @Column({
+ type: DataType.STRING,
+ allowNull: false,
+ })
+ @ApiProperty()
+ user_sub!: string
+
+ @Column({
+ type: DataType.STRING,
+ allowNull: false,
+ })
+ @ApiProperty()
+ type!: string
+
+ @Column({
+ type: DataType.STRING,
+ allowNull: false,
+ })
+ @ApiProperty()
+ audkenni_sim_number!: string
+
+ @Column({
+ type: DataType.STRING,
+ allowNull: false,
+ })
+ @ApiProperty()
+ name!: string
+
+ @Column({
+ type: DataType.STRING,
+ allowNull: false,
+ })
+ @ApiProperty()
+ idp!: string
+
+ @CreatedAt
+ @ApiProperty()
+ readonly created!: CreationOptional
+
+ @UpdatedAt
+ @ApiPropertyOptional()
+ readonly modified?: Date
+}
diff --git a/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.cache.ts b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.cache.ts
new file mode 100644
index 000000000000..df16a939ee1c
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.cache.ts
@@ -0,0 +1,31 @@
+import { DynamicModule } from '@nestjs/common'
+import { CacheModule as NestCacheModule } from '@nestjs/cache-manager'
+import { redisInsStore } from 'cache-manager-ioredis-yet'
+import { createRedisCluster } from '@island.is/cache'
+import { ConfigType } from '@nestjs/config'
+import { PasskeysCoreConfig } from './passkeys-core.config'
+
+let CacheModule: DynamicModule
+
+export const CACHE_MODULE_KEY = 'PasskeysCoreModuleCache'
+
+if (process.env.NODE_ENV === 'test' || process.env.INIT_SCHEMA === 'true') {
+ CacheModule = NestCacheModule.register()
+} else {
+ CacheModule = NestCacheModule.registerAsync({
+ useFactory: (config: ConfigType) => {
+ return {
+ store: redisInsStore(
+ createRedisCluster({
+ name: 'passkeys-core',
+ ssl: config.redis.ssl,
+ nodes: config.redis.nodes,
+ }),
+ ),
+ }
+ },
+ inject: [PasskeysCoreConfig.KEY],
+ })
+}
+
+export { CacheModule }
diff --git a/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.config.ts b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.config.ts
new file mode 100644
index 000000000000..3a34c2438c9f
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.config.ts
@@ -0,0 +1,53 @@
+import { defineConfig } from '@island.is/nest/config'
+import { isRunningOnEnvironment } from '@island.is/shared/utils'
+import { z } from 'zod'
+
+const PasskeysCoreModuleSchema = z.object({
+ redis: z.object({
+ nodes: z.array(z.string()),
+ ssl: z.boolean(),
+ }),
+ passkey: z.object({
+ rpId: z.string(),
+ rpName: z.string(),
+ allowedOrigins: z.array(z.string()),
+ challengeTtl: z.number(),
+ maxAgeDays: z.number(),
+ }),
+})
+
+export const PasskeysCoreConfig = defineConfig({
+ name: 'PasskeysCoreModuleCache',
+ schema: PasskeysCoreModuleSchema,
+ load: (env) => {
+ const config: z.infer = {
+ redis: {
+ nodes: env.requiredJSON('REDIS_NODES', [
+ 'localhost:7000',
+ 'localhost:7001',
+ 'localhost:7002',
+ 'localhost:7003',
+ 'localhost:7004',
+ 'localhost:7005',
+ ]),
+ ssl: !isRunningOnEnvironment('local'),
+ },
+ passkey: {
+ rpId: env.required('PASSKEY_CORE_RP_ID', 'localhost'),
+ rpName: env.required('PASSKEY_CORE_RP_NAME', 'Island.is'),
+ allowedOrigins: env
+ .required('PASSKEY_CORE_ALLOWED_ORIGINS', 'http://localhost:4200')
+ .split(','),
+ challengeTtl: Number(
+ env.required(
+ 'PASSKEY_CORE_CHALLENGE_TTL_MS',
+ (2 * 60 * 1000).toString(),
+ ),
+ ),
+ maxAgeDays: env.optionalJSON('PASSKEY_CORE_MAX_AGE_DAYS') ?? 365,
+ },
+ }
+
+ return config
+ },
+})
diff --git a/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.module.ts b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.module.ts
new file mode 100644
index 000000000000..725ef4cdac14
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.module.ts
@@ -0,0 +1,12 @@
+import { Module } from '@nestjs/common'
+import { PasskeysCoreService } from './passkeys-core.service'
+import { SequelizeModule } from '@nestjs/sequelize'
+import { PasskeyModel } from './models/passkey.model'
+import { CacheModule } from './passkeys-core.cache'
+
+@Module({
+ imports: [SequelizeModule.forFeature([PasskeyModel]), CacheModule],
+ providers: [PasskeysCoreService],
+ exports: [PasskeysCoreService],
+})
+export class PasskeysCoreModule {}
diff --git a/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.service.spec.ts b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.service.spec.ts
new file mode 100644
index 000000000000..e0a522801041
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.service.spec.ts
@@ -0,0 +1,249 @@
+import { Module, Type } from '@nestjs/common'
+import {
+ SequelizeModule,
+ getModelToken,
+ getConnectionToken,
+} from '@nestjs/sequelize'
+import assert from 'assert'
+import { Sequelize } from 'sequelize-typescript'
+import type {
+ VerifyAuthenticationResponseOpts,
+ VerifyRegistrationResponseOpts,
+} from '@simplewebauthn/server'
+
+import { TestApp, testServer, useDatabase } from '@island.is/testing/nest'
+
+import { SequelizeConfigService } from '../core/sequelizeConfig.service'
+import { PasskeyModel } from './models/passkey.model'
+import { PasskeysCoreModule } from './passkeys-core.module'
+import { PasskeysCoreService } from './passkeys-core.service'
+import { ConfigModule } from '@nestjs/config'
+import { PasskeysCoreConfig } from './passkeys-core.config'
+
+const {
+ verifyRegistrationResponse,
+ verifyAuthenticationResponse,
+} = require('@simplewebauthn/server') // eslint-disable-line @typescript-eslint/no-var-requires
+
+jest.mock('@simplewebauthn/server', () => ({
+ __esModule: true,
+ ...jest.requireActual('@simplewebauthn/server'),
+ verifyRegistrationResponse: jest.fn(),
+ verifyAuthenticationResponse: jest.fn(),
+}))
+
+const TEST_AUTHORIZATION_TOKEN =
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiR2VydmltYWR1ciB0ZXN0IiwiaWRwIjoiZ2VydmltYWR1ciJ9.nwPzZbpXWWBh2WFoHdCY0q9EwRBKBWwANVqF_c0cIPs'
+
+@Module({
+ imports: [
+ ConfigModule.forRoot({
+ isGlobal: true,
+ load: [PasskeysCoreConfig],
+ }),
+ SequelizeModule.forRootAsync({
+ useClass: SequelizeConfigService,
+ }),
+ PasskeysCoreModule,
+ ],
+})
+class TestModule {}
+
+const USER_SUB = '1234567890'
+
+describe('PasskeyCoreService', () => {
+ let app: TestApp
+ let passkeysCoreService: PasskeysCoreService
+ let passkeyModel: typeof PasskeyModel
+ let sequelize: Sequelize
+
+ beforeAll(async () => {
+ app = await testServer({
+ appModule: TestModule,
+ hooks: [
+ useDatabase({ type: 'postgres', provider: SequelizeConfigService }),
+ ],
+ })
+
+ sequelize = await app.resolve(getConnectionToken() as Type)
+
+ passkeysCoreService = app.get(PasskeysCoreService)
+ passkeyModel = app.get(getModelToken(PasskeyModel))
+ })
+
+ afterEach(async () => {
+ await sequelize.transaction(async (transaction) => {
+ await passkeyModel.destroy({ where: {}, transaction })
+ })
+ })
+
+ afterAll(async () => {
+ await app.cleanUp()
+ jest.restoreAllMocks()
+ })
+
+ describe('register', () => {
+ it('should generate registration options', async () => {
+ const user = {
+ sub: USER_SUB,
+ authorization: TEST_AUTHORIZATION_TOKEN,
+ } as any // since only user.sub is used in the function
+
+ const opts = await passkeysCoreService.generateRegistrationOptions(user)
+
+ assert(opts)
+ expect(typeof opts.challenge).toBe('string')
+ expect(opts.rp.id).toBe('localhost')
+ })
+
+ it('should verify registration', async () => {
+ const user = {
+ sub: USER_SUB,
+ authorization: TEST_AUTHORIZATION_TOKEN,
+ } as any // since only user.sub is used in the function
+
+ await passkeysCoreService.generateRegistrationOptions(user)
+
+ // mock the verifyRegistration method since we don't have a browser client to create it
+ verifyRegistrationResponse.mockImplementation(
+ async (options: VerifyRegistrationResponseOpts) => {
+ return {
+ verified: true,
+ registrationInfo: {
+ credentialID: options.response.id,
+ credentialPublicKey:
+ Buffer.from('test-public-key').toString('base64'),
+ counter: 0,
+ },
+ }
+ },
+ )
+
+ const verification = await passkeysCoreService.verifyRegistration(user, {
+ // Mock the registration response that the client would send
+ id: '1337',
+ // ... only the id is used in the mocked function above
+ } as any)
+
+ expect(verification.verified).toBe(true)
+ })
+ })
+
+ describe('authenticate', () => {
+ it('should not generate authentication options when passkey is not found for user', async () => {
+ const user = {
+ sub: USER_SUB,
+ authorization: TEST_AUTHORIZATION_TOKEN,
+ } as any // since only user.sub is used in the function
+
+ await expect(() =>
+ passkeysCoreService.generateAuthenticationOptions(user),
+ ).rejects.toThrow('Passkey not found')
+ })
+
+ it('should generate authentication options when passkey is found for user', async () => {
+ // Create a passkey for the user
+ await passkeyModel.create({
+ user_sub: USER_SUB,
+ passkey_id: '123',
+ public_key: new TextEncoder().encode('public_key'),
+ audkenni_sim_number: '123',
+ name: 'Tester',
+ type: 'IslandisApp',
+ idp: 'gervimadur',
+ })
+
+ const user = {
+ sub: USER_SUB,
+ authorization: TEST_AUTHORIZATION_TOKEN,
+ } as any // since only user.sub is used in the function
+
+ const opts = await passkeysCoreService.generateAuthenticationOptions(user)
+
+ assert(opts)
+ expect(typeof opts.challenge).toBe('string')
+ })
+
+ it('should not verify authentication using a passkey older than config.maxAgeDays', async () => {
+ const PASSKEY_ID = '1337'
+ const PASSKEY_AGE_IN_DAYS = 366 // 1 year and 1 day ago (1 day over default maxAgeDays)
+
+ // Create a passkey for the user
+ await passkeyModel.create({
+ user_sub: USER_SUB,
+ passkey_id: PASSKEY_ID,
+ public_key: new TextEncoder().encode('public_key'),
+ audkenni_sim_number: '123',
+ name: 'Tester',
+ type: 'IslandisApp',
+ idp: 'gervimadur',
+ created: new Date(
+ Date.now() - 1000 * 60 * 60 * 24 * PASSKEY_AGE_IN_DAYS,
+ ),
+ })
+
+ const user = {
+ sub: USER_SUB,
+ authorization: TEST_AUTHORIZATION_TOKEN,
+ } as any // since only user.sub is used in the function
+
+ await expect(() =>
+ passkeysCoreService.generateAuthenticationOptions(user),
+ ).rejects.toThrow('Passkey not found')
+ })
+
+ it('should verify authentication based on authentication options', async () => {
+ const PASSKEY_ID = '1337'
+
+ // Create a passkey for the user
+ await passkeyModel.create({
+ user_sub: USER_SUB,
+ passkey_id: PASSKEY_ID,
+ public_key: new TextEncoder().encode('public_key'),
+ audkenni_sim_number: '123',
+ name: 'Tester',
+ type: 'IslandisApp',
+ idp: 'gervimadur',
+ })
+
+ const user = {
+ sub: USER_SUB,
+ authorization: TEST_AUTHORIZATION_TOKEN,
+ } as any // since only user.sub is used in the function
+
+ const opts = await passkeysCoreService.generateAuthenticationOptions(user)
+
+ // mock the verifyAuthentication method since we don't have a browser client to create it
+ verifyAuthenticationResponse.mockImplementation(
+ async (options: VerifyAuthenticationResponseOpts) => {
+ return {
+ verified: true,
+ registrationInfo: {
+ credentialID: options.response.id,
+ credentialPublicKey:
+ Buffer.from('test-public-key').toString('base64'),
+ counter: 0,
+ },
+ }
+ },
+ )
+
+ const verification = await passkeysCoreService.verifyAuthentication({
+ // Mock the authentication response that the client would send
+ id: PASSKEY_ID,
+ // ... only the id is used in the mocked function above
+ // and then the clientDataJSON is used by the service function
+ response: {
+ clientDataJSON: Buffer.from(
+ JSON.stringify({
+ // to find the challenge from the authentication options
+ challenge: opts.challenge,
+ }),
+ ).toString('base64'),
+ },
+ } as any)
+
+ expect(verification.verified).toBe(true)
+ })
+ })
+})
diff --git a/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.service.ts b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.service.ts
new file mode 100644
index 000000000000..a47bedb8e5f3
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.service.ts
@@ -0,0 +1,283 @@
+import { BadRequestException, Inject, Injectable } from '@nestjs/common'
+import { InjectModel } from '@nestjs/sequelize'
+import { Cache as CacheManager } from 'cache-manager'
+import { InferAttributes, InferCreationAttributes, Op } from 'sequelize'
+import addDays from 'date-fns/addDays'
+
+import {
+ // Authentication
+ generateAuthenticationOptions,
+ verifyAuthenticationResponse,
+ // Registration
+ generateRegistrationOptions,
+ verifyRegistrationResponse,
+} from '@simplewebauthn/server'
+
+import type {
+ GenerateRegistrationOptionsOpts,
+ VerifiedRegistrationResponse,
+ VerifyRegistrationResponseOpts,
+} from '@simplewebauthn/server'
+
+import type {
+ RegistrationResponseJSON,
+ PublicKeyCredentialRequestOptionsJSON,
+ PublicKeyCredentialCreationOptionsJSON,
+ AuthenticationResponseJSON,
+} from '@simplewebauthn/types'
+
+import type { User } from '@island.is/auth-nest-tools'
+import type { Logger } from '@island.is/logging'
+import { LOGGER_PROVIDER } from '@island.is/logging'
+
+import { PasskeyModel } from './models/passkey.model'
+import { getTokenInfo, getUserId } from './passkeys-core.utils'
+import { CACHE_MANAGER } from '@nestjs/cache-manager'
+import { PasskeysCoreConfig } from './passkeys-core.config'
+import { ConfigType } from '@nestjs/config'
+
+const PASSKEY_TYPE = 'IslandApp'
+
+@Injectable()
+export class PasskeysCoreService {
+ constructor(
+ @InjectModel(PasskeyModel)
+ private passkeyModel: typeof PasskeyModel,
+ @Inject(LOGGER_PROVIDER)
+ private logger: Logger,
+ @Inject(CACHE_MANAGER)
+ private readonly cacheManager: CacheManager,
+ @Inject(PasskeysCoreConfig.KEY)
+ private readonly config: ConfigType,
+ ) {}
+
+ async generateRegistrationOptions(user: User) {
+ const tokenInfo = getTokenInfo(user.authorization)
+
+ const opts: GenerateRegistrationOptionsOpts = {
+ rpName: this.config.passkey.rpName,
+ rpID: this.config.passkey.rpId,
+ userName: tokenInfo.name,
+ timeout: 60000,
+ attestationType: 'direct',
+ authenticatorSelection: {
+ residentKey: 'discouraged',
+ userVerification: 'required',
+ },
+ // ES256 and RS256
+ supportedAlgorithmIDs: [-7, -257],
+ }
+
+ const options = await generateRegistrationOptions(opts)
+
+ // Save to redis
+ await this.saveToCache(getUserId(user), options.challenge)
+
+ return {
+ ...options,
+ user: {
+ ...options.user,
+ displayName: tokenInfo.name,
+ },
+ } as PublicKeyCredentialCreationOptionsJSON
+ }
+
+ async verifyRegistration(
+ user: User,
+ verificationResponse: RegistrationResponseJSON,
+ ) {
+ const tokenInfo = getTokenInfo(user.authorization)
+
+ const expectedChallenge = await this.getFromCache(getUserId(user))
+
+ let verification: VerifiedRegistrationResponse | undefined
+ try {
+ const verificationOptions: VerifyRegistrationResponseOpts = {
+ response: verificationResponse,
+ expectedChallenge,
+ expectedOrigin: this.config.passkey.allowedOrigins,
+ expectedRPID: this.config.passkey.rpId,
+ requireUserVerification: false,
+ }
+
+ verification = await verifyRegistrationResponse(verificationOptions)
+ } catch (e) {
+ this.logger.error('Registration verification failed', e)
+ throw new BadRequestException('Registration verification failed')
+ }
+
+ const { verified, registrationInfo } = verification
+
+ const success = verified && registrationInfo
+
+ if (!success) {
+ throw new BadRequestException('Registration verification failed')
+ }
+
+ const passkey = {
+ passkey_id: registrationInfo.credentialID,
+ public_key: Buffer.from(registrationInfo.credentialPublicKey),
+ user_sub: getUserId(user),
+ type: PASSKEY_TYPE,
+ audkenni_sim_number: user.audkenniSimNumber ?? '',
+ name: tokenInfo.name,
+ idp: tokenInfo.idp,
+ }
+
+ await this.passkeyModel.upsert(passkey, {
+ conflictFields: ['user_sub', 'type'],
+ })
+
+ return { verified }
+ }
+
+ async generateAuthenticationOptions(
+ user: User,
+ ): Promise {
+ const passkey = await this.passkeyModel.findOne({
+ where: {
+ user_sub: user.sub,
+ created: {
+ [Op.gte]: addDays(new Date(), -this.config.passkey.maxAgeDays),
+ },
+ },
+ })
+
+ if (!passkey) {
+ throw new BadRequestException('Passkey not found')
+ }
+
+ // Generate the authentication options
+ const options = await generateAuthenticationOptions({
+ rpID: this.config.passkey.rpId,
+ allowCredentials: [
+ {
+ id: passkey.passkey_id,
+ transports: ['internal'],
+ },
+ ],
+ })
+
+ // Save to redis
+ await this.saveToCache(options.challenge, passkey.passkey_id)
+
+ return options
+ }
+
+ async verifyAuthenticationString(responseAsString: string) {
+ try {
+ const decodedJson = Buffer.from(responseAsString, 'base64').toString(
+ 'utf-8',
+ )
+ const parsedJson = JSON.parse(decodedJson) as AuthenticationResponseJSON
+
+ return this.verifyAuthentication(parsedJson)
+ } catch (e) {
+ this.logger.error('Invalid passkey format', e)
+ throw new BadRequestException('Invalid passkey format')
+ }
+ }
+
+ async verifyAuthentication(response: AuthenticationResponseJSON) {
+ const passkey = await this.passkeyModel.findOne({
+ where: {
+ passkey_id: response.id,
+ created: {
+ [Op.gte]: addDays(new Date(), -this.config.passkey.maxAgeDays),
+ },
+ },
+ })
+
+ if (!passkey) {
+ throw new BadRequestException('Passkey not found')
+ }
+
+ let challenge: string
+
+ try {
+ challenge = JSON.parse(
+ Buffer.from(response.response.clientDataJSON, 'base64').toString(
+ 'utf-8',
+ ),
+ ).challenge
+ } catch (e) {
+ this.logger.log('Invalid clientDataJSON', e)
+ throw new BadRequestException('Invalid clientDataJSON')
+ }
+
+ const expectedPasskeyId = await this.getFromCache(challenge)
+
+ if (expectedPasskeyId !== passkey.passkey_id) {
+ throw new BadRequestException('Passkey not found')
+ }
+
+ let verification
+ try {
+ verification = await verifyAuthenticationResponse({
+ response,
+ expectedChallenge: challenge,
+ expectedOrigin: this.config.passkey.allowedOrigins,
+ expectedRPID: this.config.passkey.rpId,
+ authenticator: {
+ credentialID: passkey.passkey_id,
+ credentialPublicKey: passkey.public_key,
+ counter: 0, // TODO: Store in db and increment on authentication
+ transports: ['internal'],
+ },
+ })
+ } catch (error) {
+ this.logger.error('Auth verification failed', error)
+ throw new BadRequestException('Auth verification failed')
+ }
+
+ const { verified } = verification
+
+ if (!verified) {
+ throw new BadRequestException('Auth verification failed')
+ }
+
+ return {
+ verified,
+ idp: passkey.idp,
+ sub: passkey.user_sub,
+ }
+ }
+
+ async deletePasskeyById(passkeyId: string) {
+ await this.passkeyModel.destroy({
+ where: {
+ passkey_id: passkeyId,
+ },
+ })
+ }
+
+ async deletePasskeyByUser(user: User) {
+ await this.passkeyModel.destroy({
+ where: {
+ user_sub: getUserId(user),
+ },
+ })
+ }
+
+ private async saveToCache(
+ key: string,
+ value: string,
+ ttl = this.config.passkey.challengeTtl,
+ ) {
+ await this.cacheManager.set(key, value, ttl)
+ }
+
+ private async getFromCache(key: string, deleteAfterGetting = true) {
+ const value = await this.cacheManager.get(key)
+
+ if (!value || typeof value !== 'string' || !value.length) {
+ throw new BadRequestException('Not found')
+ }
+
+ if (deleteAfterGetting) {
+ await this.cacheManager.del(key)
+ }
+
+ return value
+ }
+}
diff --git a/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.utils.ts b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.utils.ts
new file mode 100644
index 000000000000..7f7292f95788
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/passkeys-core/passkeys-core.utils.ts
@@ -0,0 +1,30 @@
+import jwt from 'jsonwebtoken'
+import pick from 'lodash/pick'
+
+import type { User } from '@island.is/auth-nest-tools'
+import { BadRequestException } from '@nestjs/common'
+
+export const getUserId = (user: User) => {
+ if (!user.sub) {
+ throw new BadRequestException('No user sub found')
+ }
+
+ return user.sub
+}
+
+interface TokenInfo {
+ name: string
+ idp: string
+}
+
+export const getTokenInfo = (token: string) => {
+ const decodedToken = jwt.decode(token.replace('Bearer ', ''), {
+ complete: true,
+ })
+
+ if (!decodedToken) {
+ throw new BadRequestException('Invalid token')
+ }
+
+ return pick(decodedToken.payload, ['name', 'idp']) as TokenInfo
+}
diff --git a/libs/clients/althingi-ombudsman/src/lib/apiConfiguration.ts b/libs/clients/althingi-ombudsman/src/lib/apiConfiguration.ts
index 474d7717b658..f321975b8dde 100644
--- a/libs/clients/althingi-ombudsman/src/lib/apiConfiguration.ts
+++ b/libs/clients/althingi-ombudsman/src/lib/apiConfiguration.ts
@@ -1,9 +1,5 @@
import { createEnhancedFetch } from '@island.is/clients/middlewares'
-import {
- ConfigType,
- IdsClientConfig,
- XRoadConfig,
-} from '@island.is/nest/config'
+import { ConfigType, XRoadConfig } from '@island.is/nest/config'
import { AlthingiOmbudsmanClientConfig } from './clients-althingi-ombudsman.config'
import { Configuration } from '../gen/fetch/dev'
@@ -12,7 +8,6 @@ export const ApiConfiguration = {
useFactory: (
xRoadConfig: ConfigType,
config: ConfigType,
- idsClientConfig: ConfigType,
) =>
new Configuration({
fetchApi: createEnhancedFetch({
@@ -26,9 +21,5 @@ export const ApiConfiguration = {
'X-Road-Client': xRoadConfig.xRoadClient,
},
}),
- inject: [
- XRoadConfig.KEY,
- AlthingiOmbudsmanClientConfig.KEY,
- IdsClientConfig.KEY,
- ],
+ inject: [XRoadConfig.KEY, AlthingiOmbudsmanClientConfig.KEY],
}
diff --git a/libs/clients/auth/public-api/src/lib/apis.ts b/libs/clients/auth/public-api/src/lib/apis.ts
index ff759d6fddc0..a573e66d3fe6 100644
--- a/libs/clients/auth/public-api/src/lib/apis.ts
+++ b/libs/clients/auth/public-api/src/lib/apis.ts
@@ -1,7 +1,11 @@
-import { ActorDelegationsApi, Configuration } from '../../gen/fetch'
+import {
+ ActorDelegationsApi,
+ PasskeysApi,
+ Configuration,
+} from '../../gen/fetch'
import { ApiConfiguration } from './apiConfiguration'
-export const exportedApis = [ActorDelegationsApi].map((Api) => ({
+export const exportedApis = [ActorDelegationsApi, PasskeysApi].map((Api) => ({
provide: Api,
useFactory: (configuration: Configuration) => new Api(configuration),
inject: [ApiConfiguration.provide],
diff --git a/libs/clients/criminal-record/src/index.ts b/libs/clients/criminal-record/src/index.ts
index 7d4c8aa33427..73105ddb07fd 100644
--- a/libs/clients/criminal-record/src/index.ts
+++ b/libs/clients/criminal-record/src/index.ts
@@ -3,3 +3,4 @@ export * from './lib/criminalRecordApi.module'
export * from './lib/criminalRecordApi.types'
export { CriminalRecordApi } from './lib/criminalRecordApi.service'
+export { CriminalRecordClientConfig } from './lib/criminalRecordApi.config'
diff --git a/libs/clients/criminal-record/src/lib/apiConfiguration.ts b/libs/clients/criminal-record/src/lib/apiConfiguration.ts
new file mode 100644
index 000000000000..34451056ec17
--- /dev/null
+++ b/libs/clients/criminal-record/src/lib/apiConfiguration.ts
@@ -0,0 +1,25 @@
+import { ConfigType, XRoadConfig } from '@island.is/nest/config'
+import { CriminalRecordClientConfig } from './criminalRecordApi.config'
+import { Configuration } from '../../gen/fetch'
+import { createEnhancedFetch } from '@island.is/clients/middlewares'
+
+export const ApiConfiguration = {
+ provide: 'CriminalRecordClientApiConfiguration',
+ useFactory: (
+ xRoadConfig: ConfigType,
+ config: ConfigType,
+ ) =>
+ new Configuration({
+ fetchApi: createEnhancedFetch({
+ name: 'clients-criminal-record',
+ organizationSlug: 'rikislogreglustjori',
+ }),
+ basePath: `${xRoadConfig.xRoadBasePath}/${config.xRoadServicePath}`,
+ headers: {
+ 'X-Road-Client': xRoadConfig.xRoadClient,
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ }),
+ inject: [XRoadConfig.KEY, CriminalRecordClientConfig.KEY],
+}
diff --git a/libs/clients/criminal-record/src/lib/criminalRecordApi.config.ts b/libs/clients/criminal-record/src/lib/criminalRecordApi.config.ts
new file mode 100644
index 000000000000..7afb400a90b9
--- /dev/null
+++ b/libs/clients/criminal-record/src/lib/criminalRecordApi.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from '@island.is/nest/config'
+import * as z from 'zod'
+const schema = z.object({
+ xRoadServicePath: z.string(),
+})
+
+export const CriminalRecordClientConfig = defineConfig>({
+ name: 'CriminalRecordClient',
+ load(env) {
+ return {
+ xRoadServicePath: env.required(
+ 'XROAD_CRIMINAL_RECORD_PATH',
+ 'r1/IS-DEV/GOV/10005/Logreglan-Protected/Sakavottord-PDF-v2',
+ ),
+ }
+ },
+})
diff --git a/libs/clients/criminal-record/src/lib/criminalRecordApi.module.ts b/libs/clients/criminal-record/src/lib/criminalRecordApi.module.ts
index b14d43d398a6..1c205dc22154 100644
--- a/libs/clients/criminal-record/src/lib/criminalRecordApi.module.ts
+++ b/libs/clients/criminal-record/src/lib/criminalRecordApi.module.ts
@@ -1,54 +1,19 @@
-import { DynamicModule } from '@nestjs/common'
-import {
- createEnhancedFetch,
- EnhancedFetchOptions,
-} from '@island.is/clients/middlewares'
+import { Module } from '@nestjs/common'
import { CriminalRecordApi } from './criminalRecordApi.service'
-import { CrimeCertificateApi, Configuration } from '../../gen/fetch'
+import { Configuration, CrimeCertificateApi } from '../../gen/fetch'
+import { ApiConfiguration } from './apiConfiguration'
-export interface CriminalRecordApiConfig {
- xroadBaseUrl: string
- xroadClientId: string
- xroadPath: string
- fetchOptions?: Partial
-}
-
-const configFactory = (config: CriminalRecordApiConfig, basePath: string) => ({
- fetchApi: createEnhancedFetch({
- name: 'clients-criminal-record',
- organizationSlug: 'rikislogreglustjori',
- ...config.fetchOptions,
- }),
- headers: {
- 'X-Road-Client': config.xroadClientId,
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- },
- basePath,
+@Module({
+ providers: [
+ ApiConfiguration,
+ {
+ provide: CriminalRecordApi,
+ useFactory: (configuration: Configuration) => {
+ return new CriminalRecordApi(new CrimeCertificateApi(configuration))
+ },
+ inject: [ApiConfiguration.provide],
+ },
+ ],
+ exports: [CriminalRecordApi],
})
-
-export class CriminalRecordApiModule {
- static register(config: CriminalRecordApiConfig): DynamicModule {
- return {
- module: CriminalRecordApiModule,
- providers: [
- {
- provide: CriminalRecordApi,
- useFactory: () => {
- const api = new CrimeCertificateApi(
- new Configuration(
- configFactory(
- config,
- `${config.xroadBaseUrl}/${config.xroadPath}`,
- ),
- ),
- )
-
- return new CriminalRecordApi(api)
- },
- },
- ],
- exports: [CriminalRecordApi],
- }
- }
-}
+export class CriminalRecordApiModule {}
diff --git a/libs/clients/data-protection-complaint/src/index.ts b/libs/clients/data-protection-complaint/src/index.ts
index ee1489664d88..763106640e32 100644
--- a/libs/clients/data-protection-complaint/src/index.ts
+++ b/libs/clients/data-protection-complaint/src/index.ts
@@ -9,4 +9,4 @@ export {
DocumentInfo,
} from './gen/fetch'
export { TokenMiddleware } from './lib/data-protection-complaint-client.middleware'
-export { DataProtectionComplaintClientConfig } from './lib/config/'
+export { DataProtectionComplaintClientConfig } from './lib/data-protection-complaint-client.config'
diff --git a/libs/clients/data-protection-complaint/src/lib/apiConfiguration.ts b/libs/clients/data-protection-complaint/src/lib/apiConfiguration.ts
new file mode 100644
index 000000000000..5d8d873f4a18
--- /dev/null
+++ b/libs/clients/data-protection-complaint/src/lib/apiConfiguration.ts
@@ -0,0 +1,26 @@
+import { ConfigType, XRoadConfig } from '@island.is/nest/config'
+import { DataProtectionComplaintClientConfig } from './data-protection-complaint-client.config'
+import { Configuration } from '../gen/fetch'
+import { createEnhancedFetch } from '@island.is/clients/middlewares'
+
+export const ApiConfiguration = {
+ provide: 'DataProtectionClientApiConfiguration',
+ useFactory: (
+ xRoadConfig: ConfigType,
+ config: ConfigType,
+ ) =>
+ new Configuration({
+ fetchApi: createEnhancedFetch({
+ name: 'data-protection-complaint-client',
+ organizationSlug: 'personuvernd',
+ logErrorResponseBody: true,
+ timeout: 60 * 1000, // 60 sec
+ }),
+ basePath: `${xRoadConfig.xRoadBasePath}/r1/${config.XRoadProviderId}`,
+ headers: {
+ 'X-Road-Client': xRoadConfig.xRoadClient,
+ Accept: 'application/json',
+ },
+ }),
+ inject: [XRoadConfig.KEY, DataProtectionComplaintClientConfig.KEY],
+}
diff --git a/libs/clients/data-protection-complaint/src/lib/apis.ts b/libs/clients/data-protection-complaint/src/lib/apis.ts
new file mode 100644
index 000000000000..d68ec30f55d3
--- /dev/null
+++ b/libs/clients/data-protection-complaint/src/lib/apis.ts
@@ -0,0 +1,23 @@
+import {
+ CaseApi,
+ ClientsApi,
+ Configuration,
+ DocumentApi,
+ MemoApi,
+ SecurityApi,
+} from '../gen/fetch'
+import { ApiConfiguration } from './apiConfiguration'
+
+export const exportedApis = [
+ DocumentApi,
+ CaseApi,
+ SecurityApi,
+ MemoApi,
+ ClientsApi,
+].map((Api) => ({
+ provide: Api,
+ useFactory: (configuration: Configuration) => {
+ return new Api(configuration)
+ },
+ inject: [ApiConfiguration.provide],
+}))
diff --git a/libs/clients/data-protection-complaint/src/lib/config/config.ts b/libs/clients/data-protection-complaint/src/lib/config/config.ts
index 04771bfd5c3f..6040aae65d5a 100644
--- a/libs/clients/data-protection-complaint/src/lib/config/config.ts
+++ b/libs/clients/data-protection-complaint/src/lib/config/config.ts
@@ -2,7 +2,5 @@ export const CLIENT_CONFIG = 'CLIENT_CONFIG'
export interface DataProtectionComplaintClientConfig {
username: string
password: string
- xRoadBaseUrl: string
XRoadProviderId: string
- xRoadClientId: string
}
diff --git a/libs/clients/data-protection-complaint/src/lib/data-protection-complaint-client.config.ts b/libs/clients/data-protection-complaint/src/lib/data-protection-complaint-client.config.ts
new file mode 100644
index 000000000000..2e3b6b6df78f
--- /dev/null
+++ b/libs/clients/data-protection-complaint/src/lib/data-protection-complaint-client.config.ts
@@ -0,0 +1,25 @@
+import { defineConfig } from '@island.is/nest/config'
+import * as z from 'zod'
+
+const schema = z.object({
+ username: z.string(),
+ password: z.string(),
+ XRoadProviderId: z.string(),
+})
+
+export const DataProtectionComplaintClientConfig = defineConfig<
+ z.infer
+>({
+ name: 'DataProtectionComplaintClient',
+ schema,
+ load(env) {
+ return {
+ password: env.required('DATA_PROTECTION_COMPLAINT_API_PASSWORD', ''),
+ username: env.required('DATA_PROTECTION_COMPLAINT_API_USERNAME', ''),
+ XRoadProviderId: env.required(
+ 'DATA_PROTECTION_COMPLAINT_XROAD_PROVIDER_ID',
+ '',
+ ),
+ }
+ },
+})
diff --git a/libs/clients/data-protection-complaint/src/lib/data-protection-complaint.module.ts b/libs/clients/data-protection-complaint/src/lib/data-protection-complaint.module.ts
index c0eae63e2d38..6fe914b8ae45 100644
--- a/libs/clients/data-protection-complaint/src/lib/data-protection-complaint.module.ts
+++ b/libs/clients/data-protection-complaint/src/lib/data-protection-complaint.module.ts
@@ -1,93 +1,25 @@
-import { DynamicModule } from '@nestjs/common'
-import {
- CaseApi,
- ClientsApi,
- Configuration,
- DocumentApi,
- MemoApi,
- SecurityApi,
-} from '../gen/fetch'
+import { Module } from '@nestjs/common'
+import { SecurityApi } from '../gen/fetch'
-import { logger } from '@island.is/logging'
-import { createEnhancedFetch } from '@island.is/clients/middlewares'
import { TokenMiddleware } from './data-protection-complaint-client.middleware'
-import { CLIENT_CONFIG, DataProtectionComplaintClientConfig } from './config'
+import { exportedApis } from './apis'
+import { ApiConfiguration } from './apiConfiguration'
+import { ConfigType } from '@island.is/nest/config'
+import { DataProtectionComplaintClientConfig } from './data-protection-complaint-client.config'
-export class ClientsDataProtectionComplaintModule {
- static register(config: DataProtectionComplaintClientConfig): DynamicModule {
- if (!config.xRoadBaseUrl) {
- logger.error('DataProtectionClient xRoadPath not provided.')
- }
- if (!config.password) {
- logger.error('DataProtectionClient password not provided.')
- }
- if (!config.username) {
- logger.error('DataProtectionClient username not provided.')
- }
- if (!config.xRoadClientId) {
- logger.error('DataProtectionClient xRoadClientId not provided.')
- }
- if (!config.XRoadProviderId) {
- logger.error('DataProtectionClient XRoadProviderId not provided.')
- }
-
- const basePath = `${config.xRoadBaseUrl}/r1/${config.XRoadProviderId}`
- const exportedApis = [
- DocumentApi,
- CaseApi,
- SecurityApi,
- MemoApi,
- ClientsApi,
- ]
- return {
- module: ClientsDataProtectionComplaintModule,
- providers: [
- {
- provide: CLIENT_CONFIG,
- useFactory: () => config,
- },
- {
- provide: SecurityApi,
- useFactory: () => {
- return new SecurityApi(
- new Configuration({
- fetchApi: createEnhancedFetch({
- name: 'data-protection-complaint-client',
- organizationSlug: 'personuvernd',
- logErrorResponseBody: true,
- timeout: 60 * 1000, // 60 sec
- }),
- basePath: basePath,
- headers: {
- 'X-Road-Client': config.xRoadClientId,
- Accept: 'application/json',
- },
- }),
- )
- },
- },
- TokenMiddleware,
- ...exportedApis.map((Api) => ({
- provide: Api,
- useFactory: () => {
- return new Api(
- new Configuration({
- fetchApi: createEnhancedFetch({
- name: 'data-protection-complaint-client',
- logErrorResponseBody: true,
- timeout: 60 * 1000, // 60 sec
- }),
- basePath: basePath,
- headers: {
- 'X-Road-Client': config.xRoadClientId,
- Accept: 'application/json',
- },
- }),
- )
- },
- })),
- ],
- exports: [...exportedApis, TokenMiddleware],
- }
- }
-}
+@Module({
+ exports: [...exportedApis, TokenMiddleware],
+ providers: [
+ ApiConfiguration,
+ {
+ provide: TokenMiddleware,
+ useFactory: (
+ config: ConfigType,
+ securityApi,
+ ) => new TokenMiddleware(config, securityApi),
+ inject: [DataProtectionComplaintClientConfig.KEY, SecurityApi],
+ },
+ ...exportedApis,
+ ],
+})
+export class ClientsDataProtectionComplaintModule {}
diff --git a/libs/clients/icelandic-health-insurance/health-insurance/src/index.ts b/libs/clients/icelandic-health-insurance/health-insurance/src/index.ts
index c5a4fab73a86..037c7eae8b9c 100644
--- a/libs/clients/icelandic-health-insurance/health-insurance/src/index.ts
+++ b/libs/clients/icelandic-health-insurance/health-insurance/src/index.ts
@@ -1,3 +1,3 @@
-export { HealthInsuranceV2Client } from './lib/clients-health-insurance-v2.module'
+export { HealthInsuranceV2ClientModule } from './lib/clients-health-insurance-v2.module'
export { DocumentApi, Configuration, PersonApi } from '../gen/fetch'
-export { HealthInsuranceV2Options } from './lib/clients-health-insurance-v2.config'
+export { HealthInsuranceV2ClientConfig } from './lib/clients-health-insurance-v2.config'
diff --git a/libs/clients/icelandic-health-insurance/health-insurance/src/lib/apiConfiguration.ts b/libs/clients/icelandic-health-insurance/health-insurance/src/lib/apiConfiguration.ts
new file mode 100644
index 000000000000..a2ae482ffe3e
--- /dev/null
+++ b/libs/clients/icelandic-health-insurance/health-insurance/src/lib/apiConfiguration.ts
@@ -0,0 +1,29 @@
+import { ConfigType, XRoadConfig } from '@island.is/nest/config'
+import { createEnhancedFetch } from '@island.is/clients/middlewares'
+import { Configuration } from '../../gen/fetch'
+import { HealthInsuranceV2ClientConfig } from './clients-health-insurance-v2.config'
+
+export const ApiConfiguration = {
+ provide: 'HealthInsuranceV2ClientApiConfiguration',
+ useFactory: (
+ xRoadConfig: ConfigType,
+ config: ConfigType,
+ ) =>
+ new Configuration({
+ fetchApi: createEnhancedFetch({
+ name: 'clients-health-insurance',
+ organizationSlug: 'sjukratryggingar',
+ logErrorResponseBody: true,
+ timeout: 20000, // needed because the external service is taking a while to respond to submitting the document
+ }),
+ basePath: `${xRoadConfig.xRoadBasePath}/r1/${config.xRoadServicePath}/islandis`,
+ headers: {
+ 'X-Road-Client': xRoadConfig.xRoadClient,
+ userName: `${config.username}`,
+ password: `${config.password}`,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ }),
+ inject: [XRoadConfig.KEY, HealthInsuranceV2ClientConfig.KEY],
+}
diff --git a/libs/clients/icelandic-health-insurance/health-insurance/src/lib/apis.ts b/libs/clients/icelandic-health-insurance/health-insurance/src/lib/apis.ts
new file mode 100644
index 000000000000..147cbbed733b
--- /dev/null
+++ b/libs/clients/icelandic-health-insurance/health-insurance/src/lib/apis.ts
@@ -0,0 +1,9 @@
+import { Configuration, DocumentApi, PersonApi } from '../../gen/fetch'
+import { ApiConfiguration } from './apiConfiguration'
+export const exportedApis = [DocumentApi, PersonApi].map((Api) => ({
+ provide: Api,
+ useFactory: (configuration: Configuration) => {
+ return new Api(configuration)
+ },
+ inject: [ApiConfiguration.provide],
+}))
diff --git a/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.config.ts b/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.config.ts
index 65027011cb20..d27d11bb823d 100644
--- a/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.config.ts
+++ b/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.config.ts
@@ -1,7 +1,24 @@
-export interface HealthInsuranceV2Options {
- xRoadBaseUrl: string
- xRoadProviderId: string
- xRoadClientId: string
- username: string
- password: string
-}
+import { defineConfig } from '@island.is/nest/config'
+import { z } from 'zod'
+
+const schema = z.object({
+ xRoadServicePath: z.string(),
+ username: z.string(),
+ password: z.string(),
+})
+export const HealthInsuranceV2ClientConfig = defineConfig<
+ z.infer
+>({
+ name: 'HealthInsuranceClient',
+ schema,
+ load(env) {
+ return {
+ xRoadServicePath: env.required(
+ 'XROAD_HEALTH_INSURANCE_ID',
+ 'IS-DEV/GOV/10007/SJUKRA-Protected',
+ ),
+ username: env.required('XROAD_HEALTH_INSURANCE_V2_XROAD_USERNAME', ''),
+ password: env.required('XROAD_HEALTH_INSURANCE_V2_XROAD_PASSWORD', ''),
+ }
+ },
+})
diff --git a/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.module.ts b/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.module.ts
index 828548a611da..6f5a991659b4 100644
--- a/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.module.ts
+++ b/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.module.ts
@@ -1,49 +1,9 @@
-import { DynamicModule } from '@nestjs/common'
-import { Configuration, DocumentApi, PersonApi } from '../../gen/fetch'
-import { HealthInsuranceV2Options } from './clients-health-insurance-v2.config'
-import { createEnhancedFetch } from '@island.is/clients/middlewares'
+import { Module } from '@nestjs/common'
+import { ApiConfiguration } from './apiConfiguration'
+import { exportedApis } from './apis'
-export class HealthInsuranceV2Client {
- static register(options: HealthInsuranceV2Options): DynamicModule {
- const { password, username, xRoadBaseUrl, xRoadClientId, xRoadProviderId } =
- options
- const basePath = `${xRoadBaseUrl}/r1/${xRoadProviderId}/islandis`
-
- const configuration = new Configuration({
- fetchApi: createEnhancedFetch({
- name: 'clients-health-insurance',
- organizationSlug: 'sjukratryggingar',
- logErrorResponseBody: true,
- timeout: 20000, // needed because the external service is taking a while to respond to submitting the document
- }),
- basePath: basePath,
- headers: {
- 'X-Road-Client': xRoadClientId,
- userName: `${username}`,
- password: `${password}`,
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- },
- })
-
- return {
- module: HealthInsuranceV2Client,
- imports: [],
- providers: [
- {
- provide: DocumentApi,
- useFactory: () => {
- return new DocumentApi(configuration)
- },
- },
- {
- provide: PersonApi,
- useFactory: () => {
- return new PersonApi(configuration)
- },
- },
- ],
- exports: [DocumentApi, PersonApi],
- }
- }
-}
+@Module({
+ exports: [...exportedApis],
+ providers: [ApiConfiguration, ...exportedApis],
+})
+export class HealthInsuranceV2ClientModule {}
diff --git a/libs/clients/signature-collection/jest.config.ts b/libs/clients/signature-collection/jest.config.ts
index d413ac4483d5..0b4181a9887a 100644
--- a/libs/clients/signature-collection/jest.config.ts
+++ b/libs/clients/signature-collection/jest.config.ts
@@ -2,10 +2,14 @@
export default {
displayName: 'clients-signature-collection',
preset: '../../../jest.preset.js',
+ globals: {},
testEnvironment: 'node',
transform: {
- '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }],
+ '^.+\\.[tj]sx?$': [
+ 'ts-jest',
+ { tsconfig: `${__dirname}/tsconfig.spec.json` },
+ ],
},
- moduleFileExtensions: ['ts', 'js', 'html'],
- coverageDirectory: '../../../coverage/libs/clients/signature-collection',
+ moduleFileExtensions: ['ts', 'js'],
+ coverageDirectory: '/coverage/libs/clients/signature-collection',
}
diff --git a/libs/clients/signature-collection/project.json b/libs/clients/signature-collection/project.json
index 5d7ac56fbd14..20d5326d4719 100644
--- a/libs/clients/signature-collection/project.json
+++ b/libs/clients/signature-collection/project.json
@@ -13,7 +13,7 @@
},
"test": {
"executor": "@nx/jest:jest",
- "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "outputs": ["{workspaceRoot}/coverage/libs/clients/signature-collection"],
"options": {
"jestConfig": "libs/clients/signature-collection/jest.config.ts",
"passWithNoTests": true
diff --git a/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts b/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts
new file mode 100644
index 000000000000..f6c469a453f0
--- /dev/null
+++ b/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts
@@ -0,0 +1,370 @@
+import { SignatureCollectionClientService } from './signature-collection.service'
+import {
+ MedmaelalistarApi,
+ MedmaelasofnunApi,
+ MedmaeliApi,
+ FrambodApi,
+ MedmaelalistiDTO,
+ MedmaelasofnunExtendedDTO,
+ EinstaklingurKosningInfoDTO,
+} from '../../gen/fetch'
+import { SignatureCollectionSharedClientService } from './signature-collection-shared.service'
+import { Test, TestingModule } from '@nestjs/testing'
+import { CreateListInput } from './signature-collection.types'
+import { User } from '@island.is/auth-nest-tools'
+import { LoggingModule } from '@island.is/logging'
+
+const user: User = {
+ nationalId: '0101302399',
+ authorization: '',
+ scope: [],
+ client: '',
+}
+const sofnun: MedmaelasofnunExtendedDTO[] = [
+ {
+ id: 123,
+ sofnunStart: new Date('01.01.1900'),
+ sofnunEnd: new Date('01.01.2199'),
+ svaedi: [{ id: 123 }],
+ frambodList: [{ id: 123, kennitala: '0101010119' }],
+ kosning: { id: 123, erMedmaelakosning: true },
+ kosningTegund: 'Forsetakosning',
+ },
+]
+const sofnunUser: EinstaklingurKosningInfoDTO = {
+ svaedi: { id: 123 },
+ kennitala: '0101302399',
+ maFrambod: true,
+ maFrambodInfo: { aldur: true, rikisfang: true },
+ frambod: { id: 123, kennitala: '0101302399' },
+}
+
+describe('MyService', () => {
+ let service: SignatureCollectionClientService
+ let listarApi: MedmaelalistarApi
+ let sofnunApi: MedmaelasofnunApi
+ let medmaeliApi: MedmaeliApi
+ let frambodApi: FrambodApi
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ SignatureCollectionClientService,
+ MedmaelalistarApi,
+ MedmaelasofnunApi,
+ MedmaeliApi,
+ FrambodApi,
+ SignatureCollectionSharedClientService,
+ ],
+ imports: [LoggingModule],
+ }).compile()
+ service = module.get(
+ SignatureCollectionClientService,
+ )
+ listarApi = module.get(MedmaelalistarApi)
+ sofnunApi = module.get(MedmaelasofnunApi)
+ medmaeliApi = module.get(MedmaeliApi)
+ frambodApi = module.get(FrambodApi)
+ })
+
+ it('should be defined', () => {
+ expect(service).toBeDefined()
+ })
+ it('canSign', async () => {
+ // User is not allowed to have more than one signature
+ // They are marked as invalid but count as participation
+
+ // Arrange
+ // Act
+ const success = await service.canSign({
+ activeSignature: undefined,
+ signatures: [],
+ requirementsMet: true,
+ canSignInfo: { aldur: true, rikisfang: true },
+ })
+ const alreadySigned = await service.canSign({
+ activeSignature: { id: 123 },
+ })
+ const canNotSign = await service.canSign({
+ activeSignature: undefined,
+ canSignInfo: { aldur: false, rikisfang: false },
+ })
+ const invalidSignature = await service.canSign({
+ activeSignature: undefined,
+ signatures: [
+ {
+ id: '123',
+ created: new Date(),
+ isDigital: true,
+ isInitialType: true,
+ listId: '123',
+ signee: { name: '', nationalId: '', address: '' },
+ valid: false,
+ },
+ ],
+ requirementsMet: true,
+ canSignInfo: { aldur: true, rikisfang: true },
+ })
+ // Assert
+ expect(success).toEqual({ success: true, reasons: [] })
+ expect(alreadySigned).toEqual({
+ success: false,
+ reasons: ['alreadySigned'],
+ })
+ expect(canNotSign).toEqual({
+ success: false,
+ reasons: ['underAge', 'noCitizenship'],
+ })
+ expect(invalidSignature).toEqual({
+ success: false,
+ reasons: ['noInvalidSignature'],
+ })
+ })
+
+ it('getLists should return lists', async () => {
+ // Arrange
+ const lists: MedmaelalistiDTO[] = [
+ {
+ id: 123,
+ medmaelasofnun: {
+ id: 123,
+ sofnunEnd: new Date('01.01.2199'),
+ sofnunStart: new Date('01.01.1900'),
+ },
+ frambod: { id: 123, kennitala: '0101016789' },
+ svaedi: { id: 123 },
+ dagsetningLokar: new Date('01.01.2199'),
+ listaLokad: false,
+ },
+ {
+ id: 321,
+ medmaelasofnun: { id: 321, sofnunEnd: new Date() },
+ frambod: { id: 321, kennitala: '0202026789' },
+ svaedi: { id: 321 },
+ dagsetningLokar: new Date('01.01.1900'),
+ listaLokad: true,
+ },
+ ]
+
+ const mappedActiveLists = [
+ {
+ id: '123',
+ candidate: {
+ id: '123',
+ nationalId: '0101016789',
+ },
+ active: true,
+ area: { id: '123' },
+ collectionId: '123',
+ },
+ ]
+ const mappedAllLists = [
+ {
+ id: '123',
+ candidate: {
+ id: '123',
+ nationalId: '0101016789',
+ },
+ active: true,
+ area: { id: '123' },
+ collectionId: '123',
+ },
+ {
+ id: '321',
+ candidate: {
+ id: '321',
+ nationalId: '0202026789',
+ },
+ active: false,
+ area: { id: '321' },
+ collectionId: '321',
+ },
+ ]
+
+ jest
+ .spyOn(listarApi, 'medmaelalistarGet')
+ .mockImplementation(({ svaediID }) =>
+ Promise.resolve(
+ svaediID === 123 ? lists.filter((l) => l.svaedi?.id === 123) : lists,
+ ),
+ )
+ // Act
+ const all = await service.getLists({})
+ const allArea = await service.getLists({ areaId: '123' })
+ const active = await service.getLists({ areaId: '123', onlyActive: true })
+ // Assert
+ expect(all).toMatchObject(mappedAllLists)
+ expect(allArea).toMatchObject(
+ mappedAllLists.filter((l) => l.area.id === '123'),
+ )
+ expect(active).toMatchObject(mappedActiveLists)
+ })
+
+ it('createLists should create lists', async () => {
+ // Arrange
+ const input: CreateListInput = {
+ collectionId: '123',
+ owner: {
+ email: 'jon@jonsson.is',
+ name: 'Jón Jónsson',
+ nationalId: '0101302399',
+ phone: '9999999',
+ },
+ areas: [{ areaId: '123' }, { areaId: '321' }],
+ }
+ const lists: MedmaelalistiDTO[] = [
+ {
+ id: 123,
+ medmaelasofnun: {
+ id: 123,
+ sofnunStart: new Date('01.01.1900'),
+ sofnunEnd: new Date('01.01.2199'),
+ },
+ frambod: { id: 123, kennitala: '0101016789' },
+ svaedi: { id: 123 },
+ dagsetningLokar: new Date('01.01.2199'),
+ listaLokad: false,
+ },
+ ]
+
+ jest
+ .spyOn(sofnunApi, 'medmaelasofnunGet')
+ .mockReturnValue(Promise.resolve(sofnun))
+ jest
+ .spyOn(listarApi, 'medmaelalistarAddListarPost')
+ .mockReturnValueOnce(Promise.resolve(lists))
+ jest
+ .spyOn(service, 'getApiWithAuth')
+ .mockReturnValueOnce(sofnunApi)
+ .mockReturnValueOnce(listarApi)
+ jest
+ .spyOn(sofnunApi, 'medmaelasofnunIDEinsInfoKennitalaGet')
+ .mockReturnValue(Promise.resolve(sofnunUser))
+
+ // Act
+ const result = await service.createLists(input, user)
+
+ // Assert
+ expect(result).toEqual({
+ slug: '/umsoknir/maela-med-frambodi/?candidate=123',
+ })
+ })
+
+ it('removeLists', async () => {
+ // Arrange
+ jest
+ .spyOn(sofnunApi, 'medmaelasofnunGet')
+ .mockReturnValue(Promise.resolve(sofnun))
+ jest
+ .spyOn(sofnunApi, 'medmaelasofnunIDEinsInfoKennitalaGet')
+ .mockReturnValue(Promise.resolve(sofnunUser))
+ jest
+ .spyOn(service, 'getApiWithAuth')
+ .mockImplementation((api, _) =>
+ api instanceof MedmaelasofnunApi ? sofnunApi : frambodApi,
+ )
+ jest
+ .spyOn(frambodApi, 'frambodIDRemoveFrambodUserPost')
+ .mockReturnValueOnce(Promise.resolve({}))
+ // Act
+ const notOwner = await service.removeLists(
+ { collectionId: '', listIds: [''] },
+ { ...user, nationalId: '1234567910' },
+ )
+ const notOpen = await service.removeLists(
+ { collectionId: '', listIds: [''] },
+ user,
+ )
+ const presidentialResult = await service.removeLists(
+ { collectionId: '123' },
+ user,
+ )
+ // Assert
+ expect(notOwner).toEqual({
+ success: false,
+ reasons: ['notOwner'],
+ })
+ expect(notOpen).toEqual({
+ success: false,
+ reasons: ['collectionNotOpen'],
+ })
+ expect(presidentialResult).toEqual({ success: true })
+ })
+
+ it('signList', async () => {
+ // Arrange
+ jest
+ .spyOn(sofnunApi, 'medmaelasofnunGet')
+ .mockReturnValue(Promise.resolve(sofnun))
+ jest
+ .spyOn(sofnunApi, 'medmaelasofnunIDEinsInfoKennitalaGet')
+ .mockReturnValueOnce(
+ Promise.resolve({
+ ...sofnunUser,
+ medmaeli: [{ id: 111, medmaeliTegundNr: 1 }],
+ }),
+ )
+ .mockReturnValue(Promise.resolve(sofnunUser))
+ jest
+ .spyOn(service, 'getApiWithAuth')
+ .mockImplementation((api, _) =>
+ api instanceof MedmaelasofnunApi
+ ? sofnunApi
+ : api instanceof FrambodApi
+ ? frambodApi
+ : listarApi,
+ )
+ jest.spyOn(listarApi, 'medmaelalistarIDAddMedmaeliPost').mockReturnValue(
+ Promise.resolve({
+ kennitala: '0101302399',
+ medmaeliTegundNr: 1,
+ id: 999,
+ medmaelalistiID: 888,
+ }),
+ )
+ // Act
+ const alreadySigned = service.signList('123123', user)
+ const success = await service.signList('123123', user)
+ // Assert
+ expect(alreadySigned).rejects.toThrow('User has already signed a list')
+ expect(success).toMatchObject({
+ id: '999',
+ isDigital: true,
+ listId: '888',
+ valid: true,
+ })
+ })
+
+ it('unsignList', async () => {
+ // Arrange
+ jest
+ .spyOn(service, 'getApiWithAuth')
+ .mockImplementation((api, _) =>
+ api instanceof MedmaeliApi ? medmaeliApi : sofnunApi,
+ )
+ jest
+ .spyOn(sofnunApi, 'medmaelasofnunGet')
+ .mockReturnValue(Promise.resolve(sofnun))
+ jest
+ .spyOn(sofnunApi, 'medmaelasofnunIDEinsInfoKennitalaGet')
+ .mockReturnValue(
+ Promise.resolve({
+ ...sofnunUser,
+ medmaeli: [{ id: 111, medmaeliTegundNr: 1, medmaelalistiID: 999 }],
+ }),
+ )
+ jest
+ .spyOn(medmaeliApi, 'medmaeliIDRemoveMedmaeliUserPost')
+ .mockReturnValue(Promise.resolve({}))
+ // Act
+ const noSignature = await service.unsignList('', user)
+ const success = await service.unsignList('999', user)
+ // Assert
+ expect(noSignature).toEqual({
+ success: false,
+ reasons: ['signatureNotFound'],
+ })
+ expect(success).toEqual({
+ success: true,
+ })
+ })
+})
diff --git a/libs/clients/signature-collection/src/lib/signature-collection.service.ts b/libs/clients/signature-collection/src/lib/signature-collection.service.ts
index d1a1e7183abf..27e29398325d 100644
--- a/libs/clients/signature-collection/src/lib/signature-collection.service.ts
+++ b/libs/clients/signature-collection/src/lib/signature-collection.service.ts
@@ -34,7 +34,7 @@ export class SignatureCollectionClientService {
private sharedService: SignatureCollectionSharedClientService,
) {}
- private getApiWithAuth(api: T, auth: Auth) {
+ getApiWithAuth(api: T, auth: Auth) {
return api.withMiddleware(new AuthMiddleware(auth)) as T
}
@@ -270,7 +270,6 @@ export class SignatureCollectionClientService {
async getSignee(auth: User, nationalId?: string): Promise {
const collection = await this.currentCollection()
const { id, isPresidential, isActive, areas } = collection
-
const user = await this.getApiWithAuth(
this.collectionsApi,
auth,
diff --git a/libs/clients/signature-collection/tsconfig.json b/libs/clients/signature-collection/tsconfig.json
index 8122543a9ab0..667a3463d1d1 100644
--- a/libs/clients/signature-collection/tsconfig.json
+++ b/libs/clients/signature-collection/tsconfig.json
@@ -1,14 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
- "compilerOptions": {
- "module": "commonjs",
- "forceConsistentCasingInFileNames": true,
- "strict": true,
- "noImplicitOverride": true,
- "noPropertyAccessFromIndexSignature": true,
- "noImplicitReturns": true,
- "noFallthroughCasesInSwitch": true
- },
"files": [],
"include": [],
"references": [
diff --git a/libs/clients/social-insurance-administration/src/lib/apiProvider.ts b/libs/clients/social-insurance-administration/src/lib/apiProvider.ts
index d56d65232517..caba70bc5010 100644
--- a/libs/clients/social-insurance-administration/src/lib/apiProvider.ts
+++ b/libs/clients/social-insurance-administration/src/lib/apiProvider.ts
@@ -22,38 +22,50 @@ import {
import { ConfigFactory } from './configFactory'
import { SocialInsuranceAdministrationClientConfig } from './socialInsuranceAdministrationClient.config'
-const apiCollection: Array<{ api: Api; scopes: Array }> = [
+const apiCollection: Array<{
+ api: Api
+ scopes: Array
+ autoAuth: boolean
+}> = [
{
api: ApplicationWriteApi,
scopes: ['@tr.is/umsoknir:write'],
+ autoAuth: true,
},
{
api: ApplicationApi,
scopes: ['@tr.is/umsoknir:read'],
+ autoAuth: true,
},
{
api: ApplicantApi,
scopes: ['@tr.is/umsaekjandi:read'],
+ autoAuth: true,
},
{
api: GeneralApi,
scopes: ['@tr.is/almennt:read'],
+ autoAuth: true,
},
{
api: DocumentsApi,
scopes: ['@tr.is/fylgiskjol:write'],
+ autoAuth: true,
},
{
api: IncomePlanApi,
scopes: ['@tr.is/tekjuaetlun:read'],
+ autoAuth: true,
},
{
api: PaymentPlanApi,
scopes: ['@tr.is/greidsluaetlun:read'],
+ autoAuth: true,
},
{
api: PensionCalculatorApi,
scopes: ['@tr.is/stadgreidsla:read'],
+ autoAuth: false,
},
]
@@ -67,7 +79,13 @@ export const apiProvider = apiCollection.map((apiRecord) => ({
) => {
return new apiRecord.api(
new Configuration(
- ConfigFactory(xroadConfig, config, idsClientConfig, apiRecord.scopes),
+ ConfigFactory(
+ xroadConfig,
+ config,
+ idsClientConfig,
+ apiRecord.scopes,
+ apiRecord.autoAuth,
+ ),
),
)
},
diff --git a/libs/clients/social-insurance-administration/src/lib/configFactory.ts b/libs/clients/social-insurance-administration/src/lib/configFactory.ts
index 78a6943846f9..a06f2b91472c 100644
--- a/libs/clients/social-insurance-administration/src/lib/configFactory.ts
+++ b/libs/clients/social-insurance-administration/src/lib/configFactory.ts
@@ -12,19 +12,21 @@ export const ConfigFactory = (
config: ConfigType,
idsClientConfig: ConfigType,
scopes: Array,
+ autoAuth: boolean,
) => ({
fetchApi: createEnhancedFetch({
name: 'clients-tr',
organizationSlug: 'tryggingastofnun',
- autoAuth: idsClientConfig.isConfigured
- ? {
- mode: 'tokenExchange',
- issuer: idsClientConfig.issuer,
- clientId: idsClientConfig.clientId,
- clientSecret: idsClientConfig.clientSecret,
- scope: scopes,
- }
- : undefined,
+ autoAuth:
+ autoAuth && idsClientConfig.isConfigured
+ ? {
+ mode: 'tokenExchange',
+ issuer: idsClientConfig.issuer,
+ clientId: idsClientConfig.clientId,
+ clientSecret: idsClientConfig.clientSecret,
+ scope: scopes,
+ }
+ : undefined,
}),
basePath: `${xroadConfig.xRoadBasePath}/r1/${config.xRoadServicePath}`,
headers: {
diff --git a/libs/clients/vmst/src/index.ts b/libs/clients/vmst/src/index.ts
index fe8e5041a35b..fc6ebb160dcf 100644
--- a/libs/clients/vmst/src/index.ts
+++ b/libs/clients/vmst/src/index.ts
@@ -1,2 +1,3 @@
export { VMSTModule } from './lib/vmst.module'
export * from '../gen/fetch'
+export * from './lib/vmst.config'
diff --git a/libs/clients/vmst/src/lib/apiConfiguration.ts b/libs/clients/vmst/src/lib/apiConfiguration.ts
new file mode 100644
index 000000000000..e3833f0ad5d1
--- /dev/null
+++ b/libs/clients/vmst/src/lib/apiConfiguration.ts
@@ -0,0 +1,30 @@
+import { ConfigType, XRoadConfig } from '@island.is/nest/config'
+import { isRunningOnEnvironment } from '@island.is/shared/utils'
+import { VmstClientConfig } from './vmst.config'
+import { Configuration } from '../../gen/fetch'
+import { createWrappedFetchWithLogging } from './utils'
+import { createXRoadAPIPath } from '@island.is/shared/utils/server'
+
+const isRunningOnProduction = isRunningOnEnvironment('production')
+
+export const ApiConfiguration = {
+ provide: 'VmstClientApiConfiguration',
+ useFactory: (
+ xRoadConfig: ConfigType,
+ config: ConfigType,
+ ) =>
+ new Configuration({
+ fetchApi: isRunningOnProduction ? fetch : createWrappedFetchWithLogging,
+ basePath: createXRoadAPIPath(
+ config.xroadBasePathWithEnv,
+ config.vmstMemberClass,
+ config.vmstMemberCode,
+ config.vmstApiPath,
+ ),
+ headers: {
+ 'api-key': config.apiKey,
+ 'X-Road-Client': xRoadConfig.xRoadClient,
+ },
+ }),
+ inject: [XRoadConfig.KEY, VmstClientConfig.KEY],
+}
diff --git a/libs/clients/vmst/src/lib/apis.ts b/libs/clients/vmst/src/lib/apis.ts
new file mode 100644
index 000000000000..422a19775d39
--- /dev/null
+++ b/libs/clients/vmst/src/lib/apis.ts
@@ -0,0 +1,23 @@
+import {
+ ParentalLeaveApi,
+ PensionApi,
+ PregnancyApi,
+ UnionApi,
+ ApplicationInformationApi,
+ Configuration,
+} from '../../gen/fetch'
+import { ApiConfiguration } from './apiConfiguration'
+
+export const exportedApis = [
+ ParentalLeaveApi,
+ PensionApi,
+ PregnancyApi,
+ UnionApi,
+ ApplicationInformationApi,
+].map((Api) => ({
+ provide: Api,
+ useFactory: (configuration: Configuration) => {
+ return new Api(configuration)
+ },
+ inject: [ApiConfiguration.provide],
+}))
diff --git a/libs/clients/vmst/src/lib/vmst.config.ts b/libs/clients/vmst/src/lib/vmst.config.ts
new file mode 100644
index 000000000000..4cb018c15208
--- /dev/null
+++ b/libs/clients/vmst/src/lib/vmst.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig } from '@island.is/nest/config'
+import { XRoadMemberClass } from '@island.is/shared/utils/server'
+import * as z from 'zod'
+
+const schema = z.object({
+ apiKey: z.string(),
+ vmstMemberCode: z.string(),
+ vmstMemberClass: z.nativeEnum(XRoadMemberClass),
+ vmstApiPath: z.string(),
+ xroadBasePathWithEnv: z.string(),
+})
+
+const XROAD_VMST_MEMBER_CLASS = XRoadMemberClass.GovernmentInstitution
+
+export const VmstClientConfig = defineConfig>({
+ name: 'VmstClient',
+ schema,
+ load(env) {
+ return {
+ apiKey: env.required('XROAD_VMST_API_KEY', ''),
+ vmstMemberCode: env.required('XROAD_VMST_MEMBER_CODE', ''),
+ vmstMemberClass: XROAD_VMST_MEMBER_CLASS,
+ vmstApiPath: env.required('XROAD_VMST_API_PATH', ''),
+ xroadBasePathWithEnv: env.required('XROAD_BASE_PATH_WITH_ENV', ''),
+ }
+ },
+})
diff --git a/libs/clients/vmst/src/lib/vmst.module.ts b/libs/clients/vmst/src/lib/vmst.module.ts
index 8ca975ef5fb5..f9e378ca4207 100644
--- a/libs/clients/vmst/src/lib/vmst.module.ts
+++ b/libs/clients/vmst/src/lib/vmst.module.ts
@@ -1,63 +1,14 @@
-import { DynamicModule } from '@nestjs/common'
-import fetch from 'isomorphic-fetch'
-
-import { logger } from '@island.is/logging'
-import { isRunningOnEnvironment } from '@island.is/shared/utils'
-
-import {
- Configuration,
- ParentalLeaveApi,
- PensionApi,
- PregnancyApi,
- UnionApi,
- ApplicationInformationApi,
-} from '../../gen/fetch'
-import { createWrappedFetchWithLogging } from './utils'
-
-const isRunningOnProduction = isRunningOnEnvironment('production')
+import { Module } from '@nestjs/common'
+import { exportedApis } from './apis'
+import { ApiConfiguration } from './apiConfiguration'
export interface VMSTModuleConfig {
apiKey: string
xRoadPath: string
xRoadClient: string
}
-
-export class VMSTModule {
- static register(config: VMSTModuleConfig): DynamicModule {
- if (!config.apiKey) {
- logger.error('VMSTModule XROAD_VMST_API_KEY not provided.')
- }
-
- if (!config.xRoadClient) {
- logger.error('VMSTModule XROAD_CLIENT_ID not provided.')
- }
-
- const headers = {
- 'api-key': config.apiKey,
- 'X-Road-Client': config.xRoadClient,
- }
-
- const providerConfiguration = new Configuration({
- fetchApi: isRunningOnProduction ? fetch : createWrappedFetchWithLogging,
- basePath: config.xRoadPath,
- headers,
- })
-
- const exportedApis = [
- ParentalLeaveApi,
- PensionApi,
- PregnancyApi,
- UnionApi,
- ApplicationInformationApi,
- ]
-
- return {
- module: VMSTModule,
- providers: exportedApis.map((Api) => ({
- provide: Api,
- useFactory: () => new Api(providerConfiguration),
- })),
- exports: exportedApis,
- }
- }
-}
+@Module({
+ exports: [...exportedApis],
+ providers: [ApiConfiguration, ...exportedApis],
+})
+export class VMSTModule {}
diff --git a/libs/feature-flags/src/lib/features.ts b/libs/feature-flags/src/lib/features.ts
index a9f8d0ed02bb..689b90680b3f 100644
--- a/libs/feature-flags/src/lib/features.ts
+++ b/libs/feature-flags/src/lib/features.ts
@@ -88,6 +88,10 @@ export enum Features {
isDelegationNotificationEnabled = 'isDelegationNotificationEnabled',
shouldSendEmailNotificationsToDelegations = 'shouldSendEmailNotificationsToDelegations',
+
+ // Single sign on passkeys
+ isPasskeyRegistrationEnabled = 'isPasskeyRegistrationEnabled',
+ isPasskeyAuthEnabled = 'isPasskeyAuthEnabled',
}
export enum ServerSideFeature {
diff --git a/libs/island-ui/core/src/lib/Select/Components/index.tsx b/libs/island-ui/core/src/lib/Select/Components/index.tsx
index 7d903a020f21..161a9afd7d35 100644
--- a/libs/island-ui/core/src/lib/Select/Components/index.tsx
+++ b/libs/island-ui/core/src/lib/Select/Components/index.tsx
@@ -19,12 +19,42 @@ import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore make web strict
GroupBase,
+ MultiValueProps,
+ MultiValueGenericProps,
} from 'react-select'
import { Icon } from '../../IconRC/Icon'
import { Option as OptionType } from '../Select.types'
import * as styles from '../Select.css'
+export const MultiValue = <
+ Value,
+ IsMulti extends boolean,
+ Group extends GroupBase,
+>(
+ props: MultiValueProps,
+) => {
+ return (
+
+ {props.children}
+
+ )
+}
+
+export const MultiValueLabel = <
+ Value,
+ IsMulti extends boolean,
+ Group extends GroupBase,
+>(
+ props: MultiValueGenericProps,
+) => {
+ return (
+
+ {props.children}
+
+ )
+}
+
export const Menu = <
Value,
IsMulti extends boolean,
diff --git a/libs/island-ui/core/src/lib/Select/Select.css.ts b/libs/island-ui/core/src/lib/Select/Select.css.ts
index a11de649d1b8..438d3d799ffc 100644
--- a/libs/island-ui/core/src/lib/Select/Select.css.ts
+++ b/libs/island-ui/core/src/lib/Select/Select.css.ts
@@ -22,6 +22,62 @@ export const valueContainer = style(
'valueContainer',
)
+export const multiValue = style(
+ {
+ backgroundColor: theme.color.blue200,
+ color: theme.color.blue600,
+ fontWeight: theme.typography.medium,
+ borderRadius: theme.border.radius.large,
+ overflow: 'hidden',
+ },
+ 'multiValue',
+)
+
+export const multiValueLabel = style(
+ {
+ color: theme.color.blue600,
+ fontWeight: theme.typography.medium,
+ borderRadius: 0,
+ },
+ 'multiValueLabel',
+)
+
+globalStyle(`${wrapper} .island-select__multi-value`, {
+ borderRadius: theme.border.radius.large,
+})
+
+globalStyle(
+ `${wrapper} .island-select__value-container--is-multi.island-select__value-container--has-value .island-select__input-container`,
+ {
+ width: 'auto',
+ },
+)
+
+globalStyle(`${wrapper} .island-select__multi-value__label`, {
+ borderRadius: 0,
+ paddingLeft: '0.5rem',
+ paddingBottom: '0.33rem',
+ paddingTop: '0.33rem',
+ paddingRight: 0,
+ lineHeight: 1,
+})
+
+globalStyle(`${wrapper} .island-select__multi-value__remove`, {
+ borderRadius: 0,
+ paddingLeft: '0.25rem',
+ paddingRight: '0.33rem',
+ paddingBottom: '0.33rem',
+ paddingTop: '0.33rem',
+ transition: 'background .2s, color .2s',
+ fontWeight: theme.typography.medium,
+ cursor: 'pointer',
+})
+
+globalStyle(`${wrapper} .island-select__multi-value__remove:hover`, {
+ backgroundColor: theme.color.blue600,
+ color: theme.color.white,
+})
+
globalStyle(`${wrapper} ${valueContainer} .island-select__input-container`, {
margin: 0,
})
diff --git a/libs/island-ui/core/src/lib/Select/Select.stories.tsx b/libs/island-ui/core/src/lib/Select/Select.stories.tsx
index e38845851965..07893a28fcf9 100644
--- a/libs/island-ui/core/src/lib/Select/Select.stories.tsx
+++ b/libs/island-ui/core/src/lib/Select/Select.stories.tsx
@@ -28,6 +28,7 @@ const config: Meta = {
isDisabled: { description: 'Is select field disabled' },
isClearable: { description: 'Is select field clearable' },
isSearchable: { description: 'Is select field searchable' },
+ isMulti: { description: 'Can select field select multiple options' },
required: { description: 'Is select field required' },
hasError: { description: 'Does select field has error' },
errorMessage: {
@@ -72,6 +73,7 @@ Default.args = {
isDisabled: false,
isClearable: false,
isSearchable: false,
+ isMulti: false,
hasError: false,
required: false,
errorMessage: undefined,
@@ -91,6 +93,12 @@ NoOptions.args = {
noOptionsMessage: 'No options',
}
+export const Multiple = Template.bind({})
+Multiple.args = {
+ ...Default.args,
+ isMulti: true,
+}
+
export const SizeSm = Template.bind({})
SizeSm.args = {
...Default.args,
diff --git a/libs/island-ui/core/src/lib/Select/Select.tsx b/libs/island-ui/core/src/lib/Select/Select.tsx
index 66a36c9eb3db..cf3aa1e242c3 100644
--- a/libs/island-ui/core/src/lib/Select/Select.tsx
+++ b/libs/island-ui/core/src/lib/Select/Select.tsx
@@ -15,6 +15,8 @@ import {
Placeholder,
SingleValue,
ValueContainer,
+ MultiValue,
+ MultiValueLabel,
customStyles,
} from './Components'
import { Option as OptionType, SelectProps } from './Select.types'
@@ -41,6 +43,8 @@ export const Select = <
icon = 'chevronDown',
isSearchable = true,
isCreatable = false,
+ isMulti,
+ closeMenuOnSelect = !isMulti,
size = 'md',
backgroundColor = 'white',
required,
@@ -92,6 +96,8 @@ export const Select = <
isOptionDisabled={(option) => !!option.disabled}
hasError={hasError}
isSearchable={isSearchable}
+ isMulti={isMulti}
+ closeMenuOnSelect={closeMenuOnSelect}
size={size}
required={required}
formatGroupLabel={formatGroupLabel}
@@ -109,6 +115,8 @@ export const Select = <
IndicatorsContainer,
Menu,
Option,
+ MultiValue,
+ MultiValueLabel,
}}
isClearable
backspaceRemovesValue
@@ -150,6 +158,8 @@ export const Select = <
dataTestId={dataTestId}
icon={icon}
placeholder={placeholder}
+ isMulti={isMulti}
+ closeMenuOnSelect={closeMenuOnSelect}
defaultValue={defaultValue}
isOptionDisabled={(option) => !!option.disabled}
hasError={hasError}
@@ -168,6 +178,8 @@ export const Select = <
IndicatorsContainer,
Menu,
Option,
+ MultiValue,
+ MultiValueLabel,
}}
isClearable={isClearable}
backspaceRemovesValue={isClearable}
diff --git a/libs/judicial-system/types/src/index.ts b/libs/judicial-system/types/src/index.ts
index e31ea10789fe..7a2e398371fd 100644
--- a/libs/judicial-system/types/src/index.ts
+++ b/libs/judicial-system/types/src/index.ts
@@ -73,10 +73,12 @@ export {
isRequestCaseState,
isIndictmentCaseTransition,
isRequestCaseTransition,
+ DistrictCourtLocation,
} from './lib/case'
export type {
CrimeScene,
CrimeSceneMap,
+ DistrictCourts,
IndictmentSubtypeMap,
} from './lib/case'
diff --git a/libs/judicial-system/types/src/lib/case.ts b/libs/judicial-system/types/src/lib/case.ts
index 85d807939542..510654bd1945 100644
--- a/libs/judicial-system/types/src/lib/case.ts
+++ b/libs/judicial-system/types/src/lib/case.ts
@@ -440,3 +440,24 @@ export const isRequestCaseTransition = (
transition as RequestCaseTransition,
)
}
+
+export type DistrictCourts =
+ | 'Héraðsdómur Reykjavíkur'
+ | 'Héraðsdómur Reykjaness'
+ | 'Héraðsdómur Vesturlands'
+ | 'Héraðsdómur Vestfjarða'
+ | 'Héraðsdómur Norðurlands vestra'
+ | 'Héraðsdómur Norðurlands eystra'
+ | 'Héraðsdómur Austurlands'
+ | 'Héraðsdómur Suðurlands'
+
+export const DistrictCourtLocation: Record = {
+ 'Héraðsdómur Reykjavíkur': 'Dómhúsið við Lækjartorg, Reykjavík',
+ 'Héraðsdómur Reykjaness': 'Fjarðargata 9, Hafnarfirði',
+ 'Héraðsdómur Vesturlands': 'Bjarnarbraut 8, Borgarnesi',
+ 'Héraðsdómur Vestfjarða': 'Hafnarstræti 9, Ísafirði',
+ 'Héraðsdómur Norðurlands vestra': 'Skagfirðingabraut 21, Sauðárkróki',
+ 'Héraðsdómur Norðurlands eystra': 'Hafnarstræti 107, 4. hæð, Akureyri',
+ 'Héraðsdómur Austurlands': 'Lyngás 15, Egilsstöðum',
+ 'Héraðsdómur Suðurlands': 'Austurvegur 4, Selfossi',
+}
diff --git a/libs/portals/admin/application-system/src/lib/messages.ts b/libs/portals/admin/application-system/src/lib/messages.ts
index bf19d2f46517..58cc573c8d16 100644
--- a/libs/portals/admin/application-system/src/lib/messages.ts
+++ b/libs/portals/admin/application-system/src/lib/messages.ts
@@ -4,11 +4,11 @@ export const m = defineMessages({
// General
applicationSystem: {
id: 'admin-portal.application-system:name',
- defaultMessage: 'Umsóknarkerfi Ísland.is',
+ defaultMessage: 'Umsóknarkerfi',
},
applicationSystemDescription: {
id: 'admin-portal.application-system:description',
- defaultMessage: 'Tölfræðigögn og umsjá um umsóknarkerfi Ísland.is',
+ defaultMessage: 'Tölfræðigögn og umsjá umsóknarkerfis',
},
overview: {
id: 'admin-portal.application-system:overview',
diff --git a/libs/portals/admin/regulations-admin/src/components/EditBasics.tsx b/libs/portals/admin/regulations-admin/src/components/EditBasics.tsx
index e380ffb3c4bb..333df35ec545 100644
--- a/libs/portals/admin/regulations-admin/src/components/EditBasics.tsx
+++ b/libs/portals/admin/regulations-admin/src/components/EditBasics.tsx
@@ -26,6 +26,9 @@ import ConfirmModal from './ConfirmModal/ConfirmModal'
import { ReferenceText } from './impacts/ReferenceText'
import { DraftChangeForm, DraftImpactForm } from '../state/types'
+const updateText =
+ 'Ósamræmi er í texta stofnreglugerðar og breytingareglugerðar. Texti breytingareglugerðar þarf að samræmast breytingum sem gerðar hafa verið á stofnreglugerð, eigi breytingarnar að færast inn með réttum hætti.'
+
export const EditBasics = () => {
const t = useLocale().formatMessage
const { draft, actions } = useDraftingState()
@@ -174,7 +177,7 @@ export const EditBasics = () => {
{
isModalVisible
}
title="Uppfæra texta"
- message={
- 'Uppfæra texta reglugerðar með breytingum frá fyrsta skrefi. Allur viðbættur texti í núverandi skrefi verður hreinsaður út.'
- }
+ message={updateText}
onConfirm={() => {
updateEditorText()
setIsModalVisible(false)
diff --git a/libs/portals/admin/regulations-admin/src/components/EditImpacts.tsx b/libs/portals/admin/regulations-admin/src/components/EditImpacts.tsx
index b5b64e888141..65bced317d36 100644
--- a/libs/portals/admin/regulations-admin/src/components/EditImpacts.tsx
+++ b/libs/portals/admin/regulations-admin/src/components/EditImpacts.tsx
@@ -138,6 +138,14 @@ export const EditImpacts = () => {
{' '}
hefur ekki verið flutt á island.is.
Vinsamlegast hafið samband við ritstjóra.
+
+
+ reglugerdir@dmr.is
+
) : lastItem(draft.impacts[selRegOption.value])?.type === 'repeal' ? (
diff --git a/libs/portals/admin/regulations-admin/src/components/EditorInput.tsx b/libs/portals/admin/regulations-admin/src/components/EditorInput.tsx
index 8fd7bd1d5f6e..d818b05940cf 100644
--- a/libs/portals/admin/regulations-admin/src/components/EditorInput.tsx
+++ b/libs/portals/admin/regulations-admin/src/components/EditorInput.tsx
@@ -11,6 +11,7 @@ import cn from 'classnames'
import { HTMLText, useDomid } from '@island.is/regulations'
import { RegulationDraftId } from '@island.is/regulations/admin'
import { useFileUploader } from '../utils/fileUploader'
+import { fileUrl } from '../utils/dataHooks'
const KB = 1024
@@ -80,6 +81,7 @@ export const EditorInput = (props: EditorInputProps) => {
classes={classes}
fileUploader={fileUploader()}
baseText={baseText}
+ uploadUrl={`${fileUrl}/admin-drafts/files/${props.draftId}`}
onFocus={() => {
setHasFocus(true)
}}
diff --git a/libs/portals/admin/regulations-admin/src/utils/dataHooks.ts b/libs/portals/admin/regulations-admin/src/utils/dataHooks.ts
index e824195c2f18..cba4e71d4a79 100644
--- a/libs/portals/admin/regulations-admin/src/utils/dataHooks.ts
+++ b/libs/portals/admin/regulations-admin/src/utils/dataHooks.ts
@@ -24,6 +24,8 @@ import { getEditUrl } from './routing'
import { createHash } from 'crypto'
import { RegulationDraftTypes, StepNames } from '../types'
+export const fileUrl = 'https://files.reglugerd.is'
+
type QueryResult =
| {
data: T
@@ -133,7 +135,7 @@ export const useS3Upload = () => {
})
return
}
- const location = `https://files.reglugerd.is/${key}`
+ const location = `${fileUrl}/${key}`
setUploadLocation(location)
setUploadStatus({ uploading: false })
})
diff --git a/libs/portals/admin/regulations-admin/src/utils/formatAmendingRegulation.ts b/libs/portals/admin/regulations-admin/src/utils/formatAmendingRegulation.ts
index e1e7f2b5ad2c..16e5cb96c244 100644
--- a/libs/portals/admin/regulations-admin/src/utils/formatAmendingRegulation.ts
+++ b/libs/portals/admin/regulations-admin/src/utils/formatAmendingRegulation.ts
@@ -16,6 +16,13 @@ const removeRegPrefix = (title: string) => {
return title
}
+const removeRegNamePrefix = (name: string) => {
+ if (/^0+/.test(name)) {
+ return name.replace(/^0+/, '')
+ }
+ return name
+}
+
export const formatAmendingRegTitle = (draft: RegDraftForm) => {
const impactArray = Object.values(draft.impacts)
@@ -27,18 +34,16 @@ export const formatAmendingRegTitle = (draft: RegDraftForm) => {
const amendingTitles = amendingArray.map(
(item, i) =>
- `${i === 0 ? `${PREFIX_AMENDING}` : ''}${item.name.replace(
- /^0+/,
- '',
- )}${removeRegPrefix(item.regTitle)}`,
+ `${i === 0 ? `${PREFIX_AMENDING}` : ''}${removeRegNamePrefix(
+ item.name,
+ )} ${removeRegPrefix(item.regTitle)}`,
)
const repealTitles = repealArray.map(
(item, i) =>
- `${i === 0 ? `${PREFIX_REPEALING}` : ''}${item.name.replace(
- /^0+/,
- '',
- )}${removeRegPrefix(item.regTitle)}`,
+ `${i === 0 ? `${PREFIX_REPEALING}` : ''}${removeRegNamePrefix(
+ item.name,
+ )} ${removeRegPrefix(item.regTitle)}`,
)
return PREFIX + [...amendingTitles, ...repealTitles].join(' og ')
@@ -50,13 +55,16 @@ export const formatAmendingRegTitle = (draft: RegDraftForm) => {
// ----------------------------------------------------------------------
export const formatAmendingRegBody = (
- regName: string,
+ name: string,
repeal?: boolean,
diff?: HTMLText | string | undefined,
+ regTitle?: string,
) => {
+ const regName = removeRegNamePrefix(name)
if (repeal) {
- const text =
- `Reglugerð nr. ${regName} ásamt síðari breytingum fellur brott
` as HTMLText
+ const text = `Reglugerð nr. ${regName} ${
+ regTitle ? regTitle.replace(/\.$/, '') + ' ' : ''
+ }fellur brott.
` as HTMLText
const gildistaka =
`Reglugerð þessi er sett með heimild í [].
Reglugerðin öðlast þegar gildi
` as HTMLText
return [text, gildistaka]
@@ -104,7 +112,21 @@ export const formatAmendingRegBody = (
let isNumberList = false
let isLetterList = false
if (element.classList.contains('article__title')) {
- articleTitle = element.innerText
+ const clone = element.cloneNode(true)
+
+ if (clone instanceof Element) {
+ const emElement = clone.querySelector('em')
+ if (emElement) {
+ emElement.parentNode?.removeChild(emElement)
+ }
+
+ const textContent = clone.textContent?.trim() ?? ''
+
+ articleTitle = textContent
+ console.log(textContent)
+ } else {
+ articleTitle = element.innerText
+ }
testGroup.title = articleTitle
isArticleTitle = true
paragraph = 0 // Reset paragraph count for the new article
@@ -156,16 +178,16 @@ export const formatAmendingRegBody = (
if (isParagraph) {
// Paragraph was deleted
pushHtml =
- `${paragraph}. mgr. ${articleTitle} ${regNameDisplay} fellur brott
` as HTMLText
+ `${paragraph}. mgr. ${articleTitle} ${regNameDisplay} fellur brott.
` as HTMLText
} else if (isArticleTitle) {
// Title was deleted
pushHtml =
- `Fyrirsögn ${articleTitle} ${regNameDisplay} fellur brott
` as HTMLText
+ `Fyrirsögn ${articleTitle} ${regNameDisplay} fellur brott.
` as HTMLText
} else if (isLetterList || isNumberList) {
// List was deleted
pushHtml = `${
isLetterList ? 'Stafliðir' : 'Töluliðir'
- } eftir ${paragraph}. mgr. ${articleTitle} ${regNameDisplay} falla brott
` as HTMLText
+ } eftir ${paragraph}. mgr. ${articleTitle} ${regNameDisplay} falla brott.
` as HTMLText
} else {
// We don't know what you deleted, but there was a deletion, and here's the deletelog:
pushHtml =
@@ -249,7 +271,7 @@ export const formatAmendingRegBody = (
if (testGroup.isDeletion === true) {
const articleTitleNumber = testGroup.title
additionArray.push([
- `${articleTitleNumber} ${regNameDisplay} fellur brott
` as HTMLText,
+ `${articleTitleNumber} ${regNameDisplay} fellur brott.
` as HTMLText,
])
} else if (testGroup.isAddition === true) {
let prevArticleTitle = ''
@@ -286,6 +308,7 @@ export const formatAmendingBodyWithArticlePrefix = (
item.type === 'repeal' || draftImpactLength > 1 ? item.name : '',
item.type === 'repeal',
item.type === 'amend' ? item.diff?.value : undefined,
+ item.regTitle,
),
)
const flatArray = flatten(impactArray)
diff --git a/libs/service-portal/core/src/components/ActionCard/ActionCard.css.ts b/libs/service-portal/core/src/components/ActionCard/ActionCard.css.ts
index c370bbbb5ef4..dae430067f2a 100644
--- a/libs/service-portal/core/src/components/ActionCard/ActionCard.css.ts
+++ b/libs/service-portal/core/src/components/ActionCard/ActionCard.css.ts
@@ -30,9 +30,11 @@ export const avatar = style({
})
export const circleImg = style({
- width: '28px',
height: 'auto',
+ width: 'auto',
display: 'flex',
+ maxHeight: 38,
+ maxWidth: 38,
})
export const button = style({
diff --git a/libs/service-portal/documents/src/components/DocumentLine/AvatarImage.tsx b/libs/service-portal/documents/src/components/DocumentLine/AvatarImage.tsx
index ac96192dde52..51094f492fc0 100644
--- a/libs/service-portal/documents/src/components/DocumentLine/AvatarImage.tsx
+++ b/libs/service-portal/documents/src/components/DocumentLine/AvatarImage.tsx
@@ -13,6 +13,8 @@ interface Props {
avatar?: ReactNode
onClick?: (event: MouseEvent) => void
large?: boolean
+ as?: 'div' | 'button'
+ imageClass?: string
}
export const AvatarImage: FC = ({
@@ -20,6 +22,8 @@ export const AvatarImage: FC = ({
background,
avatar,
large,
+ imageClass,
+ as = 'button',
onClick,
}) => {
const { formatMessage } = useLocale()
@@ -38,14 +42,24 @@ export const AvatarImage: FC = ({
className={cn(styles.imageContainer, {
[styles.largeAvatar]: large,
})}
- component="button"
- aria-label={formatMessage(messages.markAsBulkSelection)}
+ component={as}
+ aria-label={
+ as === 'button'
+ ? formatMessage(messages.markAsBulkSelection)
+ : undefined
+ }
onClick={onClick}
>
{avatar ? (
avatar
) : (
-
+
)}
)
diff --git a/libs/shared/form-fields/src/lib/SelectController/SelectController.tsx b/libs/shared/form-fields/src/lib/SelectController/SelectController.tsx
index 26f7f214b255..7f8e71f72405 100644
--- a/libs/shared/form-fields/src/lib/SelectController/SelectController.tsx
+++ b/libs/shared/form-fields/src/lib/SelectController/SelectController.tsx
@@ -1,15 +1,11 @@
import React from 'react'
import { Controller, useFormContext, RegisterOptions } from 'react-hook-form'
-import {
- Select,
- Option,
- InputBackgroundColor,
- SelectProps,
-} from '@island.is/island-ui/core'
+import { Select, Option, InputBackgroundColor } from '@island.is/island-ui/core'
import { TestSupport } from '@island.is/island-ui/utils'
+import { MultiValue, SingleValue } from 'react-select'
-interface SelectControllerProps {
+interface SelectControllerProps {
error?: string
id: string
defaultValue?: Value
@@ -18,16 +14,20 @@ interface SelectControllerProps {
label: string
options?: Option[]
placeholder?: string
- onSelect?: (s: Option, onChange: (t: unknown) => void) => void
+ onSelect?: (
+ s: IsMulti extends true ? MultiValue> : Option,
+ onChange: (t: unknown) => void,
+ ) => void
backgroundColor?: InputBackgroundColor
isSearchable?: boolean
+ isMulti?: IsMulti
required?: boolean
rules?: RegisterOptions
size?: 'xs' | 'sm' | 'md'
internalKey?: string
}
-export const SelectController = ({
+export const SelectController = ({
error,
defaultValue,
disabled = false,
@@ -39,13 +39,31 @@ export const SelectController = ({
onSelect,
backgroundColor,
isSearchable,
+ isMulti,
dataTestId,
required = false,
rules,
size,
internalKey,
-}: SelectControllerProps & TestSupport) => {
+}: SelectControllerProps & TestSupport) => {
const { clearErrors } = useFormContext()
+
+ const isMultiValue = (
+ value: MultiValue> | SingleValue >,
+ ): value is MultiValue > => {
+ return Array.isArray(value)
+ }
+
+ const getValue = (value: Value | Value[]) => {
+ if (Array.isArray(value)) {
+ return value
+ .map((v) => options.find((option) => option.value === v))
+ .filter(Boolean) as Option[]
+ }
+
+ return options.find((option) => option.value === value)
+ }
+
return (
({
label={label}
dataTestId={dataTestId}
placeholder={placeholder}
- value={options.find((option) => option.value === value)}
+ value={getValue(value)}
isSearchable={isSearchable}
+ isMulti={isMulti}
size={size}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore make web strict
onChange={(newVal) => {
clearErrors(id)
- onChange(newVal?.value)
+ if (isMultiValue(newVal)) {
+ onChange(newVal.map((v) => v.value))
+ } else {
+ onChange(newVal?.value)
+ }
+
if (onSelect && newVal) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore make web strict
onSelect(newVal, onChange)
}
}}
diff --git a/package.json b/package.json
index d1020331969a..b2bcbdeadf89 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,7 @@
"@formatjs/intl-locale": "2.4.33",
"@formatjs/intl-numberformat": "7.1.5",
"@hookform/error-message": "2.0.1",
- "@island.is/regulations-tools": "0.8.1",
+ "@island.is/regulations-tools": "0.9.0",
"@keyv/redis": "2.6.1",
"@livechat/widget-core": "1.3.2",
"@nestjs/apollo": "10.1.0",
@@ -110,6 +110,7 @@
"@nestjs/terminus": "10.2.0",
"@react-pdf/renderer": "^3.1.9",
"@rehooks/component-size": "1.0.3",
+ "@simplewebauthn/server": "10.0.0",
"@sindresorhus/slugify": "1.0.0",
"@statoscope/webpack-plugin": "5.20.1",
"@storybook/core-server": "7.4.1",
@@ -485,7 +486,7 @@
"dd-trace@5.14.1": "patch:dd-trace@npm%3A5.14.1#./.yarn/patches/dd-trace-npm-5.14.1-8d45ad14d6.patch"
},
"volta": {
- "node": "18.16.0",
+ "node": "20.14.0",
"yarn": "3.2.3"
},
"packageManager": "yarn@3.2.3"
diff --git a/scripts/_hash-generated-files.sh b/scripts/_hash-generated-files.sh
deleted file mode 100755
index 87acbd3cd1aa..000000000000
--- a/scripts/_hash-generated-files.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/bin/bash
-
-patterns=(
- 'scripts/codegen.js'
- 'libs/cms/src/lib/generated/contentfulTypes.d.ts'
- 'apps/air-discount-scheme/web/i18n/withLocale.tsx'
- 'apps/air-discount-scheme/web/components/AppLayout/AppLayout.tsx'
- 'apps/air-discount-scheme/web/components/Header/Header.tsx'
- 'apps/air-discount-scheme/web/screens/**.tsx'
- 'libs/application/types/src/lib/ApplicationTypes.ts'
- 'apps/**/codegen.yml'
- 'libs/**/codegen.yml'
- 'apps/**/*.model.ts'
- 'libs/**/*.model.ts'
- 'apps/**/*.enum.ts'
- 'libs/**/*.enum.ts'
- 'apps/**/queries/**/*.tsx?'
- 'libs/**/queries/**/*.tsx?'
- 'libs/**/mutations/**/*.tsx?'
- 'libs/**/fragments/**/*.tsx?'
- 'apps/**/*.resolver.ts'
- 'libs/**/*.resolver.ts'
- 'apps/**/*.service.ts'
- 'libs/**/*.service.ts'
- 'apps/**/*.dto.ts'
- 'libs/**/*.dto.ts'
- 'apps/**/*.input.ts'
- 'libs/**/*.input.ts'
- 'apps/**/*.module.ts'
- 'libs/**/*.module.ts'
- 'apps/**/*.controller.ts'
- 'libs/**/*.controller.ts'
- 'apps/**/*.union.ts'
- 'libs/**/*.union.ts'
- 'apps/**/*.graphql.ts'
- 'apps/judicial-system/**/*.graphql'
- 'libs/**/*.graphql.ts'
- 'libs/**/*.graphql'
- 'libs/**/clientConfig.yaml'
- 'libs/**/clientConfig.json'
- 'libs/judicial-system/**'
-)
-HASH="$(for pattern in "${patterns[@]}"; do git ls-files "$pattern"; done | xargs cat | git hash-object --stdin)"
-echo -n "$HASH"
-
diff --git a/scripts/ci/_common.mjs b/scripts/ci/_common.mjs
index b52c5bd44126..874f235ed967 100644
--- a/scripts/ci/_common.mjs
+++ b/scripts/ci/_common.mjs
@@ -3,9 +3,13 @@ import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
+export const SCRIPTS_DIR = __dirname
export const ROOT = resolve(__dirname, '..', '..')
-
-export async function getPackageJSON(filePath = resolve(ROOT, 'package.json')) {
+export const MOBILE_APP_DIR = resolve(ROOT, ...'apps/native/app'.split('/'))
+export async function getPackageJSON(
+ root = ROOT,
+ filePath = resolve(ROOT, 'package.json'),
+) {
const content = JSON.parse(await readFile(filePath, 'utf-8'))
return content
}
diff --git a/scripts/ci/cache/__config.mjs b/scripts/ci/cache/__config.mjs
new file mode 100644
index 000000000000..6552ec71b40f
--- /dev/null
+++ b/scripts/ci/cache/__config.mjs
@@ -0,0 +1,174 @@
+// @ts-check
+import { resolve } from 'path'
+import { MOBILE_APP_DIR, ROOT } from './_common.mjs'
+import {
+ getPlatformString,
+ getYarnLockHash,
+ getPackageHash,
+ getNodeVersionString,
+ folderSizeIsEqualOrGreaterThan,
+ runCommand,
+ fileSizeIsEqualOrGreaterThan,
+ getPackageJSON,
+ getFilesHash,
+} from './_utils.mjs'
+
+import {
+ ENV_INIT_CACHE,
+ ENV_ENABLED_CACHE,
+ ENV_CACHE_SUCCESS,
+} from './_const.mjs'
+import { keyStorage } from './_key_storage.mjs'
+import { getGeneratedFilesHash } from './_generated_files.mjs'
+
+// When testing this is good to manipulate
+const HASH_VERSION = `newcache-${6}`
+
+export const ENABLED_MODULES = (process.env[ENV_ENABLED_CACHE] || '')
+ .split(',')
+ .map((x) => x.trim())
+ .filter((x) => x.length > 0)
+ .reduce((a, b) => {
+ a[b] = true
+ return a
+ }, {})
+
+export const cypressPath = '/github/home/.cache/Cypress'
+export const cacheSuccess = JSON.parse(process.env[ENV_CACHE_SUCCESS] ?? '{}')
+export const initCache = process.env[ENV_INIT_CACHE] === 'true'
+
+export const caches = [
+ {
+ enabled: ENABLED_MODULES['node_modules'],
+ hash: async () =>
+ keyStorage.getKey('node_modules') ??
+ `node-modules-${HASH_VERSION}-${getPlatformString()}-${await getYarnLockHash()}-${await getPackageHash()}-${await getNodeVersionString()}`,
+ name: 'Cache node_modules',
+ id: 'node_modules',
+ path: 'node_modules',
+ check: async (success, path) => {
+ if (!success) {
+ return false
+ }
+ return folderSizeIsEqualOrGreaterThan(path, 1000)
+ },
+ init: async () => {
+ const path = resolve(ROOT, './scripts/ci/10_prepare-host-deps.sh')
+ await runCommand(path, ROOT)
+ },
+ },
+ {
+ enabled: ENABLED_MODULES['mobile-node_modules'],
+ hash: async () =>
+ keyStorage.getKey('mobile-node_modules') ??
+ `mobile-node-modules-${HASH_VERSION}-${getPlatformString()}-${await getYarnLockHash()}-${await getPackageHash(
+ MOBILE_APP_DIR,
+ )}-${await getNodeVersionString()}`,
+ check: async (success, path) => {
+ if (!success) {
+ return false
+ }
+ return folderSizeIsEqualOrGreaterThan(path, 1000)
+ },
+ init: async () => {
+ const yarnLockRoot = resolve(ROOT, 'yarn.lock')
+ const yarnLock = resolve(MOBILE_APP_DIR, 'yarn.lock')
+ await runCommand(`cp ${yarnLockRoot} ${yarnLock}`, ROOT)
+ await runCommand('yarn install', MOBILE_APP_DIR)
+ await runCommand(`rm ${yarnLock}`, MOBILE_APP_DIR)
+ },
+ name: 'Cache Mobile node_modules',
+ id: 'mobile-node_modules',
+ path: 'apps/native/app/node_modules',
+ },
+ {
+ enabled: ENABLED_MODULES['generated-files'],
+ hash: async () =>
+ keyStorage.getKey('generated-files') ??
+ `generated-files-${HASH_VERSION}-${getPlatformString()}-${await getYarnLockHash()}-${await getPackageHash()}-${await getGeneratedFilesHash()}`,
+ name: 'Cache Generated Files',
+ id: 'generated-files',
+ path: 'generated_files.tar.gz',
+ dependsOn: ['node_modules', 'mobile-node_modules'],
+ check: async (success, path) => {
+ if (!success) {
+ return false
+ }
+ return fileSizeIsEqualOrGreaterThan(path, 1000)
+ },
+ post: async () => {
+ await runCommand(`tar zxvf generated_files.tar.gz`, ROOT)
+ },
+ init: async (path) => {
+ console.log(`Generating files to ${path} - THIS WILL TAKE A LOT OF TIME`)
+ const script = resolve(ROOT, 'scripts/ci/cache/generate-files.sh')
+ await runCommand(`${script}`, ROOT)
+ },
+ },
+ {
+ enabled: ENABLED_MODULES['docker'],
+ dependsOn: ['node_modules', 'mobile-node_modules', 'generated-files'],
+ hash: async () => {
+ const files = [
+ 'scripts/ci/10_prepare-docker-deps.sh',
+ 'scripts/ci/Dockerfile',
+ 'scripts/ci/get-node-version.mjs',
+ 'scripts/ci/_common.mjs',
+ ].map((file) => resolve(ROOT, file))
+ const filesHash = getFilesHash(files)
+ return `docker-${HASH_VERSION}-${getPlatformString()}-${await getYarnLockHash()}-${await getPackageHash()}-${await getNodeVersionString()}-${filesHash}`
+ },
+ name: 'Cache Docker',
+ path: ['cache', 'cache_output'],
+ id: 'docker',
+ init: async () => {
+ const path = resolve(ROOT, './scripts/ci/10_prepare-docker-deps.sh')
+ await runCommand(path, ROOT)
+ },
+ check: async (success, path) => {
+ if (!success) {
+ return false
+ }
+ const successCheck = (
+ await Promise.all(
+ path.map(async (p) => {
+ return await folderSizeIsEqualOrGreaterThan(p, 1000)
+ }),
+ )
+ ).every((x) => x)
+ return successCheck
+ },
+ },
+ {
+ enabled: ENABLED_MODULES['cypress'],
+ hash: async () => {
+ if (keyStorage.getKey('cypress')) {
+ return keyStorage.getKey('cypress')
+ }
+ const pkg = await getPackageJSON()
+ const cypressVersion = pkg?.devDependencies?.cypress
+ return `cypress-cache-${HASH_VERSION}-${getPlatformString()}-${cypressVersion}`
+ },
+ name: 'Cache Cypress',
+ id: 'cypress',
+ check: async (success, _path) => {
+ if (!success) {
+ return false
+ }
+ try {
+ await runCommand('npx cypress verify', ROOT)
+ } catch {
+ return false
+ }
+ return true
+ },
+ init: async () => {
+ const pkg = await getPackageJSON()
+ const cypressVersion = pkg?.devDependencies?.cypress
+ await runCommand('npx cypress install', ROOT, {
+ CYPRESS_INSTALL_BINARY: cypressVersion,
+ })
+ },
+ path: cypressPath || '',
+ },
+].filter((step) => step.enabled)
diff --git a/scripts/ci/cache/_common.mjs b/scripts/ci/cache/_common.mjs
new file mode 100644
index 000000000000..2889e8dcf52e
--- /dev/null
+++ b/scripts/ci/cache/_common.mjs
@@ -0,0 +1,11 @@
+// @ts-check
+import { dirname, resolve } from 'path'
+import { fileURLToPath } from 'url'
+import { ENV_KEYS } from './_const.mjs'
+const __dirname = dirname(fileURLToPath(import.meta.url))
+export const SCRIPTS_DIR = __dirname
+export const ROOT = resolve(__dirname, '..', '..', '..')
+export const MOBILE_APP_DIR = resolve(ROOT, ...'apps/native/app'.split('/'))
+export const HAS_HASH_KEYS = !!(
+ process.env[ENV_KEYS] && process.env[ENV_KEYS] != 'false'
+)
diff --git a/scripts/ci/cache/_const.mjs b/scripts/ci/cache/_const.mjs
new file mode 100644
index 000000000000..e84d77fa89ff
--- /dev/null
+++ b/scripts/ci/cache/_const.mjs
@@ -0,0 +1,7 @@
+// @ts-check
+export const ENV_INIT_CACHE = '_INIT_CACHE'
+export const ENV_KEYS = '_CACHE_KEYS'
+export const ENV_YAML_FILE = 'YAML_FILE'
+export const ENV_CACHE_SUCCESS = '_CACHE_SUCCESS'
+export const ENV_ENABLED_CACHE = 'ENABLE_CACHE'
+export const ENV_JOB_STATUS = 'JOB_STATUS'
diff --git a/scripts/ci/cache/_generated_files.mjs b/scripts/ci/cache/_generated_files.mjs
new file mode 100644
index 000000000000..58c534db8187
--- /dev/null
+++ b/scripts/ci/cache/_generated_files.mjs
@@ -0,0 +1,51 @@
+// @ts-check
+import { createHash } from 'node:crypto'
+import { readFile } from 'node:fs/promises'
+import { glob } from 'glob'
+import { ROOT } from './_common.mjs'
+import { resolve } from 'node:path'
+
+const patterns = [
+ 'scripts/codegen.js',
+ 'libs/cms/src/lib/generated/contentfulTypes.d.ts',
+ 'apps/air-discount-scheme/web/i18n/withLocale.tsx',
+ 'apps/air-discount-scheme/web/components/AppLayout/AppLayout.tsx',
+ 'apps/air-discount-scheme/web/components/Header/Header.tsx',
+ 'apps/air-discount-scheme/web/screens/**/*.tsx',
+ 'libs/application/types/src/lib/ApplicationTypes.ts',
+ '**/codegen.yml',
+ '**/*.model.ts',
+ '**/*.enum.ts',
+ '**/queries/**/*',
+ '**/mutations/**/*',
+ '**/fragments/**/*',
+ '**/*.resolver.ts',
+ '**/*.service.ts',
+ '**/*.dto.ts',
+ '**/*.input.ts',
+ '**/*.module.ts',
+ '**/*.controller.ts',
+ '**/*.union.ts',
+ '**/*.graphql.tsx?',
+ '**/*.graphql',
+ '**/clientConfig.*',
+ 'libs/judicial-system/**',
+]
+
+export async function getGeneratedFilesHash() {
+ const hash = createHash('sha1')
+ const files = await glob(patterns, {
+ cwd: ROOT,
+ nodir: true,
+ })
+ console.log(`Files to hash:`)
+ for (const _file of files) {
+ console.log(_file)
+ const file = resolve(ROOT, _file)
+ const content = await readFile(file, 'utf8')
+ hash.update(content)
+ }
+
+ const finalHash = hash.digest('hex')
+ return finalHash
+}
diff --git a/scripts/ci/cache/_get_hashes_utils.mjs b/scripts/ci/cache/_get_hashes_utils.mjs
new file mode 100644
index 000000000000..f690b0943894
--- /dev/null
+++ b/scripts/ci/cache/_get_hashes_utils.mjs
@@ -0,0 +1,34 @@
+// @ts-check
+import { exportVariable, setOutput, summary } from '@actions/core'
+import { ENV_KEYS } from './_const.mjs'
+
+const SUMMARY_TITLE = `Cache keys`
+
+export async function writeToSummary(
+ hashes,
+ enabled = !!process.env.GITHUB_STEP_SUMMARY,
+) {
+ if (!enabled) {
+ return
+ }
+ summary.addHeading(SUMMARY_TITLE)
+ summary.addTable([
+ [
+ { data: 'Key', header: true },
+ { data: 'Hash', header: true },
+ ],
+ ...Object.entries(hashes).map(([key, value]) => [key, value]),
+ ])
+}
+
+export async function writeToOutput(
+ hashes,
+ enabled = !!process.env.GITHUB_OUTPUT,
+ file = process.env.GITHUB_OUTPUT ?? '',
+) {
+ if (!enabled) {
+ return
+ }
+ exportVariable(ENV_KEYS, JSON.stringify(hashes))
+ setOutput(ENV_KEYS, JSON.stringify(hashes))
+}
diff --git a/scripts/ci/cache/_key_storage.mjs b/scripts/ci/cache/_key_storage.mjs
new file mode 100644
index 000000000000..d2f547902288
--- /dev/null
+++ b/scripts/ci/cache/_key_storage.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import { ENV_KEYS } from './_const.mjs'
+
+class KeyStorage {
+ _keys = {}
+ /**
+ * Represents a KeyStorage object.
+ * @constructor
+ * @param {string|undefined} value - The value to initialize the KeyStorage object with.
+ */
+ constructor(value = undefined) {
+ if (!value || value === 'false') {
+ return
+ }
+ try {
+ this._keys = JSON.parse(value)
+ } finally {
+ return
+ }
+ }
+ getKey(key) {
+ return this._keys[key]
+ }
+ setKey(key, value) {
+ this._keys[key] = value
+ }
+ count() {
+ return Object.keys(this._keys).length
+ }
+ json() {
+ return JSON.stringify(this._keys)
+ }
+ getKeys() {
+ return this._keys
+ }
+ deleteKey(key) {
+ delete this._keys[key]
+ }
+ hasKey(key) {
+ return this._keys.hasOwnProperty(key)
+ }
+}
+
+export const keyStorage = new KeyStorage(process.env[ENV_KEYS])
diff --git a/scripts/ci/cache/_restore_cache.mjs b/scripts/ci/cache/_restore_cache.mjs
new file mode 100644
index 000000000000..4dfb696272f6
--- /dev/null
+++ b/scripts/ci/cache/_restore_cache.mjs
@@ -0,0 +1,23 @@
+// @ts-check
+
+import { restoreCache as _restoreCache } from '@actions/cache'
+import { resolve } from 'node:path'
+import { ROOT } from './_common.mjs'
+import { retry } from './_utils.mjs'
+
+export async function restoreCache({ key, path }) {
+ let cache
+ if (!process.env.CI) {
+ // For testing
+ return false
+ }
+ const paths = (Array.isArray(path) ? path : [path]).map((e) =>
+ resolve(ROOT, e),
+ )
+ try {
+ cache = await retry(() => _restoreCache(paths, key, [], {}, true))
+ } catch (e) {
+ return false
+ }
+ return cache != undefined
+}
diff --git a/scripts/ci/cache/_save_cache.mjs b/scripts/ci/cache/_save_cache.mjs
new file mode 100644
index 000000000000..5cfed1f59e2d
--- /dev/null
+++ b/scripts/ci/cache/_save_cache.mjs
@@ -0,0 +1,22 @@
+// @ts-check
+
+import { saveCache as _saveCache } from '@actions/cache'
+import { ROOT } from './_common.mjs'
+import { resolve } from 'node:path'
+
+export async function saveCache({ key, path }) {
+ let cache
+ if (!process.env.CI) {
+ // For testing
+ return false
+ }
+ const paths = (Array.isArray(path) ? path : [path]).map((e) =>
+ resolve(ROOT, e),
+ )
+ try {
+ cache = await _saveCache(paths, key, {}, true)
+ } catch (e) {
+ return false
+ }
+ return cache != undefined
+}
diff --git a/scripts/ci/cache/_utils.mjs b/scripts/ci/cache/_utils.mjs
new file mode 100644
index 000000000000..82eedce63726
--- /dev/null
+++ b/scripts/ci/cache/_utils.mjs
@@ -0,0 +1,218 @@
+// @ts-check
+import { arch, platform } from 'node:os'
+import crypto from 'node:crypto'
+import { ROOT } from './_common.mjs'
+import { spawn } from 'node:child_process'
+import { resolve, join } from 'node:path'
+import { readFile, readdir, stat } from 'node:fs/promises'
+
+export async function getNodeVersionString() {
+ const content = await getPackageJSON()
+ const nodeVersion = content?.engines?.node
+ const yarnVersion = content?.engines?.yarn
+ if (!nodeVersion) {
+ throw new Error('Node version not defined')
+ }
+ if (!yarnVersion) {
+ throw new Error('Yarn version not defined')
+ }
+
+ return `${nodeVersion}-${yarnVersion}`
+}
+export function getPlatformString() {
+ return `${platform()}-${arch()}`
+}
+export async function getPackageHash(
+ root = ROOT,
+ keys = ['resolutions', 'dependencies', 'devDependencies'],
+) {
+ const content = await getPackageJSON(root)
+ const value = keys.reduce((a, b) => {
+ return {
+ ...a,
+ [b]: content[b],
+ }
+ }, {})
+ return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')
+}
+/**
+ * Calculates the SHA256 hash of the content of a yarn.lock file.
+ * @param {string} [root=ROOT] - The root directory of the project.
+ * @param {string} [filePath] - The path to the yarn.lock file.
+ * @returns {Promise} The SHA256 hash of the yarn.lock file content.
+ */
+export async function getYarnLockHash(
+ root = ROOT,
+ filePath = resolve(root, 'yarn.lock'),
+) {
+ return getFileHash(filePath)
+}
+
+export async function getFileHash(file) {
+ const content = await readFile(file, 'utf-8')
+ return crypto.createHash('sha256').update(content).digest('hex')
+}
+
+export async function getFilesHash(files = []) {
+ const contents = await Promise.all(
+ files.map((file) => readFile(file, 'utf-8')),
+ )
+ const combinedContent = contents.join('')
+ return crypto.createHash('sha256').update(combinedContent).digest('hex')
+}
+
+export function sleep(ms = 50) {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+export async function tryRun(fn, name, args = []) {
+ try {
+ await fn(...args)
+ return true
+ } catch (error) {
+ console.log({ type: 'RUN FAILED', name, error })
+ return false
+ }
+}
+
+/**
+ * Checks if the file size is e1qual to or greater than the specified size.
+ *
+ * @param {string} filePath - The path to the file.
+ * @param {number} size - The size to compare against.
+ * @returns {Promise} - A promise that resolves to true if the file size is equal to or greater than the specified size, otherwise false.
+ * @throws {Error} - If there is an error getting the file size.
+ */
+export async function fileSizeIsEqualOrGreaterThan(filePath, size) {
+ try {
+ const stats = await stat(filePath)
+ return stats.size >= size
+ } catch (err) {
+ throw new Error(`Error getting file size: ${err.message}`)
+ }
+}
+
+/**
+ * Calculates the total size of a folder and checks if it is equal to or greater than a specified size.
+ * @param {string} folderPath - The path to the folder.
+ * @param {number} size - The size to compare against.
+ * @param {boolean} [number=false] - Optional flag to return the total size as a number instead of a boolean.
+ * @returns {Promise} - Returns true if the folder size is equal to or greater than the specified size. If the 'number' flag is set to true, it returns the total size as a number.
+ * @throws {Error} - Throws an error if there is an issue reading the folder.
+ */
+export async function folderSizeIsEqualOrGreaterThan(
+ folderPath,
+ size,
+ number = false,
+) {
+ let totalSize = 0
+
+ try {
+ const files = await readdir(folderPath)
+ for (const file of files) {
+ const filePath = join(folderPath, file)
+ const stats = await stat(filePath)
+
+ if (stats.isFile()) {
+ totalSize += stats.size
+ } else if (stats.isDirectory()) {
+ // @ts-ignore
+ totalSize += await folderSizeIsEqualOrGreaterThan(filePath, size, true)
+ }
+ if (totalSize >= size) {
+ break
+ }
+ }
+ } catch (err) {
+ throw new Error(`Error reading folder: ${err.message}`)
+ }
+
+ return number ? totalSize : totalSize >= size
+}
+
+/**
+ * Run command in a child process.
+ * @param {string} cmd
+ * @param {string | undefined} cwd
+ */
+export async function runCommand(cmd, cwd = undefined, env = {}) {
+ return new Promise((resolve, reject) => {
+ const options = cwd ? { cwd, encoding: 'utf-8' } : {}
+ options.env = {
+ ...process.env,
+ ...env,
+ NODE_OPTIONS: '--max-old-space-size=4096',
+ }
+ options.encoding = 'utf-8'
+
+ const [command, ...args] = Array.isArray(cmd) ? cmd : cmd.split(' ')
+
+ const childProcess = spawn(command, args, options)
+ childProcess.stdout.setEncoding('utf-8')
+ const errorChunks = []
+ const outputChunks = []
+
+ childProcess.stdout.on('data', (data) => {
+ console.log(data.toString())
+ outputChunks.push(data.toString())
+ })
+
+ childProcess.stderr.on('data', (data) => {
+ console.error(data.toString())
+ errorChunks.push(data.toString())
+ })
+
+ childProcess.on('close', (code) => {
+ if (code !== 0) {
+ console.error(errorChunks.join('\n'))
+ console.error(`Failed to run command: ${cmd} returning code ${code}`)
+ console.log(outputChunks.join('\n'))
+ reject(`Error: Process exited with code ${code}`)
+ return
+ }
+ resolve(void 0)
+ })
+
+ childProcess.on('error', (error) => {
+ console.log(errorChunks.join('\n'))
+ // reject(`Error: ${error.message}`)
+ })
+ })
+}
+
+/**
+ * Retrieves the content of the package.json file.
+ *
+ * @param {string} [root=ROOT] - The root directory path.
+ * @param {string} [filePath] - The path to the package.json file.
+ * @returns {Promise} - A promise that resolves to the content of the package.json file.
+ */
+export async function getPackageJSON(
+ root = ROOT,
+ filePath = resolve(root, 'package.json'),
+) {
+ const content = JSON.parse(await readFile(filePath, 'utf-8'))
+ return content
+}
+
+export function arrayIncludesOneOf(array, values) {
+ return values.some((value) => array.includes(value))
+}
+
+export function retry(fn, retries = 5, delay = 2000) {
+ return new Promise((resolve, reject) => {
+ const attempt = async (n) => {
+ try {
+ const value = await fn()
+ resolve(value)
+ } catch (error) {
+ if (n <= 0) {
+ reject(error)
+ return
+ }
+ setTimeout(() => attempt(n - 1), delay)
+ }
+ }
+ attempt(retries)
+ })
+}
diff --git a/scripts/ci/cache/cache-action.mjs b/scripts/ci/cache/cache-action.mjs
new file mode 100644
index 000000000000..4649f63fe6df
--- /dev/null
+++ b/scripts/ci/cache/cache-action.mjs
@@ -0,0 +1,176 @@
+/**
+ * This script is used to generate a cache workflow for a CI/CD pipeline.
+ */
+
+// @ts-check
+import { resolve } from 'node:path'
+import { ENABLED_MODULES, caches } from './__config.mjs'
+import { HAS_HASH_KEYS, ROOT } from './_common.mjs'
+import { writeToSummary, writeToOutput } from './_get_hashes_utils.mjs'
+import { keyStorage } from './_key_storage.mjs'
+import { restoreCache } from './_restore_cache.mjs'
+import { saveCache } from './_save_cache.mjs'
+import { arrayIncludesOneOf, sleep, tryRun } from './_utils.mjs'
+import { ENV_ENABLED_CACHE } from './_const.mjs'
+
+if (Object.keys(ENABLED_MODULES).length === 0) {
+ throw new Error(`No cache modules enabled, set env key ${ENV_ENABLED_CACHE}`)
+}
+
+const invalidModules = Object.keys(ENABLED_MODULES).filter((e) => {
+ return caches.find((c) => c.id === e) == null
+})
+
+if (invalidModules.length > 0) {
+ throw new Error(`Invalid modules ${invalidModules.join(', ')}`)
+}
+
+console.log(`Enabled modules ${caches.map((e) => e.name).join(', ')}`)
+if (!HAS_HASH_KEYS) {
+ console.log('Generating cache hashes')
+}
+/** Generate hash */
+for (const value of caches) {
+ if (value.enabled && !HAS_HASH_KEYS) {
+ console.log(`Generating hash for ${value.name}`)
+ keyStorage.setKey(value.id, await value.hash())
+ console.log(`Hash for ${value.name} is ${keyStorage.getKey(value.id)}`)
+ }
+ if (!value.enabled && HAS_HASH_KEYS && keyStorage.hasKey(value.id)) {
+ // Delete key if not enabled
+ keyStorage.deleteKey(value.id)
+ }
+}
+if (!HAS_HASH_KEYS) {
+ // Only write summary if this is initial run
+ await writeToSummary(keyStorage.getKeys())
+}
+await writeToOutput(keyStorage.getKeys())
+
+/**
+ * @type {any[]}
+ */
+const steps = [
+ ...(await Promise.all(
+ caches.map(async (value) => {
+ if (!value.enabled) {
+ return null
+ }
+ return {
+ ...value,
+ name: value.name,
+ id: value.id,
+ dependsOn: value.dependsOn,
+ path: value.path,
+ key: await value.hash(),
+ init: value.init,
+ check: value.check,
+ }
+ }),
+ )),
+].filter((e) => e != null)
+
+// Restore cache
+const restoreJobs = await Promise.all(
+ steps.map(async (value) => {
+ const restoreSuccess = await restoreCache({
+ key: value.key,
+ path: value.path,
+ })
+ return {
+ ...value,
+ restoreSuccess,
+ }
+ }),
+)
+
+const checkCache = await Promise.all(
+ restoreJobs.map(async (e) => {
+ const isOk = e.check
+ ? await e.check(
+ e.restoreSuccess,
+ Array.isArray(e.path)
+ ? e.path.map((e) => resolve(ROOT, e))
+ : resolve(ROOT, e.path),
+ )
+ : e.restoreSuccess
+ if (isOk) {
+ console.log(`Restored cache for ${e.name}`)
+ if (e.post) {
+ await tryRun(e.post, e.name, [resolve(ROOT, e.path)])
+ }
+ }
+ return {
+ ...e,
+ isOk,
+ }
+ }),
+)
+
+const failedJobs = []
+const succesFullJobs = []
+
+for (const cache of checkCache) {
+ if (!cache.isOk) {
+ if (
+ (cache.dependsOn &&
+ arrayIncludesOneOf(
+ failedJobs.map((e) => e.name),
+ cache.dependsOn,
+ )) ||
+ HAS_HASH_KEYS
+ ) {
+ console.error(`Failed restoring cache for ${cache.name}`)
+ failedJobs.push(cache)
+ continue
+ }
+
+ const fileName = Array.isArray(cache.path)
+ ? cache.path.map((e) => resolve(ROOT, e))
+ : resolve(ROOT, cache.path)
+ console.log(
+ `Failed restoring cache for ${cache.name}, trying to init and save`,
+ )
+ const successInit = await tryRun(cache.init, cache.name, [fileName])
+ const success = await (async () => {
+ try {
+ const value = await cache.check(successInit, fileName)
+ return value
+ } catch (e) {
+ console.error(e)
+ return false
+ }
+ })()
+ if (!success) {
+ console.log(`Failed init and check for ${cache.name}`)
+ failedJobs.push(cache)
+ } else {
+ if (cache.post) {
+ await tryRun(cache.post, cache.name, [fileName])
+ }
+ const saveSuccess = await saveCache({
+ key: cache.key,
+ path: cache.path,
+ })
+ if (!saveSuccess) {
+ console.error(`Failed saving cache for ${cache.name}`)
+ failedJobs.push(cache)
+ } else {
+ console.log(`Saved cache ${cache.name}`)
+ succesFullJobs.push(cache.id)
+ if (cache.post) {
+ await tryRun(cache.post, cache.name, [fileName])
+ }
+ }
+ }
+ }
+}
+
+if (failedJobs.length > 0) {
+ console.log('Failed caches: ', failedJobs.map((e) => e.id).join(', '))
+ process.exit(1)
+} else {
+ console.log('All caches are restored successfully')
+ // Kill all promiseses we don't need
+ process.exit(0)
+}
diff --git a/scripts/ci/cache/generate-files.sh b/scripts/ci/cache/generate-files.sh
new file mode 100755
index 000000000000..c87b4de95536
--- /dev/null
+++ b/scripts/ci/cache/generate-files.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+export NODE_OPTIONS=--max-old-space-size=4096
+# shellcheck disable=SC2046
+tar zcvf generated_files.tar.gz $(./scripts/ci/get-files-touched-by.sh yarn codegen --skip-cache | xargs realpath --relative-to $(pwd))
\ No newline at end of file
diff --git a/scripts/ci/cache/package.json b/scripts/ci/cache/package.json
new file mode 100644
index 000000000000..d829160273f8
--- /dev/null
+++ b/scripts/ci/cache/package.json
@@ -0,0 +1,6 @@
+{
+ "dependencies": {
+ "@actions/cache": "3.2.4",
+ "glob": "10.4.1"
+ }
+}
diff --git a/scripts/ci/cache/yarn.lock b/scripts/ci/cache/yarn.lock
new file mode 100644
index 000000000000..c599e140db02
--- /dev/null
+++ b/scripts/ci/cache/yarn.lock
@@ -0,0 +1,840 @@
+# This file is generated by running "yarn install" inside your project.
+# Manual changes might be lost - proceed with caution!
+
+__metadata:
+ version: 6
+ cacheKey: 8
+
+"@actions/cache@npm:3.2.4":
+ version: 3.2.4
+ resolution: "@actions/cache@npm:3.2.4"
+ dependencies:
+ "@actions/core": ^1.10.0
+ "@actions/exec": ^1.0.1
+ "@actions/glob": ^0.1.0
+ "@actions/http-client": ^2.1.1
+ "@actions/io": ^1.0.1
+ "@azure/abort-controller": ^1.1.0
+ "@azure/ms-rest-js": ^2.6.0
+ "@azure/storage-blob": ^12.13.0
+ semver: ^6.3.1
+ uuid: ^3.3.3
+ checksum: 5bf5f7541bea4906b553440a9ffee5699e11dfb729365c6cb0bbd37e147a1a0993369fdad16bfa3e2b01ec7fa57dac66276278bfd4a389009246a75ea953e61d
+ languageName: node
+ linkType: hard
+
+"@actions/core@npm:^1.10.0, @actions/core@npm:^1.2.6":
+ version: 1.10.1
+ resolution: "@actions/core@npm:1.10.1"
+ dependencies:
+ "@actions/http-client": ^2.0.1
+ uuid: ^8.3.2
+ checksum: 96524c2725e70e3c3176b4e4d93a1358a86f3c5ca777db9a2f65eadfa672f00877db359bf60fffc416c33838ffb4743db93bcc5bf53e76199dd28bf7f7ff8e80
+ languageName: node
+ linkType: hard
+
+"@actions/exec@npm:^1.0.1":
+ version: 1.1.1
+ resolution: "@actions/exec@npm:1.1.1"
+ dependencies:
+ "@actions/io": ^1.0.1
+ checksum: d976e66dd51ab03d76a143da8e1406daa1bcdee06046168e6e0bec681c87a12999eefaad7a81cb81f28e4190610f55a58b8458ae4b82cbaaba13200490f4e8c2
+ languageName: node
+ linkType: hard
+
+"@actions/glob@npm:^0.1.0":
+ version: 0.1.2
+ resolution: "@actions/glob@npm:0.1.2"
+ dependencies:
+ "@actions/core": ^1.2.6
+ minimatch: ^3.0.4
+ checksum: 655532d35a47ccf4240d3c682f5e9591b61f07c8e382bedcf6fb4ace5b67b6cdf3043004d93d8b6e6342fb33a2e97eba3e76c8b5cc9b8ea1b89bf6857803f19a
+ languageName: node
+ linkType: hard
+
+"@actions/http-client@npm:^2.0.1, @actions/http-client@npm:^2.1.1":
+ version: 2.2.1
+ resolution: "@actions/http-client@npm:2.2.1"
+ dependencies:
+ tunnel: ^0.0.6
+ undici: ^5.25.4
+ checksum: c51c003cd697289136c0e81c0f9b8e57a9bb1a038dc7c9a91a71c02f4ae5e27ef7d3e305aefa7c815604049209d114c06e9991a5c5eaa055508519329267f962
+ languageName: node
+ linkType: hard
+
+"@actions/io@npm:^1.0.1":
+ version: 1.1.3
+ resolution: "@actions/io@npm:1.1.3"
+ checksum: 42841ac2b8a7afb29456b9edb5534dbe00148893c794bdbc17d29166847c51c884e2a7c087a489a428250a78e7b54bc761ba3b55eb2f97d9600e9193b60caf0b
+ languageName: node
+ linkType: hard
+
+"@azure/abort-controller@npm:^1.0.0, @azure/abort-controller@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@azure/abort-controller@npm:1.1.0"
+ dependencies:
+ tslib: ^2.2.0
+ checksum: 0f45e504d4aea799486867179afe7589255f6c111951279958e9d0aa5faebb2c96b8f88e3e3c958ce07b02bcba0b0cddb1bbec94705f573a48ecdb93eec1a92a
+ languageName: node
+ linkType: hard
+
+"@azure/abort-controller@npm:^2.0.0":
+ version: 2.1.2
+ resolution: "@azure/abort-controller@npm:2.1.2"
+ dependencies:
+ tslib: ^2.6.2
+ checksum: 22176c04ea01498311c6bbd336669f6e3faffad1cbb0c9ebc6ee9c1ff2cf958fd17ce73c7354b99d8bda9fcd311325ece7bee248875279174e3fc460e8b1a63d
+ languageName: node
+ linkType: hard
+
+"@azure/core-auth@npm:^1.1.4, @azure/core-auth@npm:^1.4.0":
+ version: 1.7.2
+ resolution: "@azure/core-auth@npm:1.7.2"
+ dependencies:
+ "@azure/abort-controller": ^2.0.0
+ "@azure/core-util": ^1.1.0
+ tslib: ^2.6.2
+ checksum: dcbba47f32e4cc929e078fd5d714af43185f3781ff5c19f01cba9e0d9078690e716be8172dd77a13aa3a81380d338a678b974bc5cbaa2e0d8629fb262ee3f0df
+ languageName: node
+ linkType: hard
+
+"@azure/core-client@npm:^1.3.0, @azure/core-client@npm:^1.6.2":
+ version: 1.9.2
+ resolution: "@azure/core-client@npm:1.9.2"
+ dependencies:
+ "@azure/abort-controller": ^2.0.0
+ "@azure/core-auth": ^1.4.0
+ "@azure/core-rest-pipeline": ^1.9.1
+ "@azure/core-tracing": ^1.0.0
+ "@azure/core-util": ^1.6.1
+ "@azure/logger": ^1.0.0
+ tslib: ^2.6.2
+ checksum: 961b829dfda4f734a763e9480a2ea622a7031ba2da4126d0add6e351a9f73ddc5782bf2b766735d976b61da3857014e0a90223d1f85d1c68468747a7a56851c3
+ languageName: node
+ linkType: hard
+
+"@azure/core-http-compat@npm:^2.0.0":
+ version: 2.1.2
+ resolution: "@azure/core-http-compat@npm:2.1.2"
+ dependencies:
+ "@azure/abort-controller": ^2.0.0
+ "@azure/core-client": ^1.3.0
+ "@azure/core-rest-pipeline": ^1.3.0
+ checksum: 387d0187607d95a6876f63d4b689210bce6ad243f48e56413136ba3875a8a9c4e238813307fb0cf0c53298f4b9d0893d04321c9331812bc74cf0f4e3e6872069
+ languageName: node
+ linkType: hard
+
+"@azure/core-lro@npm:^2.2.0":
+ version: 2.7.2
+ resolution: "@azure/core-lro@npm:2.7.2"
+ dependencies:
+ "@azure/abort-controller": ^2.0.0
+ "@azure/core-util": ^1.2.0
+ "@azure/logger": ^1.0.0
+ tslib: ^2.6.2
+ checksum: dc2e5bbb004a86704bcf584422cd099b7a6beef57ce6501afacced65f4f3b5fbba57a2439f701687237867552a661fd6568f8b3c9e3eacdfd9039004772f85b0
+ languageName: node
+ linkType: hard
+
+"@azure/core-paging@npm:^1.1.1":
+ version: 1.6.2
+ resolution: "@azure/core-paging@npm:1.6.2"
+ dependencies:
+ tslib: ^2.6.2
+ checksum: 4b57f953998473ee784c3ea774a8b54f4be0ec239bd43cbabe28113ca18f141455289713302d4fcd802898dd7ab58380ff575b7ce9400ec1ec20c505791c0b25
+ languageName: node
+ linkType: hard
+
+"@azure/core-rest-pipeline@npm:^1.10.1, @azure/core-rest-pipeline@npm:^1.3.0, @azure/core-rest-pipeline@npm:^1.9.1":
+ version: 1.16.0
+ resolution: "@azure/core-rest-pipeline@npm:1.16.0"
+ dependencies:
+ "@azure/abort-controller": ^2.0.0
+ "@azure/core-auth": ^1.4.0
+ "@azure/core-tracing": ^1.0.1
+ "@azure/core-util": ^1.9.0
+ "@azure/logger": ^1.0.0
+ http-proxy-agent: ^7.0.0
+ https-proxy-agent: ^7.0.0
+ tslib: ^2.6.2
+ checksum: ebb4597d18c0d75a6bf699e90c40247d8e1117498462c6e00bd83694cf65efaab16d3324e28d9e01b187242aa3788ba24ab2729632933731e991eaf6f70343d7
+ languageName: node
+ linkType: hard
+
+"@azure/core-tracing@npm:^1.0.0, @azure/core-tracing@npm:^1.0.1":
+ version: 1.1.2
+ resolution: "@azure/core-tracing@npm:1.1.2"
+ dependencies:
+ tslib: ^2.6.2
+ checksum: dc4133115051460748e281c80fdf90a6052350e558716f1afef4b3f8973c4fdd93538ba99c58613b390e9013d0c738108a0155e1903bbbdd8f7910a7a3be1c21
+ languageName: node
+ linkType: hard
+
+"@azure/core-util@npm:^1.1.0, @azure/core-util@npm:^1.2.0, @azure/core-util@npm:^1.6.1, @azure/core-util@npm:^1.9.0":
+ version: 1.9.0
+ resolution: "@azure/core-util@npm:1.9.0"
+ dependencies:
+ "@azure/abort-controller": ^2.0.0
+ tslib: ^2.6.2
+ checksum: 9246dc5bd246e7b94883ea8130fce04e2f22abd1e94afcff7a3e92a4c2da5e9b382dbf89a606b21d70bc8b01c7c89c84e803ca9da27f78d87f72bdff91ec7380
+ languageName: node
+ linkType: hard
+
+"@azure/core-xml@npm:^1.3.2":
+ version: 1.4.2
+ resolution: "@azure/core-xml@npm:1.4.2"
+ dependencies:
+ fast-xml-parser: ^4.3.2
+ tslib: ^2.6.2
+ checksum: f3815db55f0280db6080a3fc5b6187ad45cacf5c3842ea806a5565886edf2ebf2fa9fc1175407cf58d462341962a8396513943543b332b37b313ac3a79dc28fa
+ languageName: node
+ linkType: hard
+
+"@azure/logger@npm:^1.0.0":
+ version: 1.1.2
+ resolution: "@azure/logger@npm:1.1.2"
+ dependencies:
+ tslib: ^2.6.2
+ checksum: 70a777125e972e4de5bb82d3d5c7dd6422399d16421c0af65169c0d4cbcbc1ce8b3eab8bf1e867e1fb67759b4c5e733e2ce102ff3f39ab6dab270766b9367c68
+ languageName: node
+ linkType: hard
+
+"@azure/ms-rest-js@npm:^2.6.0":
+ version: 2.7.0
+ resolution: "@azure/ms-rest-js@npm:2.7.0"
+ dependencies:
+ "@azure/core-auth": ^1.1.4
+ abort-controller: ^3.0.0
+ form-data: ^2.5.0
+ node-fetch: ^2.6.7
+ tslib: ^1.10.0
+ tunnel: 0.0.6
+ uuid: ^8.3.2
+ xml2js: ^0.5.0
+ checksum: 38434010f3fc54a625f637a7758358d7ce0ad3e55ce9a6c7490bf05bbec8ea75ae95fe80041d2376beb3ef78ee6e55858bd0541477d7a88703246e368cfd59c1
+ languageName: node
+ linkType: hard
+
+"@azure/storage-blob@npm:^12.13.0":
+ version: 12.23.0
+ resolution: "@azure/storage-blob@npm:12.23.0"
+ dependencies:
+ "@azure/abort-controller": ^1.0.0
+ "@azure/core-auth": ^1.4.0
+ "@azure/core-client": ^1.6.2
+ "@azure/core-http-compat": ^2.0.0
+ "@azure/core-lro": ^2.2.0
+ "@azure/core-paging": ^1.1.1
+ "@azure/core-rest-pipeline": ^1.10.1
+ "@azure/core-tracing": ^1.0.0
+ "@azure/core-util": ^1.6.1
+ "@azure/core-xml": ^1.3.2
+ "@azure/logger": ^1.0.0
+ events: ^3.0.0
+ tslib: ^2.2.0
+ checksum: 7f09b4bfae0d73f7cf8f383038edeaf8bc8610960bc24a9b8c140f4084bb3afd62971fb56e2ff5e013168165dcfff7b9a545f6cc837fe2669618184e4b77c107
+ languageName: node
+ linkType: hard
+
+"@fastify/busboy@npm:^2.0.0":
+ version: 2.1.1
+ resolution: "@fastify/busboy@npm:2.1.1"
+ checksum: 42c32ef75e906c9a4809c1e1930a5ca6d4ddc8d138e1a8c8ba5ea07f997db32210617d23b2e4a85fe376316a41a1a0439fc6ff2dedf5126d96f45a9d80754fb2
+ languageName: node
+ linkType: hard
+
+"@isaacs/cliui@npm:^8.0.2":
+ version: 8.0.2
+ resolution: "@isaacs/cliui@npm:8.0.2"
+ dependencies:
+ string-width: ^5.1.2
+ string-width-cjs: "npm:string-width@^4.2.0"
+ strip-ansi: ^7.0.1
+ strip-ansi-cjs: "npm:strip-ansi@^6.0.1"
+ wrap-ansi: ^8.1.0
+ wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0"
+ checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb
+ languageName: node
+ linkType: hard
+
+"@pkgjs/parseargs@npm:^0.11.0":
+ version: 0.11.0
+ resolution: "@pkgjs/parseargs@npm:0.11.0"
+ checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f
+ languageName: node
+ linkType: hard
+
+"abort-controller@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "abort-controller@npm:3.0.0"
+ dependencies:
+ event-target-shim: ^5.0.0
+ checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75
+ languageName: node
+ linkType: hard
+
+"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0":
+ version: 7.1.1
+ resolution: "agent-base@npm:7.1.1"
+ dependencies:
+ debug: ^4.3.4
+ checksum: 51c158769c5c051482f9ca2e6e1ec085ac72b5a418a9b31b4e82fe6c0a6699adb94c1c42d246699a587b3335215037091c79e0de512c516f73b6ea844202f037
+ languageName: node
+ linkType: hard
+
+"ansi-regex@npm:^5.0.1":
+ version: 5.0.1
+ resolution: "ansi-regex@npm:5.0.1"
+ checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b
+ languageName: node
+ linkType: hard
+
+"ansi-regex@npm:^6.0.1":
+ version: 6.0.1
+ resolution: "ansi-regex@npm:6.0.1"
+ checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169
+ languageName: node
+ linkType: hard
+
+"ansi-styles@npm:^4.0.0":
+ version: 4.3.0
+ resolution: "ansi-styles@npm:4.3.0"
+ dependencies:
+ color-convert: ^2.0.1
+ checksum: 513b44c3b2105dd14cc42a19271e80f386466c4be574bccf60b627432f9198571ebf4ab1e4c3ba17347658f4ee1711c163d574248c0c1cdc2d5917a0ad582ec4
+ languageName: node
+ linkType: hard
+
+"ansi-styles@npm:^6.1.0":
+ version: 6.2.1
+ resolution: "ansi-styles@npm:6.2.1"
+ checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9
+ languageName: node
+ linkType: hard
+
+"asynckit@npm:^0.4.0":
+ version: 0.4.0
+ resolution: "asynckit@npm:0.4.0"
+ checksum: 7b78c451df768adba04e2d02e63e2d0bf3b07adcd6e42b4cf665cb7ce899bedd344c69a1dcbce355b5f972d597b25aaa1c1742b52cffd9caccb22f348114f6be
+ languageName: node
+ linkType: hard
+
+"balanced-match@npm:^1.0.0":
+ version: 1.0.2
+ resolution: "balanced-match@npm:1.0.2"
+ checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65
+ languageName: node
+ linkType: hard
+
+"brace-expansion@npm:^1.1.7":
+ version: 1.1.11
+ resolution: "brace-expansion@npm:1.1.11"
+ dependencies:
+ balanced-match: ^1.0.0
+ concat-map: 0.0.1
+ checksum: faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07
+ languageName: node
+ linkType: hard
+
+"brace-expansion@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "brace-expansion@npm:2.0.1"
+ dependencies:
+ balanced-match: ^1.0.0
+ checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1
+ languageName: node
+ linkType: hard
+
+"color-convert@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "color-convert@npm:2.0.1"
+ dependencies:
+ color-name: ~1.1.4
+ checksum: 79e6bdb9fd479a205c71d89574fccfb22bd9053bd98c6c4d870d65c132e5e904e6034978e55b43d69fcaa7433af2016ee203ce76eeba9cfa554b373e7f7db336
+ languageName: node
+ linkType: hard
+
+"color-name@npm:~1.1.4":
+ version: 1.1.4
+ resolution: "color-name@npm:1.1.4"
+ checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610
+ languageName: node
+ linkType: hard
+
+"combined-stream@npm:^1.0.6":
+ version: 1.0.8
+ resolution: "combined-stream@npm:1.0.8"
+ dependencies:
+ delayed-stream: ~1.0.0
+ checksum: 49fa4aeb4916567e33ea81d088f6584749fc90c7abec76fd516bf1c5aa5c79f3584b5ba3de6b86d26ddd64bae5329c4c7479343250cfe71c75bb366eae53bb7c
+ languageName: node
+ linkType: hard
+
+"concat-map@npm:0.0.1":
+ version: 0.0.1
+ resolution: "concat-map@npm:0.0.1"
+ checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af
+ languageName: node
+ linkType: hard
+
+"cross-spawn@npm:^7.0.0":
+ version: 7.0.3
+ resolution: "cross-spawn@npm:7.0.3"
+ dependencies:
+ path-key: ^3.1.0
+ shebang-command: ^2.0.0
+ which: ^2.0.1
+ checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52
+ languageName: node
+ linkType: hard
+
+"debug@npm:4, debug@npm:^4.3.4":
+ version: 4.3.5
+ resolution: "debug@npm:4.3.5"
+ dependencies:
+ ms: 2.1.2
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ checksum: 7c002b51e256257f936dda09eb37167df952758c57badf6bf44bdc40b89a4bcb8e5a0a2e4c7b53f97c69e2970dd5272d33a757378a12c8f8e64ea7bf99e8e86e
+ languageName: node
+ linkType: hard
+
+"delayed-stream@npm:~1.0.0":
+ version: 1.0.0
+ resolution: "delayed-stream@npm:1.0.0"
+ checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020
+ languageName: node
+ linkType: hard
+
+"eastasianwidth@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "eastasianwidth@npm:0.2.0"
+ checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed
+ languageName: node
+ linkType: hard
+
+"emoji-regex@npm:^8.0.0":
+ version: 8.0.0
+ resolution: "emoji-regex@npm:8.0.0"
+ checksum: d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192
+ languageName: node
+ linkType: hard
+
+"emoji-regex@npm:^9.2.2":
+ version: 9.2.2
+ resolution: "emoji-regex@npm:9.2.2"
+ checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601
+ languageName: node
+ linkType: hard
+
+"event-target-shim@npm:^5.0.0":
+ version: 5.0.1
+ resolution: "event-target-shim@npm:5.0.1"
+ checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166
+ languageName: node
+ linkType: hard
+
+"events@npm:^3.0.0":
+ version: 3.3.0
+ resolution: "events@npm:3.3.0"
+ checksum: f6f487ad2198aa41d878fa31452f1a3c00958f46e9019286ff4787c84aac329332ab45c9cdc8c445928fc6d7ded294b9e005a7fce9426488518017831b272780
+ languageName: node
+ linkType: hard
+
+"fast-xml-parser@npm:^4.3.2":
+ version: 4.4.0
+ resolution: "fast-xml-parser@npm:4.4.0"
+ dependencies:
+ strnum: ^1.0.5
+ bin:
+ fxparser: src/cli/cli.js
+ checksum: ad33a4b5165a0ffcb6e17ae78825bd4619a8298844a8a8408f2ea141a0d2d9439d18865dc5254162f09fe54d510ff18e5d5c0a190869cab21fc745ee66be816b
+ languageName: node
+ linkType: hard
+
+"foreground-child@npm:^3.1.0":
+ version: 3.2.1
+ resolution: "foreground-child@npm:3.2.1"
+ dependencies:
+ cross-spawn: ^7.0.0
+ signal-exit: ^4.0.1
+ checksum: 3e2e844d6003c96d70affe8ae98d7eaaba269a868c14d997620c088340a8775cd5d2d9043e6ceebae1928d8d9a874911c4d664b9a267e8995945df20337aebc0
+ languageName: node
+ linkType: hard
+
+"form-data@npm:^2.5.0":
+ version: 2.5.1
+ resolution: "form-data@npm:2.5.1"
+ dependencies:
+ asynckit: ^0.4.0
+ combined-stream: ^1.0.6
+ mime-types: ^2.1.12
+ checksum: 5134ada56cc246b293a1ac7678dba6830000603a3979cf83ff7b2f21f2e3725202237cfb89e32bcb38a1d35727efbd3c3a22e65b42321e8ade8eec01ce755d08
+ languageName: node
+ linkType: hard
+
+"glob@npm:10.4.1":
+ version: 10.4.1
+ resolution: "glob@npm:10.4.1"
+ dependencies:
+ foreground-child: ^3.1.0
+ jackspeak: ^3.1.2
+ minimatch: ^9.0.4
+ minipass: ^7.1.2
+ path-scurry: ^1.11.1
+ bin:
+ glob: dist/esm/bin.mjs
+ checksum: 5d33c686c80bf6877f4284adf99a8c3cbb2a6eccbc92342943fe5d4b42c01d78c1881f2223d950c92a938d0f857e12e37b86a8e5483ab2141822e053b67d0dde
+ languageName: node
+ linkType: hard
+
+"http-proxy-agent@npm:^7.0.0":
+ version: 7.0.2
+ resolution: "http-proxy-agent@npm:7.0.2"
+ dependencies:
+ agent-base: ^7.1.0
+ debug: ^4.3.4
+ checksum: 670858c8f8f3146db5889e1fa117630910101db601fff7d5a8aa637da0abedf68c899f03d3451cac2f83bcc4c3d2dabf339b3aa00ff8080571cceb02c3ce02f3
+ languageName: node
+ linkType: hard
+
+"https-proxy-agent@npm:^7.0.0":
+ version: 7.0.4
+ resolution: "https-proxy-agent@npm:7.0.4"
+ dependencies:
+ agent-base: ^7.0.2
+ debug: 4
+ checksum: daaab857a967a2519ddc724f91edbbd388d766ff141b9025b629f92b9408fc83cee8a27e11a907aede392938e9c398e240d643e178408a59e4073539cde8cfe9
+ languageName: node
+ linkType: hard
+
+"is-fullwidth-code-point@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "is-fullwidth-code-point@npm:3.0.0"
+ checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348
+ languageName: node
+ linkType: hard
+
+"isexe@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "isexe@npm:2.0.0"
+ checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62
+ languageName: node
+ linkType: hard
+
+"jackspeak@npm:^3.1.2":
+ version: 3.4.0
+ resolution: "jackspeak@npm:3.4.0"
+ dependencies:
+ "@isaacs/cliui": ^8.0.2
+ "@pkgjs/parseargs": ^0.11.0
+ dependenciesMeta:
+ "@pkgjs/parseargs":
+ optional: true
+ checksum: 350f6f311018bb175ffbe736b19c26ac0b134bb5a17a638169e89594eb0c24ab1c658ab3a2fda24ff63b3b19292e1a5ec19d2255bc526df704e8168d392bef85
+ languageName: node
+ linkType: hard
+
+"lru-cache@npm:^10.2.0":
+ version: 10.2.2
+ resolution: "lru-cache@npm:10.2.2"
+ checksum: 98e8fc93691c546f719a76103ef2bee5a3ac823955c755a47641ec41f8c7fafa1baeaba466937cc1cbfa9cfd47e03536d10e2db3158a64ad91ff3a58a32c893e
+ languageName: node
+ linkType: hard
+
+"mime-db@npm:1.52.0":
+ version: 1.52.0
+ resolution: "mime-db@npm:1.52.0"
+ checksum: 0d99a03585f8b39d68182803b12ac601d9c01abfa28ec56204fa330bc9f3d1c5e14beb049bafadb3dbdf646dfb94b87e24d4ec7b31b7279ef906a8ea9b6a513f
+ languageName: node
+ linkType: hard
+
+"mime-types@npm:^2.1.12":
+ version: 2.1.35
+ resolution: "mime-types@npm:2.1.35"
+ dependencies:
+ mime-db: 1.52.0
+ checksum: 89a5b7f1def9f3af5dad6496c5ed50191ae4331cc5389d7c521c8ad28d5fdad2d06fd81baf38fed813dc4e46bb55c8145bb0ff406330818c9cf712fb2e9b3836
+ languageName: node
+ linkType: hard
+
+"minimatch@npm:^3.0.4":
+ version: 3.1.2
+ resolution: "minimatch@npm:3.1.2"
+ dependencies:
+ brace-expansion: ^1.1.7
+ checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a
+ languageName: node
+ linkType: hard
+
+"minimatch@npm:^9.0.4":
+ version: 9.0.4
+ resolution: "minimatch@npm:9.0.4"
+ dependencies:
+ brace-expansion: ^2.0.1
+ checksum: cf717f597ec3eed7dabc33153482a2e8d49f4fd3c26e58fd9c71a94c5029a0838728841b93f46bf1263b65a8010e2ee800d0dc9b004ab8ba8b6d1ec07cc115b5
+ languageName: node
+ linkType: hard
+
+"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.1.2":
+ version: 7.1.2
+ resolution: "minipass@npm:7.1.2"
+ checksum: 2bfd325b95c555f2b4d2814d49325691c7bee937d753814861b0b49d5edcda55cbbf22b6b6a60bb91eddac8668771f03c5ff647dcd9d0f798e9548b9cdc46ee3
+ languageName: node
+ linkType: hard
+
+"ms@npm:2.1.2":
+ version: 2.1.2
+ resolution: "ms@npm:2.1.2"
+ checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f
+ languageName: node
+ linkType: hard
+
+"node-fetch@npm:^2.6.7":
+ version: 2.7.0
+ resolution: "node-fetch@npm:2.7.0"
+ dependencies:
+ whatwg-url: ^5.0.0
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+ checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5
+ languageName: node
+ linkType: hard
+
+"path-key@npm:^3.1.0":
+ version: 3.1.1
+ resolution: "path-key@npm:3.1.1"
+ checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020
+ languageName: node
+ linkType: hard
+
+"path-scurry@npm:^1.11.1":
+ version: 1.11.1
+ resolution: "path-scurry@npm:1.11.1"
+ dependencies:
+ lru-cache: ^10.2.0
+ minipass: ^5.0.0 || ^6.0.2 || ^7.0.0
+ checksum: 890d5abcd593a7912dcce7cf7c6bf7a0b5648e3dee6caf0712c126ca0a65c7f3d7b9d769072a4d1baf370f61ce493ab5b038d59988688e0c5f3f646ee3c69023
+ languageName: node
+ linkType: hard
+
+"root-workspace-0b6124@workspace:.":
+ version: 0.0.0-use.local
+ resolution: "root-workspace-0b6124@workspace:."
+ dependencies:
+ "@actions/cache": 3.2.4
+ glob: 10.4.1
+ languageName: unknown
+ linkType: soft
+
+"sax@npm:>=0.6.0":
+ version: 1.4.1
+ resolution: "sax@npm:1.4.1"
+ checksum: 3ad64df16b743f0f2eb7c38ced9692a6d924f1cd07bbe45c39576c2cf50de8290d9d04e7b2228f924c7d05fecc4ec5cf651423278e0c7b63d260c387ef3af84a
+ languageName: node
+ linkType: hard
+
+"semver@npm:^6.3.1":
+ version: 6.3.1
+ resolution: "semver@npm:6.3.1"
+ bin:
+ semver: bin/semver.js
+ checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2
+ languageName: node
+ linkType: hard
+
+"shebang-command@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "shebang-command@npm:2.0.0"
+ dependencies:
+ shebang-regex: ^3.0.0
+ checksum: 6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa
+ languageName: node
+ linkType: hard
+
+"shebang-regex@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "shebang-regex@npm:3.0.0"
+ checksum: 1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222
+ languageName: node
+ linkType: hard
+
+"signal-exit@npm:^4.0.1":
+ version: 4.1.0
+ resolution: "signal-exit@npm:4.1.0"
+ checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549
+ languageName: node
+ linkType: hard
+
+"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0":
+ version: 4.2.3
+ resolution: "string-width@npm:4.2.3"
+ dependencies:
+ emoji-regex: ^8.0.0
+ is-fullwidth-code-point: ^3.0.0
+ strip-ansi: ^6.0.1
+ checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb
+ languageName: node
+ linkType: hard
+
+"string-width@npm:^5.0.1, string-width@npm:^5.1.2":
+ version: 5.1.2
+ resolution: "string-width@npm:5.1.2"
+ dependencies:
+ eastasianwidth: ^0.2.0
+ emoji-regex: ^9.2.2
+ strip-ansi: ^7.0.1
+ checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193
+ languageName: node
+ linkType: hard
+
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1":
+ version: 6.0.1
+ resolution: "strip-ansi@npm:6.0.1"
+ dependencies:
+ ansi-regex: ^5.0.1
+ checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c
+ languageName: node
+ linkType: hard
+
+"strip-ansi@npm:^7.0.1":
+ version: 7.1.0
+ resolution: "strip-ansi@npm:7.1.0"
+ dependencies:
+ ansi-regex: ^6.0.1
+ checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d
+ languageName: node
+ linkType: hard
+
+"strnum@npm:^1.0.5":
+ version: 1.0.5
+ resolution: "strnum@npm:1.0.5"
+ checksum: 651b2031db5da1bf4a77fdd2f116a8ac8055157c5420f5569f64879133825915ad461513e7202a16d7fec63c54fd822410d0962f8ca12385c4334891b9ae6dd2
+ languageName: node
+ linkType: hard
+
+"tr46@npm:~0.0.3":
+ version: 0.0.3
+ resolution: "tr46@npm:0.0.3"
+ checksum: 726321c5eaf41b5002e17ffbd1fb7245999a073e8979085dacd47c4b4e8068ff5777142fc6726d6ca1fd2ff16921b48788b87225cbc57c72636f6efa8efbffe3
+ languageName: node
+ linkType: hard
+
+"tslib@npm:^1.10.0":
+ version: 1.14.1
+ resolution: "tslib@npm:1.14.1"
+ checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd
+ languageName: node
+ linkType: hard
+
+"tslib@npm:^2.2.0, tslib@npm:^2.6.2":
+ version: 2.6.3
+ resolution: "tslib@npm:2.6.3"
+ checksum: 74fce0e100f1ebd95b8995fbbd0e6c91bdd8f4c35c00d4da62e285a3363aaa534de40a80db30ecfd388ed7c313c42d930ee0eaf108e8114214b180eec3dbe6f5
+ languageName: node
+ linkType: hard
+
+"tunnel@npm:0.0.6, tunnel@npm:^0.0.6":
+ version: 0.0.6
+ resolution: "tunnel@npm:0.0.6"
+ checksum: c362948df9ad34b649b5585e54ce2838fa583aa3037091aaed66793c65b423a264e5229f0d7e9a95513a795ac2bd4cb72cda7e89a74313f182c1e9ae0b0994fa
+ languageName: node
+ linkType: hard
+
+"undici@npm:^5.25.4":
+ version: 5.28.4
+ resolution: "undici@npm:5.28.4"
+ dependencies:
+ "@fastify/busboy": ^2.0.0
+ checksum: a8193132d84540e4dc1895ecc8dbaa176e8a49d26084d6fbe48a292e28397cd19ec5d13bc13e604484e76f94f6e334b2bdc740d5f06a6e50c44072818d0c19f9
+ languageName: node
+ linkType: hard
+
+"uuid@npm:^3.3.3":
+ version: 3.4.0
+ resolution: "uuid@npm:3.4.0"
+ bin:
+ uuid: ./bin/uuid
+ checksum: 58de2feed61c59060b40f8203c0e4ed7fd6f99d42534a499f1741218a1dd0c129f4aa1de797bcf822c8ea5da7e4137aa3673431a96dae729047f7aca7b27866f
+ languageName: node
+ linkType: hard
+
+"uuid@npm:^8.3.2":
+ version: 8.3.2
+ resolution: "uuid@npm:8.3.2"
+ bin:
+ uuid: dist/bin/uuid
+ checksum: 5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df
+ languageName: node
+ linkType: hard
+
+"webidl-conversions@npm:^3.0.0":
+ version: 3.0.1
+ resolution: "webidl-conversions@npm:3.0.1"
+ checksum: c92a0a6ab95314bde9c32e1d0a6dfac83b578f8fa5f21e675bc2706ed6981bc26b7eb7e6a1fab158e5ce4adf9caa4a0aee49a52505d4d13c7be545f15021b17c
+ languageName: node
+ linkType: hard
+
+"whatwg-url@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "whatwg-url@npm:5.0.0"
+ dependencies:
+ tr46: ~0.0.3
+ webidl-conversions: ^3.0.0
+ checksum: b8daed4ad3356cc4899048a15b2c143a9aed0dfae1f611ebd55073310c7b910f522ad75d727346ad64203d7e6c79ef25eafd465f4d12775ca44b90fa82ed9e2c
+ languageName: node
+ linkType: hard
+
+"which@npm:^2.0.1":
+ version: 2.0.2
+ resolution: "which@npm:2.0.2"
+ dependencies:
+ isexe: ^2.0.0
+ bin:
+ node-which: ./bin/node-which
+ checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1
+ languageName: node
+ linkType: hard
+
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+ version: 7.0.0
+ resolution: "wrap-ansi@npm:7.0.0"
+ dependencies:
+ ansi-styles: ^4.0.0
+ string-width: ^4.1.0
+ strip-ansi: ^6.0.0
+ checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b
+ languageName: node
+ linkType: hard
+
+"wrap-ansi@npm:^8.1.0":
+ version: 8.1.0
+ resolution: "wrap-ansi@npm:8.1.0"
+ dependencies:
+ ansi-styles: ^6.1.0
+ string-width: ^5.0.1
+ strip-ansi: ^7.0.1
+ checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238
+ languageName: node
+ linkType: hard
+
+"xml2js@npm:^0.5.0":
+ version: 0.5.0
+ resolution: "xml2js@npm:0.5.0"
+ dependencies:
+ sax: ">=0.6.0"
+ xmlbuilder: ~11.0.0
+ checksum: 1aa71d62e5bc2d89138e3929b9ea46459157727759cbc62ef99484b778641c0cd21fb637696c052d901a22f82d092a3e740a16b4ce218e81ac59b933535124ea
+ languageName: node
+ linkType: hard
+
+"xmlbuilder@npm:~11.0.0":
+ version: 11.0.1
+ resolution: "xmlbuilder@npm:11.0.1"
+ checksum: 7152695e16f1a9976658215abab27e55d08b1b97bca901d58b048d2b6e106b5af31efccbdecf9b07af37c8377d8e7e821b494af10b3a68b0ff4ae60331b415b0
+ languageName: node
+ linkType: hard
diff --git a/scripts/ci/get-node-modules-hash.mjs b/scripts/ci/get-node-modules-hash.mjs
index 45ee96b9bb3e..18c5477a8732 100755
--- a/scripts/ci/get-node-modules-hash.mjs
+++ b/scripts/ci/get-node-modules-hash.mjs
@@ -41,7 +41,10 @@ async function getPackageHash(
return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')
}
-async function getYarnLockHash(filePath = resolve(ROOT, 'yarn.lock')) {
+async function getYarnLockHash(
+ root = ROOT,
+ filePath = resolve(root, 'yarn.lock'),
+) {
const content = await readFile(filePath, 'utf-8')
return crypto.createHash('sha256').update(content).digest('hex')
}
diff --git a/yarn.lock b/yarn.lock
index d26e315babf0..0813a74c5607 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10622,6 +10622,13 @@ __metadata:
languageName: node
linkType: hard
+"@hexagon/base64@npm:^1.1.27":
+ version: 1.1.28
+ resolution: "@hexagon/base64@npm:1.1.28"
+ checksum: b2c99863ec1401c61cf127603928c4194ecc434728b1522c3a892ff2adacb4aca853f5770628e5305d7f360a518f68740073a289706220ad7711c3b4b0a19f66
+ languageName: node
+ linkType: hard
+
"@hookform/error-message@npm:2.0.1":
version: 2.0.1
resolution: "@hookform/error-message@npm:2.0.1"
@@ -10776,7 +10783,7 @@ __metadata:
react-dom: 18.2.0
react-intl: 5.20.12
react-native: 0.71.1
- react-native-app-auth: 6.4.3
+ react-native-app-auth: 7.2.0
react-native-blob-util: 0.16.4
react-native-code-push: 7.1.0
react-native-device-info: 10.3.0
@@ -10802,9 +10809,9 @@ __metadata:
languageName: unknown
linkType: soft
-"@island.is/regulations-tools@npm:0.8.1":
- version: 0.8.1
- resolution: "@island.is/regulations-tools@npm:0.8.1"
+"@island.is/regulations-tools@npm:0.9.0":
+ version: 0.9.0
+ resolution: "@island.is/regulations-tools@npm:0.9.0"
dependencies:
"@hugsmidjan/htmldiff-js": ^1.3.0
"@hugsmidjan/qj": ^4.10.2
@@ -10818,7 +10825,7 @@ __metadata:
peerDependencies:
react: ">=16.8 <20"
react-dom: ">=16.8 <20"
- checksum: bdf33c19b3b69e1661c08befb5ab1d578b4aa3923da22bef6665a8f1890ba3b46895d0a919b6f110ef25d8f84757050187ccd014c8bfefc0c2866cbc7764608e
+ checksum: 9d4950e590721f78c60644ee7f2c16dfb132960c9383e71cce3dbe9037b8e734c6e8609eb44a623e843ec13187759efd45245078ba4e9c60d456068378a28e62
languageName: node
linkType: hard
@@ -11536,6 +11543,13 @@ __metadata:
languageName: node
linkType: hard
+"@levischuck/tiny-cbor@npm:^0.2.2":
+ version: 0.2.2
+ resolution: "@levischuck/tiny-cbor@npm:0.2.2"
+ checksum: e8011eeaa317e280c3dd543e63c1c92a8f0db6fb9635dd47fce2b0128bef5bdb001c5ba0e99edbd0fd1c1ecbc4e5f360d6351c3beb85dec1e55ff51b7afd186c
+ languageName: node
+ linkType: hard
+
"@lezer/common@npm:^1.0.0":
version: 1.0.2
resolution: "@lezer/common@npm:1.0.2"
@@ -13218,6 +13232,41 @@ __metadata:
languageName: node
linkType: hard
+"@peculiar/asn1-android@npm:^2.3.10":
+ version: 2.3.10
+ resolution: "@peculiar/asn1-android@npm:2.3.10"
+ dependencies:
+ "@peculiar/asn1-schema": ^2.3.8
+ asn1js: ^3.0.5
+ tslib: ^2.6.2
+ checksum: 882a1b5129c9ad99abf7e8bc1b4373c454af1f8921e571a638e0ec27e77327703db0cb669cd13bc71f4c8e2e7f39e344d6c6741405184148ea1e3bd4b9d5fb59
+ languageName: node
+ linkType: hard
+
+"@peculiar/asn1-ecc@npm:^2.3.8":
+ version: 2.3.8
+ resolution: "@peculiar/asn1-ecc@npm:2.3.8"
+ dependencies:
+ "@peculiar/asn1-schema": ^2.3.8
+ "@peculiar/asn1-x509": ^2.3.8
+ asn1js: ^3.0.5
+ tslib: ^2.6.2
+ checksum: c7db2004a03f88c35fcd21957c8c15bfbf29c137a636bbc35d51dcebbc287ec253762e127d50197f0b5910600e3641a738c8ca756a15741fc09272272b0303f4
+ languageName: node
+ linkType: hard
+
+"@peculiar/asn1-rsa@npm:^2.3.8":
+ version: 2.3.8
+ resolution: "@peculiar/asn1-rsa@npm:2.3.8"
+ dependencies:
+ "@peculiar/asn1-schema": ^2.3.8
+ "@peculiar/asn1-x509": ^2.3.8
+ asn1js: ^3.0.5
+ tslib: ^2.6.2
+ checksum: d9bf0f143686b475d3cc9f9b7d948826dc8c8764bc865697705351278541f0bf31a8f788ec8ff8bf6e4150b04aa65b20853bda45d77e4abbac717d7019e6fd56
+ languageName: node
+ linkType: hard
+
"@peculiar/asn1-schema@npm:^2.1.6":
version: 2.3.0
resolution: "@peculiar/asn1-schema@npm:2.3.0"
@@ -13229,6 +13278,30 @@ __metadata:
languageName: node
linkType: hard
+"@peculiar/asn1-schema@npm:^2.3.8":
+ version: 2.3.8
+ resolution: "@peculiar/asn1-schema@npm:2.3.8"
+ dependencies:
+ asn1js: ^3.0.5
+ pvtsutils: ^1.3.5
+ tslib: ^2.6.2
+ checksum: 1f4dd421f1411df8bc52bca12b1cef710434c13ff0a8b5746ede42b10d62b5ad06a3925c4a6db53102aaf1e589947539a6955fa8554a9b8ebb1ffa38b0155a24
+ languageName: node
+ linkType: hard
+
+"@peculiar/asn1-x509@npm:^2.3.8":
+ version: 2.3.8
+ resolution: "@peculiar/asn1-x509@npm:2.3.8"
+ dependencies:
+ "@peculiar/asn1-schema": ^2.3.8
+ asn1js: ^3.0.5
+ ipaddr.js: ^2.1.0
+ pvtsutils: ^1.3.5
+ tslib: ^2.6.2
+ checksum: 23856e5d024298447afca55bd68d19a7440c0ae076437aee5ced26a0fa2e4efa3e0e4a354fa6ee9968d62ac21ee1c2186fc427942bacfc824d3a3a4d2e80d14b
+ languageName: node
+ linkType: hard
+
"@peculiar/json-schema@npm:^1.1.12":
version: 1.1.12
resolution: "@peculiar/json-schema@npm:1.1.12"
@@ -14803,6 +14876,30 @@ __metadata:
languageName: node
linkType: hard
+"@simplewebauthn/server@npm:10.0.0":
+ version: 10.0.0
+ resolution: "@simplewebauthn/server@npm:10.0.0"
+ dependencies:
+ "@hexagon/base64": ^1.1.27
+ "@levischuck/tiny-cbor": ^0.2.2
+ "@peculiar/asn1-android": ^2.3.10
+ "@peculiar/asn1-ecc": ^2.3.8
+ "@peculiar/asn1-rsa": ^2.3.8
+ "@peculiar/asn1-schema": ^2.3.8
+ "@peculiar/asn1-x509": ^2.3.8
+ "@simplewebauthn/types": ^10.0.0
+ cross-fetch: ^4.0.0
+ checksum: 259d4b9548c4e86e80bf1d7bbda3a070adc42f78e82e147dfd96edfb01605863c7f1f4d20fb1aec61ce82359cbe2ff4b85c925464bc976706da96a21aa0a2a04
+ languageName: node
+ linkType: hard
+
+"@simplewebauthn/types@npm:^10.0.0":
+ version: 10.0.0
+ resolution: "@simplewebauthn/types@npm:10.0.0"
+ checksum: 77dfae2d90a4a6e64aa60e8d1e4f7bcf09e583adb6590a8d1e4fe1aefae0e4fac7db32907e76ee934bf8df665400143dabe39133cab3961dbbf5b10677183174
+ languageName: node
+ linkType: hard
+
"@sinclair/typebox@npm:^0.24.1":
version: 0.24.46
resolution: "@sinclair/typebox@npm:0.24.46"
@@ -25462,6 +25559,15 @@ __metadata:
languageName: node
linkType: hard
+"cross-fetch@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "cross-fetch@npm:4.0.0"
+ dependencies:
+ node-fetch: ^2.6.12
+ checksum: ecca4f37ffa0e8283e7a8a590926b66713a7ef7892757aa36c2d20ffa27b0ac5c60dcf453119c809abe5923fc0bae3702a4d896bfb406ef1077b0d0018213e24
+ languageName: node
+ linkType: hard
+
"cross-inspect@npm:1.0.0":
version: 1.0.0
resolution: "cross-inspect@npm:1.0.0"
@@ -34178,7 +34284,7 @@ __metadata:
"@graphql-codegen/typescript-react-apollo": 3.3.3
"@graphql-codegen/typescript-resolvers": 2.7.3
"@hookform/error-message": 2.0.1
- "@island.is/regulations-tools": 0.8.1
+ "@island.is/regulations-tools": 0.9.0
"@keyv/redis": 2.6.1
"@livechat/widget-core": 1.3.2
"@nestjs/apollo": 10.1.0
@@ -34215,6 +34321,7 @@ __metadata:
"@playwright/test": 1.29
"@react-pdf/renderer": ^3.1.9
"@rehooks/component-size": 1.0.3
+ "@simplewebauthn/server": 10.0.0
"@sindresorhus/slugify": 1.0.0
"@statoscope/webpack-plugin": 5.20.1
"@storybook/addon-a11y": 7.4.1
@@ -43693,6 +43800,15 @@ __metadata:
languageName: node
linkType: hard
+"pvtsutils@npm:^1.3.5":
+ version: 1.3.5
+ resolution: "pvtsutils@npm:1.3.5"
+ dependencies:
+ tslib: ^2.6.1
+ checksum: e734516b3cb26086c18bd9c012fefe818928a5073178842ab7e62885a090f1dd7bda9c7bb8cd317167502cb8ec86c0b1b0ccd71dac7ab469382a4518157b0d12
+ languageName: node
+ linkType: hard
+
"pvutils@npm:^1.1.3":
version: 1.1.3
resolution: "pvutils@npm:1.1.3"
@@ -44420,15 +44536,15 @@ __metadata:
languageName: node
linkType: hard
-"react-native-app-auth@npm:6.4.3":
- version: 6.4.3
- resolution: "react-native-app-auth@npm:6.4.3"
+"react-native-app-auth@npm:7.2.0":
+ version: 7.2.0
+ resolution: "react-native-app-auth@npm:7.2.0"
dependencies:
invariant: 2.2.4
react-native-base64: 0.0.2
peerDependencies:
react-native: ">=0.63.0"
- checksum: 5edf1e3b390d4feafa7d1f7c2488564c130ef9625283e83e803d896832494da121e032d61b2be062ed1be8bb0506e7176c8b3fdd30c0dd3152aa6ae1b97e3a32
+ checksum: 68f6a5343210041ec8bf0018b12007c8c3700473d6a46f2a543b935190141b002d6beb0b451531cf35d0c51fd920f73d7a02ec99868b912ca1a5bd618cf987d7
languageName: node
linkType: hard
@@ -50497,7 +50613,7 @@ __metadata:
languageName: node
linkType: hard
-"tslib@npm:2.6.2":
+"tslib@npm:2.6.2, tslib@npm:^2.6.1, tslib@npm:^2.6.2":
version: 2.6.2
resolution: "tslib@npm:2.6.2"
checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad