From 048e2f46b212faea4f30edd3f2fe27a427cbb09f Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Fri, 19 Jul 2024 14:27:21 +0200 Subject: [PATCH 01/21] Re-add missing code quality steps --- .github/workflows/code-quality.yml | 100 ++++++++++++++++++- e2e/cypress/e2e/create-elastic-index.feature | 2 +- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index db8e4f6c..40800116 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -15,6 +15,14 @@ env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres?schema=public jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Review Dependencies + uses: actions/dependency-review-action@v4 + install: runs-on: ubuntu-latest steps: @@ -55,10 +63,100 @@ jobs: - name: Install e2e node dependencies run: cd e2e && npm ci + test: + runs-on: ubuntu-latest + needs: install + services: + db: + image: postgis/postgis + ports: + - "5432:5432" + env: + POSTGRES_USER: ${{ env.DB_USERNAME }} + POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }} + POSTGRES_DB: ${{ env.DB_DATABASE }} + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.12.1 + ports: + - "9200:9200" + env: + ES_JAVA_OPTS: -Xms512m -Xmx512m + xpack.security.enabled: false + discovery.type: single-node + cluster.routing.allocation.disk.threshold_enabled: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Restore cached node modules + uses: actions/cache/restore@v4 + with: + path: ./node_modules + key: "${{ runner.os }}-node_modules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" + - name: Migrate database + run: npm run prisma -- migrate deploy + - name: Run tests + run: npm run test + + lint: + runs-on: ubuntu-latest + needs: install + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Restore cached node modules + uses: actions/cache/restore@v4 + with: + path: ./node_modules + key: "${{ runner.os }}-node_modules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" + - name: Run lint + run: npm run lint + - name: Run prettier + run: npx prettier --check . + + # It would be cleaner and probably more performant to replace this build step + # with either a non-emitting build or a simple type check. + # We only have `build` available for now, + # since the project is currently split across a multitude of small packages, + # all of which have to specify their own commands. + # (Daniel von Atzigen, 2024-04-12) + build: + runs-on: ubuntu-latest + needs: + - test + - lint + - dependency-review + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Restore cached node modules + uses: actions/cache/restore@v4 + with: + path: ./node_modules + key: "${{ runner.os }}-node_modules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" + - name: Reset nx + run: npx nx reset + - name: Run build + run: npm run build + cypress: runs-on: ubuntu-latest needs: - - install + - test + - lint + - dependency-review strategy: # https://github.com/cypress-io/github-action/issues/48 fail-fast: false diff --git a/e2e/cypress/e2e/create-elastic-index.feature b/e2e/cypress/e2e/create-elastic-index.feature index 082738ed..493e7e57 100644 --- a/e2e/cypress/e2e/create-elastic-index.feature +++ b/e2e/cypress/e2e/create-elastic-index.feature @@ -2,7 +2,7 @@ Feature: Elastic index Background: Given The user is logged in - Given User has admin permissions + And User has admin permissions Scenario: User creates elastic index When A user clicks administration menu button From a9ffd53381d81895200f68230c54a9db48df7fb8 Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Thu, 18 Jul 2024 11:21:50 +0200 Subject: [PATCH 02/21] Add CHANGELOG.md --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b42a8c1e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +## [Unreleased] + +### Added + +- Suchfilter zeigen Anzahl Ergebnisse und sind inital verfügbar +- Mehrsprachigkeit von Assets +- Versionsnummer der Applikation wird angezeigt +- Direktauswahl von Assets ohne Suche +- Testing +- Regeneriere Elasticsearch Index via Admin Panel + +### Changed + +- UI Refactoring: Neuanordnung der Container +- UI Refactoring: Suchergebnisse als Tabelle +- Update Dependencies + +### Fixed + +- Error Handling From e56f30e4e2d29dfa6dfa08ebe8e9a4f18133cc7f Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Mon, 22 Jul 2024 10:30:58 +0200 Subject: [PATCH 03/21] Comment out cypress tests until fixed --- .github/workflows/code-quality.yml | 129 ++++++++++++++--------------- 1 file changed, 64 insertions(+), 65 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 40800116..73496df4 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -150,68 +150,67 @@ jobs: run: npx nx reset - name: Run build run: npm run build - - cypress: - runs-on: ubuntu-latest - needs: - - test - - lint - - dependency-review - strategy: - # https://github.com/cypress-io/github-action/issues/48 - fail-fast: false - matrix: - # Use 2 parallel instances - containers: [1, 2] - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Restore cached node modules - uses: actions/cache/restore@v4 - with: - path: ./node_modules - key: "${{ runner.os }}-node_modules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" - - name: Start services - env: - DB_USER: ${{ env.DB_USERNAME }} - DB_PASSWORD: ${{ env.DB_PASSWORD }} - run: | - cd development - chmod +x ./init/elasticsearch/init.sh - sed -i 's/- \.\/volumes\/elasticsearch\/data:\/usr\/share\/elasticsearch\/data//g' ./docker-compose.yaml - docker compose up -d db oidc elasticsearch - sleep 60 - - name: Migrate database - run: npm run prisma -- migrate deploy - - name: Restore cached e2e node modules - uses: actions/cache/restore@v4 - with: - path: ./e2e/node_modules - key: "${{ runner.os }}-node_modules_e2e-${{ hashFiles('./e2e/package-lock.json') }}" - - name: Cypress run - uses: cypress-io/github-action@v6 - with: - command: | - npx cypress run \ - --browser edge \ - --record \ - --parallel \ - --key ${{ secrets.CYPRESS_RECORD_KEY }} \ - --ci-build-id ${{ github.repository }}-${{ github.head_ref || github.ref_name }}-${{ github.sha }} - build: npm run build - start: npm start - wait-on: "http://localhost:4200" - wait-on-timeout: 120 - working-directory: ./e2e - env: - VITE_APP_VERSION: 0.0.99+dev - TZ: Europe/Zurich - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Stop services - run: | - cd development - docker compose down +# cypress: +# runs-on: ubuntu-latest +# needs: +# - test +# - lint +# - dependency-review +# strategy: +# # https://github.com/cypress-io/github-action/issues/48 +# fail-fast: false +# matrix: +# # Use 2 parallel instances +# containers: [1, 2] +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# - name: Setup node +# uses: actions/setup-node@v4 +# with: +# node-version: ${{ env.NODE_VERSION }} +# - name: Restore cached node modules +# uses: actions/cache/restore@v4 +# with: +# path: ./node_modules +# key: "${{ runner.os }}-node_modules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" +# - name: Start services +# env: +# DB_USER: ${{ env.DB_USERNAME }} +# DB_PASSWORD: ${{ env.DB_PASSWORD }} +# run: | +# cd development +# chmod +x ./init/elasticsearch/init.sh +# sed -i 's/- \.\/volumes\/elasticsearch\/data:\/usr\/share\/elasticsearch\/data//g' ./docker-compose.yaml +# docker compose up -d db oidc elasticsearch +# sleep 60 +# - name: Migrate database +# run: npm run prisma -- migrate deploy +# - name: Restore cached e2e node modules +# uses: actions/cache/restore@v4 +# with: +# path: ./e2e/node_modules +# key: "${{ runner.os }}-node_modules_e2e-${{ hashFiles('./e2e/package-lock.json') }}" +# - name: Cypress run +# uses: cypress-io/github-action@v6 +# with: +# command: | +# npx cypress run \ +# --browser edge \ +# --record \ +# --parallel \ +# --key ${{ secrets.CYPRESS_RECORD_KEY }} \ +# --ci-build-id ${{ github.repository }}-${{ github.head_ref || github.ref_name }}-${{ github.sha }} +# build: npm run build +# start: npm start +# wait-on: "http://localhost:4200" +# wait-on-timeout: 120 +# working-directory: ./e2e +# env: +# VITE_APP_VERSION: 0.0.99+dev +# TZ: Europe/Zurich +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +# - name: Stop services +# run: | +# cd development +# docker compose down From f60da23c693503e39917ef0f7358e0b296989520 Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Thu, 18 Jul 2024 15:55:23 +0200 Subject: [PATCH 04/21] Ignore asset selections when polygon is being drawn --- .../src/lib/components/map-controls/draw-controls.ts | 4 ++++ libs/asset-viewer/src/lib/components/map/map.component.ts | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/libs/asset-viewer/src/lib/components/map-controls/draw-controls.ts b/libs/asset-viewer/src/lib/components/map-controls/draw-controls.ts index 08a204fb..b2feca2c 100644 --- a/libs/asset-viewer/src/lib/components/map-controls/draw-controls.ts +++ b/libs/asset-viewer/src/lib/components/map-controls/draw-controls.ts @@ -91,6 +91,10 @@ export class DrawControl extends Control { return this._isDrawing$.asObservable(); } + get isDrawing(): boolean { + return this._isDrawing$.value; + } + setPolygon(polygon: LV95[] | null) { this._isDrawing$.next(false); this._polygon$.next(polygon); diff --git a/libs/asset-viewer/src/lib/components/map/map.component.ts b/libs/asset-viewer/src/lib/components/map/map.component.ts index 906bb76b..ca3ace2c 100644 --- a/libs/asset-viewer/src/lib/components/map/map.component.ts +++ b/libs/asset-viewer/src/lib/components/map/map.component.ts @@ -15,7 +15,7 @@ import { import { AppState } from '@asset-sg/client-shared'; import { ORD } from '@asset-sg/core'; import { Store } from '@ngrx/store'; -import { asapScheduler, first, identity, skip, Subscription, switchMap } from 'rxjs'; +import { asapScheduler, filter, first, identity, skip, Subscription, switchMap } from 'rxjs'; import { AllStudyService } from '../../services/all-study.service'; import * as searchActions from '../../state/asset-search/asset-search.actions'; import { @@ -127,7 +127,9 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { this.controller.addControl(this.controls.zoom); this.controller.addControl(this.controls.draw); - this.subscription.add(this.controller.assetsClick$.subscribe(this.assetsClick)); + this.subscription.add( + this.controller.assetsClick$.pipe(filter(() => !this.controls.draw.isDrawing)).subscribe(this.assetsClick) + ); this.subscription.add(this.controller.assetsHover$.subscribe(this.assetsHover)); // Some bindings can be initialized only after the map has fully loaded, From db2b18673ccb6278e6243f5f051931eaadac1981 Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Fri, 19 Jul 2024 09:12:05 +0200 Subject: [PATCH 05/21] Fix author filters overflowing on new line --- .../asset-search-refine.component.html | 1 + .../asset-search-refine.component.scss | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html index 3d0c4e9f..a48f6b98 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html +++ b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html @@ -54,6 +54,7 @@

search.author

{{ author.name }} ({{ author.count }}) diff --git a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss index b6614e73..fc039f61 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss +++ b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss @@ -10,12 +10,10 @@ flex-direction: column; } -.mat-option { - ::ng-deep .mat-mdc-option .mdc-list-item__primary-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } +::ng-deep .mat-mdc-option.author > .mdc-list-item__primary-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } @include drawerPanel.draw-panel-header; From 60bde4c1bde0c4331bb32e19cc7b485f00c8346e Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Mon, 22 Jul 2024 11:41:29 +0200 Subject: [PATCH 06/21] Make author count always visible in asset filter --- .../asset-search-refine.component.html | 7 +++++- .../asset-search-refine.component.scss | 25 ++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html index a48f6b98..182706d6 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html +++ b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html @@ -55,9 +55,14 @@

search.author

*ngFor="let author of filteredAuthors" [value]="author.name" class="author" + [matTooltip]="author.name" + matTooltipPosition="right" (onSelectionChange)="updateAuthor($event, author.contactId)" > - {{ author.name }} ({{ author.count }}) +
+ {{ author.name }} +
+
({{ author.count }})
diff --git a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss index fc039f61..ca4bb81a 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss +++ b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss @@ -10,12 +10,6 @@ flex-direction: column; } -::ng-deep .mat-mdc-option.author > .mdc-list-item__primary-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - @include drawerPanel.draw-panel-header; @include drawerPanel.draw-panel-header-underline; @@ -70,3 +64,22 @@ section.filters { .input-clear-button { color: variables.$cyan-09; } + +::ng-deep .mat-mdc-option.author > .mdc-list-item__primary-text { + display: flex; + justify-content: space-between; + width: 100%; + gap: 1rem; + + & > .author__name { + flex: 1 1 100%; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + & > .author__count { + flex: auto; + } +} From 9026060c9ad0a506bf7beb6a10dd5784f279f720 Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Mon, 22 Jul 2024 15:01:46 +0200 Subject: [PATCH 07/21] Disable map click event when drawing polygon --- .../src/lib/components/map-controls/draw-controls.ts | 11 ++++++----- .../src/lib/components/map/map-controller.ts | 12 ++++++++++++ .../src/lib/components/map/map.component.ts | 10 ++++++---- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/libs/asset-viewer/src/lib/components/map-controls/draw-controls.ts b/libs/asset-viewer/src/lib/components/map-controls/draw-controls.ts index b2feca2c..fa62b3ff 100644 --- a/libs/asset-viewer/src/lib/components/map-controls/draw-controls.ts +++ b/libs/asset-viewer/src/lib/components/map-controls/draw-controls.ts @@ -5,11 +5,8 @@ import { Control } from 'ol/control'; import Feature from 'ol/Feature'; import { Polygon } from 'ol/geom'; import Draw, { DrawEvent } from 'ol/interaction/Draw'; -import Map from 'ol/Map'; -import MapBrowserEvent from 'ol/MapBrowserEvent'; import VectorSource from 'ol/source/Vector'; -import View from 'ol/View'; -import { BehaviorSubject, distinctUntilChanged, filter, fromEventPattern, map, Observable, Subscription } from 'rxjs'; +import { asapScheduler, BehaviorSubject, filter, fromEventPattern, map, Observable, Subscription } from 'rxjs'; export class DrawControl extends Control { private readonly polygonSource: VectorSource; @@ -76,7 +73,11 @@ export class DrawControl extends Control { return polygon; }) ) - .subscribe(this.setPolygon.bind(this)); + .subscribe((polygon) => + asapScheduler.schedule(() => { + this.setPolygon(polygon); + }) + ); } toggle(): void { diff --git a/libs/asset-viewer/src/lib/components/map/map-controller.ts b/libs/asset-viewer/src/lib/components/map/map-controller.ts index a747f06d..32dc1184 100644 --- a/libs/asset-viewer/src/lib/components/map/map-controller.ts +++ b/libs/asset-viewer/src/lib/components/map/map-controller.ts @@ -45,6 +45,12 @@ export class MapController { */ private activeAsset: AssetEditDetail | null = null; + /** + * Whether clicking things on the map is currently allowed. + * @private + */ + private isClickEnabled = true; + private readonly _isInitialized$ = new BehaviorSubject(false); constructor(element: HTMLElement) { @@ -99,6 +105,10 @@ export class MapController { return this._isInitialized$.asObservable(); } + setClickEnabled(isEnabled: boolean): void { + this.isClickEnabled = isEnabled; + } + addControl(control: Control): void { this.map.addControl(control); } @@ -302,6 +312,8 @@ export class MapController { (h) => this.map.on('click', h), (h) => this.map.un('click', h) ).pipe( + filter(() => this.isClickEnabled), + // Check if the click has hit a study point, and use only that point if so. switchMap(async (event) => { let assetId: number | null = null; diff --git a/libs/asset-viewer/src/lib/components/map/map.component.ts b/libs/asset-viewer/src/lib/components/map/map.component.ts index ca3ace2c..4450b9a1 100644 --- a/libs/asset-viewer/src/lib/components/map/map.component.ts +++ b/libs/asset-viewer/src/lib/components/map/map.component.ts @@ -15,7 +15,7 @@ import { import { AppState } from '@asset-sg/client-shared'; import { ORD } from '@asset-sg/core'; import { Store } from '@ngrx/store'; -import { asapScheduler, filter, first, identity, skip, Subscription, switchMap } from 'rxjs'; +import { asapScheduler, first, identity, skip, Subscription, switchMap } from 'rxjs'; import { AllStudyService } from '../../services/all-study.service'; import * as searchActions from '../../state/asset-search/asset-search.actions'; import { @@ -127,9 +127,11 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { this.controller.addControl(this.controls.zoom); this.controller.addControl(this.controls.draw); - this.subscription.add( - this.controller.assetsClick$.pipe(filter(() => !this.controls.draw.isDrawing)).subscribe(this.assetsClick) - ); + this.controls.draw.isDrawing$.subscribe((isDrawing) => { + this.controller.setClickEnabled(!isDrawing); + }); + + this.subscription.add(this.controller.assetsClick$.subscribe(this.assetsClick)); this.subscription.add(this.controller.assetsHover$.subscribe(this.assetsHover)); // Some bindings can be initialized only after the map has fully loaded, From 2adc87ecbb55540e887ae79a947878d3370069ef Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Mon, 22 Jul 2024 11:05:34 +0200 Subject: [PATCH 08/21] Bump node version to 22.x --- .github/workflows/code-quality.yml | 20 +- apps/client-asset-sg/docker/Dockerfile | 2 +- apps/server-asset-sg/docker/Dockerfile | 4 +- package-lock.json | 8647 ++++++++++-------------- package.json | 14 +- 5 files changed, 3457 insertions(+), 5230 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 73496df4..b64c0fda 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -8,7 +8,7 @@ on: - "!main" env: - NODE_VERSION: "20.x" + NODE_VERSION: "22.x" DB_USERNAME: postgres DB_PASSWORD: postgres DB_DATABASE: postgres @@ -39,25 +39,25 @@ jobs: uses: actions/cache@v4 with: path: ${{ steps.npm-cache-dir.outputs.dir }} - key: "${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}" + key: "${{ runner.os }}-npm-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}" restore-keys: | ${{ runner.os }}-npm- - name: Cache node modules uses: actions/cache@v4 with: path: ./node_modules - key: "${{ runner.os }}-node_modules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" + key: "${{ runner.os }}-node_modules-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" restore-keys: | ${{ runner.os }}-node_modules- - name: Cache e2e node modules uses: actions/cache@v4 with: path: ./e2e/node_modules - key: "${{ runner.os }}-node_modules_e2e-${{ hashFiles('./e2e/package-lock.json') }}" + key: "${{ runner.os }}-node_modules_e2e-${{ env.NODE_VERSION }}-${{ hashFiles('./e2e/package-lock.json') }}" restore-keys: | ${{ runner.os }}-node_modules_e2e- - name: Install node dependencies - run: npm ci + run: npm install - name: Generate prisma types run: npm run prisma -- generate - name: Install e2e node dependencies @@ -96,7 +96,7 @@ jobs: uses: actions/cache/restore@v4 with: path: ./node_modules - key: "${{ runner.os }}-node_modules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" + key: "${{ runner.os }}-node_modules-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" - name: Migrate database run: npm run prisma -- migrate deploy - name: Run tests @@ -116,7 +116,7 @@ jobs: uses: actions/cache/restore@v4 with: path: ./node_modules - key: "${{ runner.os }}-node_modules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" + key: "${{ runner.os }}-node_modules-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" - name: Run lint run: npm run lint - name: Run prettier @@ -145,7 +145,7 @@ jobs: uses: actions/cache/restore@v4 with: path: ./node_modules - key: "${{ runner.os }}-node_modules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" + key: "${{ runner.os }}-node_modules-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" - name: Reset nx run: npx nx reset - name: Run build @@ -173,7 +173,7 @@ jobs: # uses: actions/cache/restore@v4 # with: # path: ./node_modules -# key: "${{ runner.os }}-node_modules-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" +# key: "${{ runner.os }}-node_modules-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('**/schema.prisma') }}" # - name: Start services # env: # DB_USER: ${{ env.DB_USERNAME }} @@ -190,7 +190,7 @@ jobs: # uses: actions/cache/restore@v4 # with: # path: ./e2e/node_modules -# key: "${{ runner.os }}-node_modules_e2e-${{ hashFiles('./e2e/package-lock.json') }}" +# key: "${{ runner.os }}-node_modules_e2e-${{ env.NODE_VERSION }}-${{ hashFiles('./e2e/package-lock.json') }}" # - name: Cypress run # uses: cypress-io/github-action@v6 # with: diff --git a/apps/client-asset-sg/docker/Dockerfile b/apps/client-asset-sg/docker/Dockerfile index 615c37cc..84e9a7a4 100644 --- a/apps/client-asset-sg/docker/Dockerfile +++ b/apps/client-asset-sg/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine as app-builder +FROM node:22-alpine as app-builder ARG APP_VERSION ENV APP_VERSION=${APP_VERSION} diff --git a/apps/server-asset-sg/docker/Dockerfile b/apps/server-asset-sg/docker/Dockerfile index 1e68d0f8..b39cbfe2 100644 --- a/apps/server-asset-sg/docker/Dockerfile +++ b/apps/server-asset-sg/docker/Dockerfile @@ -1,6 +1,6 @@ ARG APP_VERSION -FROM node:20-alpine as api-builder +FROM node:22-alpine as api-builder ENV APP_VERSION=${APP_VERSION} ENV NODE_ENV=development @@ -12,7 +12,7 @@ RUN npm install && ./node_modules/.bin/prisma generate RUN npx nx build server-asset-sg --configuration=production # final image build -FROM node:20-alpine +FROM node:22-alpine ENV APP_VERSION=${APP_VERSION} ENV NODE_ENV=production diff --git a/package-lock.json b/package-lock.json index 477ab27c..95c90370 100644 --- a/package-lock.json +++ b/package-lock.json @@ -142,6 +142,13 @@ "vite-plugin-eslint": "^1.8.1", "vite-tsconfig-paths": "^4.3.2", "vitest": "1.3.1" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "19.2.1", + "@nx/nx-darwin-x64": "19.2.1", + "@nx/nx-linux-x64-gnu": "19.2.1", + "@nx/nx-win32-x64-msvc": "19.2.1", + "@rollup/rollup-linux-x64-gnu": "4.16.0" } }, "node_modules/@adobe/css-tools": { @@ -303,2050 +310,1102 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/aix-ppc64": { + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-x64": { "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", - "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", "cpu": [ - "ppc64" + "x64" ], "optional": true, "os": [ - "aix" + "win32" ], "engines": { "node": ">=12" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", - "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" + "node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": { + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, "engines": { - "node": ">=12" + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm64": { + "node_modules/@angular-devkit/build-angular/node_modules/esbuild": { "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", - "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", - "cpu": [ - "arm64" - ], + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "hasInstallScript": true, "optional": true, - "os": [ - "android" - ], + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", - "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" + "node_modules/@angular-devkit/build-angular/node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, "engines": { - "node": ">=12" + "node": "^10 || ^12 || >=14" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", - "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@angular-devkit/build-angular/node_modules/vite": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.7.tgz", + "integrity": "sha512-sgnEEFTZYMui/sTlH1/XEnVNHMujOahPLGMxn1+5sIT45Xjng1Ec1K78jRP15dSmVgg5WBin9yO81j3o9OxofA==", + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=12" + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", - "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", "cpu": [ "x64" ], "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": ">=12" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", - "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", - "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1703.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.8.tgz", + "integrity": "sha512-9u6fl8VVOxcLOEMzrUeaybSvi9hSLSRucHnybneYrabsgreDo32tuy/4G8p6YAHQjpWEj9jvF9Um13ertdni5Q==", + "dependencies": { + "@angular-devkit/architect": "0.1703.8", + "rxjs": "7.8.1" + }, "engines": { - "node": ">=12" + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", - "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@angular-devkit/core": { + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.8.tgz", + "integrity": "sha512-Q8q0voCGudbdCgJ7lXdnyaxKHbNQBARH68zPQV72WT8NWy+Gw/tys870i6L58NWbBaCJEUcIj/kb6KoakSRu+Q==", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, "engines": { - "node": ">=12" + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", - "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@angular-devkit/schematics": { + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.8.tgz", + "integrity": "sha512-QRVEYpIfgkprNHc916JlPuNbLzOgrm9DZalHasnLUz4P6g7pR21olb8YCyM2OTJjombNhya9ZpckcADU5Qyvlg==", + "dependencies": { + "@angular-devkit/core": "17.3.8", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, "engines": { - "node": ">=12" + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", - "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.3.0.tgz", + "integrity": "sha512-ejfNzRuBeHUV8m2fkgs+M809rj5STuCuQo4fdfc6ccQpzXDI6Ha7BKpTznWfg5g529q/wrkoGSGgFxU9Yc2/dQ==", + "dev": true + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.3.0.tgz", + "integrity": "sha512-81cQbOEPoQupFX8WmpqZn+y8VA7JdVRGBtt+uJNKBXcJknTpPWdLBZRFlgVakmC24iEZ0Fint/N3NBBQI3mz2A==", + "dev": true, + "dependencies": { + "@angular-eslint/utils": "17.3.0", + "@typescript-eslint/utils": "7.2.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-loong64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", - "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.3.0.tgz", + "integrity": "sha512-9l/aRfpE9MCRVDWRb+rSB9Zei0paep1vqV6M/87VUnzBnzqeMRnVuPvQowilh2zweVSGKBF25Vp4HkwOL6ExDQ==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.3.0", + "@angular-eslint/utils": "17.3.0", + "@typescript-eslint/type-utils": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "aria-query": "5.3.0", + "axobject-query": "4.0.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", - "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "node_modules/@angular-eslint/template-parser": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.3.0.tgz", + "integrity": "sha512-m+UzAnWgtjeS0x6skSmR0eXltD/p7HZA+c8pPyAkiHQzkxE7ohhfyZc03yWGuYJvWQUqQAKKdO/nQop14TP0bg==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.3.0", + "eslint-scope": "^8.0.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", - "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "node_modules/@angular-eslint/utils": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.3.0.tgz", + "integrity": "sha512-PJT9pxWqpvI9OXO+7L5SIVhvMW+RFjeafC7PYjtvSbNFpz+kF644BiAcfMJ0YqBnkrw3JXt+RAX25CT4mXIoXw==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.3.0", + "@typescript-eslint/utils": "7.2.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", - "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@angular/animations": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.10.tgz", + "integrity": "sha512-9fR5snTuG4aM2K54TG/6DXcKXMDKZMovZhjQOxO8l68/oqn6fKrHs8DLzckFs0XGRZ+2OyURH8WggFm1Z828rA==", + "dependencies": { + "tslib": "^2.3.0" + }, "engines": { - "node": ">=12" + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.10" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-s390x": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", - "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "node_modules/@angular/cdk": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.10.tgz", + "integrity": "sha512-b1qktT2c1TTTe5nTji/kFAVW92fULK0YhYAvJ+BjZTPKu2FniZNe8o4qqQ0pUuvtMu+ZQxp/QqFYoidIVCjScg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", - "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@angular/cli": { + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.8.tgz", + "integrity": "sha512-X5ZOQ6ZTKVHjhIsfl32ZRqbs+FUoeHLbT7x4fh2Os/8ObDDwrUcCJPqxe2b2RB5E2d0vepYigknHeLE7gwzlNQ==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1703.8", + "@angular-devkit/core": "17.3.8", + "@angular-devkit/schematics": "17.3.8", + "@schematics/angular": "17.3.8", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "4.1.2", + "inquirer": "9.2.15", + "jsonc-parser": "3.2.1", + "npm-package-arg": "11.0.1", + "npm-pick-manifest": "9.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "pacote": "17.0.6", + "resolve": "1.22.8", + "semver": "7.6.0", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, "engines": { - "node": ">=12" + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", - "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], + "node_modules/@angular/common": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.10.tgz", + "integrity": "sha512-6SfD21M3LujymmZsZQIxAsV8Bj5u6He6ImZ+p2rr7FAhFxpVJyKldK8LCmJcFsBD4srpQcxEZ0iDxXvg+0ihAw==", + "dependencies": { + "tslib": "^2.3.0" + }, "engines": { - "node": ">=12" + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.10", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", - "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/sunos-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", - "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", - "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", - "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", - "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": { - "version": "10.4.18", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", - "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001591", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/esbuild": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", - "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", - "hasInstallScript": true, - "optional": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.1", - "@esbuild/android-arm": "0.20.1", - "@esbuild/android-arm64": "0.20.1", - "@esbuild/android-x64": "0.20.1", - "@esbuild/darwin-arm64": "0.20.1", - "@esbuild/darwin-x64": "0.20.1", - "@esbuild/freebsd-arm64": "0.20.1", - "@esbuild/freebsd-x64": "0.20.1", - "@esbuild/linux-arm": "0.20.1", - "@esbuild/linux-arm64": "0.20.1", - "@esbuild/linux-ia32": "0.20.1", - "@esbuild/linux-loong64": "0.20.1", - "@esbuild/linux-mips64el": "0.20.1", - "@esbuild/linux-ppc64": "0.20.1", - "@esbuild/linux-riscv64": "0.20.1", - "@esbuild/linux-s390x": "0.20.1", - "@esbuild/linux-x64": "0.20.1", - "@esbuild/netbsd-x64": "0.20.1", - "@esbuild/openbsd-x64": "0.20.1", - "@esbuild/sunos-x64": "0.20.1", - "@esbuild/win32-arm64": "0.20.1", - "@esbuild/win32-ia32": "0.20.1", - "@esbuild/win32-x64": "0.20.1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.7.tgz", - "integrity": "sha512-sgnEEFTZYMui/sTlH1/XEnVNHMujOahPLGMxn1+5sIT45Xjng1Ec1K78jRP15dSmVgg5WBin9yO81j3o9OxofA==", + "node_modules/@angular/compiler": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.10.tgz", + "integrity": "sha512-6Ce4siHyF0fCZBDm/cz+blJByGDu1/hbPkQVGmk5HGZTmCUeKkgyjoM6bZr7ssAsyGDRwxBh2SGHO4Ce31vuPA==", "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" - }, - "bin": { - "vite": "bin/vite.js" + "tslib": "^2.3.0" }, "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@angular/core": "17.3.10" }, "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { + "@angular/core": { "optional": true } } }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } - }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.1703.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.8.tgz", - "integrity": "sha512-9u6fl8VVOxcLOEMzrUeaybSvi9hSLSRucHnybneYrabsgreDo32tuy/4G8p6YAHQjpWEj9jvF9Um13ertdni5Q==", - "dependencies": { - "@angular-devkit/architect": "0.1703.8", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^4.0.0" - } - }, - "node_modules/@angular-devkit/core": { - "version": "17.3.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.8.tgz", - "integrity": "sha512-Q8q0voCGudbdCgJ7lXdnyaxKHbNQBARH68zPQV72WT8NWy+Gw/tys870i6L58NWbBaCJEUcIj/kb6KoakSRu+Q==", - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/schematics": { - "version": "17.3.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.8.tgz", - "integrity": "sha512-QRVEYpIfgkprNHc916JlPuNbLzOgrm9DZalHasnLUz4P6g7pR21olb8YCyM2OTJjombNhya9ZpckcADU5Qyvlg==", - "dependencies": { - "@angular-devkit/core": "17.3.8", - "jsonc-parser": "3.2.1", - "magic-string": "0.30.8", - "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.3.0.tgz", - "integrity": "sha512-ejfNzRuBeHUV8m2fkgs+M809rj5STuCuQo4fdfc6ccQpzXDI6Ha7BKpTznWfg5g529q/wrkoGSGgFxU9Yc2/dQ==", - "dev": true - }, - "node_modules/@angular-eslint/eslint-plugin": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.3.0.tgz", - "integrity": "sha512-81cQbOEPoQupFX8WmpqZn+y8VA7JdVRGBtt+uJNKBXcJknTpPWdLBZRFlgVakmC24iEZ0Fint/N3NBBQI3mz2A==", - "dev": true, - "dependencies": { - "@angular-eslint/utils": "17.3.0", - "@typescript-eslint/utils": "7.2.0" - }, - "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.3.0.tgz", - "integrity": "sha512-9l/aRfpE9MCRVDWRb+rSB9Zei0paep1vqV6M/87VUnzBnzqeMRnVuPvQowilh2zweVSGKBF25Vp4HkwOL6ExDQ==", - "dev": true, - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.3.0", - "@angular-eslint/utils": "17.3.0", - "@typescript-eslint/type-utils": "7.2.0", - "@typescript-eslint/utils": "7.2.0", - "aria-query": "5.3.0", - "axobject-query": "4.0.0" - }, - "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/template-parser": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.3.0.tgz", - "integrity": "sha512-m+UzAnWgtjeS0x6skSmR0eXltD/p7HZA+c8pPyAkiHQzkxE7ohhfyZc03yWGuYJvWQUqQAKKdO/nQop14TP0bg==", - "dev": true, - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.3.0", - "eslint-scope": "^8.0.0" - }, - "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/utils": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.3.0.tgz", - "integrity": "sha512-PJT9pxWqpvI9OXO+7L5SIVhvMW+RFjeafC7PYjtvSbNFpz+kF644BiAcfMJ0YqBnkrw3JXt+RAX25CT4mXIoXw==", - "dev": true, - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.3.0", - "@typescript-eslint/utils": "7.2.0" - }, - "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", - "typescript": "*" - } - }, - "node_modules/@angular/animations": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.10.tgz", - "integrity": "sha512-9fR5snTuG4aM2K54TG/6DXcKXMDKZMovZhjQOxO8l68/oqn6fKrHs8DLzckFs0XGRZ+2OyURH8WggFm1Z828rA==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/core": "17.3.10" - } - }, - "node_modules/@angular/cdk": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.10.tgz", - "integrity": "sha512-b1qktT2c1TTTe5nTji/kFAVW92fULK0YhYAvJ+BjZTPKu2FniZNe8o4qqQ0pUuvtMu+ZQxp/QqFYoidIVCjScg==", - "dependencies": { - "tslib": "^2.3.0" - }, - "optionalDependencies": { - "parse5": "^7.1.2" - }, - "peerDependencies": { - "@angular/common": "^17.0.0 || ^18.0.0", - "@angular/core": "^17.0.0 || ^18.0.0", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/cli": { - "version": "17.3.8", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.8.tgz", - "integrity": "sha512-X5ZOQ6ZTKVHjhIsfl32ZRqbs+FUoeHLbT7x4fh2Os/8ObDDwrUcCJPqxe2b2RB5E2d0vepYigknHeLE7gwzlNQ==", - "dev": true, - "dependencies": { - "@angular-devkit/architect": "0.1703.8", - "@angular-devkit/core": "17.3.8", - "@angular-devkit/schematics": "17.3.8", - "@schematics/angular": "17.3.8", - "@yarnpkg/lockfile": "1.1.0", - "ansi-colors": "4.1.3", - "ini": "4.1.2", - "inquirer": "9.2.15", - "jsonc-parser": "3.2.1", - "npm-package-arg": "11.0.1", - "npm-pick-manifest": "9.0.0", - "open": "8.4.2", - "ora": "5.4.1", - "pacote": "17.0.6", - "resolve": "1.22.8", - "semver": "7.6.0", - "symbol-observable": "4.0.0", - "yargs": "17.7.2" - }, - "bin": { - "ng": "bin/ng.js" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/common": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.10.tgz", - "integrity": "sha512-6SfD21M3LujymmZsZQIxAsV8Bj5u6He6ImZ+p2rr7FAhFxpVJyKldK8LCmJcFsBD4srpQcxEZ0iDxXvg+0ihAw==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/core": "17.3.10", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/compiler": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.10.tgz", - "integrity": "sha512-6Ce4siHyF0fCZBDm/cz+blJByGDu1/hbPkQVGmk5HGZTmCUeKkgyjoM6bZr7ssAsyGDRwxBh2SGHO4Ce31vuPA==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/core": "17.3.10" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - } - } - }, - "node_modules/@angular/compiler-cli": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.10.tgz", - "integrity": "sha512-85SBphqRj3szac3FbeYgEZ+I6WaAlo5h7JX06BdjOLLiaoIwlFhLeAuG+jVekseV+95grFUxIsCMphWHi2e6hQ==", - "dependencies": { - "@babel/core": "7.23.9", - "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^3.0.0", - "convert-source-map": "^1.5.1", - "reflect-metadata": "^0.2.0", - "semver": "^7.0.0", - "tslib": "^2.3.0", - "yargs": "^17.2.1" - }, - "bin": { - "ng-xi18n": "bundles/src/bin/ng_xi18n.js", - "ngc": "bundles/src/bin/ngc.js", - "ngcc": "bundles/ngcc/index.js" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/compiler": "17.3.10", - "typescript": ">=5.2 <5.5" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@angular/compiler-cli/node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" - }, - "node_modules/@angular/core": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.10.tgz", - "integrity": "sha512-ocEKu7X0yFCOvgJn1uZy76qjhsjKvULrO1k/BuIX0nwhp61DTGYTvCqKmwCBLM8/gvcKYH5vMKMHoQKtiSGE0A==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.14.0" - } - }, - "node_modules/@angular/forms": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.10.tgz", - "integrity": "sha512-0VZWSXDi2M3DAGJlpdV3lo73Yo/73GPRqmfTOrvIoUIenFg5Dz6oNGzvt/1aRkRn6HKccjix6iMpH91EN65pWA==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/common": "17.3.10", - "@angular/core": "17.3.10", - "@angular/platform-browser": "17.3.10", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/language-service": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.3.10.tgz", - "integrity": "sha512-6y0yEnjuKGCnH+YxmZZUC/KEb2ZuB5z7y0AOj4PwOladMWSwHv71x1rz5MokBVBf7ZTeN2w89f9jSWBzSz+fPw==", - "dev": true, - "engines": { - "node": "^18.13.0 || >=20.9.0" - } - }, - "node_modules/@angular/material": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.10.tgz", - "integrity": "sha512-hHMQES0tQPH5JW33W+mpBPuM8ybsloDTqFPuRV8cboDjosAWfJhzAKF3ozICpNlUrs62La/2Wu/756GcQrxebg==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/auto-init": "15.0.0-canary.7f224ddd4.0", - "@material/banner": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/card": "15.0.0-canary.7f224ddd4.0", - "@material/checkbox": "15.0.0-canary.7f224ddd4.0", - "@material/chips": "15.0.0-canary.7f224ddd4.0", - "@material/circular-progress": "15.0.0-canary.7f224ddd4.0", - "@material/data-table": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dialog": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/drawer": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/fab": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/form-field": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/image-list": "15.0.0-canary.7f224ddd4.0", - "@material/layout-grid": "15.0.0-canary.7f224ddd4.0", - "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", - "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu": "15.0.0-canary.7f224ddd4.0", - "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", - "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", - "@material/radio": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/segmented-button": "15.0.0-canary.7f224ddd4.0", - "@material/select": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/slider": "15.0.0-canary.7f224ddd4.0", - "@material/snackbar": "15.0.0-canary.7f224ddd4.0", - "@material/switch": "15.0.0-canary.7f224ddd4.0", - "@material/tab": "15.0.0-canary.7f224ddd4.0", - "@material/tab-bar": "15.0.0-canary.7f224ddd4.0", - "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", - "@material/textfield": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tooltip": "15.0.0-canary.7f224ddd4.0", - "@material/top-app-bar": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/animations": "^17.0.0 || ^18.0.0", - "@angular/cdk": "17.3.10", - "@angular/common": "^17.0.0 || ^18.0.0", - "@angular/core": "^17.0.0 || ^18.0.0", - "@angular/forms": "^17.0.0 || ^18.0.0", - "@angular/platform-browser": "^17.0.0 || ^18.0.0", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/material-date-fns-adapter": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/material-date-fns-adapter/-/material-date-fns-adapter-17.3.10.tgz", - "integrity": "sha512-Q4QAPGImZTjKW9ZhLSTkBeQX21I0dtak3JbexYx4CN/pHxKRpen6KaVAEqiORqq6vNUP2Kwb7cZznQyj6L7oQw==", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/core": "^17.0.0 || ^18.0.0", - "@angular/material": "17.3.10", - "date-fns": ">2.20.0 <4.0" - } - }, - "node_modules/@angular/platform-browser": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.10.tgz", - "integrity": "sha512-LEhBDOKm2A7nRmZqsafVp6OinRDG1OYZBSqjnT1jZ+f0CRRFIXz6aJ0TMPoU6vq9SLRJ7vrGD9P/eBf2hW00NQ==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/animations": "17.3.10", - "@angular/common": "17.3.10", - "@angular/core": "17.3.10" - }, - "peerDependenciesMeta": { - "@angular/animations": { - "optional": true - } - } - }, - "node_modules/@angular/platform-browser-dynamic": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.10.tgz", - "integrity": "sha512-TW6G4+isdHM2ssQTRTobeAKtR2516pJ25BSwRb+9+Jw/ZAEYOOi+KQyofIFYQccaUjb3+LpjRcaZbtZ9m/Ispg==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/common": "17.3.10", - "@angular/compiler": "17.3.10", - "@angular/core": "17.3.10", - "@angular/platform-browser": "17.3.10" - } - }, - "node_modules/@angular/router": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.10.tgz", - "integrity": "sha512-HlZlR9BOLoEKGOSMjmL5EfYL7F7PeDifbFi0dYWNcrG8zFrVKFklB1cuBdJhfPZgYhDEoGms/EToD71tg5wliA==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/common": "17.3.10", - "@angular/core": "17.3.10", - "@angular/platform-browser": "17.3.10", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@aws-crypto/crc32": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", - "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", - "dependencies": { - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/crc32/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@aws-crypto/crc32c": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz", - "integrity": "sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==", - "dependencies": { - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/crc32c/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@aws-crypto/ie11-detection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", - "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", - "dependencies": { - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@aws-crypto/sha1-browser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz", - "integrity": "sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==", - "dependencies": { - "@aws-crypto/ie11-detection": "^3.0.0", - "@aws-crypto/supports-web-crypto": "^3.0.0", - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/sha1-browser/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", - "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", - "dependencies": { - "@aws-crypto/ie11-detection": "^3.0.0", - "@aws-crypto/sha256-js": "^3.0.0", - "@aws-crypto/supports-web-crypto": "^3.0.0", - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", - "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", - "dependencies": { - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", - "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", - "dependencies": { - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@aws-crypto/util": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", - "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/util/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@aws-sdk/client-s3": { - "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.582.0.tgz", - "integrity": "sha512-yp3oIN48sQSJ01JF707KcOLAb7+UxcU6uYH0J48AG61z18tJ0SdE7KG2QPEFbK1RRyYXdHd8VLkbTVP+iwCLmw==", - "dependencies": { - "@aws-crypto/sha1-browser": "3.0.0", - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sso-oidc": "3.582.0", - "@aws-sdk/client-sts": "3.582.0", - "@aws-sdk/core": "3.582.0", - "@aws-sdk/credential-provider-node": "3.582.0", - "@aws-sdk/middleware-bucket-endpoint": "3.577.0", - "@aws-sdk/middleware-expect-continue": "3.577.0", - "@aws-sdk/middleware-flexible-checksums": "3.577.0", - "@aws-sdk/middleware-host-header": "3.577.0", - "@aws-sdk/middleware-location-constraint": "3.577.0", - "@aws-sdk/middleware-logger": "3.577.0", - "@aws-sdk/middleware-recursion-detection": "3.577.0", - "@aws-sdk/middleware-sdk-s3": "3.582.0", - "@aws-sdk/middleware-signing": "3.577.0", - "@aws-sdk/middleware-ssec": "3.577.0", - "@aws-sdk/middleware-user-agent": "3.577.0", - "@aws-sdk/region-config-resolver": "3.577.0", - "@aws-sdk/signature-v4-multi-region": "3.582.0", - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-endpoints": "3.577.0", - "@aws-sdk/util-user-agent-browser": "3.577.0", - "@aws-sdk/util-user-agent-node": "3.577.0", - "@aws-sdk/xml-builder": "3.575.0", - "@smithy/config-resolver": "^3.0.0", - "@smithy/core": "^2.0.1", - "@smithy/eventstream-serde-browser": "^3.0.0", - "@smithy/eventstream-serde-config-resolver": "^3.0.0", - "@smithy/eventstream-serde-node": "^3.0.0", - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/hash-blob-browser": "^3.0.0", - "@smithy/hash-node": "^3.0.0", - "@smithy/hash-stream-node": "^3.0.0", - "@smithy/invalid-dependency": "^3.0.0", - "@smithy/md5-js": "^3.0.0", - "@smithy/middleware-content-length": "^3.0.0", - "@smithy/middleware-endpoint": "^3.0.0", - "@smithy/middleware-retry": "^3.0.1", - "@smithy/middleware-serde": "^3.0.0", - "@smithy/middleware-stack": "^3.0.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/url-parser": "^3.0.0", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.1", - "@smithy/util-defaults-mode-node": "^3.0.1", - "@smithy/util-endpoints": "^2.0.0", - "@smithy/util-retry": "^3.0.0", - "@smithy/util-stream": "^3.0.1", - "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.582.0.tgz", - "integrity": "sha512-C6G2vNREANe5uUCYrTs8vvGhIrrS1GRoTjr0f5qmkZDuAtuBsQNoTF6Rt+0mDwXXBYW3FcNhZntaNCGVhXlugA==", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.582.0", - "@aws-sdk/middleware-host-header": "3.577.0", - "@aws-sdk/middleware-logger": "3.577.0", - "@aws-sdk/middleware-recursion-detection": "3.577.0", - "@aws-sdk/middleware-user-agent": "3.577.0", - "@aws-sdk/region-config-resolver": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-endpoints": "3.577.0", - "@aws-sdk/util-user-agent-browser": "3.577.0", - "@aws-sdk/util-user-agent-node": "3.577.0", - "@smithy/config-resolver": "^3.0.0", - "@smithy/core": "^2.0.1", - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/hash-node": "^3.0.0", - "@smithy/invalid-dependency": "^3.0.0", - "@smithy/middleware-content-length": "^3.0.0", - "@smithy/middleware-endpoint": "^3.0.0", - "@smithy/middleware-retry": "^3.0.1", - "@smithy/middleware-serde": "^3.0.0", - "@smithy/middleware-stack": "^3.0.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/url-parser": "^3.0.0", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.1", - "@smithy/util-defaults-mode-node": "^3.0.1", - "@smithy/util-endpoints": "^2.0.0", - "@smithy/util-middleware": "^3.0.0", - "@smithy/util-retry": "^3.0.0", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.582.0.tgz", - "integrity": "sha512-g4uiD4GUR03CqY6LwdocJxO+fHSBk/KNXBGJv1ENCcPmK3jpEI8xBggIQOQl3NWjDeP07bpIb8+UhgSoYAYtkg==", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.582.0", - "@aws-sdk/core": "3.582.0", - "@aws-sdk/credential-provider-node": "3.582.0", - "@aws-sdk/middleware-host-header": "3.577.0", - "@aws-sdk/middleware-logger": "3.577.0", - "@aws-sdk/middleware-recursion-detection": "3.577.0", - "@aws-sdk/middleware-user-agent": "3.577.0", - "@aws-sdk/region-config-resolver": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-endpoints": "3.577.0", - "@aws-sdk/util-user-agent-browser": "3.577.0", - "@aws-sdk/util-user-agent-node": "3.577.0", - "@smithy/config-resolver": "^3.0.0", - "@smithy/core": "^2.0.1", - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/hash-node": "^3.0.0", - "@smithy/invalid-dependency": "^3.0.0", - "@smithy/middleware-content-length": "^3.0.0", - "@smithy/middleware-endpoint": "^3.0.0", - "@smithy/middleware-retry": "^3.0.1", - "@smithy/middleware-serde": "^3.0.0", - "@smithy/middleware-stack": "^3.0.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/url-parser": "^3.0.0", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.1", - "@smithy/util-defaults-mode-node": "^3.0.1", - "@smithy/util-endpoints": "^2.0.0", - "@smithy/util-middleware": "^3.0.0", - "@smithy/util-retry": "^3.0.0", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-sts": { - "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.582.0.tgz", - "integrity": "sha512-3gaYyQkt8iTSStnjv6kJoPGDJUaPbhcgBOrXhUNbWUgAlgw7Y1aI1MYt3JqvVN4jtiCLwjuiAQATU/8elbqPdQ==", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sso-oidc": "3.582.0", - "@aws-sdk/core": "3.582.0", - "@aws-sdk/credential-provider-node": "3.582.0", - "@aws-sdk/middleware-host-header": "3.577.0", - "@aws-sdk/middleware-logger": "3.577.0", - "@aws-sdk/middleware-recursion-detection": "3.577.0", - "@aws-sdk/middleware-user-agent": "3.577.0", - "@aws-sdk/region-config-resolver": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-endpoints": "3.577.0", - "@aws-sdk/util-user-agent-browser": "3.577.0", - "@aws-sdk/util-user-agent-node": "3.577.0", - "@smithy/config-resolver": "^3.0.0", - "@smithy/core": "^2.0.1", - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/hash-node": "^3.0.0", - "@smithy/invalid-dependency": "^3.0.0", - "@smithy/middleware-content-length": "^3.0.0", - "@smithy/middleware-endpoint": "^3.0.0", - "@smithy/middleware-retry": "^3.0.1", - "@smithy/middleware-serde": "^3.0.0", - "@smithy/middleware-stack": "^3.0.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/url-parser": "^3.0.0", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.1", - "@smithy/util-defaults-mode-node": "^3.0.1", - "@smithy/util-endpoints": "^2.0.0", - "@smithy/util-middleware": "^3.0.0", - "@smithy/util-retry": "^3.0.0", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" + "node_modules/@angular/compiler-cli": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.10.tgz", + "integrity": "sha512-85SBphqRj3szac3FbeYgEZ+I6WaAlo5h7JX06BdjOLLiaoIwlFhLeAuG+jVekseV+95grFUxIsCMphWHi2e6hQ==", + "dependencies": { + "@babel/core": "7.23.9", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" }, "engines": { - "node": ">=16.0.0" + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/compiler": "17.3.10", + "typescript": ">=5.2 <5.5" } }, - "node_modules/@aws-sdk/core": { - "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.582.0.tgz", - "integrity": "sha512-ofmD96IQc9g1dbyqlCyxu5fCG7kIl9p1NoN5+vGBUyLdbmPCV3Pdg99nRHYEJuv2MgGx5AUFGDPMHcqbJpnZIw==", + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dependencies": { - "@smithy/core": "^2.0.1", - "@smithy/protocol-http": "^4.0.0", - "@smithy/signature-v4": "^3.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "fast-xml-parser": "4.2.5", - "tslib": "^2.6.2" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.577.0.tgz", - "integrity": "sha512-Jxu255j0gToMGEiqufP8ZtKI8HW90lOLjwJ3LrdlD/NLsAY0tOQf1fWc53u28hWmmNGMxmCrL2p66IOgMDhDUw==", + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/compiler-cli/node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/@angular/core": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.10.tgz", + "integrity": "sha512-ocEKu7X0yFCOvgJn1uZy76qjhsjKvULrO1k/BuIX0nwhp61DTGYTvCqKmwCBLM8/gvcKYH5vMKMHoQKtiSGE0A==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" + "tslib": "^2.3.0" }, "engines": { - "node": ">=16.0.0" + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.0" } }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.582.0.tgz", - "integrity": "sha512-kGOUKw5ryPkDIYB69PjK3SicVLTbWB06ouFN2W1EvqUJpkQGPAUGzYcomKtt3mJaCTf/1kfoaHwARAl6KKSP8Q==", + "node_modules/@angular/forms": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.10.tgz", + "integrity": "sha512-0VZWSXDi2M3DAGJlpdV3lo73Yo/73GPRqmfTOrvIoUIenFg5Dz6oNGzvt/1aRkRn6HKccjix6iMpH91EN65pWA==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/fetch-http-handler": "^3.0.1", - "@smithy/node-http-handler": "^3.0.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/smithy-client": "^3.0.1", - "@smithy/types": "^3.0.0", - "@smithy/util-stream": "^3.0.1", - "tslib": "^2.6.2" + "tslib": "^2.3.0" }, "engines": { - "node": ">=16.0.0" + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.10", + "@angular/core": "17.3.10", + "@angular/platform-browser": "17.3.10", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.582.0.tgz", - "integrity": "sha512-GWcjHx6ErcZAi5GZ7kItX7E6ygYmklm9tD9dbCWdsnis7IiWfYZNMXFQEwKCubUmhT61zjGZGDUiRcqVeZu1Aw==", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.577.0", - "@aws-sdk/credential-provider-process": "3.577.0", - "@aws-sdk/credential-provider-sso": "3.582.0", - "@aws-sdk/credential-provider-web-identity": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@smithy/credential-provider-imds": "^3.0.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" - }, + "node_modules/@angular/language-service": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.3.10.tgz", + "integrity": "sha512-6y0yEnjuKGCnH+YxmZZUC/KEb2ZuB5z7y0AOj4PwOladMWSwHv71x1rz5MokBVBf7ZTeN2w89f9jSWBzSz+fPw==", + "dev": true, "engines": { - "node": ">=16.0.0" + "node": "^18.13.0 || >=20.9.0" + } + }, + "node_modules/@angular/material": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.10.tgz", + "integrity": "sha512-hHMQES0tQPH5JW33W+mpBPuM8ybsloDTqFPuRV8cboDjosAWfJhzAKF3ozICpNlUrs62La/2Wu/756GcQrxebg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/auto-init": "15.0.0-canary.7f224ddd4.0", + "@material/banner": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/card": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/chips": "15.0.0-canary.7f224ddd4.0", + "@material/circular-progress": "15.0.0-canary.7f224ddd4.0", + "@material/data-table": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dialog": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/drawer": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/fab": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/form-field": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/image-list": "15.0.0-canary.7f224ddd4.0", + "@material/layout-grid": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/radio": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/segmented-button": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/slider": "15.0.0-canary.7f224ddd4.0", + "@material/snackbar": "15.0.0-canary.7f224ddd4.0", + "@material/switch": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-bar": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/textfield": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tooltip": "15.0.0-canary.7f224ddd4.0", + "@material/top-app-bar": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.3.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.582.0" + "@angular/animations": "^17.0.0 || ^18.0.0", + "@angular/cdk": "17.3.10", + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/forms": "^17.0.0 || ^18.0.0", + "@angular/platform-browser": "^17.0.0 || ^18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.582.0.tgz", - "integrity": "sha512-T8OLA/2xayRMT8z2eIZgo8tBAamTsBn7HWc8mL1a9yzv5OCPYvucNmbO915DY8u4cNbMl2dcB9frfVxIrahCXw==", + "node_modules/@angular/material-date-fns-adapter": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/material-date-fns-adapter/-/material-date-fns-adapter-17.3.10.tgz", + "integrity": "sha512-Q4QAPGImZTjKW9ZhLSTkBeQX21I0dtak3JbexYx4CN/pHxKRpen6KaVAEqiORqq6vNUP2Kwb7cZznQyj6L7oQw==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.577.0", - "@aws-sdk/credential-provider-http": "3.582.0", - "@aws-sdk/credential-provider-ini": "3.582.0", - "@aws-sdk/credential-provider-process": "3.577.0", - "@aws-sdk/credential-provider-sso": "3.582.0", - "@aws-sdk/credential-provider-web-identity": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@smithy/credential-provider-imds": "^3.0.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" + "tslib": "^2.3.0" }, - "engines": { - "node": ">=16.0.0" + "peerDependencies": { + "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/material": "17.3.10", + "date-fns": ">2.20.0 <4.0" } }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.577.0.tgz", - "integrity": "sha512-Gin6BWtOiXxIgITrJ3Nwc+Y2P1uVT6huYR4EcbA/DJUPWyO0n9y5UFLewPvVbLkRn15JeEqErBLUrHclkiOKtw==", + "node_modules/@angular/platform-browser": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.10.tgz", + "integrity": "sha512-LEhBDOKm2A7nRmZqsafVp6OinRDG1OYZBSqjnT1jZ+f0CRRFIXz6aJ0TMPoU6vq9SLRJ7vrGD9P/eBf2hW00NQ==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" + "tslib": "^2.3.0" }, "engines": { - "node": ">=16.0.0" + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/animations": "17.3.10", + "@angular/common": "17.3.10", + "@angular/core": "17.3.10" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } } }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.582.0.tgz", - "integrity": "sha512-PSiBX6YvJaodGSVg6dReWfeYgK5Tl4fUi0GMuD9WXo/ckfxAPdDFtIfVR6VkSPUrkZj26uw1Pwqeefp2H5phag==", + "node_modules/@angular/platform-browser-dynamic": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.10.tgz", + "integrity": "sha512-TW6G4+isdHM2ssQTRTobeAKtR2516pJ25BSwRb+9+Jw/ZAEYOOi+KQyofIFYQccaUjb3+LpjRcaZbtZ9m/Ispg==", "dependencies": { - "@aws-sdk/client-sso": "3.582.0", - "@aws-sdk/token-providers": "3.577.0", - "@aws-sdk/types": "3.577.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/shared-ini-file-loader": "^3.0.0", - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" + "tslib": "^2.3.0" }, "engines": { - "node": ">=16.0.0" + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.10", + "@angular/compiler": "17.3.10", + "@angular/core": "17.3.10", + "@angular/platform-browser": "17.3.10" } }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.577.0.tgz", - "integrity": "sha512-ZGHGNRaCtJJmszb9UTnC7izNCtRUttdPlLdMkh41KPS32vfdrBDHs1JrpbZijItRj1xKuOXsiYSXLAaHGcLh8Q==", + "node_modules/@angular/router": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.10.tgz", + "integrity": "sha512-HlZlR9BOLoEKGOSMjmL5EfYL7F7PeDifbFi0dYWNcrG8zFrVKFklB1cuBdJhfPZgYhDEoGms/EToD71tg5wliA==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/property-provider": "^3.0.0", - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" + "tslib": "^2.3.0" }, "engines": { - "node": ">=16.0.0" + "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.577.0" + "@angular/common": "17.3.10", + "@angular/core": "17.3.10", + "@angular/platform-browser": "17.3.10", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.577.0.tgz", - "integrity": "sha512-twlkNX2VofM6kHXzDEiJOiYCc9tVABe5cbyxMArRWscIsCWG9mamPhC77ezG4XsN9dFEwVdxEYD5Crpm/5EUiw==", + "node_modules/@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-arn-parser": "3.568.0", - "@smithy/node-config-provider": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/types": "^3.0.0", - "@smithy/util-config-provider": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" } }, - "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.577.0.tgz", - "integrity": "sha512-6dPp8Tv4F0of4un5IAyG6q++GrRrNQQ4P2NAMB1W0VO4JoEu1C8GievbbDLi88TFIFmtKpnHB0ODCzwnoe8JsA==", + "node_modules/@aws-crypto/crc32/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/crc32c": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz", + "integrity": "sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" } }, - "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.577.0.tgz", - "integrity": "sha512-IHAUEipIfagjw92LV8SOSBiCF7ZnqfHcw14IkcZW2/mfrCy1Fh/k40MoS/t3Tro2tQ91rgQPwUoSgB/QCi2Org==", + "node_modules/@aws-crypto/crc32c/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", "dependencies": { - "@aws-crypto/crc32": "3.0.0", - "@aws-crypto/crc32c": "3.0.0", - "@aws-sdk/types": "3.577.0", - "@smithy/is-array-buffer": "^3.0.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/types": "^3.0.0", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" + "tslib": "^1.11.1" } }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.577.0.tgz", - "integrity": "sha512-9ca5MJz455CODIVXs0/sWmJm7t3QO4EUa1zf8pE8grLpzf0J94bz/skDWm37Pli13T3WaAQBHCTiH2gUVfCsWg==", + "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz", + "integrity": "sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/protocol-http": "^4.0.0", - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" } }, - "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.577.0.tgz", - "integrity": "sha512-DKPTD2D2s+t2QUo/IXYtVa/6Un8GZ+phSTBkyBNx2kfZz4Kwavhl/JJzSqTV3GfCXkVdFu7CrjoX7BZ6qWeTUA==", + "node_modules/@aws-crypto/sha1-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" } }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.577.0.tgz", - "integrity": "sha512-aPFGpGjTZcJYk+24bg7jT4XdIp42mFXSuPt49lw5KygefLyJM/sB0bKKqPYYivW0rcuZ9brQ58eZUNthrzYAvg==", + "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" } }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.577.0.tgz", - "integrity": "sha512-pn3ZVEd2iobKJlR3H+bDilHjgRnNrQ6HMmK9ZzZw89Ckn3Dcbv48xOv4RJvu0aU8SDLl/SNCxppKjeLDTPGBNA==", + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.582.0.tgz", + "integrity": "sha512-yp3oIN48sQSJ01JF707KcOLAb7+UxcU6uYH0J48AG61z18tJ0SdE7KG2QPEFbK1RRyYXdHd8VLkbTVP+iwCLmw==", "dependencies": { + "@aws-crypto/sha1-browser": "3.0.0", + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sso-oidc": "3.582.0", + "@aws-sdk/client-sts": "3.582.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/credential-provider-node": "3.582.0", + "@aws-sdk/middleware-bucket-endpoint": "3.577.0", + "@aws-sdk/middleware-expect-continue": "3.577.0", + "@aws-sdk/middleware-flexible-checksums": "3.577.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-location-constraint": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-sdk-s3": "3.582.0", + "@aws-sdk/middleware-signing": "3.577.0", + "@aws-sdk/middleware-ssec": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.577.0", + "@aws-sdk/region-config-resolver": "3.577.0", + "@aws-sdk/signature-v4-multi-region": "3.582.0", "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.577.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@aws-sdk/xml-builder": "3.575.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/eventstream-serde-browser": "^3.0.0", + "@smithy/eventstream-serde-config-resolver": "^3.0.0", + "@smithy/eventstream-serde-node": "^3.0.0", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-blob-browser": "^3.0.0", + "@smithy/hash-node": "^3.0.0", + "@smithy/hash-stream-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/md5-js": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-stream": "^3.0.1", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3": { + "node_modules/@aws-sdk/client-sso": { "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.582.0.tgz", - "integrity": "sha512-PJqQpLoLaZPRI4L/XZUeHkd9UVK8VAr9R38wv0osGeMTvzD9iwzzk0I2TtBqFda/5xEB1YgVYZwyqvmStXmttg==", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.582.0.tgz", + "integrity": "sha512-C6G2vNREANe5uUCYrTs8vvGhIrrS1GRoTjr0f5qmkZDuAtuBsQNoTF6Rt+0mDwXXBYW3FcNhZntaNCGVhXlugA==", "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.577.0", + "@aws-sdk/region-config-resolver": "3.577.0", "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-arn-parser": "3.568.0", + "@aws-sdk/util-endpoints": "3.577.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", "@smithy/protocol-http": "^4.0.0", - "@smithy/signature-v4": "^3.0.0", "@smithy/smithy-client": "^3.0.1", "@smithy/types": "^3.0.0", - "@smithy/util-config-provider": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-signing": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.577.0.tgz", - "integrity": "sha512-QS/dh3+NqZbXtY0j/DZ867ogP413pG5cFGqBy9OeOhDMsolcwLrQbi0S0c621dc1QNq+er9ffaMhZ/aPkyXXIg==", + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.582.0.tgz", + "integrity": "sha512-g4uiD4GUR03CqY6LwdocJxO+fHSBk/KNXBGJv1ENCcPmK3jpEI8xBggIQOQl3NWjDeP07bpIb8+UhgSoYAYtkg==", "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.582.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/credential-provider-node": "3.582.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.577.0", + "@aws-sdk/region-config-resolver": "3.577.0", "@aws-sdk/types": "3.577.0", - "@smithy/property-provider": "^3.0.0", + "@aws-sdk/util-endpoints": "3.577.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", "@smithy/protocol-http": "^4.0.0", - "@smithy/signature-v4": "^3.0.0", + "@smithy/smithy-client": "^3.0.1", "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.577.0.tgz", - "integrity": "sha512-i2BPJR+rp8xmRVIGc0h1kDRFcM2J9GnClqqpc+NLSjmYadlcg4mPklisz9HzwFVcRPJ5XcGf3U4BYs5G8+iTyg==", + "node_modules/@aws-sdk/client-sts": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.582.0.tgz", + "integrity": "sha512-3gaYyQkt8iTSStnjv6kJoPGDJUaPbhcgBOrXhUNbWUgAlgw7Y1aI1MYt3JqvVN4jtiCLwjuiAQATU/8elbqPdQ==", "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sso-oidc": "3.582.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/credential-provider-node": "3.582.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.577.0", + "@aws-sdk/region-config-resolver": "3.577.0", "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.577.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.577.0.tgz", - "integrity": "sha512-P55HAXgwmiHHpFx5JEPvOnAbfhN7v6sWv9PBQs+z2tC7QiBcPS0cdJR6PfV7J1n4VPK52/OnrK3l9VxdQ7Ms0g==", + "node_modules/@aws-sdk/core": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.582.0.tgz", + "integrity": "sha512-ofmD96IQc9g1dbyqlCyxu5fCG7kIl9p1NoN5+vGBUyLdbmPCV3Pdg99nRHYEJuv2MgGx5AUFGDPMHcqbJpnZIw==", "dependencies": { - "@aws-sdk/types": "3.577.0", - "@aws-sdk/util-endpoints": "3.577.0", + "@smithy/core": "^2.0.1", "@smithy/protocol-http": "^4.0.0", + "@smithy/signature-v4": "^3.0.0", + "@smithy/smithy-client": "^3.0.1", "@smithy/types": "^3.0.0", + "fast-xml-parser": "4.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/credential-provider-env": { "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.577.0.tgz", - "integrity": "sha512-4ChCFACNwzqx/xjg3zgFcW8Ali6R9C95cFECKWT/7CUM1D0MGvkclSH2cLarmHCmJgU6onKkJroFtWp0kHhgyg==", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.577.0.tgz", + "integrity": "sha512-Jxu255j0gToMGEiqufP8ZtKI8HW90lOLjwJ3LrdlD/NLsAY0tOQf1fWc53u28hWmmNGMxmCrL2p66IOgMDhDUw==", "dependencies": { "@aws-sdk/types": "3.577.0", - "@smithy/node-config-provider": "^3.0.0", + "@smithy/property-provider": "^3.0.0", "@smithy/types": "^3.0.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/signature-v4-multi-region": { + "node_modules/@aws-sdk/credential-provider-http": { "version": "3.582.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.582.0.tgz", - "integrity": "sha512-aFCOjjNqEX2l+V8QjOWy5F7CtHIC/RlYdBuv3No6yxn+pMvVUUe6zdMk2yHWcudVpHWsyvcZzAUBliAPeFLPsQ==", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.582.0.tgz", + "integrity": "sha512-kGOUKw5ryPkDIYB69PjK3SicVLTbWB06ouFN2W1EvqUJpkQGPAUGzYcomKtt3mJaCTf/1kfoaHwARAl6KKSP8Q==", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.582.0", "@aws-sdk/types": "3.577.0", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/property-provider": "^3.0.0", "@smithy/protocol-http": "^4.0.0", - "@smithy/signature-v4": "^3.0.0", + "@smithy/smithy-client": "^3.0.1", "@smithy/types": "^3.0.0", + "@smithy/util-stream": "^3.0.1", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.577.0.tgz", - "integrity": "sha512-0CkIZpcC3DNQJQ1hDjm2bdSy/Xjs7Ny5YvSsacasGOkNfk+FdkiQy6N67bZX3Zbc9KIx+Nz4bu3iDeNSNplnnQ==", + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.582.0.tgz", + "integrity": "sha512-GWcjHx6ErcZAi5GZ7kItX7E6ygYmklm9tD9dbCWdsnis7IiWfYZNMXFQEwKCubUmhT61zjGZGDUiRcqVeZu1Aw==", "dependencies": { + "@aws-sdk/credential-provider-env": "3.577.0", + "@aws-sdk/credential-provider-process": "3.577.0", + "@aws-sdk/credential-provider-sso": "3.582.0", + "@aws-sdk/credential-provider-web-identity": "3.577.0", "@aws-sdk/types": "3.577.0", + "@smithy/credential-provider-imds": "^3.0.0", "@smithy/property-provider": "^3.0.0", "@smithy/shared-ini-file-loader": "^3.0.0", "@smithy/types": "^3.0.0", @@ -2356,75 +1415,70 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.577.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.577.0.tgz", - "integrity": "sha512-FT2JZES3wBKN/alfmhlo+3ZOq/XJ0C7QOZcDNrpKjB0kqYoKjhVKZ/Hx6ArR0czkKfHzBBEs6y40ebIHx2nSmA==", - "dependencies": { - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.568.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.568.0.tgz", - "integrity": "sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" + "@aws-sdk/client-sts": "^3.582.0" } }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.577.0.tgz", - "integrity": "sha512-FjuUz1Kdy4Zly2q/c58tpdqHd6z7iOdU/caYzoc8jwgAHBDBbIJNQLCU9hXJnPV2M8pWxQDyIZsoVwtmvErPzw==", + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.582.0.tgz", + "integrity": "sha512-T8OLA/2xayRMT8z2eIZgo8tBAamTsBn7HWc8mL1a9yzv5OCPYvucNmbO915DY8u4cNbMl2dcB9frfVxIrahCXw==", "dependencies": { + "@aws-sdk/credential-provider-env": "3.577.0", + "@aws-sdk/credential-provider-http": "3.582.0", + "@aws-sdk/credential-provider-ini": "3.582.0", + "@aws-sdk/credential-provider-process": "3.577.0", + "@aws-sdk/credential-provider-sso": "3.582.0", + "@aws-sdk/credential-provider-web-identity": "3.577.0", "@aws-sdk/types": "3.577.0", + "@smithy/credential-provider-imds": "^3.0.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", "@smithy/types": "^3.0.0", - "@smithy/util-endpoints": "^2.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.568.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.568.0.tgz", - "integrity": "sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==", + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.577.0.tgz", + "integrity": "sha512-Gin6BWtOiXxIgITrJ3Nwc+Y2P1uVT6huYR4EcbA/DJUPWyO0n9y5UFLewPvVbLkRn15JeEqErBLUrHclkiOKtw==", "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.577.0.tgz", - "integrity": "sha512-zEAzHgR6HWpZOH7xFgeJLc6/CzMcx4nxeQolZxVZoB5pPaJd3CjyRhZN0xXeZB0XIRCWmb4yJBgyiugXLNMkLA==", + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.582.0.tgz", + "integrity": "sha512-PSiBX6YvJaodGSVg6dReWfeYgK5Tl4fUi0GMuD9WXo/ckfxAPdDFtIfVR6VkSPUrkZj26uw1Pwqeefp2H5phag==", "dependencies": { + "@aws-sdk/client-sso": "3.582.0", + "@aws-sdk/token-providers": "3.577.0", "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", "@smithy/types": "^3.0.0", - "bowser": "^2.11.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.577.0.tgz", - "integrity": "sha512-XqvtFjbSMtycZTWVwDe8DRWovuoMbA54nhUoZwVU6rW9OSD6NZWGR512BUGHFaWzW0Wg8++Dj10FrKTG2XtqfA==", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.577.0.tgz", + "integrity": "sha512-ZGHGNRaCtJJmszb9UTnC7izNCtRUttdPlLdMkh41KPS32vfdrBDHs1JrpbZijItRj1xKuOXsiYSXLAaHGcLh8Q==", "dependencies": { "@aws-sdk/types": "3.577.0", - "@smithy/node-config-provider": "^3.0.0", + "@smithy/property-provider": "^3.0.0", "@smithy/types": "^3.0.0", "tslib": "^2.6.2" }, @@ -2432,513 +1486,462 @@ "node": ">=16.0.0" }, "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/util-utf8-browser": { - "version": "3.259.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", - "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", - "dependencies": { - "tslib": "^2.3.1" + "@aws-sdk/client-sts": "^3.577.0" } }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.575.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.575.0.tgz", - "integrity": "sha512-cWgAwmbFYNCFzPwxL705+lWps0F3ZvOckufd2KKoEZUmtpVw9/txUXNrPySUXSmRTSRhoatIMABNfStWR043bQ==", + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.577.0.tgz", + "integrity": "sha512-twlkNX2VofM6kHXzDEiJOiYCc9tVABe5cbyxMArRWscIsCWG9mamPhC77ezG4XsN9dFEwVdxEYD5Crpm/5EUiw==", "dependencies": { + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-arn-parser": "3.568.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", "@smithy/types": "^3.0.0", + "@smithy/util-config-provider": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", - "dependencies": { - "@babel/highlight": "^7.24.2", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", - "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.577.0.tgz", + "integrity": "sha512-6dPp8Tv4F0of4un5IAyG6q++GrRrNQQ4P2NAMB1W0VO4JoEu1C8GievbbDLi88TFIFmtKpnHB0ODCzwnoe8JsA==", "dependencies": { - "@babel/types": "^7.22.15" + "@aws-sdk/types": "3.577.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.577.0.tgz", + "integrity": "sha512-IHAUEipIfagjw92LV8SOSBiCF7ZnqfHcw14IkcZW2/mfrCy1Fh/k40MoS/t3Tro2tQ91rgQPwUoSgB/QCi2Org==", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "@aws-crypto/crc32": "3.0.0", + "@aws-crypto/crc32c": "3.0.0", + "@aws-sdk/types": "3.577.0", + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", - "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.577.0.tgz", + "integrity": "sha512-9ca5MJz455CODIVXs0/sWmJm7t3QO4EUa1zf8pE8grLpzf0J94bz/skDWm37Pli13T3WaAQBHCTiH2gUVfCsWg==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.24.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "semver": "^6.3.1" + "@aws-sdk/types": "3.577.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.577.0.tgz", + "integrity": "sha512-DKPTD2D2s+t2QUo/IXYtVa/6Un8GZ+phSTBkyBNx2kfZz4Kwavhl/JJzSqTV3GfCXkVdFu7CrjoX7BZ6qWeTUA==", "dependencies": { - "@babel/types": "^7.24.5" + "@aws-sdk/types": "3.577.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.577.0.tgz", + "integrity": "sha512-aPFGpGjTZcJYk+24bg7jT4XdIp42mFXSuPt49lw5KygefLyJM/sB0bKKqPYYivW0rcuZ9brQ58eZUNthrzYAvg==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" + "@aws-sdk/types": "3.577.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.577.0.tgz", + "integrity": "sha512-pn3ZVEd2iobKJlR3H+bDilHjgRnNrQ6HMmK9ZzZw89Ckn3Dcbv48xOv4RJvu0aU8SDLl/SNCxppKjeLDTPGBNA==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.582.0.tgz", + "integrity": "sha512-PJqQpLoLaZPRI4L/XZUeHkd9UVK8VAr9R38wv0osGeMTvzD9iwzzk0I2TtBqFda/5xEB1YgVYZwyqvmStXmttg==", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-arn-parser": "3.568.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/signature-v4": "^3.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/util-config-provider": "^3.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "node_modules/@aws-sdk/middleware-signing": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.577.0.tgz", + "integrity": "sha512-QS/dh3+NqZbXtY0j/DZ867ogP413pG5cFGqBy9OeOhDMsolcwLrQbi0S0c621dc1QNq+er9ffaMhZ/aPkyXXIg==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/signature-v4": "^3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-middleware": "^3.0.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.577.0.tgz", + "integrity": "sha512-i2BPJR+rp8xmRVIGc0h1kDRFcM2J9GnClqqpc+NLSjmYadlcg4mPklisz9HzwFVcRPJ5XcGf3U4BYs5G8+iTyg==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@aws-sdk/types": "3.577.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.577.0.tgz", + "integrity": "sha512-P55HAXgwmiHHpFx5JEPvOnAbfhN7v6sWv9PBQs+z2tC7QiBcPS0cdJR6PfV7J1n4VPK52/OnrK3l9VxdQ7Ms0g==", "dependencies": { - "@babel/types": "^7.22.5" + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.577.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", - "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.577.0.tgz", + "integrity": "sha512-4ChCFACNwzqx/xjg3zgFcW8Ali6R9C95cFECKWT/7CUM1D0MGvkclSH2cLarmHCmJgU6onKkJroFtWp0kHhgyg==", "dependencies": { - "@babel/types": "^7.24.5" + "@aws-sdk/types": "3.577.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.582.0.tgz", + "integrity": "sha512-aFCOjjNqEX2l+V8QjOWy5F7CtHIC/RlYdBuv3No6yxn+pMvVUUe6zdMk2yHWcudVpHWsyvcZzAUBliAPeFLPsQ==", "dependencies": { - "@babel/types": "^7.24.0" + "@aws-sdk/middleware-sdk-s3": "3.582.0", + "@aws-sdk/types": "3.577.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/signature-v4": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "node_modules/@aws-sdk/token-providers": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.577.0.tgz", + "integrity": "sha512-0CkIZpcC3DNQJQ1hDjm2bdSy/Xjs7Ny5YvSsacasGOkNfk+FdkiQy6N67bZX3Zbc9KIx+Nz4bu3iDeNSNplnnQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" + "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@aws-sdk/client-sso-oidc": "^3.577.0" } }, - "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "node_modules/@aws-sdk/types": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.577.0.tgz", + "integrity": "sha512-FT2JZES3wBKN/alfmhlo+3ZOq/XJ0C7QOZcDNrpKjB0kqYoKjhVKZ/Hx6ArR0czkKfHzBBEs6y40ebIHx2nSmA==", "dependencies": { - "@babel/types": "^7.24.5" + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.568.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.568.0.tgz", + "integrity": "sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==", "dependencies": { - "@babel/types": "^7.22.5" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.577.0.tgz", + "integrity": "sha512-FjuUz1Kdy4Zly2q/c58tpdqHd6z7iOdU/caYzoc8jwgAHBDBbIJNQLCU9hXJnPV2M8pWxQDyIZsoVwtmvErPzw==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/types": "^3.0.0", + "@smithy/util-endpoints": "^2.0.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.568.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.568.0.tgz", + "integrity": "sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", - "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.577.0.tgz", + "integrity": "sha512-zEAzHgR6HWpZOH7xFgeJLc6/CzMcx4nxeQolZxVZoB5pPaJd3CjyRhZN0xXeZB0XIRCWmb4yJBgyiugXLNMkLA==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@aws-sdk/types": "3.577.0", + "@smithy/types": "^3.0.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.577.0.tgz", + "integrity": "sha512-XqvtFjbSMtycZTWVwDe8DRWovuoMbA54nhUoZwVU6rW9OSD6NZWGR512BUGHFaWzW0Wg8++Dj10FrKTG2XtqfA==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" + "tslib": "^2.3.1" } }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "node_modules/@aws-sdk/xml-builder": { + "version": "3.575.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.575.0.tgz", + "integrity": "sha512-cWgAwmbFYNCFzPwxL705+lWps0F3ZvOckufd2KKoEZUmtpVw9/txUXNrPySUXSmRTSRhoatIMABNfStWR043bQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "node_modules/@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "engines": { - "node": ">=6.9.0" + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", - "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dependencies": { - "@babel/helper-function-name": "^7.23.0", - "@babel/template": "^7.24.0", - "@babel/types": "^7.24.5" + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", - "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", - "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.24.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", - "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", + "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.24.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -2947,296 +1950,342 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.1.tgz", - "integrity": "sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==", + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-decorators": "^7.24.1" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "devOptional": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz", - "integrity": "sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", + "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/types": "^7.24.5" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/types": "^7.24.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", - "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", - "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/types": "^7.22.5" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-syntax-jsx": { + "node_modules/@babel/helper-replace-supers": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", - "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "node_modules/@babel/helper-simple-access": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/types": "^7.24.5" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/types": "^7.22.5" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/types": "^7.22.5" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", + "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-function-name": "^7.23.0", + "@babel/template": "^7.24.0", + "@babel/types": "^7.24.5" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "node_modules/@babel/helpers": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "node_modules/@babel/highlight": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-validator-identifier": "^7.24.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "bin": { + "parser": "bin/babel-parser.js" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", + "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-syntax-typescript": { + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", - "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", + "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.24.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", + "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3245,12 +2294,14 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { + "node_modules/@babel/plugin-proposal-decorators": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", - "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.1.tgz", + "integrity": "sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-decorators": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -3259,16 +2310,10 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", - "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "engines": { "node": ">=6.9.0" }, @@ -3276,42 +2321,46 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", - "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "devOptional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", - "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -3320,12 +2369,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-class-properties": { + "node_modules/@babel/plugin-syntax-decorators": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", - "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz", + "integrity": "sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { @@ -3335,61 +2383,48 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", - "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.4", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { - "@babel/core": "^7.12.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", - "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-split-export-declaration": "^7.24.5", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", "dependencies": { - "@babel/types": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-computed-properties": { + "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", - "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", + "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/template": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3398,26 +2433,33 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", - "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.10.4" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-dotall-regex": { + "node_modules/@babel/plugin-syntax-jsx": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", - "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", + "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { @@ -3427,103 +2469,78 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", - "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", - "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", - "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", - "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", - "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", - "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", - "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -3532,12 +2549,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", - "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -3546,13 +2563,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "node_modules/@babel/plugin-syntax-typescript": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", - "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", + "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3561,26 +2577,26 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", - "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-modules-amd": { + "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", - "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", + "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { @@ -3590,14 +2606,15 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", - "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { "node": ">=6.9.0" @@ -3606,15 +2623,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", - "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -3623,12 +2639,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-umd": { + "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", - "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", + "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { @@ -3638,26 +2653,26 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", + "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-new-target": { + "node_modules/@babel/plugin-transform-class-properties": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", - "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", + "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { @@ -3667,28 +2682,35 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", - "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", + "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.4", "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.12.0" } }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", - "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", + "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-split-export-declaration": "^7.24.5", + "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" @@ -3697,30 +2719,24 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-object-rest-spread": { + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", - "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-object-super": { + "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", - "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", + "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", "dependencies": { "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-replace-supers": "^7.24.1" + "@babel/template": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3729,13 +2745,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", - "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", + "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -3744,14 +2759,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", - "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", + "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3760,12 +2774,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", - "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", + "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3774,13 +2788,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-private-methods": { + "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", - "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", + "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -3789,15 +2803,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", - "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", + "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.5", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3806,12 +2818,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-property-literals": { + "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", - "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", + "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -3820,13 +2833,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-regenerator": { + "node_modules/@babel/plugin-transform-for-of": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", - "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", + "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", "dependencies": { "@babel/helper-plugin-utils": "^7.24.0", - "regenerator-transform": "^0.15.2" + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -3835,11 +2848,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-reserved-words": { + "node_modules/@babel/plugin-transform-function-name": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", - "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", + "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { @@ -3849,17 +2864,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", - "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", + "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.24.0", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", - "semver": "^6.3.1" + "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -3868,18 +2879,10 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { + "node_modules/@babel/plugin-transform-literals": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", - "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", + "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", "dependencies": { "@babel/helper-plugin-utils": "^7.24.0" }, @@ -3890,13 +2893,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-spread": { + "node_modules/@babel/plugin-transform-logical-assignment-operators": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", - "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", + "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", "dependencies": { "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { "node": ">=6.9.0" @@ -3905,10 +2908,10 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-sticky-regex": { + "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", - "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", + "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", "dependencies": { "@babel/helper-plugin-utils": "^7.24.0" }, @@ -3919,11 +2922,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-template-literals": { + "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", - "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", + "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { @@ -3933,12 +2937,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", - "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-simple-access": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -3947,15 +2953,15 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.5.tgz", - "integrity": "sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==", + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", + "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.5", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/plugin-syntax-typescript": "^7.24.1" + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -3964,11 +2970,12 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-unicode-escapes": { + "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", - "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", + "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { @@ -3978,27 +2985,26 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", - "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-unicode-regex": { + "node_modules/@babel/plugin-transform-new-target": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", - "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", + "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { @@ -4008,106 +3014,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", - "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", - "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", + "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.9", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.9", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.24.0", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", - "core-js-compat": "^3.31.0", - "semver": "^6.3.1" + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -4116,37 +3029,45 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", + "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", + "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-typescript": { + "node_modules/@babel/plugin-transform-object-super": { "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz", - "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", + "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-syntax-jsx": "^7.24.1", - "@babel/plugin-transform-modules-commonjs": "^7.24.1", - "@babel/plugin-transform-typescript": "^7.24.1" + "@babel/helper-replace-supers": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -4155,825 +3076,899 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, - "node_modules/@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", + "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", "dependencies": { - "regenerator-runtime": "^0.14.0" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", + "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/traverse": { + "node_modules/@babel/plugin-transform-parameters": { "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", + "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", + "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", "dependencies": { - "@babel/types": "^7.24.5", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { + "node_modules/@babel/plugin-transform-private-property-in-object": { "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", + "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", "dependencies": { - "@babel/types": "^7.24.5" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", + "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "devOptional": true + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", + "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "optional": true, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", + "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=0.1.90" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", + "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", + "semver": "^6.3.1" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", + "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@csstools/postcss-color-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", - "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", - "dev": true, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", + "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", + "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.2" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", - "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", - "dev": true, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", + "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.2" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", - "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", - "dev": true, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", + "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.2" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", - "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", - "dev": true, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.5.tgz", + "integrity": "sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-typescript": "^7.24.1" }, "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.2" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", - "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", - "dev": true, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", + "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.2" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", - "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", - "dev": true, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", + "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.2" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", - "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", - "dev": true, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", + "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.2" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", - "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", - "dev": true, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", + "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { - "node": "^12 || ^14 || >=16" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.3" + "@babel/core": "^7.0.0" } }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", - "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", - "dev": true, + "node_modules/@babel/preset-env": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", + "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", + "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.24.0", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=6.9.0" }, "peerDependencies": { - "postcss": "^8.2" + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@csstools/postcss-unset-value": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", - "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", - "dev": true, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" }, "peerDependencies": { - "postcss": "^8.2" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@csstools/selector-specificity": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", - "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", - "dev": true, - "engines": { - "node": "^14 || ^16 || >=18" + "node_modules/@babel/preset-typescript": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz", + "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-syntax-jsx": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-typescript": "^7.24.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "postcss-selector-parser": "^6.0.10" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", - "dev": true, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "performance-now": "^2.1.0", - "qs": "6.10.4", - "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" + "regenerator-runtime": "^0.14.0" }, "engines": { - "node": ">= 6" + "node": ">=6.9.0" } }, - "node_modules/@cypress/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { - "node": ">= 0.12" + "node": ">=6.9.0" } }, - "node_modules/@cypress/request/node_modules/qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", - "dev": true, + "node_modules/@babel/traverse": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", "dependencies": { - "side-channel": "^1.0.4" + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", + "globals": "^11.1.0" }, "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6.9.0" } }, - "node_modules/@cypress/request/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "dependencies": { + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", - "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", - "dev": true, + "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, + "node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", "dependencies": { - "ms": "^2.1.1" + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@devexperts/remote-data-ts": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@devexperts/remote-data-ts/-/remote-data-ts-2.1.1.tgz", - "integrity": "sha512-z0RblCmeQjYt0vM1g1qR2SzHM1FXZXpBEzokYAojN8n4yC2jIInngeggmqPTOZna2+i7HInDoDm0CGK50W7Llg==", - "peerDependencies": { - "fp-ts": "^2.0.0", - "io-ts": "^2.0.0", - "io-ts-types": "^0.5.7" - } + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "devOptional": true }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, "engines": { - "node": ">=10.0.0" + "node": ">=0.1.90" } }, - "node_modules/@elastic/elasticsearch": { - "version": "8.13.1", - "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.13.1.tgz", - "integrity": "sha512-2G4Vu6OHw4+XTrp7AGIcOEezpPEoVrWg2JTK1v/exEKSLYquZkUdd+m4yOL3/UZ6bTj7hmXwrmYzW76BnLCkJQ==", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dependencies": { - "@elastic/transport": "~8.4.1", - "tslib": "^2.4.0" + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@elastic/transport": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.4.1.tgz", - "integrity": "sha512-/SXVuVnuU5b4dq8OFY4izG+dmGla185PcoqgK6+AJMpmOeY1QYVNbWtCwvSvoAANN5D/wV+EBU8+x7Vf9EphbA==", + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dependencies": { - "debug": "^4.3.4", - "hpagent": "^1.0.0", - "ms": "^2.1.3", - "secure-json-parse": "^2.4.0", - "tslib": "^2.4.0", - "undici": "^5.22.1" - }, - "engines": { - "node": ">=16" + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@elastic/transport/node_modules/undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "node_modules/@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "dev": true, "dependencies": { - "@fastify/busboy": "^2.0.0" + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=14.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=12" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], + "node_modules/@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=12" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], + "node_modules/@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=12" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, "engines": { - "node": ">=12" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=12" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], + "node_modules/@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=12" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=12" + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=12" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], + "node_modules/@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", "dev": true, - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], + "node_modules/@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.10" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], + "node_modules/@cypress/request": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.10.4", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, "engines": { - "node": ">=12" + "node": ">= 6" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], + "node_modules/@cypress/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, "engines": { - "node": ">=12" + "node": ">= 0.12" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { - "node": ">=12" + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], + "node_modules/@cypress/request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "bin": { + "uuid": "dist/bin/uuid" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "ms": "^2.1.1" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" + "node_modules/@devexperts/remote-data-ts": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@devexperts/remote-data-ts/-/remote-data-ts-2.1.1.tgz", + "integrity": "sha512-z0RblCmeQjYt0vM1g1qR2SzHM1FXZXpBEzokYAojN8n4yC2jIInngeggmqPTOZna2+i7HInDoDm0CGK50W7Llg==", + "peerDependencies": { + "fp-ts": "^2.0.0", + "io-ts": "^2.0.0", + "io-ts-types": "^0.5.7" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "engines": { - "node": ">=12" + "node": ">=10.0.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], + "node_modules/@elastic/elasticsearch": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.13.1.tgz", + "integrity": "sha512-2G4Vu6OHw4+XTrp7AGIcOEezpPEoVrWg2JTK1v/exEKSLYquZkUdd+m4yOL3/UZ6bTj7hmXwrmYzW76BnLCkJQ==", + "dependencies": { + "@elastic/transport": "~8.4.1", + "tslib": "^2.4.0" + }, "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], + "node_modules/@elastic/transport": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.4.1.tgz", + "integrity": "sha512-/SXVuVnuU5b4dq8OFY4izG+dmGla185PcoqgK6+AJMpmOeY1QYVNbWtCwvSvoAANN5D/wV+EBU8+x7Vf9EphbA==", + "dependencies": { + "debug": "^4.3.4", + "hpagent": "^1.0.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0", + "tslib": "^2.4.0", + "undici": "^5.22.1" + }, "engines": { - "node": ">=12" + "node": ">=16" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], + "node_modules/@elastic/transport/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=14.0" } }, "node_modules/@esbuild/win32-x64": { @@ -8821,118 +7816,54 @@ "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@nx/node": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@nx/node/-/node-19.2.1.tgz", - "integrity": "sha512-AA3EyTWBISY7ntrWDCBSdT4TJN+NjIsEWrX83MFXGHxD7MSeVTaR5gB2ePikXrQke9xJiQ1OWsD/uPTPfzvhmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nrwl/node": "19.2.1", - "@nx/devkit": "19.2.1", - "@nx/eslint": "19.2.1", - "@nx/jest": "19.2.1", - "@nx/js": "19.2.1", - "tslib": "^2.3.0" - } - }, - "node_modules/@nx/nx-darwin-arm64": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-19.2.1.tgz", - "integrity": "sha512-9VsVmb3ayzSYYIQniL+y8EbCgDgJvMxKU/wOau9vPYo+L/Alu3QF22yJ1XiI1RwnEzZpvE2dceGb48tZU1jD3A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-darwin-x64": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-19.2.1.tgz", - "integrity": "sha512-p0ulI83LQBDuRpO6td+qbUh70oPFuiIo2MylBIOE5EcVO5z8qOh7JPvk8fkoEmMXEZe7hgG3Cq5+dN7LxHx9kw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-freebsd-x64": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-19.2.1.tgz", - "integrity": "sha512-NFF7h5rH3D0hT34CVG5ms95CZp+/m2KP4ZEczRn+wRzWeqouRAJBGXOCiMZrmzd7orjuHVVXrghfvBzpte1stw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "@jridgewell/sourcemap-codec": "^1.4.13" + }, "engines": { - "node": ">= 10" + "node": ">=12" } }, - "node_modules/@nx/nx-linux-arm-gnueabihf": { + "node_modules/@nx/node": { "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-19.2.1.tgz", - "integrity": "sha512-ebvNguGQUIRJzFHi8im5lsbNbIFRnPaz6sbb0zFhu/W9/J+k3EWDjtOwtXoV6dnFmlRj7UbTyhMcu5SiWitdVQ==", - "cpu": [ - "arm" - ], + "resolved": "https://registry.npmjs.org/@nx/node/-/node-19.2.1.tgz", + "integrity": "sha512-AA3EyTWBISY7ntrWDCBSdT4TJN+NjIsEWrX83MFXGHxD7MSeVTaR5gB2ePikXrQke9xJiQ1OWsD/uPTPfzvhmA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@nrwl/node": "19.2.1", + "@nx/devkit": "19.2.1", + "@nx/eslint": "19.2.1", + "@nx/jest": "19.2.1", + "@nx/js": "19.2.1", + "tslib": "^2.3.0" } }, - "node_modules/@nx/nx-linux-arm64-gnu": { + "node_modules/@nx/nx-darwin-arm64": { "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-19.2.1.tgz", - "integrity": "sha512-aVqEZrDnZlqcJCKnrEON2JltdM4VxgHJSkJKLKuUeg3SUTskur7LH9M5KFhRgTo/tW+t2y9EuHDO6O3fAdcsYw==", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-19.2.1.tgz", + "integrity": "sha512-9VsVmb3ayzSYYIQniL+y8EbCgDgJvMxKU/wOau9vPYo+L/Alu3QF22yJ1XiI1RwnEzZpvE2dceGb48tZU1jD3A==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": ">= 10" } }, - "node_modules/@nx/nx-linux-arm64-musl": { + "node_modules/@nx/nx-darwin-x64": { "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-19.2.1.tgz", - "integrity": "sha512-W1g0IZ+4eNizXkUcsucSmXCaYmNj7ODX9L5wtZ8xMlLhdoq+EML8X/IpEPE8qIcYDEDUFgTPXb2zQsHwhgVXPw==", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-19.2.1.tgz", + "integrity": "sha512-p0ulI83LQBDuRpO6td+qbUh70oPFuiIo2MylBIOE5EcVO5z8qOh7JPvk8fkoEmMXEZe7hgG3Cq5+dN7LxHx9kw==", "cpu": [ - "arm64" + "x64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": ">= 10" @@ -8954,38 +7885,6 @@ "node": ">= 10" } }, - "node_modules/@nx/nx-linux-x64-musl": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-19.2.1.tgz", - "integrity": "sha512-J8IwFAfhZ16n1lKb1B7MSDQ7IScF+Y0IFKl48ze3ixD3sMr48KTDKetqqxjel1pNeUchtV5xFsAlWuHpp5s58Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-19.2.1.tgz", - "integrity": "sha512-6vmImBTJUr73gPlb70AGKlEoE8ogZCzXaX7me2wE2n6H6ks7FZqHl3X0RxQDPG6rf6TAJB4YLpWLZDNZLWXSYA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nx/nx-win32-x64-msvc": { "version": "19.2.1", "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-19.2.1.tgz", @@ -9766,6 +8665,7 @@ "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -9778,6 +8678,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "android" @@ -9790,6 +8691,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -9802,6 +8704,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -9814,6 +8717,7 @@ "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -9826,6 +8730,7 @@ "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -9838,6 +8743,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -9850,6 +8756,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -9862,6 +8769,7 @@ "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -9874,6 +8782,7 @@ "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -9886,18 +8795,20 @@ "cpu": [ "s390x" ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.0.tgz", + "integrity": "sha512-SjsBA1a9wrEleNneGEsR40HdxKdwCatyHC547o/XINqwPW4cqTYiNy/lL1WTJYWU/KgWIb8HH4SgmFStbWoBzw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -9910,6 +8821,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -9922,6 +8834,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -9934,6 +8847,7 @@ "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -18461,6 +17375,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -22932,504 +21847,236 @@ } }, "node_modules/lint-staged/node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lint-staged/node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/lint-staged/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/lint-staged/node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "path-key": "^4.0.0" }, "engines": { - "node": ">=18" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/yaml": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", - "integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==", + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" }, "engines": { - "node": ">= 14" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/listr2": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", - "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.1", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, + "license": "MIT", "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" + "node": ">=12" }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/lint-staged/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/listr2/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/lint-staged/node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, + "license": "MIT", "engines": { - "node": ">=7.0.0" + "node": ">=6" } }, - "node_modules/listr2/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/lint-staged/node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "engines": { - "node": ">=6.11.5" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", - "engines": { - "node": ">= 12.13.0" - } + "node_modules/lint-staged/node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "node_modules/lint-staged/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, + "license": "ISC", "engines": { "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/locate-path": { + "node_modules/lint-staged/node_modules/slice-ansi": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "node_modules/lint-staged/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/lint-staged/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/lint-staged/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", + "integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" }, "engines": { - "node": ">=8" + "node": ">= 14" } }, - "node_modules/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", "dev": true, "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=10" + "node": ">=10.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } } }, - "node_modules/log-update/node_modules/ansi-styles": { + "node_modules/listr2/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", @@ -23444,7 +22091,7 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/color-convert": { + "node_modules/listr2/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", @@ -23456,1168 +22103,1084 @@ "node": ">=7.0.0" } }, - "node_modules/log-update/node_modules/color-name": { + "node_modules/listr2/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/magicast": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", - "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.24.4", - "@babel/types": "^7.24.0", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "node_modules/make-fetch-happen": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", - "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", - "dev": true, - "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "devOptional": true, "dependencies": { - "tmpl": "1.0.5" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/mapbox-to-css-font": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.4.tgz", - "integrity": "sha512-X1dtuTuH2D1MRMuductMZCLV/fy9EoIgqW/lmu8vQSAhEatx/tdFebkYT3TVhdTwqFDHbLEgQBD3IKA4KI7aoQ==" - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "engines": { - "node": ">= 0.6" + "node": ">=6.11.5" } }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "dependencies": { - "fs-monkey": "^1.0.4" - }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", "engines": { - "node": ">= 4.0.0" + "node": ">= 12.13.0" } }, - "node_modules/merge-anything": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", - "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, "dependencies": { - "is-what": "^4.1.8" + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" }, "engines": { - "node": ">=12.13" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/mesqueeb" + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/merge-anything/node_modules/is-what": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", - "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", - "engines": { - "node": ">=12.13" + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" + "engines": { + "node": ">=8" } }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true }, - "node_modules/mgrs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", - "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">=8.6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=8.6" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">= 0.6" + "node": ">=7.0.0" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { - "mime-db": "1.52.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mini-css-extract-plugin": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", - "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" + "color-convert": "^2.0.1" }, "engines": { - "node": ">= 12.13.0" + "node": ">=8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + "node_modules/log-update/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" } }, - "node_modules/minipass": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", - "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", - "devOptional": true, - "engines": { - "node": ">=16 || 14 >=14.17" + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" } }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "devOptional": true, - "dependencies": { - "minipass": "^7.0.3" - }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12" } }, - "node_modules/minipass-fetch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", - "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", - "dev": true, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "@jridgewell/sourcemap-codec": "^1.4.15" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" + "node": ">=12" } }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "devOptional": true, + "node_modules/magicast": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", + "dev": true, "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" } }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "devOptional": true, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dependencies": { - "yallist": "^4.0.0" + "semver": "^6.0.0" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true - }, - "node_modules/minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", - "dev": true, - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/minipass-json-stream/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" }, "engines": { - "node": ">=8" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/minipass-json-stream/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "devOptional": true, "dependencies": { - "minipass": "^3.0.0" - }, + "tmpl": "1.0.5" + } + }, + "node_modules/mapbox-to-css-font": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.4.tgz", + "integrity": "sha512-X1dtuTuH2D1MRMuductMZCLV/fy9EoIgqW/lmu8vQSAhEatx/tdFebkYT3TVhdTwqFDHbLEgQBD3IKA4KI7aoQ==" + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "devOptional": true, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dependencies": { - "yallist": "^4.0.0" + "fs-monkey": "^1.0.4" }, "engines": { - "node": ">=8" + "node": ">= 4.0.0" } }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", "dependencies": { - "minipass": "^3.0.0" + "is-what": "^4.1.8" }, "engines": { - "node": ">=8" + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, + "node_modules/merge-anything/node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", "engines": { - "node": ">=8" + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "devOptional": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "engines": { "node": ">= 8" } }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "devOptional": true, - "dependencies": { - "yallist": "^4.0.0" - }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", + "license": "MIT" }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dependencies": { - "minimist": "^1.2.6" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">=8.6" } }, - "node_modules/mlly": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", - "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.1.0", - "ufo": "^1.5.3" + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/monocle-ts": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/monocle-ts/-/monocle-ts-2.3.13.tgz", - "integrity": "sha512-D5Ygd3oulEoAm3KuGO0eeJIrhFf1jlQIoEVV2DYsZUMz42j4tGxgct97Aq68+F8w4w4geEnwFa8HayTS/7lpKQ==", - "peerDependencies": { - "fp-ts": "^2.5.0" + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" } }, - "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "engines": { - "node": ">=10" + "node": ">= 0.6" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/muggle-string": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", - "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", - "dev": true - }, - "node_modules/multer": { - "version": "1.4.4-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", - "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 0.6" } }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" } }, - "node_modules/multimatch": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", - "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "node_modules/mini-css-extract-plugin": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", "dependencies": { - "@types/minimatch": "^3.0.3", - "array-differ": "^3.0.0", - "array-union": "^2.1.0", - "arrify": "^2.0.1", - "minimatch": "^3.0.4" + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" }, "engines": { - "node": ">=10" + "node": ">= 12.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, - "node_modules/multimatch/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" }, - "node_modules/multimatch/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", + "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", + "devOptional": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "devOptional": true, + "dependencies": { + "minipass": "^7.0.3" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - }, - "node_modules/needle": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", - "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", - "optional": true, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, "dependencies": { - "iconv-lite": "^0.6.3", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" }, "engines": { - "node": ">= 4.4.x" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" } }, - "node_modules/needle/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "devOptional": true, "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "minipass": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node_modules/newtype-ts": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/newtype-ts/-/newtype-ts-0.3.5.tgz", - "integrity": "sha512-v83UEQMlVR75yf1OUdoSFssjitxzjZlqBAjiGQ4WJaML8Jdc68LJ+BaSAXUmKY4bNzp7hygkKLYTsDi14PxI2g==", - "peerDependencies": { - "fp-ts": "^2.0.0", - "monocle-ts": "^2.0.0" - } + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true }, - "node_modules/ng-morph": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-2.2.5.tgz", - "integrity": "sha512-plxrHfcz7aNRcTCxS9tUxI1F5Vfx5CZAAw8NAnJCyCb41Js6S+EyPDvn2v7H2QyfZzXU1H0BAqtiRQv/rE2zTA==", + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, "dependencies": { - "jsonc-parser": "3.0.0", - "minimatch": "3.0.5", - "multimatch": "5.0.0", - "ts-morph": "10.0.2" - }, - "peerDependencies": { - "@angular-devkit/core": ">=11.0.0", - "@angular-devkit/schematics": ">=11.0.0" + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" } }, - "node_modules/ng-morph/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/ng-morph/node_modules/jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==" + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, - "node_modules/ng-morph/node_modules/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "devOptional": true, "dependencies": { - "brace-expansion": "^1.1.7" + "minipass": "^3.0.0" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/ng-packagr": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-17.3.0.tgz", - "integrity": "sha512-kMSqxeDgv88SWCoapWNRRN1UdBgwu9/Pw/j7u2WFGmzrIWUFivNWBBSSL94kMxr2La+Z9wMwiL8EwKNvmCpg2A==", + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "devOptional": true, "dependencies": { - "@rollup/plugin-json": "^6.0.1", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/wasm-node": "^4.5.0", - "ajv": "^8.12.0", - "ansi-colors": "^4.1.3", - "browserslist": "^4.22.1", - "cacache": "^18.0.0", - "chokidar": "^3.5.3", - "commander": "^12.0.0", - "convert-source-map": "^2.0.0", - "dependency-graph": "^1.0.0", - "esbuild-wasm": "^0.20.0", - "fast-glob": "^3.3.1", - "find-cache-dir": "^3.3.2", - "injection-js": "^2.4.0", - "jsonc-parser": "^3.2.0", - "less": "^4.2.0", - "ora": "^5.1.0", - "piscina": "^4.4.0", - "postcss": "^8.4.31", - "rxjs": "^7.8.1", - "sass": "^1.69.5" - }, - "bin": { - "ng-packagr": "cli/main.js" + "yallist": "^4.0.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "optionalDependencies": { - "esbuild": "^0.20.0", - "rollup": "^4.5.0" - }, - "peerDependencies": { - "@angular/compiler-cli": "^17.0.0 || ^17.2.0-next.0 || ^17.3.0-next.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "tslib": "^2.3.0", - "typescript": ">=5.2 <5.5" - }, - "peerDependenciesMeta": { - "tailwindcss": { - "optional": true - } + "node": ">=8" } }, - "node_modules/ng-packagr/node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "dev": true, - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "minipass": "^3.0.0" + }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/ng-packagr/node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/ng-packagr/node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, "engines": { - "node": ">=12" + "node": ">= 8" } }, - "node_modules/ng-packagr/node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/ng-packagr/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" } }, - "node_modules/ng-packagr/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], + "node_modules/mlly": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", + "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.0", + "ufo": "^1.5.3" } }, - "node_modules/ng-packagr/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" + "node_modules/monocle-ts": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/monocle-ts/-/monocle-ts-2.3.13.tgz", + "integrity": "sha512-D5Ygd3oulEoAm3KuGO0eeJIrhFf1jlQIoEVV2DYsZUMz42j4tGxgct97Aq68+F8w4w4geEnwFa8HayTS/7lpKQ==", + "peerDependencies": { + "fp-ts": "^2.5.0" } }, - "node_modules/ng-packagr/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "engines": { - "node": ">=12" + "node": ">=10" } }, - "node_modules/ng-packagr/node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true + }, + "node_modules/multer": { + "version": "1.4.4-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", + "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, "engines": { - "node": ">=12" + "node": ">= 6.0.0" } }, - "node_modules/ng-packagr/node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" } }, - "node_modules/ng-packagr/node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ng-packagr/node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "node_modules/multimatch/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/ng-packagr/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/multimatch/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=12" + "node": "*" } }, - "node_modules/ng-packagr/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "engines": { - "node": ">=12" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ng-packagr/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, "engines": { - "node": ">=12" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/ng-packagr/node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, "engines": { - "node": ">=12" + "node": ">= 4.4.x" } }, - "node_modules/ng-packagr/node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/ng-packagr/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "engines": { - "node": ">=12" + "node": ">= 0.6" } }, - "node_modules/ng-packagr/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/newtype-ts": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/newtype-ts/-/newtype-ts-0.3.5.tgz", + "integrity": "sha512-v83UEQMlVR75yf1OUdoSFssjitxzjZlqBAjiGQ4WJaML8Jdc68LJ+BaSAXUmKY4bNzp7hygkKLYTsDi14PxI2g==", + "peerDependencies": { + "fp-ts": "^2.0.0", + "monocle-ts": "^2.0.0" } }, - "node_modules/ng-packagr/node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "node_modules/ng-morph": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-2.2.5.tgz", + "integrity": "sha512-plxrHfcz7aNRcTCxS9tUxI1F5Vfx5CZAAw8NAnJCyCb41Js6S+EyPDvn2v7H2QyfZzXU1H0BAqtiRQv/rE2zTA==", + "dependencies": { + "jsonc-parser": "3.0.0", + "minimatch": "3.0.5", + "multimatch": "5.0.0", + "ts-morph": "10.0.2" + }, + "peerDependencies": { + "@angular-devkit/core": ">=11.0.0", + "@angular-devkit/schematics": ">=11.0.0" } }, - "node_modules/ng-packagr/node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], + "node_modules/ng-morph/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/ng-morph/node_modules/jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==" + }, + "node_modules/ng-morph/node_modules/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=12" + "node": "*" } }, - "node_modules/ng-packagr/node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], + "node_modules/ng-packagr": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-17.3.0.tgz", + "integrity": "sha512-kMSqxeDgv88SWCoapWNRRN1UdBgwu9/Pw/j7u2WFGmzrIWUFivNWBBSSL94kMxr2La+Z9wMwiL8EwKNvmCpg2A==", + "devOptional": true, + "dependencies": { + "@rollup/plugin-json": "^6.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/wasm-node": "^4.5.0", + "ajv": "^8.12.0", + "ansi-colors": "^4.1.3", + "browserslist": "^4.22.1", + "cacache": "^18.0.0", + "chokidar": "^3.5.3", + "commander": "^12.0.0", + "convert-source-map": "^2.0.0", + "dependency-graph": "^1.0.0", + "esbuild-wasm": "^0.20.0", + "fast-glob": "^3.3.1", + "find-cache-dir": "^3.3.2", + "injection-js": "^2.4.0", + "jsonc-parser": "^3.2.0", + "less": "^4.2.0", + "ora": "^5.1.0", + "piscina": "^4.4.0", + "postcss": "^8.4.31", + "rxjs": "^7.8.1", + "sass": "^1.69.5" + }, + "bin": { + "ng-packagr": "cli/main.js" + }, "engines": { - "node": ">=12" + "node": "^18.13.0 || >=20.9.0" + }, + "optionalDependencies": { + "esbuild": "^0.20.0", + "rollup": "^4.5.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0 || ^17.2.0-next.0 || ^17.3.0-next.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.2 <5.5" + }, + "peerDependenciesMeta": { + "tailwindcss": { + "optional": true + } } }, "node_modules/ng-packagr/node_modules/@esbuild/win32-x64": { @@ -24705,32 +23268,12 @@ "@angular/core": ">=13.0.0" } }, - "node_modules/nice-napi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", - "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "!win32" - ], - "dependencies": { - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.2" - } - }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "license": "MIT" }, - "node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "optional": true - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -24798,17 +23341,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/node-gyp-build": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", - "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-gyp/node_modules/glob": { "version": "10.3.16", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", @@ -27904,6 +26436,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/rrweb-cssom": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", @@ -30681,336 +29226,6 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/vite/node_modules/@esbuild/win32-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", diff --git a/package.json b/package.json index 9930abd9..c43f4fe7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build": "npx nx run-many -t build -p server-asset-sg client-asset-sg --configuration=production", "build:server": "npx nx build server-asset-sg --configuration=production", "build:client": "npx nx build client-asset-sg --configuration=production", - "prisma": "dotenv -e apps/server-asset-sg/.env.local -- npx prisma", + "prisma": "npx dotenv -e apps/server-asset-sg/.env.local -- npx prisma", "test": "npx nx run-many --target=test --all", "format": "npm run prettier --write . && npm run lint:fix", "lint": "npx prettier --check . && npx nx run-many --target=lint --all", @@ -154,10 +154,22 @@ "vite-tsconfig-paths": "^4.3.2", "vitest": "1.3.1" }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "19.2.1", + "@nx/nx-darwin-x64": "19.2.1", + "@nx/nx-linux-x64-gnu": "19.2.1", + "@nx/nx-win32-x64-msvc": "19.2.1", + "@rollup/rollup-linux-x64-gnu": "4.16.0" + }, "prisma": { "schema": "apps/server-asset-sg/prisma/schema.prisma" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" + }, + "overrides": { + "vite": { + "rollup": "npm:@rollup/wasm-node" + } } } From 4bcfa391162650066f71f89d603222f6798ecfdd Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Tue, 23 Jul 2024 09:49:25 +0200 Subject: [PATCH 09/21] Add minimum date for `createDate` filter --- .../asset-search-refine.component.html | 2 +- .../asset-search-refine.component.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html index 182706d6..370ab92c 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html +++ b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html @@ -85,7 +85,7 @@

search.documentDate

- + + + + + diff --git a/libs/admin/src/lib/components/user-edit/user-edit.component.scss b/libs/admin/src/lib/components/user-edit/user-edit.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/libs/admin/src/lib/components/user-edit/user-edit.component.ts b/libs/admin/src/lib/components/user-edit/user-edit.component.ts new file mode 100644 index 00000000..f3351249 --- /dev/null +++ b/libs/admin/src/lib/components/user-edit/user-edit.component.ts @@ -0,0 +1,43 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { User, Workgroup } from '../../services/admin.service'; + +@Component({ + selector: 'asset-sg-user-edit', + templateUrl: './user-edit.component.html', + styleUrls: ['./user-edit.component.scss'], +}) +export class UserEditComponent implements OnInit { + @Input() user?: User; + @Input() workgroups: Workgroup[] = []; + + public formGroup = new FormGroup({ + isAdmin: new FormControl(false), + lang: new FormControl('de'), + asViewer: new FormControl([]), + asEditor: new FormControl([]), + asMasterEditor: new FormControl([]), + }); + + public ngOnInit() { + this.initializeForm(); + } + + private initializeForm() { + this.formGroup.patchValue({ + isAdmin: this.user?.isAdmin ?? false, + asViewer: + this.user?.workgroups + .filter((workgroup) => workgroup.role === 'Viewer') + .map((workgroup) => workgroup.workgroupId) ?? [], + asEditor: + this.user?.workgroups + .filter((workgroup) => workgroup.role === 'Editor') + .map((workgroup) => workgroup.workgroupId) ?? [], + asMasterEditor: + this.user?.workgroups + .filter((workgroup) => workgroup.role === 'MasterEditor') + .map((workgroup) => workgroup.workgroupId) ?? [], + }); + } +} diff --git a/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts b/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts index be29377e..52ea8f6e 100644 --- a/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts +++ b/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts @@ -2,13 +2,13 @@ import { Dialog, DialogRef } from '@angular/cdk/dialog'; import { Component, EventEmitter, + inject, Input, Output, QueryList, TemplateRef, ViewChild, ViewChildren, - inject, } from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; import { ButtonComponent, ViewChildMarker } from '@asset-sg/client-shared'; @@ -53,6 +53,7 @@ export class UserExpandedComponent { public editForm = this._formBulder.group({ email: new FormControl(''), role: new FormControl(UserRoleEnum.viewer), + isAdmin: new FormControl(false), lang: new FormControl('de'), }); public UserRole = UserRoleEnum; @@ -63,6 +64,7 @@ export class UserExpandedComponent { public get user(): User | undefined { return this._user; } + public set user(value: User | undefined) { this._user = value; if (value) { @@ -73,6 +75,7 @@ export class UserExpandedComponent { }); } } + private _user?: User | undefined; disableEverything() { @@ -83,6 +86,7 @@ export class UserExpandedComponent { } }); } + submit() { this.disableEverything(); if (this.user) { diff --git a/libs/admin/src/lib/components/users/users.component.html b/libs/admin/src/lib/components/users/users.component.html new file mode 100644 index 00000000..48bb856e --- /dev/null +++ b/libs/admin/src/lib/components/users/users.component.html @@ -0,0 +1,39 @@ +
+ @if (selectedUser) { + + } + + + + + + + + + + + + + + + + + + + + + + + + + +
Name{{ user.email }}Workgroups + @for (workgroup of user.workgroups; track workgroup.workgroupId) { +
{{ workgroup.workgroup.name }}: {{ workgroup.role }}
+ } +
Admin + + Sprache{{ user.lang }}Aktionen + +
+
diff --git a/libs/admin/src/lib/components/users/users.component.scss b/libs/admin/src/lib/components/users/users.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/libs/admin/src/lib/components/users/users.component.ts b/libs/admin/src/lib/components/users/users.component.ts new file mode 100644 index 00000000..19d044e3 --- /dev/null +++ b/libs/admin/src/lib/components/users/users.component.ts @@ -0,0 +1,51 @@ +import { Component, inject, OnDestroy, OnInit } from '@angular/core'; +import { MatCheckboxChange } from '@angular/material/checkbox'; +import { Subscription } from 'rxjs'; +import { AdminService, User, Workgroup } from '../../services/admin.service'; + +@Component({ + selector: 'asset-sg-users', + templateUrl: './users.component.html', + styleUrls: ['./users.component.scss'], +}) +export class UsersComponent implements OnInit, OnDestroy { + public users: User[] = []; + public workgroups: Workgroup[] = []; + public selectedUser?: User; + public mode: 'edit' | 'create' | undefined = undefined; + protected readonly COLUMNS = ['email', 'workgroups', 'isAdmin', 'languages', 'actions']; + + private _adminService = inject(AdminService); + private subscriptions: Subscription = new Subscription(); + + public ngOnInit(): void { + this.initSubscriptions(); + } + + public ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } + + public update(user: User, event: MatCheckboxChange) { + this._adminService.updateUser({ ...user, isAdmin: event.checked }).subscribe(); + } + + public edit(user: User): void { + this.selectedUser = user; + } + + private initSubscriptions(): void { + this.subscriptions.add( + this._adminService.getUsersNew().subscribe((users) => { + this.users = users; + }) + ); + this.subscriptions.add( + this._adminService.getWorkgroups().subscribe((workgroups) => { + this.workgroups = workgroups; + }) + ); + } + + protected readonly console = console; +} diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts new file mode 100644 index 00000000..c041cb07 --- /dev/null +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts @@ -0,0 +1,62 @@ +import { Component, inject, Input, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { AdminService, User, Workgroup } from '../../services/admin.service'; + +@Component({ + selector: 'asset-sg-workgroup-edit', + templateUrl: './workgroup-edit.component.html', + styleUrls: ['./workgroup-edit.component.scss'], +}) +export class WorkgroupEditComponent implements OnInit { + private _adminService = inject(AdminService); + + @Input() workgroup: Workgroup | null = null; + @Input() users: User[] = []; + @Input() mode: 'edit' | 'create' | undefined = undefined; + + public readonly formGroup: FormGroup = new FormGroup({ + name: new FormControl('', Validators.required), + viewers: new FormControl(), + editors: new FormControl(), + masterEditors: new FormControl(), + status: new FormControl(), + }); + + public ngOnInit() { + this.initializeForm(); + console.log(this.workgroup); + } + + public initializeForm() { + this.formGroup.patchValue({ + name: this.workgroup?.name ?? '', + viewers: this.workgroup?.users.filter((user) => user.role === 'Viewer').map((user) => user.userId) ?? [], + editors: this.workgroup?.users.filter((user) => user.role === 'Editor').map((user) => user.userId) ?? [], + masterEditors: + this.workgroup?.users.filter((user) => user.role === 'MasterEditor').map((user) => user.userId) ?? [], + status: !!this.workgroup?.disabled_at, + }); + } + + public save() { + if (!this.formGroup.valid) { + return; + } + const workgroup: Omit = { + name: this.formGroup.controls['name'].value, + users: [ + ...this.formGroup.controls['viewers'].value.map((userId: number) => ({ userId, role: 'Viewer' })), + ...this.formGroup.controls['editors'].value.map((userId: number) => ({ userId, role: 'Editor' })), + ...this.formGroup.controls['masterEditors'].value.map((userId: number) => ({ userId, role: 'MasterEditor' })), + ], + assets: this.workgroup?.assets ?? [], + disabled_at: this.formGroup.controls['status'].value ? new Date() : null, + }; + + if (this.mode === 'create') { + this._adminService.createWorkgroup(workgroup).subscribe((res) => console.log(res)); + } else { + this._adminService.updateWorkgroups(this.workgroup?.id ?? 0, workgroup).subscribe((res) => console.log(res)); + } + } +} diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html new file mode 100644 index 00000000..47396d16 --- /dev/null +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html @@ -0,0 +1,27 @@ +
+
+
+ + Name + + + Deaktiviert + + + {{ user.email }} + + + + + {{ user.email }} + + + + + {{ user.email }} + + +
+ +
+
diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.scss b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.scss new file mode 100644 index 00000000..c1f7262a --- /dev/null +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.scss @@ -0,0 +1,4 @@ +.form { + display: flex; + flex-direction: column; +} diff --git a/libs/admin/src/lib/components/workgroups/workgroups.component.html b/libs/admin/src/lib/components/workgroups/workgroups.component.html new file mode 100644 index 00000000..38973a4a --- /dev/null +++ b/libs/admin/src/lib/components/workgroups/workgroups.component.html @@ -0,0 +1,31 @@ +
+ + @if (selectedWorkgroup || mode === 'create') { + + + } + + + + + + + + + + + + + + + + + + + +
Name{{ workgroup.name }}Deaktiviert{{ workgroup.disabled_at ? workgroup.disabled_at : "-" }}Anzahl Benutzer{{ workgroup.users.length }}Aktionen + + + +
+
diff --git a/libs/admin/src/lib/components/workgroups/workgroups.component.scss b/libs/admin/src/lib/components/workgroups/workgroups.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/libs/admin/src/lib/components/workgroups/workgroups.component.ts b/libs/admin/src/lib/components/workgroups/workgroups.component.ts new file mode 100644 index 00000000..c756b4eb --- /dev/null +++ b/libs/admin/src/lib/components/workgroups/workgroups.component.ts @@ -0,0 +1,54 @@ +import { Component, inject, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { AdminService, User, Workgroup } from '../../services/admin.service'; + +@Component({ + selector: 'asset-sg-workgroups', + templateUrl: './workgroups.component.html', + styleUrls: ['./workgroups.component.scss'], +}) +export class WorkgroupsComponent implements OnInit, OnDestroy { + public users: User[] = []; + public workgroups: Workgroup[] = []; + public mode: 'edit' | 'create' | undefined = undefined; + public selectedWorkgroup: Workgroup | null = null; + protected readonly COLUMNS = ['name', 'status', 'users', 'actions']; + + private _adminService = inject(AdminService); + private subscriptions: Subscription = new Subscription(); + + public ngOnInit(): void { + this.initSubscriptions(); + } + + public ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } + + public selectWorkgroup(workgroup: Workgroup): void { + console.log('here'); + this.selectedWorkgroup = workgroup; + } + + public createWorkgroup(): void { + this.mode = 'create'; + } + + public deactivateWorkgroup(workgroup: Workgroup): void { + const { id, ...disabledWorkgroup } = { ...workgroup, disabled_at: new Date() }; + this._adminService.updateWorkgroups(id, disabledWorkgroup).subscribe(); + } + + private initSubscriptions(): void { + this.subscriptions.add( + this._adminService.getUsersNew().subscribe((users) => { + this.users = users; + }) + ); + this.subscriptions.add( + this._adminService.getWorkgroups().subscribe((workgroups) => { + this.workgroups = workgroups; + }) + ); + } +} diff --git a/libs/admin/src/lib/services/admin.service.ts b/libs/admin/src/lib/services/admin.service.ts index 92d6d173..23709154 100644 --- a/libs/admin/src/lib/services/admin.service.ts +++ b/libs/admin/src/lib/services/admin.service.ts @@ -1,12 +1,39 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { ApiError, httpErrorResponseError } from '@asset-sg/client-shared'; -import { OE, ORD, decodeError } from '@asset-sg/core'; -import { User, Users } from '@asset-sg/shared'; +import { decodeError, OE, ORD } from '@asset-sg/core'; +import { Users } from '@asset-sg/shared'; import * as RD from '@devexperts/remote-data-ts'; +import { Role } from '@prisma/client'; import * as E from 'fp-ts/Either'; import { flow } from 'fp-ts/function'; -import { map, startWith, tap } from 'rxjs'; +import { map, Observable, startWith, tap } from 'rxjs'; + +export interface Workgroup { + id: number; + name: string; + assets: { assetId: number }[]; + users: { userId: number; role: Role }[]; + disabled_at: Date | null; +} + +export interface WorkgroupOnUser { + workgroupId: number; + role: Role; + workgroup: { + name: string; + }; +} + +export interface User { + id: string; + name: string; + email: string; + lang: string; + role: 'Viewer' | 'Editor' | 'MasterEditor'; + isAdmin: boolean; + workgroups: WorkgroupOnUser[]; +} @Injectable({ providedIn: 'root', @@ -24,19 +51,31 @@ export class AdminService { ); } - public updateUser(user: User): ORD.ObservableRemoteData { + public getUsersNew(): Observable { + return this._httpClient.get('/api/admin/user').pipe(map((res) => res as User[])); + } + + public getWorkgroups(): Observable { + return this._httpClient.get('/api/workgroups').pipe(map((res) => res as Workgroup[])); + } + + public createWorkgroup(workgroup: Omit): Observable { + return this._httpClient.post(`/api/workgroups`, workgroup).pipe(map((res) => res as Workgroup)); + } + + public updateWorkgroups(id: number, workgroup: Omit): Observable { + return this._httpClient.put(`/api/workgroups/${id}`, workgroup).pipe(map((res) => res as Workgroup)); + } + + public updateUser(user: User): Observable { return this._httpClient - .patch(`/api/admin/user/${user.id}`, { + .put(`/api/users/${user.id}`, { role: user.role, lang: user.lang, + workgroups: user.workgroups, + isAdmin: user.isAdmin, }) - .pipe( - map(() => E.right(undefined)), - // TODO need to test instance of HttpErrorResponse here - OE.catchErrorW(httpErrorResponseError), - map(RD.fromEither), - startWith(RD.pending) - ); + .pipe(map((res) => res as User)); } public deleteUser(id: string): ORD.ObservableRemoteData { diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts index 08936441..ec5c3ccc 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts @@ -284,6 +284,7 @@ export class AssetEditorTabPageComponent { newStatusWorkItemCode: O.fromNullable(this._form.getRawValue().administration.newStatusWorkItemCode), assetMainId: O.fromNullable(this._form.getRawValue().references.assetMain?.assetId), siblingAssetIds: this._form.getRawValue().references.siblingAssets.map((asset) => asset.assetId), + workgroupId: 1, }; this._showProgressBar$.next(true); if (this._form.getRawValue().general.id === 0) { diff --git a/libs/shared/src/lib/models/asset-edit.ts b/libs/shared/src/lib/models/asset-edit.ts index 37806b93..b963198f 100644 --- a/libs/shared/src/lib/models/asset-edit.ts +++ b/libs/shared/src/lib/models/asset-edit.ts @@ -87,6 +87,7 @@ export const BaseAssetEditDetail = { siblingYAssets: C.array(LinkedAsset), statusWorks: C.array(StatusWork), assetFiles: C.array(AssetFile), + workgroupId: C.number, }; export const AssetEditDetail = C.struct({ diff --git a/libs/shared/src/lib/models/patch-asset.ts b/libs/shared/src/lib/models/patch-asset.ts index a0a5ad7f..a47cdb0a 100644 --- a/libs/shared/src/lib/models/patch-asset.ts +++ b/libs/shared/src/lib/models/patch-asset.ts @@ -36,5 +36,6 @@ export const PatchAsset = C.struct({ siblingAssetIds: C.array(C.number), newStudies: C.array(C.string), newStatusWorkItemCode: CT.optionFromNullable(C.string), + workgroupId: C.number, }); export type PatchAsset = C.TypeOf; diff --git a/package.json b/package.json index a5db4836..88af284a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build": "npx nx run-many -t build -p server-asset-sg client-asset-sg --configuration=production", "build:server": "npx nx build server-asset-sg --configuration=production", "build:client": "npx nx build client-asset-sg --configuration=production", - "prisma": "npx dotenv -e apps/server-asset-sg/.env -e apps/server-asset-sg/.env.local -- npx prisma", + "prisma": "dotenv -e apps/server-asset-sg/.env -e apps/server-asset-sg/.env.local -- npx prisma", "test": "npx nx run-many --target=test --all", "format": "npm run prettier --write . && npm run lint:fix", "lint": "npx prettier --check . && npx nx run-many --target=lint --all", diff --git a/test/setup-db.ts b/test/setup-db.ts index 96280ebc..1b8412d4 100644 --- a/test/setup-db.ts +++ b/test/setup-db.ts @@ -1,11 +1,11 @@ import { PrismaClient } from '@prisma/client'; -import { assetKindItems } from './data/asset-kind-item'; -import { statusAssetUseItems } from './data/status-asset-use-item'; import { assetFormatItems } from './data/asset-format-item'; +import { assetKindItems } from './data/asset-kind-item'; +import { contactKindItems } from './data/contact-kind-item'; import { languageItems } from './data/language-items'; -import { statusWorkItems } from './data/status-work-item'; import { manCatLabelItems } from './data/man-cat-label-item'; -import { contactKindItems } from './data/contact-kind-item'; +import { statusAssetUseItems } from './data/status-asset-use-item'; +import { statusWorkItems } from './data/status-work-item'; const clearDB = async (prisma: PrismaClient, dbName: string): Promise => { const tables = await prisma.$queryRawUnsafe(` @@ -26,7 +26,6 @@ const clearDB = async (prisma: PrismaClient, dbName: string): Promise => { }; export const setupDB = async (prisma: PrismaClient): Promise => { - await clearDB(prisma, 'auth'); await clearDB(prisma, 'public'); await prisma.statusAssetUseItem.createMany({ data: statusAssetUseItems, skipDuplicates: true }); @@ -36,6 +35,7 @@ export const setupDB = async (prisma: PrismaClient): Promise => { await prisma.statusWorkItem.createMany({ data: statusWorkItems, skipDuplicates: true }); await prisma.manCatLabelItem.createMany({ data: manCatLabelItems, skipDuplicates: true }); await prisma.contactKindItem.createMany({ data: contactKindItems, skipDuplicates: true }); + await setupDefaultWorkgroup(prisma); }; export const clearPrismaAssets = async (prisma: PrismaClient): Promise => { @@ -49,4 +49,17 @@ export const clearPrismaAssets = async (prisma: PrismaClient): Promise => await prisma.asset.deleteMany(); await prisma.internalUse.deleteMany(); await prisma.publicUse.deleteMany(); + await prisma.workgroupsOnUsers.deleteMany(); + await prisma.workgroup.deleteMany({ where: { id: { not: 1 } } }); +}; + +export const setupDefaultWorkgroup = async (prisma: PrismaClient): Promise => { + await prisma.workgroup.create({ + data: { + id: 1, + created_at: new Date(), + disabled_at: new Date(), + name: 'Default', + }, + }); }; From d5306befea4fdddc0c362fffb3c344a24007d343 Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Wed, 10 Jul 2024 09:06:51 +0200 Subject: [PATCH 14/21] Add policy-based API authorization --- .prettierignore | 1 + .../migration.sql | 77 ----- apps/server-asset-sg/prisma/schema.prisma | 5 +- apps/server-asset-sg/src/app.controller.ts | 279 ++++-------------- apps/server-asset-sg/src/app.module.ts | 38 +-- .../core/decorators/authorize.decorator.ts | 122 ++++++++ .../core/decorators/authorized.decorator.ts | 14 + .../src/core/decorators/boundary.decorator.ts | 26 ++ .../core/decorators/require-role.decorator.ts | 17 -- .../core/decorators/use-policy.decorator.ts | 6 + .../src/core/decorators/use-repo.decorator.ts | 6 + .../src/core/guards/policy.guard.ts | 114 +++++++ .../src/core/guards/role.guard.ts | 20 -- .../src/core/middleware/jwt.middleware.ts | 3 +- apps/server-asset-sg/src/core/policy.ts | 60 ++++ .../asset-edit/asset-edit.controller.ts | 72 +++++ .../asset-edit.fake.ts | 0 .../src/features/asset-edit/asset-edit.http | 75 +++++ .../features/asset-edit/asset-edit.policy.ts | 25 ++ .../asset-edit.repo.spec.ts | 0 .../asset-edit.repo.ts | 22 +- .../asset-edit.service.ts | 11 +- .../features/asset-old/asset.controller.ts | 102 ------- .../src/features/asset-old/asset.service.ts | 156 ---------- .../src/features/assets/asset.model.ts | 5 +- .../src/features/assets/asset.policy.ts | 25 ++ .../src/features/assets/asset.repo.ts | 12 +- .../src/features/assets/assets.controller.ts | 61 ++-- .../src/features/assets/assets.http | 4 - .../src/features/assets/prisma-asset.ts | 8 +- .../assets/search/asset-search.controller.ts | 13 +- .../search/asset-search.service.spec.ts | 8 +- .../assets/search/asset-search.service.ts | 253 ++++++++-------- .../assets/sync/asset-sync.controller.ts | 8 +- .../src/features/contacts/contact.policy.ts | 14 + .../features/contacts/contacts.controller.ts | 42 +-- .../favorites/favorites.controller.ts | 4 + .../src/features/files/files.controller.ts | 86 ++++++ .../features/search/find-assets-by-polygon.ts | 42 --- .../src/features/search/search-asset.ts | 162 ---------- .../features/studies/studies.controller.ts | 15 +- .../src/features/studies/study.repo.ts | 47 ++- .../src/features/users/user.model.ts | 35 +-- .../src/features/users/user.repo.ts | 19 +- .../src/features/users/users.controller.ts | 13 +- .../workgroups/workgroup.controller.ts | 71 ++--- .../features/workgroups/workgroup.model.ts | 18 +- .../features/workgroups/workgroup.policy.ts | 14 + .../workgroups/workgroup.repo.spec.ts | 4 +- .../server-asset-sg/src/models/jwt-request.ts | 8 + apps/server-asset-sg/tsconfig.app.json | 2 +- apps/server-asset-sg/tsconfig.spec.json | 2 +- libs/admin/src/lib/services/admin.service.ts | 6 +- .../src/lib/services/asset-editor.service.ts | 13 +- .../asset-search-detail.component.ts | 2 +- libs/auth/src/lib/services/auth.service.ts | 4 +- .../lib/services/favourite.service.spec.ts | 2 +- .../src/lib/services/favourite.service.ts | 2 +- .../src/lib/models/elastic-search-asset.ts | 1 + package-lock.json | 14 +- package.json | 4 +- 61 files changed, 1127 insertions(+), 1167 deletions(-) create mode 100644 apps/server-asset-sg/src/core/decorators/authorize.decorator.ts create mode 100644 apps/server-asset-sg/src/core/decorators/authorized.decorator.ts create mode 100644 apps/server-asset-sg/src/core/decorators/boundary.decorator.ts delete mode 100644 apps/server-asset-sg/src/core/decorators/require-role.decorator.ts create mode 100644 apps/server-asset-sg/src/core/decorators/use-policy.decorator.ts create mode 100644 apps/server-asset-sg/src/core/decorators/use-repo.decorator.ts create mode 100644 apps/server-asset-sg/src/core/guards/policy.guard.ts delete mode 100644 apps/server-asset-sg/src/core/guards/role.guard.ts create mode 100644 apps/server-asset-sg/src/core/policy.ts create mode 100644 apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts rename apps/server-asset-sg/src/features/{asset-old => asset-edit}/asset-edit.fake.ts (100%) create mode 100644 apps/server-asset-sg/src/features/asset-edit/asset-edit.http create mode 100644 apps/server-asset-sg/src/features/asset-edit/asset-edit.policy.ts rename apps/server-asset-sg/src/features/{asset-old => asset-edit}/asset-edit.repo.spec.ts (100%) rename apps/server-asset-sg/src/features/{asset-old => asset-edit}/asset-edit.repo.ts (95%) rename apps/server-asset-sg/src/features/{asset-old => asset-edit}/asset-edit.service.ts (91%) delete mode 100644 apps/server-asset-sg/src/features/asset-old/asset.controller.ts delete mode 100644 apps/server-asset-sg/src/features/asset-old/asset.service.ts create mode 100644 apps/server-asset-sg/src/features/assets/asset.policy.ts create mode 100644 apps/server-asset-sg/src/features/contacts/contact.policy.ts create mode 100644 apps/server-asset-sg/src/features/files/files.controller.ts delete mode 100644 apps/server-asset-sg/src/features/search/find-assets-by-polygon.ts delete mode 100644 apps/server-asset-sg/src/features/search/search-asset.ts create mode 100644 apps/server-asset-sg/src/features/workgroups/workgroup.policy.ts diff --git a/.prettierignore b/.prettierignore index 8f8e33c6..85375be3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,4 @@ /node_modules /tmp /.idea +/development/volumes diff --git a/apps/server-asset-sg/prisma/migrations/20240627070547_add_workgroups/migration.sql b/apps/server-asset-sg/prisma/migrations/20240627070547_add_workgroups/migration.sql index 7e0ab8fe..e69de29b 100644 --- a/apps/server-asset-sg/prisma/migrations/20240627070547_add_workgroups/migration.sql +++ b/apps/server-asset-sg/prisma/migrations/20240627070547_add_workgroups/migration.sql @@ -1,77 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `role` on the `asset_user` table. All the data in the column will be lost. - -*/ --- CreateEnum -CREATE TYPE "Role" AS ENUM ('viewer', 'editor', 'master-editor'); - --- AlterTable -ALTER TABLE "asset" ADD COLUMN "workgroup_id" INTEGER; - - - --- CreateTable -CREATE TABLE "workgroup" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "created_at" TIMESTAMPTZ(6) NOT NULL, - "disabled_at" TIMESTAMPTZ(6), - - CONSTRAINT "workgroup_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "workgroups_on_users" ( - "workgroup_id" INTEGER NOT NULL, - "user_id" UUID NOT NULL, - "role" "Role" NOT NULL, - - CONSTRAINT "workgroups_on_users_pkey" PRIMARY KEY ("workgroup_id","user_id") -); - --- CreateIndex -CREATE UNIQUE INDEX "workgroup_name_key" ON "workgroup"("name"); - --- AddForeignKey -ALTER TABLE "asset" ADD CONSTRAINT "asset_workgroup_id_fkey" FOREIGN KEY ("workgroup_id") REFERENCES "workgroup"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "workgroups_on_users" ADD CONSTRAINT "workgroups_on_users_workgroup_id_fkey" FOREIGN KEY ("workgroup_id") REFERENCES "workgroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "workgroups_on_users" ADD CONSTRAINT "workgroups_on_users_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "asset_user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - -INSERT INTO workgroup (name, created_at) -VALUES ('Swisstopo', NOW()); - -DO -$$ - DECLARE -swisstopo_id INTEGER; -BEGIN -SELECT id INTO swisstopo_id FROM workgroup WHERE name = 'Swisstopo'; - --- Update all assets to be assigned to the "Swisstopo" workgroup -UPDATE asset SET workgroup_id = swisstopo_id; - --- Assign all users to the "Swisstopo" workgroup with role "VIEWER" -INSERT INTO workgroups_on_users (workgroup_id, user_id, role) -SELECT swisstopo_id, id, - CASE - WHEN asset_user.role = 'admin' THEN 'master-editor'::"Role" - WHEN asset_user.role = 'editor' THEN 'editor'::"Role" - WHEN asset_user.role = 'viewer' THEN 'viewer'::"Role" - WHEN asset_user.role = 'master-editor' THEN 'master-editor'::"Role" - ELSE 'viewer'::"Role" - END -FROM asset_user; -END -$$; - --- AlterTable -ALTER TABLE "asset_user" ADD COLUMN "is_admin" BOOLEAN NOT NULL DEFAULT false; - -UPDATE asset_user SET is_admin = true WHERE role = 'admin'; - diff --git a/apps/server-asset-sg/prisma/schema.prisma b/apps/server-asset-sg/prisma/schema.prisma index d3f771bb..040d667b 100644 --- a/apps/server-asset-sg/prisma/schema.prisma +++ b/apps/server-asset-sg/prisma/schema.prisma @@ -59,8 +59,8 @@ model Asset { studyTraces StudyTrace[] typeNatRels TypeNatRel[] - workgroup Workgroup? @relation(fields: [workgroupId], references: [id]) - workgroupId Int? @map("workgroup_id") + workgroup Workgroup @relation(fields: [workgroupId], references: [id]) + workgroupId Int @map("workgroup_id") siblingXAssets AssetXAssetY[] @relation("sibling_x_asset") siblingYAssets AssetXAssetY[] @relation("sibling_y_asset") @@ -639,7 +639,6 @@ model StatusWorkItem { model AssetUser { id String @id @db.Uuid - role String email String lang String oidcId String diff --git a/apps/server-asset-sg/src/app.controller.ts b/apps/server-asset-sg/src/app.controller.ts index ffc66edd..0e141b78 100644 --- a/apps/server-asset-sg/src/app.controller.ts +++ b/apps/server-asset-sg/src/app.controller.ts @@ -1,6 +1,5 @@ -import { DT, decodeError, unknownToError } from '@asset-sg/core'; -import { AssetByTitle, PatchAsset, PatchContact, isEditor } from '@asset-sg/shared'; -import { User as AssetUser } from '@asset-sg/shared'; +import { unknownToError } from '@asset-sg/core'; +import { AssetByTitle, PatchContact } from '@asset-sg/shared'; import { Body, Controller, @@ -12,42 +11,37 @@ import { Param, ParseIntPipe, Patch, - Post, Put, Query, Redirect, - Req, - UploadedFile, - UseInterceptors, ValidationPipe, } from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; import { plainToInstance } from 'class-transformer'; +import { sequenceS } from 'fp-ts/Apply'; +import * as A from 'fp-ts/Array'; import * as E from 'fp-ts/Either'; -import { pipe } from 'fp-ts/function'; -import * as TE from 'fp-ts/TaskEither'; -import { RequireRole } from '@/core/decorators/require-role.decorator'; -import { AssetEditRepo } from '@/features/asset-old/asset-edit.repo'; -import { AssetEditDetail, AssetEditService } from '@/features/asset-old/asset-edit.service'; +import { flow, Lazy, pipe } from 'fp-ts/function'; +import * as RR from 'fp-ts/ReadonlyRecord'; +import * as TE from 'fp-ts/TaskEither'; +import { Authorize } from '@/core/decorators/authorize.decorator'; +import { CurrentUser } from '@/core/decorators/current-user.decorator'; +import { PrismaService } from '@/core/prisma.service'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; import { Contact, ContactData, ContactDataBoundary, ContactId } from '@/features/contacts/contact.model'; import { ContactRepo } from '@/features/contacts/contact.repo'; import { ContactsController } from '@/features/contacts/contacts.controller'; -import { Role, User, UserDataBoundary, UserId } from '@/features/users/user.model'; +import { User, UserDataBoundary, UserId } from '@/features/users/user.model'; import { UserRepo } from '@/features/users/user.repo'; import { UsersController } from '@/features/users/users.controller'; -import { JwtRequest } from '@/models/jwt-request'; -import { permissionDeniedError } from '@/utils/errors'; @Controller('/') export class AppController { constructor( - private readonly assetEditRepo: AssetEditRepo, - private readonly assetEditService: AssetEditService, private readonly userRepo: UserRepo, private readonly contactRepo: ContactRepo, - private readonly assetSearchService: AssetSearchService + private readonly assetSearchService: AssetSearchService, + private readonly prismaService: PrismaService ) {} @Get('/oauth-config/config') @@ -62,83 +56,11 @@ export class AppController { }; } - /** - * @deprecated - */ - @Get('/user') - @Redirect('users/current', 301) - getUser() { - // deprecated - } - - /** - * @deprecated - */ - @Get('/user/favourite') - @Redirect('../users/current/favorites', 301) - async getFavourites() { - // deprecated - } - - /** - * @deprecated - */ - @Get('/admin/user') - @Redirect('../users', 301) - getUsers() { - // deprecated - } - - /** - * @deprecated - */ - @Patch('/admin/user/:id') - @RequireRole(Role.Admin) - updateUser( - @Param('id') id: UserId, - @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) - data: UserDataBoundary - ): Promise { - return new UsersController(this.userRepo).update(id, data); - } - - /** - * @deprecated - */ - @Delete('/admin/user/:id') - @RequireRole(Role.Admin) - @HttpCode(HttpStatus.NO_CONTENT) - async deleteUser(@Param('id') id: UserId): Promise { - await new UsersController(this.userRepo).delete(id); - } - - /** - * @deprecated - */ - @Put('/contact-edit') - @RequireRole(Role.Editor) - @HttpCode(HttpStatus.CREATED) - async createContact(@Body() patch: PatchContact) { - const data: ContactData = patch; - const boundary = plainToInstance(ContactDataBoundary, data); - return new ContactsController(this.contactRepo).create(boundary); - } - - /** - * @deprecated - */ - @Patch('/contact-edit/:id') - @RequireRole(Role.Editor) - updateContact(@Param('id', ParseIntPipe) id: ContactId, patch: PatchContact): Promise { - const data: ContactData = patch; - const boundary = plainToInstance(ContactDataBoundary, data); - return new ContactsController(this.contactRepo).update(id, boundary); - } - /** * @deprecated */ @Get('/asset-edit/search') + @Authorize.User() async searchAssetsByTitle(@Query('title') title: string): Promise { try { return await this.assetSearchService.searchByTitle(title); @@ -147,133 +69,62 @@ export class AppController { } } - /** - * @deprecated - */ - @Get('/asset-edit/:assetId') - async getAsset(@Param('assetId') assetId: string): Promise { - const id = parseInt(assetId); - if (isNaN(id)) { - throw new HttpException('Resource not found', 404); - } - const asset = await this.assetEditRepo.find(id); - if (asset === null) { - throw new HttpException('Resource not found', 404); - } - return AssetEditDetail.encode(asset); - } - - /** - * @deprecated - */ - @Put('/asset-edit') - async createAsset(@Req() req: JwtRequest, @Body() patchAsset: PatchAsset) { - const e = await pipe( - TE.of(req.user as unknown as AssetUser), - TE.filterOrElseW( - (user) => isEditor(user), - () => permissionDeniedError('Not an editor') - ), - TE.bindTo('user'), - TE.bindW('patchAsset', () => TE.fromEither(pipe(PatchAsset.decode(patchAsset), E.mapLeft(decodeError)))), - TE.chainW(({ patchAsset, user }) => this.assetEditService.createAsset(user, patchAsset)) - )(); - if (E.isLeft(e)) { - console.error(e.left); - // if (e.left._tag === 'decodeError') { - // throw new HttpException(e.left.message, 400); - // } - throw new HttpException(e.left.message, 500); - } - return e.right; - } - - /** - * @deprecated - */ - @Patch('/asset-edit/:assetId') - async updateAsset(@Req() req: JwtRequest, @Param('assetId') id: string, @Body() patchAsset: PatchAsset) { - const e = await pipe( - TE.of(req.user as unknown as AssetUser), - TE.filterOrElseW( - (user) => isEditor(user), - () => permissionDeniedError('Not an editor') - ), - TE.bindTo('user'), - TE.bindW('id', () => TE.fromEither(pipe(DT.IntFromString.decode(id), E.mapLeft(decodeError)))), - TE.bindW('patchAsset', () => TE.fromEither(pipe(PatchAsset.decode(patchAsset), E.mapLeft(decodeError)))), - TE.chainW(({ id, patchAsset, user }) => this.assetEditService.updateAsset(user, id, patchAsset)) - )(); + @Get('/reference-data') + @Authorize.User() + async getReferenceData(@CurrentUser() user: User) { + const e = await getReferenceData(user, this.prismaService)(); if (E.isLeft(e)) { console.error(e.left); - // if (e.left._tag === 'decodeError') { - // throw new HttpException(e.left.message, 400); - // } throw new HttpException(e.left.message, 500); } return e.right; } +} - /** - * @deprecated - */ - @Post('/asset-edit/:assetId/file') - @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 250 * 1024 * 1024 } })) - async uploadAssetFile( - @Req() req: JwtRequest, - @Param('assetId') id: string, - @UploadedFile() file: { originalname: string; buffer: Buffer; size: number; mimetype: string } - ) { - const e = await pipe( - TE.of(req.user as unknown as AssetUser), - TE.filterOrElseW( - (user) => isEditor(user), - () => permissionDeniedError('Not an editor') - ), - TE.bindTo('user'), - TE.bindW('id', () => TE.fromEither(pipe(DT.IntFromString.decode(id), E.mapLeft(decodeError)))), - TE.chainW(({ user, id }) => - this.assetEditService.uploadFile(user, id, { - name: file.originalname, - buffer: file.buffer, - size: file.size, - mimetype: file.mimetype, - }) +const getReferenceData = (user: User, prismaService: PrismaService) => { + const qt = (f: Lazy>, key: K, newKey: string) => + pipe( + TE.tryCatch(f, unknownToError), + TE.map( + flow( + A.map(({ [key]: _key, ...rest }) => [_key as string, { [newKey]: _key, ...rest }] as const), + RR.fromEntries + ) ) - )(); - if (E.isLeft(e)) { - console.error(e.left); - // if (e.left._tag === 'decodeError') { - // throw new HttpException(e.left.message, 400); - // } - throw new HttpException(e.left.message, 500); - } - return e.right; - } - - /** - * @deprecated - */ - @Delete('/asset-edit/:assetId/file/:fileId') - async deleteAssetFile(@Req() req: JwtRequest, @Param('assetId') assetId: string, @Param('fileId') fileId: string) { - const e = await pipe( - TE.of(req.user as unknown as AssetUser), - TE.filterOrElseW( - (user) => isEditor(user), - () => permissionDeniedError('Not an editor') - ), - TE.bindTo('user'), - TE.bindW('assetId', () => TE.fromEither(pipe(DT.IntFromString.decode(assetId), E.mapLeft(decodeError)))), - TE.bindW('fileId', () => TE.fromEither(pipe(DT.IntFromString.decode(fileId), E.mapLeft(decodeError)))), - TE.chainW(({ user, assetId, fileId }) => this.assetEditService.deleteFile(user, assetId, fileId)) - )(); - if (E.isLeft(e)) { - console.error(e.left); - // if (e.left._tag === 'decodeError') { - // throw new HttpException(e.left.message, 400); - // } - throw new HttpException(e.left.message, 500); - } - return e.right; - } -} + ); + + const queries = { + // These records are all static (i.e. never change) and are shared across all assets. + assetFormatItems: qt(() => prismaService.assetFormatItem.findMany(), 'assetFormatItemCode', 'code'), + assetKindItems: qt(() => prismaService.assetKindItem.findMany(), 'assetKindItemCode', 'code'), + autoCatLabelItems: qt(() => prismaService.autoCatLabelItem.findMany(), 'autoCatLabelItemCode', 'code'), + autoObjectCatItems: qt(() => prismaService.autoObjectCatItem.findMany(), 'autoObjectCatItemCode', 'code'), + contactKindItems: qt(() => prismaService.contactKindItem.findMany(), 'contactKindItemCode', 'code'), + geomQualityItems: qt(() => prismaService.geomQualityItem.findMany(), 'geomQualityItemCode', 'code'), + languageItems: qt(() => prismaService.languageItem.findMany(), 'languageItemCode', 'code'), + legalDocItems: qt(() => prismaService.legalDocItem.findMany(), 'legalDocItemCode', 'code'), + manCatLabelItems: qt(() => prismaService.manCatLabelItem.findMany(), 'manCatLabelItemCode', 'code'), + natRelItems: qt(() => prismaService.natRelItem.findMany(), 'natRelItemCode', 'code'), + pubChannelItems: qt(() => prismaService.pubChannelItem.findMany(), 'pubChannelItemCode', 'code'), + statusAssetUseItems: qt(() => prismaService.statusAssetUseItem.findMany(), 'statusAssetUseItemCode', 'code'), + statusWorkItems: qt(() => prismaService.statusWorkItem.findMany(), 'statusWorkItemCode', 'code'), + + // Include only the contacts which are assigned to at least one asset to which the user has access. + contacts: qt( + () => + user.isAdmin + ? prismaService.contact.findMany() + : prismaService.contact.findMany({ + where: { + assetContacts: { + some: { asset: { workgroupId: { in: user.workgroups.map((it) => it.id) } } }, + }, + }, + }), + 'contactId', + 'id' + ), + }; + + return pipe(queries, sequenceS(TE.ApplicativeSeq)); +}; diff --git a/apps/server-asset-sg/src/app.module.ts b/apps/server-asset-sg/src/app.module.ts index 599a04f9..2740fb38 100644 --- a/apps/server-asset-sg/src/app.module.ts +++ b/apps/server-asset-sg/src/app.module.ts @@ -6,13 +6,12 @@ import { ScheduleModule } from '@nestjs/schedule'; import { AppController } from '@/app.controller'; import { provideElasticsearch } from '@/core/elasticsearch'; -import { RoleGuard } from '@/core/guards/role.guard'; +import { PolicyGuard } from '@/core/guards/policy.guard'; import { JwtMiddleware } from '@/core/middleware/jwt.middleware'; import { PrismaService } from '@/core/prisma.service'; -import { AssetEditRepo } from '@/features/asset-old/asset-edit.repo'; -import { AssetEditService } from '@/features/asset-old/asset-edit.service'; -import { AssetController } from '@/features/asset-old/asset.controller'; -import { AssetService } from '@/features/asset-old/asset.service'; +import { AssetEditController } from '@/features/asset-edit/asset-edit.controller'; +import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; +import { AssetEditService } from '@/features/asset-edit/asset-edit.service'; import { AssetInfoRepo } from '@/features/assets/asset-info.repo'; import { AssetRepo } from '@/features/assets/asset.repo'; import { AssetsController } from '@/features/assets/assets.controller'; @@ -23,6 +22,7 @@ import { ContactRepo } from '@/features/contacts/contact.repo'; import { ContactsController } from '@/features/contacts/contacts.controller'; import { FavoriteRepo } from '@/features/favorites/favorite.repo'; import { FavoritesController } from '@/features/favorites/favorites.controller'; +import { FilesController } from '@/features/files/files.controller'; import { OcrController } from '@/features/ocr/ocr.controller'; import { StudiesController } from '@/features/studies/studies.controller'; import { StudyRepo } from '@/features/studies/study.repo'; @@ -34,35 +34,35 @@ import { WorkgroupRepo } from '@/features/workgroups/workgroup.repo'; @Module({ controllers: [ AppController, - UsersController, - FavoritesController, - AssetSyncController, - AssetSearchController, + AssetEditController, AssetsController, - AssetController, - StudiesController, + AssetSearchController, + AssetSyncController, ContactsController, + FavoritesController, + FilesController, OcrController, + StudiesController, + UsersController, WorkgroupController, ], imports: [HttpModule, ScheduleModule.forRoot(), CacheModule.register()], providers: [ provideElasticsearch, - PrismaService, - AssetRepo, - AssetInfoRepo, - AssetService, AssetEditRepo, + AssetEditService, + AssetInfoRepo, + AssetRepo, + AssetSearchService, ContactRepo, FavoriteRepo, + PrismaService, + StudyRepo, UserRepo, WorkgroupRepo, - StudyRepo, - AssetEditService, - AssetSearchService, { provide: APP_GUARD, - useClass: RoleGuard, + useClass: PolicyGuard, }, ], }) diff --git a/apps/server-asset-sg/src/core/decorators/authorize.decorator.ts b/apps/server-asset-sg/src/core/decorators/authorize.decorator.ts new file mode 100644 index 00000000..8209b500 --- /dev/null +++ b/apps/server-asset-sg/src/core/decorators/authorize.decorator.ts @@ -0,0 +1,122 @@ +import { HttpException, HttpStatus, SetMetadata } from '@nestjs/common'; +import { Request } from 'express'; +import { SingleKeyObject } from 'type-fest'; +import { ReadRepo } from '@/core/repo'; + +type ParamType = typeof Number | typeof String; + +export interface AuthorizationTarget { + getId: (request: Request) => string | number; + findBy?: (repo: ReadRepo) => (id: never) => Promise; +} + +type TargetIdMapping = SingleKeyObject | ((req: Request) => string | number); + +export interface ShowMetadata { + action: 'show'; + target: AuthorizationTarget; +} + +export interface CreateMetadata { + action: 'create'; +} + +export interface UpdateMetadata { + action: 'update'; + target: AuthorizationTarget; +} + +export interface DeleteMetadata { + action: 'delete'; + target: AuthorizationTarget; +} + +export interface UserOnlyMetadata { + action: 'user-only'; +} + +export interface AdminOnlyMetadata { + action: 'admin-only'; +} + +export type AuthorizationMetadata = + | ShowMetadata + | CreateMetadata + | UpdateMetadata + | DeleteMetadata + | UserOnlyMetadata + | AdminOnlyMetadata; + +export const Authorize = { + Show: , R extends ReadRepo>( + mapping: TargetIdMapping, + findBy?: (repo: R) => (id: never) => Promise + ) => { + return SetMetadata('authorization', { + action: 'show', + target: { + getId: makeIdFetch(mapping), + findBy, + }, + } as ShowMetadata); + }, + + Create: () => + SetMetadata('authorization', { + action: 'create', + } as CreateMetadata), + + Update: , R extends ReadRepo>( + mapping: TargetIdMapping, + findBy?: (repo: R) => (id: never) => Promise + ) => { + return SetMetadata('authorization', { + action: 'update', + target: { + getId: makeIdFetch(mapping), + findBy, + }, + } as UpdateMetadata); + }, + + Delete: , R extends ReadRepo>( + mapping: TargetIdMapping, + findBy?: (repo: R) => (id: never) => Promise + ) => { + return SetMetadata('authorization', { + action: 'delete', + target: { + getId: makeIdFetch(mapping), + findBy, + }, + } as DeleteMetadata); + }, + + User: () => SetMetadata('authorization', { action: 'user-only' } as UserOnlyMetadata), + Admin: () => SetMetadata('authorization', { action: 'admin-only' } as AdminOnlyMetadata), +}; + +const makeIdFetch = >(mapping: TargetIdMapping) => { + if (mapping instanceof Function) { + return mapping; + } + const [name, type] = Object.entries(mapping)[0]; + return loadIdByParam(name, type); +}; + +const loadIdByParam = + (name: string, type: ParamType) => + (request: Request): string | number => { + let paramValue: string | number = request.params[name]; + if (paramValue == null) { + throw new HttpException(`Missing ${name} parameter`, HttpStatus.BAD_REQUEST); + } + if (type === String) { + return paramValue; + } + paramValue = parseInt(paramValue); + if (isNaN(paramValue)) { + throw new HttpException(`Parameter ${name} must be a number`, HttpStatus.BAD_REQUEST); + } + return paramValue; + }; diff --git a/apps/server-asset-sg/src/core/decorators/authorized.decorator.ts b/apps/server-asset-sg/src/core/decorators/authorized.decorator.ts new file mode 100644 index 00000000..5c181ae6 --- /dev/null +++ b/apps/server-asset-sg/src/core/decorators/authorized.decorator.ts @@ -0,0 +1,14 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Policy } from '@/core/policy'; +import { AuthorizedRequest } from '@/models/jwt-request'; + +export const Authorized = { + Record: createParamDecorator((_param, ctx: ExecutionContext): unknown => { + const request = ctx.switchToHttp().getRequest() as AuthorizedRequest; + return request.authorized?.record ?? null; + }), + Policy: createParamDecorator((_param, ctx: ExecutionContext): Policy => { + const request = ctx.switchToHttp().getRequest() as AuthorizedRequest; + return request.authorized?.policy ?? null; + }), +}; diff --git a/apps/server-asset-sg/src/core/decorators/boundary.decorator.ts b/apps/server-asset-sg/src/core/decorators/boundary.decorator.ts new file mode 100644 index 00000000..f194f9fc --- /dev/null +++ b/apps/server-asset-sg/src/core/decorators/boundary.decorator.ts @@ -0,0 +1,26 @@ +import { decodeError } from '@asset-sg/core'; +import { createParamDecorator, ExecutionContext, HttpException, HttpStatus, ValidationPipe } from '@nestjs/common'; +import * as E from 'fp-ts/Either'; +import * as D from 'io-ts/Decoder'; +import { Class } from 'type-fest'; +import { JwtRequest } from '@/models/jwt-request'; + +export type BoundaryType = D.Decoder | Class; + +const validationPipe = new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }); + +export const Boundary = createParamDecorator(async (dataType: BoundaryType, context: ExecutionContext) => { + const request = context.switchToHttp().getRequest() as JwtRequest; + if (dataType instanceof Function) { + return validationPipe.transform(request.body, { type: 'body', metatype: dataType }); + } else { + const data = (dataType as D.Decoder).decode(request.body); + if (E.isLeft(data)) { + throw new HttpException( + `invalid request body: ${decodeError(data.left).message}`, + HttpStatus.UNPROCESSABLE_ENTITY + ); + } + return data.right as object; + } +}); diff --git a/apps/server-asset-sg/src/core/decorators/require-role.decorator.ts b/apps/server-asset-sg/src/core/decorators/require-role.decorator.ts deleted file mode 100644 index 194474bc..00000000 --- a/apps/server-asset-sg/src/core/decorators/require-role.decorator.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Reflector } from '@nestjs/core'; - -import { Role } from '@/features/users/user.model'; - -/** - * A decorator that guards NestJS routes by requiring an authenticated user - * with a specific minimal access role to be present. - * - * @example```ts - * @Post() - * @RequireRole(Role.Editor) - * async showRoute() { - * console.log('The current user is at least an editor.'); - * } - * ``` - */ -export const RequireRole = Reflector.createDecorator(); diff --git a/apps/server-asset-sg/src/core/decorators/use-policy.decorator.ts b/apps/server-asset-sg/src/core/decorators/use-policy.decorator.ts new file mode 100644 index 00000000..aeadfa74 --- /dev/null +++ b/apps/server-asset-sg/src/core/decorators/use-policy.decorator.ts @@ -0,0 +1,6 @@ +import { Reflector } from '@nestjs/core'; + +import { Class } from 'type-fest'; +import { Policy } from '@/core/policy'; + +export const UsePolicy = Reflector.createDecorator>>(); diff --git a/apps/server-asset-sg/src/core/decorators/use-repo.decorator.ts b/apps/server-asset-sg/src/core/decorators/use-repo.decorator.ts new file mode 100644 index 00000000..41deb505 --- /dev/null +++ b/apps/server-asset-sg/src/core/decorators/use-repo.decorator.ts @@ -0,0 +1,6 @@ +import { Reflector } from '@nestjs/core'; + +import { Class } from 'type-fest'; +import { ReadRepo } from '@/core/repo'; + +export const UseRepo = Reflector.createDecorator>>(); diff --git a/apps/server-asset-sg/src/core/guards/policy.guard.ts b/apps/server-asset-sg/src/core/guards/policy.guard.ts new file mode 100644 index 00000000..241fe62d --- /dev/null +++ b/apps/server-asset-sg/src/core/guards/policy.guard.ts @@ -0,0 +1,114 @@ +import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { ModuleRef, Reflector } from '@nestjs/core'; +import { Request } from 'express'; +import { UsePolicy } from '../decorators/use-policy.decorator'; +import { + AuthorizationMetadata, + AuthorizationTarget, + CreateMetadata, + DeleteMetadata, + ShowMetadata, + UpdateMetadata, +} from '@/core/decorators/authorize.decorator'; +import { UseRepo } from '@/core/decorators/use-repo.decorator'; +import { AuthorizedRequest, JwtRequest } from '@/models/jwt-request'; + +@Injectable() +export class PolicyGuard implements CanActivate { + constructor(private readonly reflector: Reflector, private readonly moduleRef: ModuleRef) {} + + async canActivate(context: ExecutionContext): Promise { + const auth = this.reflector.get('authorization', context.getHandler()); + if (auth == null) { + return true; + } + const request = context.switchToHttp().getRequest() as JwtRequest; + if (request.user == null) { + return false; + } + switch (auth.action) { + case 'show': + return this.authorizeShow(context, auth); + case 'create': + return this.authorizeCreate(context, auth); + case 'update': + return this.authorizeUpdate(context, auth); + case 'delete': + return this.authorizeDelete(context, auth); + case 'user-only': + return this.authorizeUserOnly(context); + case 'admin-only': + return this.authorizeAdminOnly(context); + } + } + + private async authorizeShow(context: ExecutionContext, auth: ShowMetadata): Promise { + const policy = this.extractPolicy(context); + const record = await this.extractRecord(context, auth.target); + return policy.canDoEverything() || policy.canShow(record); + } + + private async authorizeCreate(context: ExecutionContext, _auth: CreateMetadata): Promise { + const policy = this.extractPolicy(context); + return policy.canDoEverything() || policy.canCreate(); + } + + private async authorizeUpdate(context: ExecutionContext, auth: UpdateMetadata): Promise { + const policy = this.extractPolicy(context); + const record = await this.extractRecord(context, auth.target); + return policy.canDoEverything() || policy.canUpdate(record); + } + + private async authorizeDelete(context: ExecutionContext, auth: DeleteMetadata): Promise { + const policy = this.extractPolicy(context); + const record = await this.extractRecord(context, auth.target); + return policy.canDoEverything() || policy.canDelete(record); + } + + private authorizeUserOnly(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() as JwtRequest; + return request.user != null; + } + + private authorizeAdminOnly(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() as JwtRequest; + return request.user.isAdmin ?? false; + } + + private extractPolicy(context: ExecutionContext) { + const policyClass = + this.reflector.get(UsePolicy, context.getHandler()) ?? this.reflector.get(UsePolicy, context.getClass()); + if (policyClass == null) { + throw new Error('missing `UsePolicy` decorator'); + } + const request = context.switchToHttp().getRequest() as JwtRequest; + const policy = new policyClass(request.user); + assignAuthorized(request, { policy }); + return policy; + } + + private async extractRecord(context: ExecutionContext, target: AuthorizationTarget): Promise { + const request = context.switchToHttp().getRequest() as JwtRequest; + const id = target.getId(request); + + const repoType = + this.reflector.get(UseRepo, context.getHandler()) ?? this.reflector.get(UseRepo, context.getClass()); + if (repoType == null) { + throw new Error('missing `UseRepo` decorator'); + } + const repo = this.moduleRef.get(repoType); + const findBy = target.findBy == null ? repo.find : target.findBy(repo); + const record = await findBy.call(repo, [id]); + if (record == null) { + throw new HttpException('Not found', HttpStatus.NOT_FOUND); + } + assignAuthorized(request, { record }); + return record; + } +} + +const assignAuthorized = (request: Request, assigns: Partial) => { + const authorizedRequest = request as AuthorizedRequest; + authorizedRequest.authorized ??= {} as AuthorizedRequest['authorized']; + Object.assign(authorizedRequest.authorized, assigns); +}; diff --git a/apps/server-asset-sg/src/core/guards/role.guard.ts b/apps/server-asset-sg/src/core/guards/role.guard.ts deleted file mode 100644 index 5290c8a8..00000000 --- a/apps/server-asset-sg/src/core/guards/role.guard.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; - -import { RequireRole } from '@/core/decorators/require-role.decorator'; -import { getRoleIndex } from '@/features/users/user.model'; -import { JwtRequest } from '@/models/jwt-request'; - -@Injectable() -export class RoleGuard implements CanActivate { - constructor(private reflector: Reflector) {} - - canActivate(context: ExecutionContext): boolean | Promise { - const role = this.reflector.get(RequireRole, context.getHandler()); - if (role == null) { - return true; - } - const request = context.switchToHttp().getRequest() as JwtRequest; - return getRoleIndex(request.user.role) >= getRoleIndex(role); - } -} diff --git a/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts b/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts index 7b74ef98..05d39558 100644 --- a/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts +++ b/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts @@ -11,7 +11,7 @@ import * as jwt from 'jsonwebtoken'; import { Jwt, JwtPayload } from 'jsonwebtoken'; import jwkToPem from 'jwk-to-pem'; -import { Role, User } from '@/features/users/user.model'; +import { User } from '@/features/users/user.model'; import { UserRepo } from '@/features/users/user.repo'; import { JwtRequest } from '@/models/jwt-request'; @@ -196,7 +196,6 @@ export class JwtMiddleware implements NestMiddleware { return await this.userRepo.create({ oidcId, email, - role: Role.Viewer, lang: 'de', isAdmin: false, workgroups: [], diff --git a/apps/server-asset-sg/src/core/policy.ts b/apps/server-asset-sg/src/core/policy.ts new file mode 100644 index 00000000..154ea950 --- /dev/null +++ b/apps/server-asset-sg/src/core/policy.ts @@ -0,0 +1,60 @@ +import { User, WorkgroupOnUser } from '@/features/users/user.model'; +import { getRoleIndex, Role, WorkgroupId } from '@/features/workgroups/workgroup.model'; + +export abstract class Policy { + private readonly workgroups = new Map(); + + constructor(protected readonly user: User) { + for (const workgroup of this.user.workgroups) { + this.workgroups.set(workgroup.id, workgroup); + } + } + + protected hasWorkgroup(ids: WorkgroupId | Iterable): boolean { + ids = typeof ids === 'number' ? [ids] : ids; + for (const id of ids) { + if (this.workgroups.has(id)) { + return true; + } + } + return false; + } + + protected withWorkgroup( + ids: WorkgroupId | Iterable, + action: (workgroup: WorkgroupOnUser) => boolean + ): boolean { + ids = typeof ids === 'number' ? [ids] : ids; + for (const id of ids) { + const workgroup = this.workgroups.get(id); + if (workgroup != null && action(workgroup)) { + return true; + } + } + return false; + } + + hasRole(role: Role, ids?: WorkgroupId | Iterable): boolean { + const roleIndex = getRoleIndex(role); + if (ids == null) { + return null != this.user.workgroups.find((group) => getRoleIndex(group.role) >= roleIndex); + } + return this.withWorkgroup(ids, (group) => getRoleIndex(group.role) >= roleIndex); + } + + canDoEverything(): boolean { + return this.user.isAdmin; + } + + abstract canShow(value: T): boolean; + + abstract canCreate(): boolean; + + canUpdate(_value: T): boolean { + return this.canCreate(); + } + + canDelete(value: T): boolean { + return this.canCreate(); + } +} diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts new file mode 100644 index 00000000..e77bfeb0 --- /dev/null +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts @@ -0,0 +1,72 @@ +import { PatchAsset } from '@asset-sg/shared'; +import { Controller, Get, HttpException, HttpStatus, Post, Put } from '@nestjs/common'; +import * as E from 'fp-ts/Either'; +import { Authorize } from '@/core/decorators/authorize.decorator'; +import { Authorized } from '@/core/decorators/authorized.decorator'; +import { Boundary } from '@/core/decorators/boundary.decorator'; +import { CurrentUser } from '@/core/decorators/current-user.decorator'; +import { UsePolicy } from '@/core/decorators/use-policy.decorator'; +import { UseRepo } from '@/core/decorators/use-repo.decorator'; +import { AssetEditPolicy } from '@/features/asset-edit/asset-edit.policy'; +import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; +import { AssetEditDetail, AssetEditService } from '@/features/asset-edit/asset-edit.service'; +import { User } from '@/features/users/user.model'; +import { Role } from '@/features/workgroups/workgroup.model'; + +@Controller('/asset-edit') +@UseRepo(AssetEditRepo) +@UsePolicy(AssetEditPolicy) +export class AssetEditController { + constructor(private readonly assetEditService: AssetEditService) {} + + @Get('/:id') + @Authorize.Show({ id: Number }) + async show(@Authorized.Record() asset: AssetEditDetail): Promise { + return AssetEditDetail.encode(asset); + } + + /** + * @deprecated + */ + @Post('/') + @Authorize.Create() + async create( + @Boundary(PatchAsset) patch: PatchAsset, + @CurrentUser() user: User, + @Authorized.Policy() policy: AssetEditPolicy + ) { + validatePatch(patch, policy); + const result = await this.assetEditService.createAsset(user, patch)(); + if (E.isLeft(result)) { + throw new HttpException(result.left.message, 500); + } + return result.right; + } + + @Put('/:id') + @Authorize.Update({ id: Number }) + async update( + @Boundary(PatchAsset) patch: PatchAsset, + @CurrentUser() user: User, + @Authorized.Record() asset: AssetEditDetail, + @Authorized.Policy() policy: AssetEditPolicy + ) { + validatePatch(patch, policy); + const result = await this.assetEditService.updateAsset(user, asset.assetId, patch)(); + if (E.isLeft(result)) { + throw new HttpException(result.left.message, 500); + } + return result.right; + } +} + +const validatePatch = (patch: PatchAsset, policy: AssetEditPolicy) => { + // Specialization of the policy where we disallow assets to be moved to another workgroup + // if the current user is not an editor for that workgroup. + if (!policy.canDoEverything() && !policy.hasRole(Role.Editor, patch.workgroupId)) { + throw new HttpException( + "Can't move asset to a workgroup for which the user is not an editor", + HttpStatus.UNPROCESSABLE_ENTITY + ); + } +}; diff --git a/apps/server-asset-sg/src/features/asset-old/asset-edit.fake.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts similarity index 100% rename from apps/server-asset-sg/src/features/asset-old/asset-edit.fake.ts rename to apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.http b/apps/server-asset-sg/src/features/asset-edit/asset-edit.http new file mode 100644 index 00000000..b11027c7 --- /dev/null +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.http @@ -0,0 +1,75 @@ +### Get asset-edit +GET {{host}}/api/asset-edit/5 +Authorization: Impersonate {{user}} + +### Create asset-edit +POST {{host}}/api/asset-edit +Authorization: Impersonate {{user}} +Content-Type: application/json + +{ + "assetContacts": [], + "assetFormatItemCode": "pdf", + "assetKindItemCode": "report", + "assetMainId": null, + "createDate": 20200409, + "ids": [], + "internalUse": { + "isAvailable": false, + "startAvailabilityDate": 20230303, + "statusAssetUseItemCode": "tobechecked" + }, + "publicUse": { + "isAvailable": true, + "startAvailabilityDate": 20220706, + "statusAssetUseItemCode": "underclarification" + }, + "isNatRel": false, + "assetLanguages": [], + "manCatLabelRefs": [], + "newStatusWorkItemCode": null, + "newStudies": [], + "receiptDate": 20240709, + "siblingAssetIds": [], + "studies": [], + "titleOriginal": "My Cool Asset", + "titlePublic": "Our Cool Asset", + "typeNatRels": [], + "workgroupId": 1 +} + +### Update asset-edit +PUT {{host}}/api/asset-edit/5 +Authorization: Impersonate {{user}} +Content-Type: application/json + +{ + "assetContacts": [], + "assetFormatItemCode": "pdf", + "assetKindItemCode": "report", + "assetMainId": null, + "createDate": 20200409, + "ids": [], + "internalUse": { + "isAvailable": false, + "startAvailabilityDate": 20230303, + "statusAssetUseItemCode": "tobechecked" + }, + "publicUse": { + "isAvailable": true, + "startAvailabilityDate": 20220706, + "statusAssetUseItemCode": "underclarification" + }, + "isNatRel": false, + "assetLanguages": [], + "manCatLabelRefs": [], + "newStatusWorkItemCode": null, + "newStudies": [], + "receiptDate": 20240709, + "siblingAssetIds": [], + "studies": [], + "titleOriginal": "My Cool Asset", + "titlePublic": "Our Cool Asset", + "typeNatRels": [], + "workgroupId": 1 +} diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.policy.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.policy.ts new file mode 100644 index 00000000..95dc6de7 --- /dev/null +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.policy.ts @@ -0,0 +1,25 @@ +import { AssetEditDetail } from '@asset-sg/shared'; +import { Policy } from '@/core/policy'; +import { Role } from '@/features/workgroups/workgroup.model'; + +export class AssetEditPolicy extends Policy { + canShow(value: AssetEditDetail): boolean { + // A user can see all assets in all workgroups that they are assigned to. + return this.hasWorkgroup(value.workgroupId); + } + + canCreate(): boolean { + // A user can create assets for workgroups for which they are an Editor. + return this.hasRole(Role.Editor); + } + + canUpdate(value: AssetEditDetail): boolean { + // A user can update assets for all workgroups for which they are an Editor. + return this.hasRole(Role.Editor, value.workgroupId); + } + + canDelete(value: AssetEditDetail): boolean { + // A user can delete assets for all workgroups for which they are an Editor. + return this.hasRole(Role.Editor, value.workgroupId); + } +} diff --git a/apps/server-asset-sg/src/features/asset-old/asset-edit.repo.spec.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.spec.ts similarity index 100% rename from apps/server-asset-sg/src/features/asset-old/asset-edit.repo.spec.ts rename to apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.spec.ts diff --git a/apps/server-asset-sg/src/features/asset-old/asset-edit.repo.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts similarity index 95% rename from apps/server-asset-sg/src/features/asset-old/asset-edit.repo.ts rename to apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts index a9d90d65..4c8ed216 100644 --- a/apps/server-asset-sg/src/features/asset-old/asset-edit.repo.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts @@ -1,5 +1,5 @@ import { decodeError, isNotNull } from '@asset-sg/core'; -import { AssetUsage, dateFromDateId, DateIdFromDate, PatchAsset, User } from '@asset-sg/shared'; +import { AssetUsage, dateFromDateId, DateIdFromDate, PatchAsset } from '@asset-sg/shared'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import * as E from 'fp-ts/Either'; @@ -9,6 +9,7 @@ import { AssetEditDetail } from './asset-edit.service'; import { PrismaService } from '@/core/prisma.service'; import { Repo, RepoListOptions } from '@/core/repo'; +import { User } from '@/features/users/user.model'; import { AssetEditDetailFromPostgres } from '@/models/asset-edit-detail'; import { createStudies, @@ -18,7 +19,7 @@ import { } from '@/utils/postgres-studies/postgres-studies'; @Injectable() -export class AssetEditRepo implements Repo { +export class AssetEditRepo implements Repo { constructor(private readonly prismaService: PrismaService) {} async find(id: number): Promise { @@ -32,6 +33,17 @@ export class AssetEditRepo implements Repo { return this.loadDetail(asset); } + async findByFile(fileId: number): Promise { + const asset = await this.prismaService.asset.findFirst({ + where: { assetFiles: { some: { fileId } } }, + select: selectPrismaAsset, + }); + if (asset === null) { + return null; + } + return this.loadDetail(asset); + } + async list({ limit, offset, ids }: RepoListOptions = {}): Promise { const assets = await this.prismaService.asset.findMany({ select: selectPrismaAsset, @@ -47,7 +59,7 @@ export class AssetEditRepo implements Repo { return await Promise.all(assets.map((it) => this.loadDetail(it))); } - async create(data: AssetData): Promise { + async create(data: AssetEditData): Promise { const asset = await this.prismaService.asset.create({ select: { assetId: true }, data: { @@ -104,7 +116,7 @@ export class AssetEditRepo implements Repo { return (await this.find(asset.assetId)) as AssetEditDetail; } - async update(id: number, data: AssetData): Promise { + async update(id: number, data: AssetEditData): Promise { // Check if a record for `id` exists, and return `null` if not. const count = await this.prismaService.asset.count({ where: { assetId: id } }); if (count === 0) { @@ -311,7 +323,7 @@ export class AssetEditRepo implements Repo { /** * The data required to create or update an {@link AssetEditDetail}. */ -export interface AssetData { +export interface AssetEditData { patch: PatchAsset; user: User; } diff --git a/apps/server-asset-sg/src/features/asset-old/asset-edit.service.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts similarity index 91% rename from apps/server-asset-sg/src/features/asset-old/asset-edit.service.ts rename to apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts index 1077c3da..5ab4064f 100644 --- a/apps/server-asset-sg/src/features/asset-old/asset-edit.service.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts @@ -1,5 +1,5 @@ import { isNotNull, unknownToUnknownError } from '@asset-sg/core'; -import { BaseAssetEditDetail, PatchAsset, User } from '@asset-sg/shared'; +import { BaseAssetEditDetail, PatchAsset } from '@asset-sg/shared'; import { Injectable } from '@nestjs/common'; import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; @@ -9,6 +9,7 @@ import { AssetEditRepo } from './asset-edit.repo'; import { PrismaService } from '@/core/prisma.service'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; +import { User } from '@/features/users/user.model'; import { notFoundError } from '@/utils/errors'; import { deleteFile } from '@/utils/file/delete-file'; import { putFile } from '@/utils/file/put-file'; @@ -22,15 +23,15 @@ export type AssetEditDetail = C.TypeOf; @Injectable() export class AssetEditService { constructor( - private readonly assetRepo: AssetEditRepo, + private readonly assetEditRepo: AssetEditRepo, private readonly prismaService: PrismaService, private readonly assetSearchService: AssetSearchService ) {} public createAsset(user: User, patch: PatchAsset) { return pipe( - TE.tryCatch(() => this.assetRepo.create({ user, patch }), unknownToUnknownError), - TE.chain(({ assetId }) => TE.tryCatch(() => this.assetRepo.find(assetId), unknownToUnknownError)), + TE.tryCatch(() => this.assetEditRepo.create({ user, patch }), unknownToUnknownError), + TE.chain(({ assetId }) => TE.tryCatch(() => this.assetEditRepo.find(assetId), unknownToUnknownError)), TE.chainW(TE.fromPredicate(isNotNull, notFoundError)), TE.tap((asset) => TE.tryCatch(() => this.assetSearchService.register(asset), unknownToUnknownError)), TE.map((asset) => AssetEditDetail.encode(asset)) @@ -39,7 +40,7 @@ export class AssetEditService { public updateAsset(user: User, assetId: number, patch: PatchAsset) { return pipe( - TE.tryCatch(() => this.assetRepo.update(assetId, { user, patch }), unknownToUnknownError), + TE.tryCatch(() => this.assetEditRepo.update(assetId, { user, patch }), unknownToUnknownError), TE.chainW(TE.fromPredicate(isNotNull, notFoundError)), TE.tap((asset) => TE.tryCatch(() => this.assetSearchService.register(asset), unknownToUnknownError)), TE.map((asset) => AssetEditDetail.encode(asset)) diff --git a/apps/server-asset-sg/src/features/asset-old/asset.controller.ts b/apps/server-asset-sg/src/features/asset-old/asset.controller.ts deleted file mode 100644 index 78097c65..00000000 --- a/apps/server-asset-sg/src/features/asset-old/asset.controller.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { DT, unknownToError } from '@asset-sg/core'; -import { Controller, Get, HttpException, Param, Query, Req, Res } from '@nestjs/common'; -import { Request, Response } from 'express'; -import * as E from 'fp-ts/Either'; -import * as D from 'io-ts/Decoder'; - -import { AssetService } from '@/features/asset-old/asset.service'; -import { AssetSearchService } from '@/features/assets/search/asset-search.service'; -import { isNotFoundError } from '@/utils/errors'; - -@Controller('/') -export class AssetController { - constructor(private readonly assetService: AssetService, private readonly assetSearchService: AssetSearchService) {} - - @Get('/asset') - // async findAssetsByPolygon(@Query() polygon: [number, number][]) { - async findAssetsByPolygon(@Req() req: Request) { - // const e = pipe( - // TE.fromEither(AssetSearchParams.decode(req.query)), - // TE.chainW(a => { - // switch (a.filterKind) { - // case 'polygon': - // return findAssetsByPolygon(a.polygon); - // case 'searchText': - // return TE.of(1); - // } - // }), - // ); - // console.log(JSON.stringify(pipe(AssetSearchParams.decode(req.query), E.mapLeft(D.draw)), null, 2)); - // return 'adsf;'; - const e = await this.assetService.searchAssets(req.query)(); - if (E.isLeft(e)) { - console.error(e.left); - throw new HttpException(e.left.message, 500); - } - return e.right; - } - - @Get('/search-asset') - async searchAsset(@Query('searchText') searchText: string) { - try { - return this.assetSearchService.searchOld(searchText, { - scope: ['titlePublic', 'titleOriginal', 'contactNames', 'sgsId'], - }); - } catch (e) { - const error = unknownToError(e); - console.error(error); - throw new HttpException(error.message, 500); - } - } - - @Get('/asset-detail/:assetId') - async getAssetDetail(@Param('assetId') assetId: string) { - const maybeAssetId = DT.IntFromString.decode(assetId); - if (E.isLeft(maybeAssetId)) { - throw new HttpException(D.draw(maybeAssetId.left), 400); - } - - const e = await this.assetService.getAssetDetail(maybeAssetId.right)(); - if (E.isLeft(e)) { - console.error(e.left); - if (isNotFoundError(e.left)) { - throw new HttpException('Resource not found', 400); - } - throw new HttpException(e.left.message, 500); - } - return e.right; - } - - @Get('/reference-data') - async getReferenceData() { - const e = await this.assetService.getReferenceData()(); - if (E.isLeft(e)) { - console.error(e.left); - throw new HttpException(e.left.message, 500); - } - return e.right; - } - - @Get('/file/:fileId') - async getFile(@Res() res: Response, @Param('fileId') fileId: string) { - const maybeFileId = DT.IntFromString.decode(fileId); - if (E.isLeft(maybeFileId)) { - throw new HttpException(D.draw(maybeFileId.left), 400); - } - - const e = await this.assetService.getFile(maybeFileId.right)(); - if (E.isLeft(e)) { - throw new HttpException(e.left.message, 500); - } - const result = e.right; - if (result.contentType) { - res.header('Content-Type', result.contentType); - } - if (result.contentLength != null) { - res.header('Content-Length', result.contentLength.toString()); - } - res.setHeader('Content-disposition', `filename="${result.fileName}"`); - - e.right.stream.pipe(res); - } -} diff --git a/apps/server-asset-sg/src/features/asset-old/asset.service.ts b/apps/server-asset-sg/src/features/asset-old/asset.service.ts deleted file mode 100644 index ffbb93b4..00000000 --- a/apps/server-asset-sg/src/features/asset-old/asset.service.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { decodeError, isNotNil, unknownToError, unknownToUnknownError } from '@asset-sg/core'; -import { AssetSearchParams, BaseAssetDetail, SearchAssetResult } from '@asset-sg/shared'; -import { Injectable } from '@nestjs/common'; -import { sequenceS } from 'fp-ts/Apply'; -import * as A from 'fp-ts/Array'; -import { flow, Lazy, pipe } from 'fp-ts/function'; -import * as O from 'fp-ts/Option'; -import * as RR from 'fp-ts/ReadonlyRecord'; -import * as TE from 'fp-ts/TaskEither'; -import * as C from 'io-ts/Codec'; -import * as D from 'io-ts/Decoder'; - -import { PrismaService } from '@/core/prisma.service'; -import { AssetSearchService } from '@/features/assets/search/asset-search.service'; -import { findAssetsByPolygon } from '@/features/search/find-assets-by-polygon'; -import { AssetDetailFromPostgres } from '@/models/AssetDetailFromPostgres'; -import { notFoundError } from '@/utils/errors'; -import { getFile } from '@/utils/file/get-file'; -import { postgresStudiesByAssetId } from '@/utils/postgres-studies/postgres-studies'; - -@Injectable() -export class AssetService { - constructor(private readonly prismaService: PrismaService, private readonly assetSearchService: AssetSearchService) {} - - getFile(fileId: number) { - return getFile(this.prismaService, fileId); - } - - searchAssets(query: unknown): TE.TaskEither { - return pipe( - TE.fromEither(AssetSearchParams.decode(query)), - TE.mapLeft((e) => new Error(D.draw(e))), - TE.chainW((a) => { - switch (a.filterKind) { - case 'polygon': - return pipe( - a.searchText, - O.fold( - () => findAssetsByPolygon(this.prismaService, a.polygon), - (searchText) => - pipe( - findAssetsByPolygon(this.prismaService, a.polygon), - TE.chainW( - SearchAssetResult.matchStrict({ - SearchAssetResultNonEmpty: (result) => - TE.tryCatch( - () => - this.assetSearchService.searchOld(searchText, { - scope: ['titlePublic', 'titleOriginal', 'contactNames'], - assetIds: result.assets.map((asset) => asset.assetId), - }), - unknownToError - ), - SearchAssetResultEmpty: TE.of, - }) - ) - ) - ) - ); - case 'searchText': - // TODO: now callSearchAssets with O.none as third parameter - return TE.of(1) as unknown as TE.TaskEither; - } - }) - ); - } - - getReferenceData() { - const qt = (f: Lazy>, key: K, newKey: string) => - pipe( - TE.tryCatch(f, unknownToError), - TE.map( - flow( - A.map(({ [key]: _key, ...rest }) => [_key as string, { [newKey]: _key, ...rest }] as const), - RR.fromEntries - ) - ) - ); - - const queries = { - assetFormatItems: qt(() => this.prismaService.assetFormatItem.findMany(), 'assetFormatItemCode', 'code'), - assetKindItems: qt(() => this.prismaService.assetKindItem.findMany(), 'assetKindItemCode', 'code'), - autoCatLabelItems: qt(() => this.prismaService.autoCatLabelItem.findMany(), 'autoCatLabelItemCode', 'code'), - autoObjectCatItems: qt(() => this.prismaService.autoObjectCatItem.findMany(), 'autoObjectCatItemCode', 'code'), - contactKindItems: qt(() => this.prismaService.contactKindItem.findMany(), 'contactKindItemCode', 'code'), - geomQualityItems: qt(() => this.prismaService.geomQualityItem.findMany(), 'geomQualityItemCode', 'code'), - languageItems: qt(() => this.prismaService.languageItem.findMany(), 'languageItemCode', 'code'), - legalDocItems: qt(() => this.prismaService.legalDocItem.findMany(), 'legalDocItemCode', 'code'), - manCatLabelItems: qt(() => this.prismaService.manCatLabelItem.findMany(), 'manCatLabelItemCode', 'code'), - natRelItems: qt(() => this.prismaService.natRelItem.findMany(), 'natRelItemCode', 'code'), - pubChannelItems: qt(() => this.prismaService.pubChannelItem.findMany(), 'pubChannelItemCode', 'code'), - statusAssetUseItems: qt(() => this.prismaService.statusAssetUseItem.findMany(), 'statusAssetUseItemCode', 'code'), - statusWorkItems: qt(() => this.prismaService.statusWorkItem.findMany(), 'statusWorkItemCode', 'code'), - contacts: qt(() => this.prismaService.contact.findMany(), 'contactId', 'id'), - }; - - return pipe(queries, sequenceS(TE.ApplicativeSeq)); - } - - getAssetDetail(assetId: number) { - const AssetDetail = C.struct({ - ...BaseAssetDetail, - studies: C.array(C.struct({ assetId: C.number, studyId: C.string, geomText: C.string })), - }); - return pipe( - TE.tryCatch( - () => - this.prismaService.asset.findUnique({ - where: { assetId }, - select: { - assetId: true, - titlePublic: true, - titleOriginal: true, - createDate: true, - lastProcessedDate: true, - assetKindItemCode: true, - assetFormatItemCode: true, - assetLanguages: { - select: { - languageItem: true, - }, - }, - internalUse: { select: { isAvailable: true } }, - publicUse: { select: { isAvailable: true } }, - ids: { select: { id: true, description: true } }, - assetContacts: { - select: { - role: true, - contact: { select: { name: true, locality: true, contactKindItemCode: true } }, - }, - }, - manCatLabelRefs: { select: { manCatLabelItemCode: true } }, - assetFormatCompositions: { select: { assetFormatItemCode: true } }, - typeNatRels: { select: { natRelItemCode: true } }, - assetMain: { select: { assetId: true, titlePublic: true } }, - subordinateAssets: { select: { assetId: true, titlePublic: true } }, - siblingYAssets: { select: { assetX: { select: { assetId: true, titlePublic: true } } } }, - siblingXAssets: { select: { assetY: { select: { assetId: true, titlePublic: true } } } }, - statusWorks: { select: { statusWorkItemCode: true, statusWorkDate: true } }, - assetFiles: { select: { file: true } }, - }, - }), - unknownToUnknownError - ), - TE.chainW(TE.fromPredicate(isNotNil, notFoundError)), - TE.chainW((a) => - pipe( - postgresStudiesByAssetId(this.prismaService, a.assetId), - TE.map((studies) => ({ ...a, studies })) - ) - ), - TE.chainW((a) => pipe(TE.fromEither(AssetDetailFromPostgres.decode(a)), TE.mapLeft(decodeError))), - TE.map(AssetDetail.encode) - ); - } -} diff --git a/apps/server-asset-sg/src/features/assets/asset.model.ts b/apps/server-asset-sg/src/features/assets/asset.model.ts index bc543d62..c83ab492 100644 --- a/apps/server-asset-sg/src/features/assets/asset.model.ts +++ b/apps/server-asset-sg/src/features/assets/asset.model.ts @@ -66,7 +66,7 @@ export interface AssetDetails { usage: AssetUsages; statuses: WorkStatus[]; studies: AssetStudy[]; - workgroupId: number | null; + workgroupId: number; } export interface AssetUsages { @@ -332,6 +332,5 @@ export class AssetDataBoundary implements AssetData { usage!: AssetUsagesBoundary; @IsNumber() - @IsNullable() - workgroupId!: number | null; + workgroupId!: number; } diff --git a/apps/server-asset-sg/src/features/assets/asset.policy.ts b/apps/server-asset-sg/src/features/assets/asset.policy.ts new file mode 100644 index 00000000..bd2e4e6d --- /dev/null +++ b/apps/server-asset-sg/src/features/assets/asset.policy.ts @@ -0,0 +1,25 @@ +import { Policy } from '@/core/policy'; +import { Asset } from '@/features/assets/asset.model'; +import { Role } from '@/features/workgroups/workgroup.model'; + +export class AssetPolicy extends Policy { + canShow(value: Asset): boolean { + // A user can see all assets in all workgroups that they are assigned to. + return this.hasWorkgroup(value.workgroupId); + } + + canCreate(): boolean { + // A user can create assets for workgroups for which they are an Editor. + return this.hasRole(Role.Editor); + } + + canUpdate(value: Asset): boolean { + // A user can update assets for all workgroups for which they are an Editor. + return this.hasRole(Role.Editor, value.workgroupId); + } + + canDelete(value: Asset): boolean { + // A user can delete assets for all workgroups for which they are an Editor. + return this.hasRole(Role.Editor, value.workgroupId); + } +} diff --git a/apps/server-asset-sg/src/features/assets/asset.repo.ts b/apps/server-asset-sg/src/features/assets/asset.repo.ts index f2155f77..a3e48abc 100644 --- a/apps/server-asset-sg/src/features/assets/asset.repo.ts +++ b/apps/server-asset-sg/src/features/assets/asset.repo.ts @@ -226,13 +226,11 @@ const mapDataToPrisma = (data: FullAssetData) => assetFormatItemCode: data.formatCode, }, }, - workgroup: data.workgroupId - ? { - connect: { - id: data.workgroupId, - }, - } - : undefined, + workgroup: { + connect: { + id: data.workgroupId, + }, + }, }); const mapDataToPrismaCreate = (data: FullAssetData): Prisma.AssetCreateInput => ({ diff --git a/apps/server-asset-sg/src/features/assets/assets.controller.ts b/apps/server-asset-sg/src/features/assets/assets.controller.ts index 56a34a50..931009c1 100644 --- a/apps/server-asset-sg/src/features/assets/assets.controller.ts +++ b/apps/server-asset-sg/src/features/assets/assets.controller.ts @@ -1,5 +1,4 @@ import { - Body, Controller, Delete, Get, @@ -10,22 +9,30 @@ import { ParseIntPipe, Post, Put, - ValidationPipe, } from '@nestjs/common'; +import { Authorize } from '@/core/decorators/authorize.decorator'; +import { Authorized } from '@/core/decorators/authorized.decorator'; +import { Boundary } from '@/core/decorators/boundary.decorator'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; -import { RequireRole } from '@/core/decorators/require-role.decorator'; +import { UsePolicy } from '@/core/decorators/use-policy.decorator'; +import { UseRepo } from '@/core/decorators/use-repo.decorator'; +import { AssetEditPolicy } from '@/features/asset-edit/asset-edit.policy'; import { AssetInfoRepo } from '@/features/assets/asset-info.repo'; -import { Asset, AssetDataBoundary, AssetId } from '@/features/assets/asset.model'; +import { Asset, AssetData, AssetDataBoundary, AssetId } from '@/features/assets/asset.model'; +import { AssetPolicy } from '@/features/assets/asset.policy'; import { AssetRepo } from '@/features/assets/asset.repo'; -import { Role, User } from '@/features/users/user.model'; +import { User } from '@/features/users/user.model'; +import { Role } from '@/features/workgroups/workgroup.model'; @Controller('/assets') +@UseRepo(AssetRepo) +@UsePolicy(AssetPolicy) export class AssetsController { constructor(private readonly assetRepo: AssetRepo, private readonly assetInfoRepo: AssetInfoRepo) {} @Get('/:id') - @RequireRole(Role.Viewer) + @Authorize.Show({ id: Number }) async show(@Param('id', ParseIntPipe) id: AssetId): Promise { const asset = await this.assetRepo.find(id); if (asset === null) { @@ -35,24 +42,26 @@ export class AssetsController { } @Post('/') - @RequireRole(Role.MasterEditor) + @Authorize.Create() async create( - @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) - data: AssetDataBoundary, - @CurrentUser() user: User + @Boundary(AssetDataBoundary) data: AssetData, + @CurrentUser() user: User, + @Authorized.Policy() policy: AssetEditPolicy ): Promise { + validateData(data, policy); return await this.assetRepo.create({ ...data, processor: user }); } @Put('/:id') - @RequireRole(Role.MasterEditor) + @Authorize.Update({ id: Number }) async update( - @Param('id', ParseIntPipe) id: AssetId, - @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) - data: AssetDataBoundary, - @CurrentUser() user: User + @Boundary(AssetDataBoundary) data: AssetData, + @CurrentUser() user: User, + @Authorized.Record() record: Asset, + @Authorized.Policy() policy: AssetEditPolicy ): Promise { - const asset = await this.assetRepo.update(id, { ...data, processor: user }); + validateData(data, policy); + const asset = await this.assetRepo.update(record.id, { ...data, processor: user }); if (asset === null) { throw new HttpException('not found', 404); } @@ -60,12 +69,20 @@ export class AssetsController { } @Delete('/:id') - @RequireRole(Role.MasterEditor) + @Authorize.Delete({ id: Number }) @HttpCode(HttpStatus.NO_CONTENT) - async delete(@Param('id', ParseIntPipe) id: AssetId): Promise { - const isOk = await this.assetRepo.delete(id); - if (!isOk) { - throw new HttpException('not found', 404); - } + async delete(@Authorized.Record() record: Asset): Promise { + await this.assetRepo.delete(record.id); } } + +const validateData = (data: AssetData, policy: AssetEditPolicy) => { + // Specialization of the policy where we disallow assets to be moved to another workgroup + // if the current user is not an editor for that workgroup. + if (!policy.canDoEverything() && !policy.hasRole(Role.Editor, data.workgroupId)) { + throw new HttpException( + "Can't move asset to a workgroup for which the user is not an editor", + HttpStatus.UNPROCESSABLE_ENTITY + ); + } +}; diff --git a/apps/server-asset-sg/src/features/assets/assets.http b/apps/server-asset-sg/src/features/assets/assets.http index 280e425d..d248a244 100644 --- a/apps/server-asset-sg/src/features/assets/assets.http +++ b/apps/server-asset-sg/src/features/assets/assets.http @@ -94,7 +94,3 @@ Content-Type: application/json ### Get asset GET {{host}}/api/assets/44383 Authorization: Impersonate {{user}} - -### List studies -GET {{host}}/api/all-study-short -Authorization: Impersonate {{user}} diff --git a/apps/server-asset-sg/src/features/assets/prisma-asset.ts b/apps/server-asset-sg/src/features/assets/prisma-asset.ts index caf9a1b6..1ac73140 100644 --- a/apps/server-asset-sg/src/features/assets/prisma-asset.ts +++ b/apps/server-asset-sg/src/features/assets/prisma-asset.ts @@ -98,11 +98,7 @@ export const assetInfoSelection = satisfy()({ createDate: true, receiptDate: true, lastProcessedDate: true, - workgroup: { - select: { - id: true, - }, - }, + workgroupId: true, }); export const assetSelection = satisfy()({ @@ -199,7 +195,7 @@ export const parseAssetFromPrisma = (data: SelectedAsset): Asset => ({ geom: it.geomText, } as AssetStudy; }), - workgroupId: data.workgroup?.id ?? null, + workgroupId: data.workgroupId, }); const parseLinkedAsset = (data: SelectedLinkedAsset): LinkedAsset => ({ diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts index 6d21584e..51f45806 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts @@ -7,17 +7,22 @@ import { } from '@asset-sg/shared'; import { Body, Controller, HttpCode, HttpStatus, Post, Query, ValidationPipe } from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; +import { Authorize } from '@/core/decorators/authorize.decorator'; +import { CurrentUser } from '@/core/decorators/current-user.decorator'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; +import { User } from '@/features/users/user.model'; @Controller('/assets/search') export class AssetSearchController { constructor(private readonly assetSearchService: AssetSearchService) {} @Post('/') + @Authorize.User() @HttpCode(HttpStatus.OK) async search( @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) query: AssetSearchQueryDTO, + @CurrentUser() user: User, @Query('limit') limit?: number, @@ -27,17 +32,19 @@ export class AssetSearchController { ): Promise { limit = limit == null ? limit : Number(limit); offset = offset == null ? offset : Number(offset); - const result = await this.assetSearchService.search(query, { limit, offset, decode: false }); + const result = await this.assetSearchService.search(query, user, { limit, offset, decode: false }); return plainToInstance(AssetSearchResultDTO, result); } @Post('/stats') + @Authorize.User() @HttpCode(HttpStatus.OK) async showStats( @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) - query: AssetSearchQueryDTO + query: AssetSearchQueryDTO, + @CurrentUser() user: User ): Promise { - const stats = await this.assetSearchService.aggregate(query); + const stats = await this.assetSearchService.aggregate(query, user); return plainToInstance(AssetSearchStatsDTO, stats); } } diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts index d5738e2f..5d59a06d 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts @@ -29,9 +29,9 @@ import { ASSET_ELASTIC_INDEX, AssetSearchService } from './asset-search.service' import { openElasticsearchClient } from '@/core/elasticsearch'; import { PrismaService } from '@/core/prisma.service'; -import { fakeAssetPatch, fakeAssetUsage, fakeContact, fakeUser } from '@/features/asset-old/asset-edit.fake'; -import { AssetData, AssetEditRepo } from '@/features/asset-old/asset-edit.repo'; -import { AssetEditDetail } from '@/features/asset-old/asset-edit.service'; +import { fakeAssetPatch, fakeAssetUsage, fakeContact, fakeUser } from '@/features/asset-edit/asset-edit.fake'; +import { AssetEditData, AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; +import { AssetEditDetail } from '@/features/asset-edit/asset-edit.service'; import { StudyRepo } from '@/features/studies/study.repo'; describe(AssetSearchService, () => { @@ -63,7 +63,7 @@ describe(AssetSearchService, () => { }); }); - const create = async (data: AssetData): Promise => { + const create = async (data: AssetEditData): Promise => { const asset = await assetRepo.create(data); await service.register(asset); return asset; diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts index 1075499e..a890ec18 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts @@ -35,9 +35,10 @@ import indexMapping from '../../../../../../development/init/elasticsearch/mappi import { AssetId } from '../asset.model'; import { PrismaService } from '@/core/prisma.service'; -import { AssetEditRepo } from '@/features/asset-old/asset-edit.repo'; +import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; import { StudyId } from '@/features/studies/study.model'; import { StudyRepo } from '@/features/studies/study.repo'; +import { User } from '@/features/users/user.model'; const INDEX = 'swissgeol_asset_asset'; export { INDEX as ASSET_ELASTIC_INDEX }; @@ -161,6 +162,7 @@ export class AssetSearchService { * Searches for assets using a {@link AssetSearchQuery}. * * @param query The query to match with. + * @param user The user whose assets are to be searched. * @param limit The maximum amount of assets to load. Defaults to `100`. * @param offset The amount of assets being skipped before loading the assets. * @param decode Whether to decode the assets. If this is set to `false`, the assets should be decoded via `AssetEditDetail` before accessing them. @@ -168,10 +170,11 @@ export class AssetSearchService { */ async search( query: AssetSearchQuery, + user: User, { limit = 100, offset = 0, decode = true }: PageOptions & { decode?: boolean } = {} ): Promise { // Apply the query to find all matching ids. - const [serializedAssets, total] = await this.searchAssetsByQuery(query, { limit, offset }); + const [serializedAssets, total] = await this.searchAssetsByQuery(query, user, { limit, offset }); // Load the matched assets from the database. const data: AssetEditDetail[] = []; @@ -195,9 +198,120 @@ export class AssetSearchService { * Aggregates the stats over all assets matching a specific {@link AssetSearchQuery}. * * @param query The query to match with. + * @param user The user whose assets are to be aggregated. */ - async aggregate(query: AssetSearchQuery): Promise { - return await this.aggregateAssetIds(query); + async aggregate(query: AssetSearchQuery, user: User): Promise { + interface Result { + minCreateDate: { value: DateId }; + maxCreateDate: { value: DateId }; + authorIds: { + buckets: AggregationBucket[]; + }; + assetKindItemCodes: { + buckets: AggregationBucket[]; + }; + languageItemCodes: { + buckets: AggregationBucket[]; + }; + geometryCodes: { + buckets: AggregationBucket[]; + }; + usageCodes: { + buckets: AggregationBucket[]; + }; + manCatLabelItemCodes: { + buckets: AggregationBucket[]; + }; + } + + interface AggregationBucket { + key: K; + doc_count: number; + } + + const aggregateGroup = async (query: AssetSearchQuery, operator: string, groupName: string, fieldName?: string) => { + const elasticDslQuery = mapQueryToElasticDsl({ ...query, [groupName]: undefined }, user); + return ( + await this.elastic.search({ + index: INDEX, + size: 0, + query: elasticDslQuery, + track_total_hits: true, + aggs: { + agg: { [operator]: { field: fieldName ?? groupName } }, + }, + }) + ).aggregations?.agg; + }; + + const elasticQuery = mapQueryToElasticDsl(query, user); + const response = await this.elastic.search({ + index: INDEX, + size: 0, + query: elasticQuery, + track_total_hits: true, + }); + const total = (response.hits.total as SearchTotalHits).value; + if (total === 0) { + return { + total: 0, + assetKindItemCodes: [], + authorIds: [], + createDate: null, + languageItemCodes: [], + geometryCodes: [], + manCatLabelItemCodes: [], + usageCodes: [], + }; + } + + const [ + assetKindItemCodes, + authorIds, + languageItemCodes, + geometryCodes, + manCatLabelItemCodes, + usageCodes, + minCreateDate, + maxCreateDate, + ] = await Promise.all([ + aggregateGroup(query, 'terms', 'assetKindItemCodes', 'assetKindItemCode'), + aggregateGroup(query, 'terms', 'authorIds'), + aggregateGroup(query, 'terms', 'languageItemCodes'), + aggregateGroup(query, 'terms', 'geometryCodes'), + aggregateGroup(query, 'terms', 'manCatLabelItemCodes'), + aggregateGroup(query, 'terms', 'usageCodes', 'usageCode'), + aggregateGroup(query, 'min', 'minCreateDate', 'createDate'), + aggregateGroup(query, 'max', 'maxCreateDate', 'createDate'), + ]); + const aggs = { + assetKindItemCodes, + authorIds, + languageItemCodes, + geometryCodes, + manCatLabelItemCodes, + usageCodes, + minCreateDate, + maxCreateDate, + } as unknown as Result; + + const mapBucket = (bucket: AggregationBucket): ValueCount => ({ + value: bucket.key, + count: bucket.doc_count, + }); + return { + total, + assetKindItemCodes: aggs.assetKindItemCodes.buckets.map(mapBucket), + authorIds: aggs.authorIds.buckets.map(mapBucket), + languageItemCodes: aggs.languageItemCodes.buckets.map(mapBucket), + geometryCodes: aggs.geometryCodes.buckets.map(mapBucket), + manCatLabelItemCodes: aggs.manCatLabelItemCodes.buckets.map(mapBucket), + usageCodes: aggs.usageCodes.buckets.map(mapBucket), + createDate: { + min: dateFromDateId(aggs.minCreateDate.value), + max: dateFromDateId(aggs.maxCreateDate.value), + }, + }; } /** @@ -237,11 +351,12 @@ export class AssetSearchService { private async searchAssetsByQuery( query: AssetSearchQuery, + user: User, page: PageOptions = {} ): Promise<[Map, number]> { const BATCH_SIZE = 10_000; - const elasticQuery = mapQueryToElasticDsl(query); + const elasticQuery = mapQueryToElasticDsl(query, user); const matchedAssets = new Map(); let lastAssetId: number | null = null; let totalCount: number | null = null; @@ -293,7 +408,6 @@ export class AssetSearchService { if (response.hits.hits.length === 0) { return [matchedAssets, totalCount]; } - for (const hit of response.hits.hits) { const assetId: number = hit.fields!['assetId'][0]; const data = hit.fields!['data'][0]; @@ -306,123 +420,6 @@ export class AssetSearchService { } } - private async aggregateAssetIds(query: AssetSearchQuery): Promise { - interface Result { - minCreateDate: { value: DateId }; - maxCreateDate: { value: DateId }; - authorIds: { - buckets: AggregationBucket[]; - }; - assetKindItemCodes: { - buckets: AggregationBucket[]; - }; - languageItemCodes: { - buckets: AggregationBucket[]; - }; - geometryCodes: { - buckets: AggregationBucket[]; - }; - usageCodes: { - buckets: AggregationBucket[]; - }; - manCatLabelItemCodes: { - buckets: AggregationBucket[]; - }; - } - - const defaultResult = () => - ({ - total: 0, - assetKindItemCodes: [], - authorIds: [], - createDate: null, - languageItemCodes: [], - geometryCodes: [], - manCatLabelItemCodes: [], - usageCodes: [], - } as AssetSearchStats); - - interface AggregationBucket { - key: K; - doc_count: number; - } - - const aggregateGroup = async (query: AssetSearchQuery, operator: string, groupName: string, fieldName?: string) => { - const elasticDslQuery = mapQueryToElasticDsl({ ...query, [groupName]: undefined }); - return ( - await this.elastic.search({ - index: INDEX, - size: 0, - query: elasticDslQuery, - track_total_hits: true, - aggs: { - agg: { [operator]: { field: fieldName ?? groupName } }, - }, - }) - ).aggregations?.agg; - }; - - const elasticQuery = mapQueryToElasticDsl(query); - const response = await this.elastic.search({ - index: INDEX, - size: 0, - query: elasticQuery, - track_total_hits: true, - }); - const total = (response.hits.total as SearchTotalHits).value; - if (total === 0) { - return defaultResult(); - } - - const [ - assetKindItemCodes, - authorIds, - languageItemCodes, - geometryCodes, - manCatLabelItemCodes, - usageCodes, - minCreateDate, - maxCreateDate, - ] = await Promise.all([ - aggregateGroup(query, 'terms', 'assetKindItemCodes', 'assetKindItemCode'), - aggregateGroup(query, 'terms', 'authorIds'), - aggregateGroup(query, 'terms', 'languageItemCodes'), - aggregateGroup(query, 'terms', 'geometryCodes'), - aggregateGroup(query, 'terms', 'manCatLabelItemCodes'), - aggregateGroup(query, 'terms', 'usageCodes', 'usageCode'), - aggregateGroup(query, 'min', 'minCreateDate', 'createDate'), - aggregateGroup(query, 'max', 'maxCreateDate', 'createDate'), - ]); - const aggs = { - assetKindItemCodes, - authorIds, - languageItemCodes, - geometryCodes, - manCatLabelItemCodes, - usageCodes, - minCreateDate, - maxCreateDate, - } as unknown as Result; - - const mapBucket = (bucket: AggregationBucket): ValueCount => ({ - value: bucket.key, - count: bucket.doc_count, - }); - return { - total, - assetKindItemCodes: aggs.assetKindItemCodes.buckets.map(mapBucket), - authorIds: aggs.authorIds.buckets.map(mapBucket), - languageItemCodes: aggs.languageItemCodes.buckets.map(mapBucket), - geometryCodes: aggs.geometryCodes.buckets.map(mapBucket), - manCatLabelItemCodes: aggs.manCatLabelItemCodes.buckets.map(mapBucket), - usageCodes: aggs.usageCodes.buckets.map(mapBucket), - createDate: { - min: dateFromDateId(aggs.minCreateDate.value), - max: dateFromDateId(aggs.maxCreateDate.value), - }, - }; - } - private async searchElasticOld(query: string, { scope, assetIds }: SearchOptions): Promise { const filters: QueryDslQueryContainer[] = []; if (assetIds != null) { @@ -667,6 +664,7 @@ export class AssetSearchService { manCatLabelItemCodes: asset.manCatLabelRefs, geometryCodes: geometryCodes.length > 0 ? [...new Set(geometryCodes)] : ['None'], studyLocations, + workgroupId: asset.workgroupId, data: JSON.stringify(AssetEditDetail.encode(asset)), }; } @@ -681,10 +679,17 @@ const mapLv95ToElastic = (lv95: LV95): ElasticPoint => { return { lat: wgs[1], lon: wgs[0] }; }; -const mapQueryToElasticDsl = (query: AssetSearchQuery): QueryDslQueryContainer => { +const mapQueryToElasticDsl = (query: AssetSearchQuery, user: User): QueryDslQueryContainer => { const scope = ['titlePublic', 'titleOriginal', 'contactNames', 'sgsId']; const queries: QueryDslQueryContainer[] = []; const filters: QueryDslQueryContainer[] = []; + if (!user.isAdmin) { + filters.push({ + terms: { + workgroupId: user.workgroups.map((it) => it.id), + }, + }); + } if (query.text != null && query.text.length > 0) { queries.push({ bool: { diff --git a/apps/server-asset-sg/src/features/assets/sync/asset-sync.controller.ts b/apps/server-asset-sg/src/features/assets/sync/asset-sync.controller.ts index da973e69..f4efc305 100644 --- a/apps/server-asset-sg/src/features/assets/sync/asset-sync.controller.ts +++ b/apps/server-asset-sg/src/features/assets/sync/asset-sync.controller.ts @@ -2,10 +2,8 @@ import fs from 'fs/promises'; import { Controller, Get, HttpException, OnApplicationBootstrap, Post, Res } from '@nestjs/common'; import { Response } from 'express'; - -import { RequireRole } from '@/core/decorators/require-role.decorator'; +import { Authorize } from '@/core/decorators/authorize.decorator'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; -import { Role } from '@/features/users/user.model'; @Controller('/assets/sync') export class AssetSyncController implements OnApplicationBootstrap { @@ -22,7 +20,7 @@ export class AssetSyncController implements OnApplicationBootstrap { } @Get('/') - @RequireRole(Role.MasterEditor) + @Authorize.Admin() async show(@Res() res: Response): Promise<{ progress: number } | void> { try { const data = await fs.readFile(assetSyncFile, { encoding: 'utf-8' }); @@ -38,7 +36,7 @@ export class AssetSyncController implements OnApplicationBootstrap { } @Post('/') - @RequireRole(Role.MasterEditor) + @Authorize.Admin() async start(@Res() res: Response): Promise { const isSyncRunning = await fs .access(assetSyncFile) diff --git a/apps/server-asset-sg/src/features/contacts/contact.policy.ts b/apps/server-asset-sg/src/features/contacts/contact.policy.ts new file mode 100644 index 00000000..9868ea18 --- /dev/null +++ b/apps/server-asset-sg/src/features/contacts/contact.policy.ts @@ -0,0 +1,14 @@ +import { Policy } from '@/core/policy'; +import { Contact } from '@/features/contacts/contact.model'; +import { Role } from '@/features/workgroups/workgroup.model'; + +export class ContactPolicy extends Policy { + canShow(_value: Contact): boolean { + return this.hasRole(Role.Editor); + } + + canCreate(): boolean { + // A user can create assets for workgroups for which they are an Editor. + return this.hasRole(Role.Editor); + } +} diff --git a/apps/server-asset-sg/src/features/contacts/contacts.controller.ts b/apps/server-asset-sg/src/features/contacts/contacts.controller.ts index 61c79b4c..e26c44fe 100644 --- a/apps/server-asset-sg/src/features/contacts/contacts.controller.ts +++ b/apps/server-asset-sg/src/features/contacts/contacts.controller.ts @@ -1,43 +1,33 @@ -import { - Body, - Controller, - HttpCode, - HttpException, - HttpStatus, - Param, - ParseIntPipe, - Post, - Put, - ValidationPipe, -} from '@nestjs/common'; - -import { RequireRole } from '@/core/decorators/require-role.decorator'; -import { Contact, ContactDataBoundary, ContactId } from '@/features/contacts/contact.model'; +import { Controller, HttpCode, HttpException, HttpStatus, Post, Put } from '@nestjs/common'; +import { Authorize } from '@/core/decorators/authorize.decorator'; +import { Authorized } from '@/core/decorators/authorized.decorator'; +import { Boundary } from '@/core/decorators/boundary.decorator'; +import { UsePolicy } from '@/core/decorators/use-policy.decorator'; +import { UseRepo } from '@/core/decorators/use-repo.decorator'; +import { Contact, ContactData, ContactDataBoundary } from '@/features/contacts/contact.model'; +import { ContactPolicy } from '@/features/contacts/contact.policy'; import { ContactRepo } from '@/features/contacts/contact.repo'; -import { Role } from '@/features/users/user.model'; @Controller('/contacts') +@UseRepo(ContactRepo) +@UsePolicy(ContactPolicy) export class ContactsController { constructor(private readonly contactRepo: ContactRepo) {} @Post('/') - @RequireRole(Role.Editor) + @Authorize.Create() @HttpCode(HttpStatus.CREATED) - create( - @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) - data: ContactDataBoundary - ): Promise { + create(@Boundary(ContactDataBoundary) data: ContactData): Promise { return this.contactRepo.create(data); } @Put('/:id') - @RequireRole(Role.Editor) + @Authorize.Update({ id: Number }) async update( - @Param('id', ParseIntPipe) id: ContactId, - @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) - data: ContactDataBoundary + @Authorized.Record() record: Contact, + @Boundary(ContactDataBoundary) data: ContactData ): Promise { - const contact = await this.contactRepo.update(id, data); + const contact = await this.contactRepo.update(record.id, data); if (contact == null) { throw new HttpException('not found', 404); } diff --git a/apps/server-asset-sg/src/features/favorites/favorites.controller.ts b/apps/server-asset-sg/src/features/favorites/favorites.controller.ts index 629f2190..668011f1 100644 --- a/apps/server-asset-sg/src/features/favorites/favorites.controller.ts +++ b/apps/server-asset-sg/src/features/favorites/favorites.controller.ts @@ -1,6 +1,7 @@ import { SearchAssetResult, SearchAssetResultEmpty } from '@asset-sg/shared'; import { Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Put } from '@nestjs/common'; +import { Authorize } from '@/core/decorators/authorize.decorator'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; import { FavoriteRepo } from '@/features/favorites/favorite.repo'; @@ -13,6 +14,7 @@ export class FavoritesController { // TODO make an alternative, new endpoint for this that does not use fp-ts. @Get('/') + @Authorize.User() async list(@CurrentUser() user: User): Promise { const favorites = await this.favoriteRepo.listByUserId(user.id); if (favorites.length === 0) { @@ -23,12 +25,14 @@ export class FavoritesController { } @Put('/:assetId') + @Authorize.User() @HttpCode(HttpStatus.NO_CONTENT) async add(@Param('assetId', ParseIntPipe) assetId: number, @CurrentUser() user: User): Promise { await this.favoriteRepo.create({ userId: user.id, assetId }); } @Delete('/:assetId') + @Authorize.User() @HttpCode(HttpStatus.NO_CONTENT) async remove(@Param('assetId', ParseIntPipe) assetId: number, @CurrentUser() user: User): Promise { await this.favoriteRepo.delete({ userId: user.id, assetId }); diff --git a/apps/server-asset-sg/src/features/files/files.controller.ts b/apps/server-asset-sg/src/features/files/files.controller.ts new file mode 100644 index 00000000..27124134 --- /dev/null +++ b/apps/server-asset-sg/src/features/files/files.controller.ts @@ -0,0 +1,86 @@ +import { AssetEditDetail } from '@asset-sg/shared'; +import { + Controller, + Delete, + Get, + HttpException, + Param, + ParseIntPipe, + Post, + Res, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { Response } from 'express'; +import * as E from 'fp-ts/Either'; +import { Authorize } from '@/core/decorators/authorize.decorator'; +import { Authorized } from '@/core/decorators/authorized.decorator'; +import { CurrentUser } from '@/core/decorators/current-user.decorator'; +import { UsePolicy } from '@/core/decorators/use-policy.decorator'; +import { UseRepo } from '@/core/decorators/use-repo.decorator'; +import { PrismaService } from '@/core/prisma.service'; +import { AssetEditPolicy } from '@/features/asset-edit/asset-edit.policy'; +import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; +import { AssetEditService } from '@/features/asset-edit/asset-edit.service'; +import { User } from '@/features/users/user.model'; +import { getFile } from '@/utils/file/get-file'; + +@Controller('/files') +@UseRepo(AssetEditRepo) +@UsePolicy(AssetEditPolicy) +export class FilesController { + constructor(private readonly assetEditService: AssetEditService, private readonly prismaService: PrismaService) {} + + @Get('/:id') + @Authorize.Show({ id: Number }, (repo: AssetEditRepo) => repo.findByFile) + async download(@Res() res: Response, @Param('id', ParseIntPipe) id: number) { + const result = await getFile(this.prismaService, id)(); + if (E.isLeft(result)) { + throw new HttpException(result.left.message, 500); + } + const file = result.right; + if (file.contentType) { + res.setHeader('Content-Type', file.contentType); + } + if (file.contentLength != null) { + res.setHeader('Content-Length', file.contentLength.toString()); + } + res.setHeader('Content-Disposition', `filename="${file.fileName}"`); + file.stream.pipe(res); + } + + @Post('/') + @Authorize.Update((req) => parseInt(req.body.assetId)) + @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 250 * 1024 * 1024 } })) + async upload( + @UploadedFile() file: Express.Multer.File, + @Authorized.Record() asset: AssetEditDetail, + @CurrentUser() user: User + ) { + const result = await this.assetEditService.uploadFile(user, asset.assetId, { + name: file.originalname, + buffer: file.buffer, + size: file.size, + mimetype: file.mimetype, + })(); + if (E.isLeft(result)) { + throw new HttpException(result.left.message, 500); + } + return result.right; + } + + @Delete('/:id') + @Authorize.Delete({ id: Number }, (repo: AssetEditRepo) => repo.findByFile) + async delete( + @Param('id', ParseIntPipe) id: number, + @Authorized.Record() asset: AssetEditDetail, + @CurrentUser() user: User + ) { + const e = await this.assetEditService.deleteFile(user, asset.assetId, id)(); + if (E.isLeft(e)) { + throw new HttpException(e.left.message, 500); + } + return e.right; + } +} diff --git a/apps/server-asset-sg/src/features/search/find-assets-by-polygon.ts b/apps/server-asset-sg/src/features/search/find-assets-by-polygon.ts deleted file mode 100644 index 7cb855f8..00000000 --- a/apps/server-asset-sg/src/features/search/find-assets-by-polygon.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { GetRightTypeOfTaskEither, unknownToError } from '@asset-sg/core'; -import { LV95 } from '@asset-sg/shared'; -import { PrismaClient } from '@prisma/client'; -import * as A from 'fp-ts/Array'; -import { pipe } from 'fp-ts/function'; -import { Eq as EqNumber } from 'fp-ts/number'; -import * as TE from 'fp-ts/TaskEither'; - -import { makeSearchAssetResult, searchAssetQuery } from './search-asset'; - -import { postgresStudiesByPolygon } from '@/utils/postgres-studies/postgres-studies'; - -const executeAssetQuery = (prismaClient: PrismaClient, assetIds: number[]) => - TE.tryCatch( - () => - prismaClient.asset.findMany({ - select: searchAssetQuery.select, - where: { assetId: { in: assetIds } }, - }), - unknownToError - ); - -export type AssetQueryResults = GetRightTypeOfTaskEither>; -export type AssetQueryResult = AssetQueryResults[number]; - -export function findAssetsByPolygon(prismaClient: PrismaClient, polygon: LV95[]) { - return pipe( - postgresStudiesByPolygon(prismaClient, polygon), - TE.bindTo('studies'), - TE.bind('assets', ({ studies }) => - executeAssetQuery( - prismaClient, - pipe( - studies, - A.map((s) => s.assetId), - A.uniq(EqNumber) - ) - ) - ), - TE.map(({ assets, studies }) => makeSearchAssetResult(assets, studies)) - ); -} diff --git a/apps/server-asset-sg/src/features/search/search-asset.ts b/apps/server-asset-sg/src/features/search/search-asset.ts deleted file mode 100644 index bdef988a..00000000 --- a/apps/server-asset-sg/src/features/search/search-asset.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { - DateId, - DateIdOrd, - SearchAsset, - SearchAssetResult, - SearchAssetResultCodec, - dateIdFromDate, - makeUsageCode, -} from '@asset-sg/shared'; -import { Prisma } from '@prisma/client'; -import * as A from 'fp-ts/Array'; -import { flow, pipe } from 'fp-ts/function'; -import * as NEA from 'fp-ts/NonEmptyArray'; -import * as O from 'fp-ts/Option'; -import * as R from 'fp-ts/Record'; - -import type { AssetQueryResult, AssetQueryResults } from './find-assets-by-polygon'; - -import { PostgresAllStudies } from '@/utils/postgres-studies/postgres-studies'; - -const makeSearchAssets = ( - assetQueryResults: NEA.NonEmptyArray, - studies: PostgresAllStudies -): NEA.NonEmptyArray => { - const studiesMap = pipe( - studies, - NEA.groupBy((a) => a.assetId.toString()) - ); - return pipe( - assetQueryResults, - NEA.map((a): SearchAsset => { - const { createDate, manCatLabelRefs, internalUse, publicUse, assetLanguages, assetContacts, ...rest } = a; - return { - ...rest, - createDate: dateIdFromDate(createDate), - manCatLabelItemCodes: manCatLabelRefs.map((m) => m.manCatLabelItemCode), - usageCode: makeUsageCode(publicUse.isAvailable, internalUse.isAvailable), - languages: assetLanguages.map((a) => ({ code: a.languageItemCode })), - contacts: assetContacts.map((c) => ({ role: c.role, id: c.contactId })), - score: 1, - studies: pipe( - studiesMap, - R.lookup(a.assetId.toString()), - O.map((ss) => ss.map((s) => ({ studyId: s.studyId, geomText: s.geomText }))), - O.getOrElseW(() => []) - ), - }; - }) - ); -}; - -const makeSearchAssetResultNonEmpty = (assets: NEA.NonEmptyArray) => { - const orderedDates: NEA.NonEmptyArray = pipe( - assets, - NEA.map((a) => a.createDate), - NEA.uniq(DateIdOrd), - NEA.sort(DateIdOrd) - ); - - return SearchAssetResultCodec.encode({ - _tag: 'SearchAssetResultNonEmpty', - aggregations: { - ranges: { createDate: { min: NEA.head(orderedDates), max: NEA.last(orderedDates) } }, - buckets: { - authorIds: pipe( - assets, - A.map((a) => a.contacts.filter((c) => c.role === 'author').map((c) => c.id)), - A.flatten, - NEA.fromArray, - O.map( - flow( - NEA.groupBy((a) => String(a)), - R.map((g) => ({ key: NEA.head(g), count: g.length })), - R.toArray, - A.map(([, value]) => value) - ) - ), - O.getOrElseW(() => []) - ), - assetKindItemCodes: makeBuckets( - pipe( - assets, - A.map((a) => a.assetKindItemCode) - ) - ), - languageItemCodes: makeBuckets( - pipe( - assets, - A.map((a) => a.languages.map((l) => l.code)), - A.flatten - ) - ), - usageCodes: makeBuckets( - pipe( - assets, - A.map((a) => a.usageCode) - ) - ), - manCatLabelItemCodes: makeBuckets( - pipe( - assets, - A.map((a) => a.manCatLabelItemCodes), - A.flatten - ) - ), - }, - }, - assets, - }); -}; - -const makeBuckets = (codes: T[]) => - pipe( - codes, - NEA.fromArray, - O.map( - flow( - NEA.groupBy((a) => String(a)), - R.map((g) => ({ key: NEA.head(g), count: g.length })), - R.toArray, - A.map(([, value]) => value) - ) - ), - O.getOrElseW(() => []) - ); - -const makeSearchAssetResultFromStudiesNonEmpty = ( - assetQueryResults: NEA.NonEmptyArray, - studies: PostgresAllStudies -) => { - const assets = makeSearchAssets(assetQueryResults, studies); - return makeSearchAssetResultNonEmpty(assets); -}; - -export const makeSearchAssetResult = ( - assetQueryResults: AssetQueryResults, - studies: PostgresAllStudies -): SearchAssetResult => - pipe( - NEA.fromArray(assetQueryResults), - O.map((a) => makeSearchAssetResultFromStudiesNonEmpty(a, studies)), - O.getOrElse(() => SearchAssetResultCodec.encode({ _tag: 'SearchAssetResultEmpty' })) - ); - -const makeSearchAssetQuery = ( - args: Prisma.SelectSubset -) => args; - -export const searchAssetQuery = makeSearchAssetQuery({ - select: { - assetId: true, - titlePublic: true, - createDate: true, - assetKindItemCode: true, - assetFormatItemCode: true, - internalUse: { select: { isAvailable: true } }, - publicUse: { select: { isAvailable: true } }, - manCatLabelRefs: { select: { manCatLabelItemCode: true } }, - assetLanguages: { select: { languageItemCode: true } }, - assetContacts: { select: { role: true, contactId: true } }, - }, -}); diff --git a/apps/server-asset-sg/src/features/studies/studies.controller.ts b/apps/server-asset-sg/src/features/studies/studies.controller.ts index 8679c596..6d95c20a 100644 --- a/apps/server-asset-sg/src/features/studies/studies.controller.ts +++ b/apps/server-asset-sg/src/features/studies/studies.controller.ts @@ -1,18 +1,19 @@ import { Readable } from 'stream'; import { Controller, Get, Res } from '@nestjs/common'; import { Response } from 'express'; -import { RequireRole } from '@/core/decorators/require-role.decorator'; +import { Authorize } from '@/core/decorators/authorize.decorator'; +import { CurrentUser } from '@/core/decorators/current-user.decorator'; import { serializeStudyAsCsv, Study } from '@/features/studies/study.model'; import { StudyRepo } from '@/features/studies/study.repo'; -import { Role } from '@/features/users/user.model'; +import { User } from '@/features/users/user.model'; @Controller('/studies') export class StudiesController { constructor(private readonly studyRepo: StudyRepo) {} @Get('/') - @RequireRole(Role.Viewer) - async list(@Res() res: Response): Promise { + @Authorize.User() + async list(@Res() res: Response, @CurrentUser() user: User): Promise { // This route loads all studies and encodes them as CSV. // CSV has been chosen as we have a large amount of studies (13'000+) // and need a concise format that can be processed in batches (which, for example, JSON can't). @@ -37,7 +38,11 @@ export class StudiesController { // The promise that is loading the next batch. // Note that this is running in parallel to the response writer. - let next: Promise | null = studyRepo.list({ limit: INITIAL_BATCH_SIZE, offset: 0 }); + let next: Promise | null = studyRepo.list({ + limit: INITIAL_BATCH_SIZE, + offset: 0, + workgroupIds: user.isAdmin ? null : user.workgroups.map((it) => it.id), + }); // The maximal size of the next batch. let nextLimit = INITIAL_BATCH_SIZE; diff --git a/apps/server-asset-sg/src/features/studies/study.repo.ts b/apps/server-asset-sg/src/features/studies/study.repo.ts index e2f8e021..d73ebb81 100644 --- a/apps/server-asset-sg/src/features/studies/study.repo.ts +++ b/apps/server-asset-sg/src/features/studies/study.repo.ts @@ -1,10 +1,10 @@ -import { LV95, LV95FromSpaceSeparatedString, parseLV95 } from '@asset-sg/shared'; +import { parseLV95 } from '@asset-sg/shared'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; -import * as E from 'fp-ts/Either'; import { PrismaService } from '@/core/prisma.service'; import { ReadRepo, RepoListOptions } from '@/core/repo'; import { Study, StudyId } from '@/features/studies/study.model'; +import { WorkgroupId } from '@/features/workgroups/workgroup.model'; @Injectable() export class StudyRepo implements ReadRepo { @@ -19,38 +19,53 @@ export class StudyRepo implements ReadRepo { return result.length === 1 ? result[0] : null; } - list({ limit, offset, ids }: RepoListOptions = {}): Promise { + list({ limit, offset, ids, workgroupIds }: ListOptions = {}): Promise { + if (workgroupIds != null && workgroupIds.length === 0) { + return Promise.resolve([]); + } + const parts: Prisma.Sql[] = []; const conditions: Prisma.Sql[] = []; + if (workgroupIds != null) { + parts.push(Prisma.sql` + LEFT JOIN asset a ON a.asset_id = s.asset_id + `); + conditions.push(Prisma.sql` + a.workgroup_id IN (${Prisma.join(workgroupIds, ',')}) + `); + } if (ids != null && ids.length > 0) { conditions.push(Prisma.sql` - WHERE study_id IN (${Prisma.join(ids, ',')}) + s.study_id IN (${Prisma.join(ids, ',')}) `); } - conditions.push(Prisma.sql` - ORDER BY asset_id + if (conditions.length != null) { + parts.push(Prisma.sql`WHERE ${Prisma.join(conditions, ' AND ')}`); + } + parts.push(Prisma.sql` + ORDER BY s.asset_id `); if (limit != null) { - conditions.push(Prisma.sql` + parts.push(Prisma.sql` LIMIT ${limit} `); } if (offset != null && offset !== 0) { - conditions.push(Prisma.sql` + parts.push(Prisma.sql` OFFSET ${offset} `); } - return this.query(Prisma.join(conditions, ' ')); + return this.query(Prisma.join(parts, ' ')); } private async query(condition: Prisma.Sql): Promise { type RawStudy = Omit & { center: string }; const studies: RawStudy[] = await this.prisma.$queryRaw` SELECT - study_id as "id", - asset_id AS "assetId", - is_point AS "isPoint", - SUBSTRING(centroid_geom_text FROM 7 FOR length(centroid_geom_text) -7) AS "center" - FROM public.all_study + s.study_id as "id", + s.asset_id AS "assetId", + s.is_point AS "isPoint", + SUBSTRING(s.centroid_geom_text FROM 7 FOR length(s.centroid_geom_text) -7) AS "center" + FROM public.all_study s ${condition} `; return studies.map((study) => { @@ -61,3 +76,7 @@ export class StudyRepo implements ReadRepo { }); } } + +interface ListOptions extends RepoListOptions { + workgroupIds?: WorkgroupId[] | null; +} diff --git a/apps/server-asset-sg/src/features/users/user.model.ts b/apps/server-asset-sg/src/features/users/user.model.ts index 977394f0..b4e6acd0 100644 --- a/apps/server-asset-sg/src/features/users/user.model.ts +++ b/apps/server-asset-sg/src/features/users/user.model.ts @@ -1,37 +1,23 @@ -import { Role as PrismaRole } from '@prisma/client'; -import { IsArray, IsBoolean, IsEnum, IsString } from 'class-validator'; +import { IsArray, IsBoolean, IsString } from 'class-validator'; +import { Role } from '@/features/workgroups/workgroup.model'; import { Data, Model } from '@/utils/data/model'; export interface User extends Model { email: string; - role: Role; lang: string; workgroups: WorkgroupOnUser[]; isAdmin: boolean; } export interface WorkgroupOnUser { - workgroupId: number; - role: PrismaRole; - workgroup: { - name: string; - }; + id: number; + role: Role; } export type UserId = string; export type UserData = Omit, 'email'>; -export enum Role { - Admin = 'admin', - Editor = 'editor', - MasterEditor = 'master-editor', - Viewer = 'viewer', -} - export class UserDataBoundary implements UserData { - @IsEnum(Role, { each: true }) - role!: Role; - @IsString() lang!: string; @@ -41,16 +27,3 @@ export class UserDataBoundary implements UserData { @IsBoolean() isAdmin!: boolean; } - -export const getRoleIndex = (role: Role): number => { - switch (role) { - case Role.Admin: - return 4; - case Role.Editor: - return 3; - case Role.MasterEditor: - return 2; - case Role.Viewer: - return 1; - } -}; diff --git a/apps/server-asset-sg/src/features/users/user.repo.ts b/apps/server-asset-sg/src/features/users/user.repo.ts index f1490dd5..ef7ce951 100644 --- a/apps/server-asset-sg/src/features/users/user.repo.ts +++ b/apps/server-asset-sg/src/features/users/user.repo.ts @@ -3,7 +3,7 @@ import { Prisma } from '@prisma/client'; import { PrismaService } from '@/core/prisma.service'; import { Repo, RepoListOptions } from '@/core/repo'; -import { Role, User, UserData, UserId } from '@/features/users/user.model'; +import { User, UserData, UserId } from '@/features/users/user.model'; import { satisfy } from '@/utils/define'; import { handlePrismaMutationError } from '@/utils/prisma'; @@ -48,7 +48,6 @@ export class UserRepo implements Repo workgroup.workgroupId) }, + workgroupId: { notIn: data.workgroups?.map((workgroup) => workgroup.id) }, }, createMany: { data: data.workgroups?.map((workgroup) => ({ - workgroupId: workgroup.workgroupId, + workgroupId: workgroup.id, role: workgroup.role, })), skipDuplicates: true, @@ -102,7 +100,6 @@ export class UserRepo implements Repo()({ id: true, - role: true, email: true, lang: true, isAdmin: true, @@ -110,11 +107,6 @@ export const userSelection = satisfy()({ select: { workgroupId: true, role: true, - workgroup: { - select: { - name: true, - }, - }, }, }, }); @@ -123,5 +115,8 @@ type SelectedUser = Prisma.AssetUserGetPayload<{ select: typeof userSelection }> const parse = (data: SelectedUser): User => ({ ...data, - role: data.role as Role, + workgroups: data.workgroups.map((it) => ({ + id: it.workgroupId, + role: it.role, + })), }); diff --git a/apps/server-asset-sg/src/features/users/users.controller.ts b/apps/server-asset-sg/src/features/users/users.controller.ts index a97827a2..792d8abe 100644 --- a/apps/server-asset-sg/src/features/users/users.controller.ts +++ b/apps/server-asset-sg/src/features/users/users.controller.ts @@ -11,9 +11,9 @@ import { ValidationPipe, } from '@nestjs/common'; +import { Authorize } from '@/core/decorators/authorize.decorator'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; -import { RequireRole } from '@/core/decorators/require-role.decorator'; -import { Role, User, UserDataBoundary, UserId } from '@/features/users/user.model'; +import { User, UserDataBoundary, UserId } from '@/features/users/user.model'; import { UserRepo } from '@/features/users/user.repo'; @Controller('/users') @@ -21,19 +21,18 @@ export class UsersController { constructor(private readonly userRepo: UserRepo) {} @Get('/current') - @RequireRole(Role.Viewer) - showCurrent(@CurrentUser() user: User): User { + showCurrent(@CurrentUser() user: User | null): User | null { return user; } @Get('/') - @RequireRole(Role.Admin) + @Authorize.Admin() list(): Promise { return this.userRepo.list(); } @Put('/:id') - @RequireRole(Role.Admin) + @Authorize.Admin() async update( @Param('id') id: UserId, @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) @@ -47,7 +46,7 @@ export class UsersController { } @Delete('/:id') - @RequireRole(Role.Admin) + @Authorize.Admin() @HttpCode(HttpStatus.NO_CONTENT) async delete(@Param('id') id: UserId): Promise { const isOk = await this.userRepo.delete(id); diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.controller.ts b/apps/server-asset-sg/src/features/workgroups/workgroup.controller.ts index 3d5f8fb4..30e6d9db 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.controller.ts +++ b/apps/server-asset-sg/src/features/workgroups/workgroup.controller.ts @@ -1,59 +1,51 @@ -import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpException, - HttpStatus, - Param, - ParseIntPipe, - Post, - Put, - ValidationPipe, -} from '@nestjs/common'; -import { RequireRole } from '@/core/decorators/require-role.decorator'; -import { Role } from '@/features/users/user.model'; -import { Workgroup, WorkgroupDataBoundary, WorkgroupId } from '@/features/workgroups/workgroup.model'; +import { Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Post, Put } from '@nestjs/common'; +import { Authorize } from '@/core/decorators/authorize.decorator'; +import { Authorized } from '@/core/decorators/authorized.decorator'; +import { Boundary } from '@/core/decorators/boundary.decorator'; +import { CurrentUser } from '@/core/decorators/current-user.decorator'; +import { UsePolicy } from '@/core/decorators/use-policy.decorator'; +import { UseRepo } from '@/core/decorators/use-repo.decorator'; +import { User } from '@/features/users/user.model'; +import { Workgroup, WorkgroupData, WorkgroupDataBoundary } from '@/features/workgroups/workgroup.model'; +import { WorkgroupPolicy } from '@/features/workgroups/workgroup.policy'; import { WorkgroupRepo } from '@/features/workgroups/workgroup.repo'; @Controller('/workgroups') +@UsePolicy(WorkgroupPolicy) +@UseRepo(WorkgroupRepo) export class WorkgroupController { constructor(private readonly workgroupRepo: WorkgroupRepo) {} - @Get('/:id') - async show(@Param('id', ParseIntPipe) id: WorkgroupId): Promise { - const workGroup = await this.workgroupRepo.find(id); - if (workGroup === null) { - throw new HttpException('not found', 404); - } - return workGroup; + @Get('/') + @Authorize.User() + async list(@CurrentUser() user: User): Promise { + return this.workgroupRepo.list({ ids: user.isAdmin ? undefined : user.workgroups.map((it) => it.id) }); } - @Get('/') - @RequireRole(Role.Viewer) - async list(): Promise { - return this.workgroupRepo.list(); + @Get('/:id') + @Authorize.Show({ id: Number }) + async show(@Authorized.Record() workgroup: Workgroup): Promise { + return workgroup; } @Post('/') - @RequireRole(Role.Admin) + @Authorize.Create() @HttpCode(HttpStatus.CREATED) async create( - @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) + @Boundary(WorkgroupDataBoundary) data: WorkgroupDataBoundary ): Promise { return this.workgroupRepo.create(data); } @Put('/:id') - @RequireRole(Role.Admin) + @Authorize.Update({ id: Number }) async update( - @Param('id', ParseIntPipe) id: WorkgroupId, - @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) - data: WorkgroupDataBoundary + @Authorized.Record() record: Workgroup, + @Boundary(WorkgroupDataBoundary) + data: WorkgroupData ): Promise { - const workgroup = await this.workgroupRepo.update(id, data); + const workgroup = await this.workgroupRepo.update(record.id, data); if (workgroup === null) { throw new HttpException('not found', 404); } @@ -61,12 +53,9 @@ export class WorkgroupController { } @Delete('/:id') - @RequireRole(Role.Admin) + @Authorize.Delete({ id: Number }) @HttpCode(HttpStatus.NO_CONTENT) - async delete(@Param('id', ParseIntPipe) id: WorkgroupId): Promise { - const isOk = await this.workgroupRepo.delete(id); - if (!isOk) { - throw new HttpException('not found', 404); - } + async delete(@Authorized.Record() record: Workgroup): Promise { + await this.workgroupRepo.delete(record.id); } } diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.model.ts b/apps/server-asset-sg/src/features/workgroups/workgroup.model.ts index fbfa98c1..abac57c6 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.model.ts +++ b/apps/server-asset-sg/src/features/workgroups/workgroup.model.ts @@ -20,10 +20,6 @@ export interface UserOnWorkgroup { userId: UserId; role: Role; } - -export type Role = PrismaRole; -export const Role = PrismaRole; - export class WorkgroupDataBoundary implements WorkgroupData { @IsString() name!: string; @@ -39,3 +35,17 @@ export class WorkgroupDataBoundary implements WorkgroupData { @Type(() => Date) disabled_at!: Date | null; } + +export type Role = PrismaRole; +export const Role = PrismaRole; + +export const getRoleIndex = (role: Role): number => { + switch (role) { + case 'Viewer': + return 0; + case 'Editor': + return 1; + case 'MasterEditor': + return 2; + } +}; diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.policy.ts b/apps/server-asset-sg/src/features/workgroups/workgroup.policy.ts new file mode 100644 index 00000000..c793c15d --- /dev/null +++ b/apps/server-asset-sg/src/features/workgroups/workgroup.policy.ts @@ -0,0 +1,14 @@ +import { Policy } from '@/core/policy'; +import { Workgroup } from '@/features/workgroups/workgroup.model'; + +export class WorkgroupPolicy extends Policy { + canShow(value: Workgroup): boolean { + // A user can see every workgroup assigned to them. + return this.hasWorkgroup(value.id); + } + + canCreate(): boolean { + // Only admins can create workgroups. + return false; + } +} diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts index bcea7dc3..dfc844a5 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts +++ b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts @@ -2,8 +2,8 @@ import { faker } from '@faker-js/faker'; // eslint-disable-next-line @nx/enforce-module-boundaries import { clearPrismaAssets, setupDB, setupDefaultWorkgroup } from '../../../../../test/setup-db'; import { PrismaService } from '@/core/prisma.service'; -import { fakeAssetPatch, fakeUser } from '@/features/asset-old/asset-edit.fake'; -import { AssetEditRepo } from '@/features/asset-old/asset-edit.repo'; +import { fakeAssetPatch, fakeUser } from '@/features/asset-edit/asset-edit.fake'; +import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; import { Role as UserRole } from '@/features/users/user.model'; import { UserRepo } from '@/features/users/user.repo'; import { WorkgroupData, Role } from '@/features/workgroups/workgroup.model'; diff --git a/apps/server-asset-sg/src/models/jwt-request.ts b/apps/server-asset-sg/src/models/jwt-request.ts index 775f52f0..143ffa53 100644 --- a/apps/server-asset-sg/src/models/jwt-request.ts +++ b/apps/server-asset-sg/src/models/jwt-request.ts @@ -1,6 +1,7 @@ import { Request } from 'express'; import * as jwt from 'jsonwebtoken'; +import { Policy } from '@/core/policy'; import { User } from '@/features/users/user.model'; export interface JwtRequest extends Request { @@ -8,3 +9,10 @@ export interface JwtRequest extends Request { accessToken: string; jwtPayload: jwt.JwtPayload; } + +export interface AuthorizedRequest extends Request { + authorized: { + record: object | null; + policy: Policy; + }; +} diff --git a/apps/server-asset-sg/tsconfig.app.json b/apps/server-asset-sg/tsconfig.app.json index 598c9bb2..9e25d0d3 100644 --- a/apps/server-asset-sg/tsconfig.app.json +++ b/apps/server-asset-sg/tsconfig.app.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", - "types": ["node"], + "types": ["node", "multer"], "emitDecoratorMetadata": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, diff --git a/apps/server-asset-sg/tsconfig.spec.json b/apps/server-asset-sg/tsconfig.spec.json index d5f7d408..1ef29a9f 100644 --- a/apps/server-asset-sg/tsconfig.spec.json +++ b/apps/server-asset-sg/tsconfig.spec.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", - "types": ["jest", "node"], + "types": ["jest", "node", "multer"], "resolveJsonModule": true, "esModuleInterop": true }, diff --git a/libs/admin/src/lib/services/admin.service.ts b/libs/admin/src/lib/services/admin.service.ts index 23709154..58e067e6 100644 --- a/libs/admin/src/lib/services/admin.service.ts +++ b/libs/admin/src/lib/services/admin.service.ts @@ -42,7 +42,7 @@ export class AdminService { private _httpClient = inject(HttpClient); public getUsers(): ORD.ObservableRemoteData { - return this._httpClient.get('/api/admin/user').pipe( + return this._httpClient.get('/api/users').pipe( map(flow(Users.decode, E.mapLeft(decodeError))), // TODO need to test instance of HttpErrorResponse here OE.catchErrorW(httpErrorResponseError), @@ -52,7 +52,7 @@ export class AdminService { } public getUsersNew(): Observable { - return this._httpClient.get('/api/admin/user').pipe(map((res) => res as User[])); + return this._httpClient.get('/api/users').pipe(map((res) => res as User[])); } public getWorkgroups(): Observable { @@ -80,7 +80,7 @@ export class AdminService { public deleteUser(id: string): ORD.ObservableRemoteData { console.log('888', id); - return this._httpClient.delete(`/api/admin/user/${id}`).pipe( + return this._httpClient.delete(`/api/users/${id}`).pipe( tap((a) => console.log('deleteUser', a)), map(() => E.right(undefined)), // TODO need to test instance of HttpErrorResponse here diff --git a/libs/asset-editor/src/lib/services/asset-editor.service.ts b/libs/asset-editor/src/lib/services/asset-editor.service.ts index b39ae7f6..66d7150e 100644 --- a/libs/asset-editor/src/lib/services/asset-editor.service.ts +++ b/libs/asset-editor/src/lib/services/asset-editor.service.ts @@ -27,7 +27,7 @@ export class AssetEditorService { public createAsset(patchAsset: PatchAsset): ORD.ObservableRemoteData { return this._httpClient - .put(`/api/asset-edit`, PatchAsset.encode(patchAsset)) + .post(`/api/asset-edit`, PatchAsset.encode(patchAsset)) .pipe( map(flow(AssetEditDetail.decode, E.mapLeft(decodeError))), OE.catchErrorW(httpErrorResponseError), @@ -41,7 +41,7 @@ export class AssetEditorService { patchAsset: PatchAsset ): ORD.ObservableRemoteData { return this._httpClient - .patch(`/api/asset-edit/${assetId}`, PatchAsset.encode(patchAsset)) + .put(`/api/asset-edit/${assetId}`, PatchAsset.encode(patchAsset)) .pipe( map(flow(AssetEditDetail.decode, E.mapLeft(decodeError))), OE.catchErrorW(httpErrorResponseError), @@ -55,7 +55,7 @@ export class AssetEditorService { ? forkJoin( fileIds.map((fileId) => { return this._httpClient - .delete(`/api/asset-edit/${assetId}/file/${fileId}`) + .delete(`/api/files/${fileId}`) .pipe(map(E.right), OE.catchErrorW(httpErrorResponseError), map(RD.fromEither), startWith(RD.pending)); }) ).pipe( @@ -76,8 +76,9 @@ export class AssetEditorService { ...files.map((file) => { const formData = new FormData(); formData.append('file', file); + formData.append('assetId', `${assetId}`); return this._httpClient - .post(`/api/asset-edit/${assetId}/file`, formData) + .post(`/api/files`, formData) .pipe(map(E.right), OE.catchErrorW(httpErrorResponseError)); }) ).pipe( @@ -96,7 +97,7 @@ export class AssetEditorService { public updateContact(contactId: number, patchContact: PatchContact): ORD.ObservableRemoteData { return this._httpClient - .patch(`/api/contact-edit/${contactId}`, PatchContact.encode(patchContact)) + .put(`/api/contacts/${contactId}`, PatchContact.encode(patchContact)) .pipe( map(flow(Contact.decode, E.mapLeft(decodeError))), OE.catchErrorW(httpErrorResponseError), @@ -107,7 +108,7 @@ export class AssetEditorService { public createContact(patchContact: PatchContact): ORD.ObservableRemoteData { return this._httpClient - .put(`/api/contact-edit`, PatchContact.encode(patchContact)) + .post(`/api/contacts`, PatchContact.encode(patchContact)) .pipe( map(flow(Contact.decode, E.mapLeft(decodeError))), OE.catchErrorW(httpErrorResponseError), diff --git a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts index bc8756a9..ed5731dc 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts +++ b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts @@ -38,7 +38,7 @@ export class AssetSearchDetailComponent { downloadFile(file: Omit, isDownload = true): void { this.activeFileDownloads.set(file.fileId, { isDownload }); - this.httpClient.get(`/api/file/${file.fileId}`, { responseType: 'blob' }).subscribe({ + this.httpClient.get(`/api/files/${file.fileId}`, { responseType: 'blob' }).subscribe({ next: (blob) => { const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); diff --git a/libs/auth/src/lib/services/auth.service.ts b/libs/auth/src/lib/services/auth.service.ts index 07aeeece..cea9aa03 100644 --- a/libs/auth/src/lib/services/auth.service.ts +++ b/libs/auth/src/lib/services/auth.service.ts @@ -78,7 +78,9 @@ export class AuthService { } private _getUserProfile() { - return this._httpClient.get('/api/user').pipe(map(decode(User)), OE.catchErrorW(httpErrorResponseOrUnknownError)); + return this._httpClient + .get('/api/users/current') + .pipe(map(decode(User)), OE.catchErrorW(httpErrorResponseOrUnknownError)); } buildAuthUrl = (path: string) => urlJoin(`/auth`, path); diff --git a/libs/favourite/src/lib/services/favourite.service.spec.ts b/libs/favourite/src/lib/services/favourite.service.spec.ts index dd4126c2..99906ded 100644 --- a/libs/favourite/src/lib/services/favourite.service.spec.ts +++ b/libs/favourite/src/lib/services/favourite.service.spec.ts @@ -33,7 +33,7 @@ describe('FavouriteService', () => { expect(favourites).toEqual(dummyFavourites); }); - const request = httpMock.expectOne(`/api/user/favourite`); + const request = httpMock.expectOne(`/api/users/current/favorites`); expect(request.request.method).toBe('GET'); request.flush(dummyFavourites); }); diff --git a/libs/favourite/src/lib/services/favourite.service.ts b/libs/favourite/src/lib/services/favourite.service.ts index bb364532..2d243874 100644 --- a/libs/favourite/src/lib/services/favourite.service.ts +++ b/libs/favourite/src/lib/services/favourite.service.ts @@ -10,6 +10,6 @@ export class FavouriteService { constructor(private http: HttpClient) {} getFavourites(): Observable { - return this.http.get(`/api/user/favourite`); + return this.http.get(`/api/favourites`); } } diff --git a/libs/shared/src/lib/models/elastic-search-asset.ts b/libs/shared/src/lib/models/elastic-search-asset.ts index 639ff0d9..be654d1e 100644 --- a/libs/shared/src/lib/models/elastic-search-asset.ts +++ b/libs/shared/src/lib/models/elastic-search-asset.ts @@ -17,6 +17,7 @@ export interface ElasticSearchAsset { manCatLabelItemCodes: string[]; geometryCodes: GeometryCode[] | ['None']; studyLocations: ElasticPoint[]; + workgroupId: number; data: SerializedAssetEditDetail; } diff --git a/package-lock.json b/package-lock.json index 95c90370..a7d6d6b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,7 @@ "solid-js": "^1.6.9", "tsafe": "^1.7.1", "tslib": "^2.3.0", - "type-fest": "^3.5.3", + "type-fest": "^4.21.0", "url-join": "^5.0.0", "vite-plugin-solid": "^2.5.0", "zone.js": "0.14.6" @@ -103,7 +103,7 @@ "@types/jest": "^29.4.4", "@types/jsonwebtoken": "^9.0.1", "@types/jwk-to-pem": "^2.0.3", - "@types/multer": "^1.4.7", + "@types/multer": "^1.4.11", "@types/node": "^18.16.9", "@types/proj4": "^2.5.5", "@types/validator": "^13.7.10", @@ -10835,6 +10835,7 @@ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", "dev": true, + "license": "MIT", "dependencies": { "@types/express": "*" } @@ -28591,11 +28592,12 @@ } }, "node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.21.0.tgz", + "integrity": "sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==", + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=14.16" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index 88af284a..3a28c6cb 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "solid-js": "^1.6.9", "tsafe": "^1.7.1", "tslib": "^2.3.0", - "type-fest": "^3.5.3", + "type-fest": "^4.21.0", "url-join": "^5.0.0", "vite-plugin-solid": "^2.5.0", "zone.js": "0.14.6" @@ -114,7 +114,7 @@ "@types/jest": "^29.4.4", "@types/jsonwebtoken": "^9.0.1", "@types/jwk-to-pem": "^2.0.3", - "@types/multer": "^1.4.7", + "@types/multer": "^1.4.11", "@types/node": "^18.16.9", "@types/proj4": "^2.5.5", "@types/validator": "^13.7.10", From d5e0be4f28517fc950a8a17fb40895edc09fc0ac Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Wed, 10 Jul 2024 18:53:49 +0200 Subject: [PATCH 15/21] Add policy-based client authorization Move models and their policies into `@shared` module Adapt new user to old frontend model Implement policy-based authorization in client code Fix user not loading after login Fix master-editors not being allowed to change the status of their assets Fix reloads on the asset-admin page causing a whitepage Remove deprecation annotation from `POST /asset-edit` route handler Respond to illegal request bodies with 403 instead of 404 Fix admins not having access to `asset-admin` page Rename `adminOnly.directive.ts` file to `admin-only.directive.ts` Remove unused `AssetRepo.list` method. Fix authorization directives for push detection Add global 403 page for forbidden resources Replace authorization decorators with `authorize` function Differentiate forbidden access types Move `lib/shared2` to `lib/shared/v2` as `@asset-sg/shared/v2` Add simplified workgroup listing to workgroup api # Conflicts: # apps/server-asset-sg/src/features/workgroups/workgroup.controller.ts # Conflicts: # apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts # apps/server-asset-sg/src/features/workgroups/workgroups.controller.ts --- apps/client-asset-sg/project.json | 2 +- apps/client-asset-sg/src/app/app-guards.ts | 25 +- .../src/app/app.component.html | 39 +- .../src/app/app.component.scss | 21 ++ apps/client-asset-sg/src/app/app.module.ts | 15 +- .../menu-bar/menu-bar.component.html | 14 +- .../components/menu-bar/menu-bar.component.ts | 11 +- .../splash-screen.component.html | 2 +- apps/client-asset-sg/src/app/i18n/de.ts | 1 + apps/client-asset-sg/src/app/i18n/en.ts | 1 + apps/client-asset-sg/src/app/i18n/fr.ts | 1 + apps/client-asset-sg/src/app/i18n/it.ts | 1 + apps/client-asset-sg/src/app/i18n/rm.ts | 1 + .../src/app/state/app.effects.ts | 23 +- .../src/environments/environment.ts | 2 +- .../migration.sql | 80 +++++ apps/server-asset-sg/src/app.controller.ts | 34 +- apps/server-asset-sg/src/app.module.ts | 10 +- apps/server-asset-sg/src/core/authorize.ts | 34 ++ .../core/decorators/authorize.decorator.ts | 110 +----- .../core/decorators/authorized.decorator.ts | 2 +- .../core/decorators/current-user.decorator.ts | 2 +- ...undary.decorator.ts => parse.decorator.ts} | 18 +- .../core/decorators/use-policy.decorator.ts | 2 +- .../src/core/decorators/use-repo.decorator.ts | 4 +- .../guards/authorization-guard.service.ts | 36 ++ .../src/core/guards/policy.guard.ts | 114 ------ .../src/core/middleware/jwt.middleware.ts | 4 +- apps/server-asset-sg/src/core/repo.ts | 14 +- .../asset-edit/asset-edit.controller.ts | 80 +++-- .../features/asset-edit/asset-edit.fake.ts | 7 +- .../features/asset-edit/asset-edit.repo.ts | 2 +- .../features/asset-edit/asset-edit.service.ts | 2 +- .../src/features/assets/asset-info.repo.ts | 2 +- .../src/features/assets/asset.model.ts | 336 ------------------ .../src/features/assets/asset.repo.ts | 36 +- .../src/features/assets/assets.controller.ts | 87 +++-- .../src/features/assets/prisma-asset.ts | 10 +- .../assets/search/asset-search.controller.ts | 2 +- .../search/asset-search.service.spec.ts | 100 +++--- .../assets/search/asset-search.service.ts | 6 +- .../src/features/contacts/contact.repo.ts | 2 +- .../features/contacts/contacts.controller.ts | 33 +- .../src/features/favorites/favorite.repo.ts | 4 +- .../favorites/favorites.controller.ts | 2 +- .../src/features/files/files.controller.ts | 59 +-- .../features/studies/studies.controller.ts | 4 +- .../src/features/studies/study.repo.ts | 6 +- .../src/features/users/user.model.ts | 29 -- .../src/features/users/user.repo.ts | 2 +- .../src/features/users/users.controller.ts | 22 +- .../workgroups/workgroup-simple.repo.ts | 54 +++ .../workgroups/workgroup.controller.ts | 61 ---- .../workgroups/workgroup.repo.spec.ts | 10 +- .../src/features/workgroups/workgroup.repo.ts | 8 +- .../workgroups/workgroups.controller.ts | 103 ++++++ .../{workgroup.http => workgroups.http} | 5 + apps/server-asset-sg/src/main.ts | 3 +- .../server-asset-sg/src/models/jwt-request.ts | 5 +- apps/server-asset-sg/tsconfig.json | 1 + jest.config.ts | 6 +- .../user-expanded.component.html | 7 - .../user-expanded/user-expanded.component.ts | 7 +- .../workgroup-edit-component.ts | 1 - .../workgroups/workgroups.component.ts | 1 - .../src/lib/asset-editor.module.ts | 55 ++- .../asset-editor-launch.component.html | 10 +- .../asset-editor-launch.component.ts | 51 +-- .../asset-editor-sync.component.html | 8 + .../asset-editor-sync.component.scss | 31 ++ .../asset-editor-sync.component.ts | 52 +++ ...t-editor-tab-administration.component.html | 2 +- ...set-editor-tab-administration.component.ts | 23 +- .../asset-editor-tab-page.component.html | 2 +- .../asset-editor-tab-usage.component.html | 22 +- .../asset-editor-tab-usage.component.ts | 33 +- .../src/lib/asset-viewer.module.ts | 6 +- .../asset-search-detail.component.html | 334 +++++++++-------- .../asset-search-detail.component.ts | 3 + .../asset-search-results.component.ts | 3 + libs/auth/src/lib/auth.module.ts | 1 - .../auth/src/lib/services/auth.interceptor.ts | 75 +++- libs/auth/src/lib/services/auth.service.ts | 39 +- libs/client-shared/src/index.ts | 6 + .../components/auth-pipes/base-auth-pipe.ts | 25 -- .../src/lib/components/auth-pipes/index.ts | 2 - .../components/auth-pipes/is-editor.pipe.ts | 16 - .../auth-pipes/is-not-master-editor.pipe.ts | 16 - .../client-shared/src/lib/components/index.ts | 1 - .../lib/directives/admin-only.directive.ts | 39 ++ .../lib/directives/base-policy.directive.ts | 67 ++++ .../lib/directives/can-create.directive.ts | 17 + .../lib/directives/can-delete.directive.ts | 20 ++ .../src/lib/directives/can-show.directive.ts | 20 ++ .../lib/directives/can-update.directive.ts | 20 ++ .../lib/state/app-shared-state.selectors.ts | 31 +- libs/client-shared/src/lib/utils/map.ts | 2 +- .../src/lib/services/favourite.service.ts | 2 +- libs/shared/src/lib/models/asset-edit.ts | 2 + libs/shared/src/lib/models/user.ts | 44 +-- libs/shared/tsconfig.spec.json | 3 +- libs/shared/v2/.babelrc | 10 + libs/shared/v2/README.md | 11 + libs/shared/v2/eslint.config.js | 3 + libs/shared/v2/jest.config.ts | 16 + libs/shared/v2/package.json | 4 + libs/shared/v2/project.json | 33 ++ libs/shared/v2/src/index.ts | 21 ++ libs/shared/v2/src/lib/models/asset.ts | 145 ++++++++ .../v2/src/lib/models/base}/local-date.ts | 0 .../shared/v2/src/lib/models/base}/model.ts | 0 libs/shared/v2/src/lib/models/contact.ts | 16 + .../shared/v2/src/lib/models/favorite.ts | 2 +- .../shared/v2/src/lib/models/study.spec.ts | 2 +- .../shared/v2/src/lib/models/study.ts | 2 +- libs/shared/v2/src/lib/models/user.ts | 40 +++ .../shared/v2/src/lib/models/workgroup.ts | 21 +- .../v2/src/lib/policies}/asset-edit.policy.ts | 17 +- .../v2/src/lib/policies}/asset.policy.ts | 12 +- .../v2/src/lib/policies/base}/policy.ts | 6 +- .../v2/src/lib/policies}/contact.policy.ts | 6 +- .../v2/src/lib/policies}/workgroup.policy.ts | 4 +- .../shared/v2/src/lib/schemas/asset.schema.ts | 205 +++++++++++ .../v2/src/lib/schemas/contact.schema.ts | 21 +- libs/shared/v2/src/lib/schemas/user.schema.ts | 13 + .../class-validator}/is-nullable.decorator.ts | 0 libs/shared/v2/tsconfig.json | 20 ++ libs/shared/v2/tsconfig.lib.json | 11 + libs/shared/v2/tsconfig.spec.json | 20 ++ tsconfig.base.json | 1 + tsconfig.spec.json | 11 + 131 files changed, 2014 insertions(+), 1470 deletions(-) create mode 100644 apps/server-asset-sg/src/core/authorize.ts rename apps/server-asset-sg/src/core/decorators/{boundary.decorator.ts => parse.decorator.ts} (59%) create mode 100644 apps/server-asset-sg/src/core/guards/authorization-guard.service.ts delete mode 100644 apps/server-asset-sg/src/core/guards/policy.guard.ts delete mode 100644 apps/server-asset-sg/src/features/assets/asset.model.ts delete mode 100644 apps/server-asset-sg/src/features/users/user.model.ts create mode 100644 apps/server-asset-sg/src/features/workgroups/workgroup-simple.repo.ts delete mode 100644 apps/server-asset-sg/src/features/workgroups/workgroup.controller.ts create mode 100644 apps/server-asset-sg/src/features/workgroups/workgroups.controller.ts rename apps/server-asset-sg/src/features/workgroups/{workgroup.http => workgroups.http} (91%) create mode 100644 libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.html create mode 100644 libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.scss create mode 100644 libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.ts delete mode 100644 libs/client-shared/src/lib/components/auth-pipes/base-auth-pipe.ts delete mode 100644 libs/client-shared/src/lib/components/auth-pipes/index.ts delete mode 100644 libs/client-shared/src/lib/components/auth-pipes/is-editor.pipe.ts delete mode 100644 libs/client-shared/src/lib/components/auth-pipes/is-not-master-editor.pipe.ts create mode 100644 libs/client-shared/src/lib/directives/admin-only.directive.ts create mode 100644 libs/client-shared/src/lib/directives/base-policy.directive.ts create mode 100644 libs/client-shared/src/lib/directives/can-create.directive.ts create mode 100644 libs/client-shared/src/lib/directives/can-delete.directive.ts create mode 100644 libs/client-shared/src/lib/directives/can-show.directive.ts create mode 100644 libs/client-shared/src/lib/directives/can-update.directive.ts create mode 100644 libs/shared/v2/.babelrc create mode 100644 libs/shared/v2/README.md create mode 100644 libs/shared/v2/eslint.config.js create mode 100644 libs/shared/v2/jest.config.ts create mode 100644 libs/shared/v2/package.json create mode 100644 libs/shared/v2/project.json create mode 100644 libs/shared/v2/src/index.ts create mode 100644 libs/shared/v2/src/lib/models/asset.ts rename {apps/server-asset-sg/src/utils/data => libs/shared/v2/src/lib/models/base}/local-date.ts (100%) rename {apps/server-asset-sg/src/utils/data => libs/shared/v2/src/lib/models/base}/model.ts (100%) create mode 100644 libs/shared/v2/src/lib/models/contact.ts rename apps/server-asset-sg/src/features/favorites/favorite.model.ts => libs/shared/v2/src/lib/models/favorite.ts (55%) rename apps/server-asset-sg/src/features/studies/study.model.spec.ts => libs/shared/v2/src/lib/models/study.spec.ts (93%) rename apps/server-asset-sg/src/features/studies/study.model.ts => libs/shared/v2/src/lib/models/study.ts (88%) create mode 100644 libs/shared/v2/src/lib/models/user.ts rename apps/server-asset-sg/src/features/workgroups/workgroup.model.ts => libs/shared/v2/src/lib/models/workgroup.ts (65%) rename {apps/server-asset-sg/src/features/asset-edit => libs/shared/v2/src/lib/policies}/asset-edit.policy.ts (58%) rename {apps/server-asset-sg/src/features/assets => libs/shared/v2/src/lib/policies}/asset.policy.ts (70%) rename {apps/server-asset-sg/src/core => libs/shared/v2/src/lib/policies/base}/policy.ts (88%) rename {apps/server-asset-sg/src/features/contacts => libs/shared/v2/src/lib/policies}/contact.policy.ts (63%) rename {apps/server-asset-sg/src/features/workgroups => libs/shared/v2/src/lib/policies}/workgroup.policy.ts (72%) create mode 100644 libs/shared/v2/src/lib/schemas/asset.schema.ts rename apps/server-asset-sg/src/features/contacts/contact.model.ts => libs/shared/v2/src/lib/schemas/contact.schema.ts (54%) create mode 100644 libs/shared/v2/src/lib/schemas/user.schema.ts rename {apps/server-asset-sg/src/core/decorators => libs/shared/v2/src/lib/utils/class-validator}/is-nullable.decorator.ts (100%) create mode 100644 libs/shared/v2/tsconfig.json create mode 100644 libs/shared/v2/tsconfig.lib.json create mode 100644 libs/shared/v2/tsconfig.spec.json create mode 100644 tsconfig.spec.json diff --git a/apps/client-asset-sg/project.json b/apps/client-asset-sg/project.json index 609265e0..2d260188 100644 --- a/apps/client-asset-sg/project.json +++ b/apps/client-asset-sg/project.json @@ -19,7 +19,7 @@ "assets": ["apps/client-asset-sg/src/favicon.ico", "apps/client-asset-sg/src/assets"], "styles": ["apps/client-asset-sg/src/styles.scss"], "scripts": [], - "allowedCommonJsDependencies": ["tsafe", "validator", "xml-utils", "pbf", "rbush", "earcut"] + "allowedCommonJsDependencies": ["tsafe", "validator", "xml-utils", "pbf", "rbush", "earcut", "@prisma/client"] }, "configurations": { "production": { diff --git a/apps/client-asset-sg/src/app/app-guards.ts b/apps/client-asset-sg/src/app/app-guards.ts index b256ac11..bdfb4628 100644 --- a/apps/client-asset-sg/src/app/app-guards.ts +++ b/apps/client-asset-sg/src/app/app-guards.ts @@ -1,29 +1,16 @@ import { inject } from '@angular/core'; import { CanActivateFn } from '@angular/router'; import { fromAppShared } from '@asset-sg/client-shared'; -import { ORD } from '@asset-sg/core'; -import { User, isAdmin, isEditor } from '@asset-sg/shared'; +import { isNotNull } from '@asset-sg/core'; +import { User } from '@asset-sg/shared/v2'; import { Store } from '@ngrx/store'; -import * as E from 'fp-ts/Either'; -import { pipe } from 'fp-ts/function'; -import { map } from 'rxjs'; +import { filter, map } from 'rxjs'; import { AppState } from './state/app-state'; -export const roleGuard = (rolePredicate: (u: User) => boolean) => { +export const roleGuard = (testUser: (u: User) => boolean) => { const store = inject(Store); - return store.select(fromAppShared.selectRDUserProfile).pipe( - ORD.filterIsCompleteEither, - map((user) => - E.isRight( - pipe( - user, - E.filterOrElseW(rolePredicate, () => undefined) - ) - ) - ) - ); + return store.select(fromAppShared.selectUser).pipe(filter(isNotNull), map(testUser)); }; -export const adminGuard: CanActivateFn = () => roleGuard(isAdmin); -export const editorGuard: CanActivateFn = () => roleGuard(isEditor); +export const adminGuard: CanActivateFn = () => roleGuard((user) => user.isAdmin); diff --git a/apps/client-asset-sg/src/app/app.component.html b/apps/client-asset-sg/src/app/app.component.html index 845b793f..05007fb3 100644 --- a/apps/client-asset-sg/src/app/app.component.html +++ b/apps/client-asset-sg/src/app/app.component.html @@ -1,4 +1,4 @@ - +
{{ error }} @@ -6,25 +6,32 @@
- - + + + @if (authState === AuthState.Success || authState === AuthState.ForbiddenResource) { + + + + +
+ @if (authState === AuthState.Success) { + + } @else { +
+

resourceForbidden

+

403 - Forbidden

+
+ } +
+
+ +
+ } @else { + }
- - - - - -
- -
-
- -
-
-
    diff --git a/apps/client-asset-sg/src/app/app.component.scss b/apps/client-asset-sg/src/app/app.component.scss index fa938a57..96f5edf3 100644 --- a/apps/client-asset-sg/src/app/app.component.scss +++ b/apps/client-asset-sg/src/app/app.component.scss @@ -71,3 +71,24 @@ asset-sg-menu-bar { /// angular-cdk-overlay is at 1000, so we go to 1500. z-index: 1500; } + +.forbidden { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + + h1 { + font-size: 4rem; + margin: 0; + color: mat.get-color-from-palette($asset-sg-warn, 300); + } + + p { + font-size: 1.5rem; + margin: 0; + padding: 1rem 0 0 0; + color: mat.get-color-from-palette($asset-sg-warn, 200); + } +} diff --git a/apps/client-asset-sg/src/app/app.module.ts b/apps/client-asset-sg/src/app/app.module.ts index a2827039..0deebf38 100644 --- a/apps/client-asset-sg/src/app/app.module.ts +++ b/apps/client-asset-sg/src/app/app.module.ts @@ -3,7 +3,7 @@ import { DialogModule } from '@angular/cdk/dialog'; import { CommonModule, NgOptimizedImage, registerLocaleData } from '@angular/common'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import locale_deCH from '@angular/common/locales/de-CH'; -import { NgModule, inject } from '@angular/core'; +import { inject, NgModule } from '@angular/core'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { BrowserModule } from '@angular/platform-browser'; @@ -11,18 +11,20 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule } from '@angular/router'; import { AuthInterceptor, AuthModule, ErrorService } from '@asset-sg/auth'; import { + AdminOnlyDirective, AlertModule, AnchorComponent, ButtonComponent, + CanCreateDirective, CURRENT_LANG, - TranslateTsLoader, currentLangFactory, icons, + TranslateTsLoader, } from '@asset-sg/client-shared'; import { storeLogger } from '@asset-sg/core'; -import { SvgIconComponent, provideSvgIcons } from '@ngneat/svg-icon'; +import { provideSvgIcons, SvgIconComponent } from '@ngneat/svg-icon'; import { EffectsModule } from '@ngrx/effects'; -import { FullRouterStateSerializer, StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store'; +import { FullRouterStateSerializer, routerReducer, StoreRouterConnectingModule } from '@ngrx/router-store'; import { StoreModule } from '@ngrx/store'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { ForModule } from '@rx-angular/template/for'; @@ -31,7 +33,7 @@ import { PushModule } from '@rx-angular/template/push'; import { environment } from '../environments/environment'; -import { adminGuard, editorGuard } from './app-guards'; +import { adminGuard } from './app-guards'; import { assetsPageMatcher } from './app-matchers'; import { AppComponent } from './app.component'; import { AppBarComponent, MenuBarComponent, NotFoundComponent, RedirectToLangComponent } from './components'; @@ -73,7 +75,6 @@ registerLocaleData(locale_deCH, 'de-CH'); { path: ':lang/asset-admin', loadChildren: () => import('@asset-sg/asset-editor').then((m) => m.AssetEditorModule), - canActivate: [editorGuard], }, { matcher: assetsPageMatcher, @@ -116,6 +117,8 @@ registerLocaleData(locale_deCH, 'de-CH'); AlertModule, NgOptimizedImage, MatProgressSpinnerModule, + AdminOnlyDirective, + CanCreateDirective, ], providers: [ provideSvgIcons(icons), diff --git a/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html b/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html index 8fef2e51..53befdd1 100644 --- a/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html +++ b/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html @@ -24,7 +24,7 @@ menuBar.favourites - + - +

    {{ "accessForbidden" | translate }} diff --git a/apps/client-asset-sg/src/app/i18n/de.ts b/apps/client-asset-sg/src/app/i18n/de.ts index 8fd0d071..c9edb25f 100644 --- a/apps/client-asset-sg/src/app/i18n/de.ts +++ b/apps/client-asset-sg/src/app/i18n/de.ts @@ -2,6 +2,7 @@ export const deAppTranslations = { logoSwissGeol: 'Logo Swissgeol Assets', welcomeTo: 'Willkommen bei', accessForbidden: 'Sie haben keinen Zugriff auf diese Applikation.', + resourceForbidden: 'Sie haben keinen Zugriff auf diese Ressource.', ok: 'OK', submit: 'Absenden', cancel: 'Abbrechen', diff --git a/apps/client-asset-sg/src/app/i18n/en.ts b/apps/client-asset-sg/src/app/i18n/en.ts index f5731511..0ec994f9 100644 --- a/apps/client-asset-sg/src/app/i18n/en.ts +++ b/apps/client-asset-sg/src/app/i18n/en.ts @@ -4,6 +4,7 @@ export const enAppTranslations: AppTranslations = { logoSwissGeol: 'Logo Swissgeol Assets', welcomeTo: 'Welcome to', accessForbidden: 'You do not have access to this application.', + resourceForbidden: 'You do not have access to this resource.', ok: 'OK', submit: 'Submit', cancel: 'Cancel', diff --git a/apps/client-asset-sg/src/app/i18n/fr.ts b/apps/client-asset-sg/src/app/i18n/fr.ts index 40ceed6d..51deb345 100644 --- a/apps/client-asset-sg/src/app/i18n/fr.ts +++ b/apps/client-asset-sg/src/app/i18n/fr.ts @@ -4,6 +4,7 @@ export const frAppTranslations: AppTranslations = { logoSwissGeol: 'Logo Swissgeol Assets', welcomeTo: 'Bienvenue sur', accessForbidden: "Vous n'avez pas accès à cette application.", + resourceForbidden: "Vous n'avez pas accès à cette ressource.", ok: 'OK', submit: 'Envoyer', cancel: 'Annuler', diff --git a/apps/client-asset-sg/src/app/i18n/it.ts b/apps/client-asset-sg/src/app/i18n/it.ts index 9f0105da..41b01736 100644 --- a/apps/client-asset-sg/src/app/i18n/it.ts +++ b/apps/client-asset-sg/src/app/i18n/it.ts @@ -4,6 +4,7 @@ export const itAppTranslations: AppTranslations = { logoSwissGeol: 'Logo Swissgeol Assets', welcomeTo: 'Benvenuti su', accessForbidden: 'Non avete accesso a questa applicazione.', + resourceForbidden: 'IT Sie haben keinen Zugriff auf diese Ressource.', ok: 'OK', submit: 'IT Absenden', cancel: 'IT Abbrechen', diff --git a/apps/client-asset-sg/src/app/i18n/rm.ts b/apps/client-asset-sg/src/app/i18n/rm.ts index 4f81129d..c6cf1b40 100644 --- a/apps/client-asset-sg/src/app/i18n/rm.ts +++ b/apps/client-asset-sg/src/app/i18n/rm.ts @@ -4,6 +4,7 @@ export const rmAppTranslations: AppTranslations = { logoSwissGeol: 'Logo Swissgeol Assets', welcomeTo: 'RM Willkommen bei', accessForbidden: 'RM Sie haben keinen Zugriff auf diese Applikation.', + resourceForbidden: 'RM Sie haben keinen Zugriff auf diese Ressource.', ok: 'OK', submit: 'RM Absenden', cancel: 'RM Abbrechen', diff --git a/apps/client-asset-sg/src/app/state/app.effects.ts b/apps/client-asset-sg/src/app/state/app.effects.ts index 34045979..d101b1ce 100644 --- a/apps/client-asset-sg/src/app/state/app.effects.ts +++ b/apps/client-asset-sg/src/app/state/app.effects.ts @@ -1,16 +1,16 @@ -import { Injectable, inject } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { NavigationEnd, Router, RouterStateSnapshot } from '@angular/router'; import { AuthService } from '@asset-sg/auth'; import { appSharedStateActions, fromAppShared } from '@asset-sg/client-shared'; import { ORD } from '@asset-sg/core'; -import { Lang, eqLangRight } from '@asset-sg/shared'; +import { eqLangRight, Lang } from '@asset-sg/shared'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { ROUTER_NAVIGATION, RouterNavigationAction } from '@ngrx/router-store'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import * as E from 'fp-ts/Either'; -import { combineLatest, distinctUntilChanged, filter, map, merge, switchMap, take } from 'rxjs'; +import { combineLatest, distinctUntilChanged, filter, map, switchMap, take } from 'rxjs'; import { AppSharedStateService } from './app-shared-state.service'; import { AppState } from './app-state'; @@ -40,19 +40,10 @@ export class AppSharedStateEffects { } }); - merge( - this.actions$.pipe( - ofType>(ROUTER_NAVIGATION), - filter((a) => !a.payload.routerState.url.match(/^\/\w\w\/a\//)), - take(1) - ), - this.actions$.pipe(ofType(appSharedStateActions.logout)) - ) - .pipe(untilDestroyed(this)) - .subscribe(() => { - this.store.dispatch(appSharedStateActions.loadUserProfile()); - this.store.dispatch(appSharedStateActions.loadReferenceData()); - }); + this.actions$.pipe(ofType(appSharedStateActions.logout), untilDestroyed(this)).subscribe(() => { + this.store.dispatch(appSharedStateActions.loadUserProfile()); + this.store.dispatch(appSharedStateActions.loadReferenceData()); + }); this.actions$ .pipe( diff --git a/apps/client-asset-sg/src/environments/environment.ts b/apps/client-asset-sg/src/environments/environment.ts index 40c57800..a1267e9c 100644 --- a/apps/client-asset-sg/src/environments/environment.ts +++ b/apps/client-asset-sg/src/environments/environment.ts @@ -1,5 +1,5 @@ import { CompileTimeEnvironment } from './environment-type'; export const environment: CompileTimeEnvironment = { - ngrxStoreLoggerEnabled: true, + ngrxStoreLoggerEnabled: false, }; diff --git a/apps/server-asset-sg/prisma/migrations/20240627070547_add_workgroups/migration.sql b/apps/server-asset-sg/prisma/migrations/20240627070547_add_workgroups/migration.sql index e69de29b..8bee5bfc 100644 --- a/apps/server-asset-sg/prisma/migrations/20240627070547_add_workgroups/migration.sql +++ b/apps/server-asset-sg/prisma/migrations/20240627070547_add_workgroups/migration.sql @@ -0,0 +1,80 @@ +/* + Warnings: + + - You are about to drop the column `role` on the `asset_user` table. All the data in the column will be lost. + +*/ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('viewer', 'editor', 'master-editor'); + +-- AlterTable +ALTER TABLE "asset" ADD COLUMN "workgroup_id" INTEGER; + + +-- CreateTable +CREATE TABLE "workgroup" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL, + "disabled_at" TIMESTAMPTZ(6), + + CONSTRAINT "workgroup_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "workgroups_on_users" ( + "workgroup_id" INTEGER NOT NULL, + "user_id" UUID NOT NULL, + "role" "Role" NOT NULL, + + CONSTRAINT "workgroups_on_users_pkey" PRIMARY KEY ("workgroup_id","user_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "workgroup_name_key" ON "workgroup"("name"); + +-- AddForeignKey +ALTER TABLE "asset" ADD CONSTRAINT "asset_workgroup_id_fkey" FOREIGN KEY ("workgroup_id") REFERENCES "workgroup"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "workgroups_on_users" ADD CONSTRAINT "workgroups_on_users_workgroup_id_fkey" FOREIGN KEY ("workgroup_id") REFERENCES "workgroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "workgroups_on_users" ADD CONSTRAINT "workgroups_on_users_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "asset_user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +INSERT INTO "workgroup" ("name", "created_at") +VALUES ('Swisstopo', NOW()); + +DO +$$ + DECLARE +swisstopo_id INTEGER; +BEGIN +SELECT "id" INTO swisstopo_id FROM "workgroup" WHERE name = 'Swisstopo'; + +-- Update all assets to be assigned to the "Swisstopo" workgroup +UPDATE "asset" SET "workgroup_id" = swisstopo_id; + +-- Assign all users to the "Swisstopo" workgroup with role "VIEWER" +INSERT INTO "workgroups_on_users" ("workgroup_id", "user_id", "role") +SELECT + swisstopo_id, + "id", + CASE + WHEN "asset_user"."role" = 'admin' THEN 'master-editor'::"Role" + WHEN "asset_user"."role" = 'editor' THEN 'editor'::"Role" + WHEN "asset_user"."role" = 'viewer' THEN 'viewer'::"Role" + WHEN "asset_user"."role" = 'master-editor' THEN 'master-editor'::"Role" + ELSE 'viewer'::"Role" + END + FROM "asset_user"; +END +$$; + +-- AlterTable +ALTER TABLE "asset_user" ADD COLUMN "is_admin" BOOLEAN NOT NULL DEFAULT false; + +UPDATE "asset_user" SET "is_admin" = true WHERE role = 'admin'; + +ALTER TABLE "asset_user" ALTER COLUMN "is_admin" SET NOT NULL; +ALTER TABLE "asset_user" DROP COLUMN "role"; diff --git a/apps/server-asset-sg/src/app.controller.ts b/apps/server-asset-sg/src/app.controller.ts index 0e141b78..2e50a00d 100644 --- a/apps/server-asset-sg/src/app.controller.ts +++ b/apps/server-asset-sg/src/app.controller.ts @@ -1,22 +1,7 @@ import { unknownToError } from '@asset-sg/core'; -import { AssetByTitle, PatchContact } from '@asset-sg/shared'; -import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpException, - HttpStatus, - Param, - ParseIntPipe, - Patch, - Put, - Query, - Redirect, - ValidationPipe, -} from '@nestjs/common'; -import { plainToInstance } from 'class-transformer'; +import { AssetByTitle } from '@asset-sg/shared'; +import { User } from '@asset-sg/shared/v2'; +import { Controller, Get, HttpException, Query } from '@nestjs/common'; import { sequenceS } from 'fp-ts/Apply'; import * as A from 'fp-ts/Array'; import * as E from 'fp-ts/Either'; @@ -28,21 +13,10 @@ import { Authorize } from '@/core/decorators/authorize.decorator'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; import { PrismaService } from '@/core/prisma.service'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; -import { Contact, ContactData, ContactDataBoundary, ContactId } from '@/features/contacts/contact.model'; -import { ContactRepo } from '@/features/contacts/contact.repo'; -import { ContactsController } from '@/features/contacts/contacts.controller'; -import { User, UserDataBoundary, UserId } from '@/features/users/user.model'; -import { UserRepo } from '@/features/users/user.repo'; -import { UsersController } from '@/features/users/users.controller'; @Controller('/') export class AppController { - constructor( - private readonly userRepo: UserRepo, - private readonly contactRepo: ContactRepo, - private readonly assetSearchService: AssetSearchService, - private readonly prismaService: PrismaService - ) {} + constructor(private readonly assetSearchService: AssetSearchService, private readonly prismaService: PrismaService) {} @Get('/oauth-config/config') getConfig() { diff --git a/apps/server-asset-sg/src/app.module.ts b/apps/server-asset-sg/src/app.module.ts index 2740fb38..a52e1fa3 100644 --- a/apps/server-asset-sg/src/app.module.ts +++ b/apps/server-asset-sg/src/app.module.ts @@ -6,7 +6,7 @@ import { ScheduleModule } from '@nestjs/schedule'; import { AppController } from '@/app.controller'; import { provideElasticsearch } from '@/core/elasticsearch'; -import { PolicyGuard } from '@/core/guards/policy.guard'; +import { AuthorizationGuard } from '@/core/guards/authorization-guard.service'; import { JwtMiddleware } from '@/core/middleware/jwt.middleware'; import { PrismaService } from '@/core/prisma.service'; import { AssetEditController } from '@/features/asset-edit/asset-edit.controller'; @@ -28,23 +28,23 @@ import { StudiesController } from '@/features/studies/studies.controller'; import { StudyRepo } from '@/features/studies/study.repo'; import { UserRepo } from '@/features/users/user.repo'; import { UsersController } from '@/features/users/users.controller'; -import { WorkgroupController } from '@/features/workgroups/workgroup.controller'; import { WorkgroupRepo } from '@/features/workgroups/workgroup.repo'; +import { WorkgroupsController } from '@/features/workgroups/workgroups.controller'; @Module({ controllers: [ AppController, AssetEditController, - AssetsController, AssetSearchController, AssetSyncController, + AssetsController, ContactsController, FavoritesController, FilesController, OcrController, StudiesController, UsersController, - WorkgroupController, + WorkgroupsController, ], imports: [HttpModule, ScheduleModule.forRoot(), CacheModule.register()], providers: [ @@ -62,7 +62,7 @@ import { WorkgroupRepo } from '@/features/workgroups/workgroup.repo'; WorkgroupRepo, { provide: APP_GUARD, - useClass: PolicyGuard, + useClass: AuthorizationGuard, }, ], }) diff --git a/apps/server-asset-sg/src/core/authorize.ts b/apps/server-asset-sg/src/core/authorize.ts new file mode 100644 index 00000000..fd7f2a5d --- /dev/null +++ b/apps/server-asset-sg/src/core/authorize.ts @@ -0,0 +1,34 @@ +import { User } from '@asset-sg/shared/v2'; +import { Policy } from '@asset-sg/shared/v2'; +import { HttpException, HttpStatus } from '@nestjs/common'; +import { Class } from 'type-fest'; + +export const authorize = (policy: Class>, currentUser: User): Authorize => { + return new Authorize(new policy(currentUser)); +}; + +class Authorize { + constructor(private readonly policy: Policy) {} + + canShow(value: T): void { + check(this.policy.canDoEverything() || this.policy.canShow(value)); + } + + canCreate(): void { + check(this.policy.canDoEverything() || this.policy.canCreate()); + } + + canUpdate(value: T): void { + check(this.policy.canDoEverything() || this.policy.canUpdate(value)); + } + + canDelete(value: T): void { + check(this.policy.canDoEverything() || this.policy.canDelete(value)); + } +} + +const check = (condition: boolean): void => { + if (!condition) { + throw new HttpException('Not authorized to access this resource', HttpStatus.FORBIDDEN); + } +}; diff --git a/apps/server-asset-sg/src/core/decorators/authorize.decorator.ts b/apps/server-asset-sg/src/core/decorators/authorize.decorator.ts index 8209b500..858c75ab 100644 --- a/apps/server-asset-sg/src/core/decorators/authorize.decorator.ts +++ b/apps/server-asset-sg/src/core/decorators/authorize.decorator.ts @@ -1,35 +1,4 @@ -import { HttpException, HttpStatus, SetMetadata } from '@nestjs/common'; -import { Request } from 'express'; -import { SingleKeyObject } from 'type-fest'; -import { ReadRepo } from '@/core/repo'; - -type ParamType = typeof Number | typeof String; - -export interface AuthorizationTarget { - getId: (request: Request) => string | number; - findBy?: (repo: ReadRepo) => (id: never) => Promise; -} - -type TargetIdMapping = SingleKeyObject | ((req: Request) => string | number); - -export interface ShowMetadata { - action: 'show'; - target: AuthorizationTarget; -} - -export interface CreateMetadata { - action: 'create'; -} - -export interface UpdateMetadata { - action: 'update'; - target: AuthorizationTarget; -} - -export interface DeleteMetadata { - action: 'delete'; - target: AuthorizationTarget; -} +import { SetMetadata } from '@nestjs/common'; export interface UserOnlyMetadata { action: 'user-only'; @@ -39,84 +8,9 @@ export interface AdminOnlyMetadata { action: 'admin-only'; } -export type AuthorizationMetadata = - | ShowMetadata - | CreateMetadata - | UpdateMetadata - | DeleteMetadata - | UserOnlyMetadata - | AdminOnlyMetadata; +export type AuthorizationMetadata = UserOnlyMetadata | AdminOnlyMetadata; export const Authorize = { - Show: , R extends ReadRepo>( - mapping: TargetIdMapping, - findBy?: (repo: R) => (id: never) => Promise - ) => { - return SetMetadata('authorization', { - action: 'show', - target: { - getId: makeIdFetch(mapping), - findBy, - }, - } as ShowMetadata); - }, - - Create: () => - SetMetadata('authorization', { - action: 'create', - } as CreateMetadata), - - Update: , R extends ReadRepo>( - mapping: TargetIdMapping, - findBy?: (repo: R) => (id: never) => Promise - ) => { - return SetMetadata('authorization', { - action: 'update', - target: { - getId: makeIdFetch(mapping), - findBy, - }, - } as UpdateMetadata); - }, - - Delete: , R extends ReadRepo>( - mapping: TargetIdMapping, - findBy?: (repo: R) => (id: never) => Promise - ) => { - return SetMetadata('authorization', { - action: 'delete', - target: { - getId: makeIdFetch(mapping), - findBy, - }, - } as DeleteMetadata); - }, - User: () => SetMetadata('authorization', { action: 'user-only' } as UserOnlyMetadata), Admin: () => SetMetadata('authorization', { action: 'admin-only' } as AdminOnlyMetadata), }; - -const makeIdFetch = >(mapping: TargetIdMapping) => { - if (mapping instanceof Function) { - return mapping; - } - const [name, type] = Object.entries(mapping)[0]; - return loadIdByParam(name, type); -}; - -const loadIdByParam = - (name: string, type: ParamType) => - (request: Request): string | number => { - let paramValue: string | number = request.params[name]; - if (paramValue == null) { - throw new HttpException(`Missing ${name} parameter`, HttpStatus.BAD_REQUEST); - } - if (type === String) { - return paramValue; - } - paramValue = parseInt(paramValue); - if (isNaN(paramValue)) { - throw new HttpException(`Parameter ${name} must be a number`, HttpStatus.BAD_REQUEST); - } - return paramValue; - }; diff --git a/apps/server-asset-sg/src/core/decorators/authorized.decorator.ts b/apps/server-asset-sg/src/core/decorators/authorized.decorator.ts index 5c181ae6..8e6aa711 100644 --- a/apps/server-asset-sg/src/core/decorators/authorized.decorator.ts +++ b/apps/server-asset-sg/src/core/decorators/authorized.decorator.ts @@ -1,5 +1,5 @@ +import { Policy } from '@asset-sg/shared/v2'; import { createParamDecorator, ExecutionContext } from '@nestjs/common'; -import { Policy } from '@/core/policy'; import { AuthorizedRequest } from '@/models/jwt-request'; export const Authorized = { diff --git a/apps/server-asset-sg/src/core/decorators/current-user.decorator.ts b/apps/server-asset-sg/src/core/decorators/current-user.decorator.ts index 6acc5283..38538239 100644 --- a/apps/server-asset-sg/src/core/decorators/current-user.decorator.ts +++ b/apps/server-asset-sg/src/core/decorators/current-user.decorator.ts @@ -1,6 +1,6 @@ +import { User } from '@asset-sg/shared/v2'; import { ExecutionContext, createParamDecorator } from '@nestjs/common'; -import { User } from '@/features/users/user.model'; import { JwtRequest } from '@/models/jwt-request'; /** diff --git a/apps/server-asset-sg/src/core/decorators/boundary.decorator.ts b/apps/server-asset-sg/src/core/decorators/parse.decorator.ts similarity index 59% rename from apps/server-asset-sg/src/core/decorators/boundary.decorator.ts rename to apps/server-asset-sg/src/core/decorators/parse.decorator.ts index f194f9fc..81564e5b 100644 --- a/apps/server-asset-sg/src/core/decorators/boundary.decorator.ts +++ b/apps/server-asset-sg/src/core/decorators/parse.decorator.ts @@ -5,15 +5,29 @@ import * as D from 'io-ts/Decoder'; import { Class } from 'type-fest'; import { JwtRequest } from '@/models/jwt-request'; -export type BoundaryType = D.Decoder | Class; +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +export type SchemaType = D.Decoder | Class; const validationPipe = new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }); -export const Boundary = createParamDecorator(async (dataType: BoundaryType, context: ExecutionContext) => { +/** + * Parses and validates the request body using the given schema type. + * + * The schema type can be either a class using `class-validator` and `class-transformer`, + * or a decoder of `io-ts`. + * + * @example + * show(@Transform(MyValueSchema) myValue: MyValue) { + * console.log(`My parsed and validated value is ${myValue}.`); + * } + */ +export const ParseBody = createParamDecorator(async (dataType: SchemaType, context: ExecutionContext) => { const request = context.switchToHttp().getRequest() as JwtRequest; if (dataType instanceof Function) { + // It's a class transformer. return validationPipe.transform(request.body, { type: 'body', metatype: dataType }); } else { + // It's a decoder. const data = (dataType as D.Decoder).decode(request.body); if (E.isLeft(data)) { throw new HttpException( diff --git a/apps/server-asset-sg/src/core/decorators/use-policy.decorator.ts b/apps/server-asset-sg/src/core/decorators/use-policy.decorator.ts index aeadfa74..9c83379e 100644 --- a/apps/server-asset-sg/src/core/decorators/use-policy.decorator.ts +++ b/apps/server-asset-sg/src/core/decorators/use-policy.decorator.ts @@ -1,6 +1,6 @@ +import { Policy } from '@asset-sg/shared/v2'; import { Reflector } from '@nestjs/core'; import { Class } from 'type-fest'; -import { Policy } from '@/core/policy'; export const UsePolicy = Reflector.createDecorator>>(); diff --git a/apps/server-asset-sg/src/core/decorators/use-repo.decorator.ts b/apps/server-asset-sg/src/core/decorators/use-repo.decorator.ts index 41deb505..e28d8187 100644 --- a/apps/server-asset-sg/src/core/decorators/use-repo.decorator.ts +++ b/apps/server-asset-sg/src/core/decorators/use-repo.decorator.ts @@ -1,6 +1,6 @@ import { Reflector } from '@nestjs/core'; import { Class } from 'type-fest'; -import { ReadRepo } from '@/core/repo'; +import { FindRepo, ReadRepo } from '@/core/repo'; -export const UseRepo = Reflector.createDecorator>>(); +export const UseRepo = Reflector.createDecorator>>(); diff --git a/apps/server-asset-sg/src/core/guards/authorization-guard.service.ts b/apps/server-asset-sg/src/core/guards/authorization-guard.service.ts new file mode 100644 index 00000000..0742cd83 --- /dev/null +++ b/apps/server-asset-sg/src/core/guards/authorization-guard.service.ts @@ -0,0 +1,36 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthorizationMetadata } from '@/core/decorators/authorize.decorator'; +import { JwtRequest } from '@/models/jwt-request'; + +@Injectable() +export class AuthorizationGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const auth = this.reflector.get('authorization', context.getHandler()); + if (auth == null) { + return true; + } + const request = context.switchToHttp().getRequest() as JwtRequest; + if (request.user == null) { + return false; + } + switch (auth.action) { + case 'user-only': + return this.authorizeUserOnly(context); + case 'admin-only': + return this.authorizeAdminOnly(context); + } + } + + private authorizeUserOnly(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() as JwtRequest; + return request.user != null; + } + + private authorizeAdminOnly(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() as JwtRequest; + return request.user.isAdmin ?? false; + } +} diff --git a/apps/server-asset-sg/src/core/guards/policy.guard.ts b/apps/server-asset-sg/src/core/guards/policy.guard.ts deleted file mode 100644 index 241fe62d..00000000 --- a/apps/server-asset-sg/src/core/guards/policy.guard.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { ModuleRef, Reflector } from '@nestjs/core'; -import { Request } from 'express'; -import { UsePolicy } from '../decorators/use-policy.decorator'; -import { - AuthorizationMetadata, - AuthorizationTarget, - CreateMetadata, - DeleteMetadata, - ShowMetadata, - UpdateMetadata, -} from '@/core/decorators/authorize.decorator'; -import { UseRepo } from '@/core/decorators/use-repo.decorator'; -import { AuthorizedRequest, JwtRequest } from '@/models/jwt-request'; - -@Injectable() -export class PolicyGuard implements CanActivate { - constructor(private readonly reflector: Reflector, private readonly moduleRef: ModuleRef) {} - - async canActivate(context: ExecutionContext): Promise { - const auth = this.reflector.get('authorization', context.getHandler()); - if (auth == null) { - return true; - } - const request = context.switchToHttp().getRequest() as JwtRequest; - if (request.user == null) { - return false; - } - switch (auth.action) { - case 'show': - return this.authorizeShow(context, auth); - case 'create': - return this.authorizeCreate(context, auth); - case 'update': - return this.authorizeUpdate(context, auth); - case 'delete': - return this.authorizeDelete(context, auth); - case 'user-only': - return this.authorizeUserOnly(context); - case 'admin-only': - return this.authorizeAdminOnly(context); - } - } - - private async authorizeShow(context: ExecutionContext, auth: ShowMetadata): Promise { - const policy = this.extractPolicy(context); - const record = await this.extractRecord(context, auth.target); - return policy.canDoEverything() || policy.canShow(record); - } - - private async authorizeCreate(context: ExecutionContext, _auth: CreateMetadata): Promise { - const policy = this.extractPolicy(context); - return policy.canDoEverything() || policy.canCreate(); - } - - private async authorizeUpdate(context: ExecutionContext, auth: UpdateMetadata): Promise { - const policy = this.extractPolicy(context); - const record = await this.extractRecord(context, auth.target); - return policy.canDoEverything() || policy.canUpdate(record); - } - - private async authorizeDelete(context: ExecutionContext, auth: DeleteMetadata): Promise { - const policy = this.extractPolicy(context); - const record = await this.extractRecord(context, auth.target); - return policy.canDoEverything() || policy.canDelete(record); - } - - private authorizeUserOnly(context: ExecutionContext): boolean { - const request = context.switchToHttp().getRequest() as JwtRequest; - return request.user != null; - } - - private authorizeAdminOnly(context: ExecutionContext): boolean { - const request = context.switchToHttp().getRequest() as JwtRequest; - return request.user.isAdmin ?? false; - } - - private extractPolicy(context: ExecutionContext) { - const policyClass = - this.reflector.get(UsePolicy, context.getHandler()) ?? this.reflector.get(UsePolicy, context.getClass()); - if (policyClass == null) { - throw new Error('missing `UsePolicy` decorator'); - } - const request = context.switchToHttp().getRequest() as JwtRequest; - const policy = new policyClass(request.user); - assignAuthorized(request, { policy }); - return policy; - } - - private async extractRecord(context: ExecutionContext, target: AuthorizationTarget): Promise { - const request = context.switchToHttp().getRequest() as JwtRequest; - const id = target.getId(request); - - const repoType = - this.reflector.get(UseRepo, context.getHandler()) ?? this.reflector.get(UseRepo, context.getClass()); - if (repoType == null) { - throw new Error('missing `UseRepo` decorator'); - } - const repo = this.moduleRef.get(repoType); - const findBy = target.findBy == null ? repo.find : target.findBy(repo); - const record = await findBy.call(repo, [id]); - if (record == null) { - throw new HttpException('Not found', HttpStatus.NOT_FOUND); - } - assignAuthorized(request, { record }); - return record; - } -} - -const assignAuthorized = (request: Request, assigns: Partial) => { - const authorizedRequest = request as AuthorizedRequest; - authorizedRequest.authorized ??= {} as AuthorizedRequest['authorized']; - Object.assign(authorizedRequest.authorized, assigns); -}; diff --git a/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts b/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts index 05d39558..976a9afd 100644 --- a/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts +++ b/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts @@ -1,3 +1,4 @@ +import { User } from '@asset-sg/shared/v2'; import { environment } from '@environment'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { HttpException, Inject, Injectable, NestMiddleware } from '@nestjs/common'; @@ -11,7 +12,6 @@ import * as jwt from 'jsonwebtoken'; import { Jwt, JwtPayload } from 'jsonwebtoken'; import jwkToPem from 'jwk-to-pem'; -import { User } from '@/features/users/user.model'; import { UserRepo } from '@/features/users/user.repo'; import { JwtRequest } from '@/models/jwt-request'; @@ -61,7 +61,7 @@ export class JwtMiddleware implements NestMiddleware { await this.initializeRequest(req, result.right.accessToken, result.right.jwtPayload as JwtPayload); next(); } else { - res.status(403).json({ error: result.left.message }); + res.status(403).json({ error: 'not authorized by eIAM' }); } } diff --git a/apps/server-asset-sg/src/core/repo.ts b/apps/server-asset-sg/src/core/repo.ts index 67e7a53b..17f2b858 100644 --- a/apps/server-asset-sg/src/core/repo.ts +++ b/apps/server-asset-sg/src/core/repo.ts @@ -1,13 +1,19 @@ export type Repo = ReadRepo & MutateRepo; -export interface ReadRepo { +export interface FindRepo { find(id: TId): Promise; +} + +export interface ListRepo { list(options?: RepoListOptions): Promise; } -export type MutateRepo = CreateRepo & - UpdateRepo & - DeleteRepo; +export interface ReadRepo extends FindRepo, ListRepo {} + +export interface MutateRepo + extends CreateRepo, + UpdateRepo, + DeleteRepo {} export interface CreateRepo { create(data: TData): Promise; diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts index e77bfeb0..650069dd 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.controller.ts @@ -1,41 +1,33 @@ import { PatchAsset } from '@asset-sg/shared'; -import { Controller, Get, HttpException, HttpStatus, Post, Put } from '@nestjs/common'; +import { User } from '@asset-sg/shared/v2'; +import { Role } from '@asset-sg/shared/v2'; +import { AssetEditPolicy } from '@asset-sg/shared/v2'; +import { Controller, Get, HttpException, HttpStatus, Param, ParseIntPipe, Post, Put } from '@nestjs/common'; import * as E from 'fp-ts/Either'; -import { Authorize } from '@/core/decorators/authorize.decorator'; -import { Authorized } from '@/core/decorators/authorized.decorator'; -import { Boundary } from '@/core/decorators/boundary.decorator'; +import { authorize } from '@/core/authorize'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; -import { UsePolicy } from '@/core/decorators/use-policy.decorator'; -import { UseRepo } from '@/core/decorators/use-repo.decorator'; -import { AssetEditPolicy } from '@/features/asset-edit/asset-edit.policy'; +import { ParseBody } from '@/core/decorators/parse.decorator'; import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; import { AssetEditDetail, AssetEditService } from '@/features/asset-edit/asset-edit.service'; -import { User } from '@/features/users/user.model'; -import { Role } from '@/features/workgroups/workgroup.model'; @Controller('/asset-edit') -@UseRepo(AssetEditRepo) -@UsePolicy(AssetEditPolicy) export class AssetEditController { - constructor(private readonly assetEditService: AssetEditService) {} + constructor(private readonly assetEditRepo: AssetEditRepo, private readonly assetEditService: AssetEditService) {} @Get('/:id') - @Authorize.Show({ id: Number }) - async show(@Authorized.Record() asset: AssetEditDetail): Promise { - return AssetEditDetail.encode(asset); + async show(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User): Promise { + const record = await this.assetEditRepo.find(id); + if (record == null) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + authorize(AssetEditPolicy, user).canShow(record); + return AssetEditDetail.encode(record); } - /** - * @deprecated - */ @Post('/') - @Authorize.Create() - async create( - @Boundary(PatchAsset) patch: PatchAsset, - @CurrentUser() user: User, - @Authorized.Policy() policy: AssetEditPolicy - ) { - validatePatch(patch, policy); + async create(@ParseBody(PatchAsset) patch: PatchAsset, @CurrentUser() user: User) { + authorize(AssetEditPolicy, user).canCreate(); + validatePatch(user, patch); const result = await this.assetEditService.createAsset(user, patch)(); if (E.isLeft(result)) { throw new HttpException(result.left.message, 500); @@ -44,15 +36,20 @@ export class AssetEditController { } @Put('/:id') - @Authorize.Update({ id: Number }) async update( - @Boundary(PatchAsset) patch: PatchAsset, - @CurrentUser() user: User, - @Authorized.Record() asset: AssetEditDetail, - @Authorized.Policy() policy: AssetEditPolicy + @Param('id', ParseIntPipe) id: number, + @ParseBody(PatchAsset) patch: PatchAsset, + @CurrentUser() user: User ) { - validatePatch(patch, policy); - const result = await this.assetEditService.updateAsset(user, asset.assetId, patch)(); + const record = await this.assetEditRepo.find(id); + if (record == null) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + + authorize(AssetEditPolicy, user).canUpdate(record); + validatePatch(user, patch, record); + + const result = await this.assetEditService.updateAsset(user, record.assetId, patch)(); if (E.isLeft(result)) { throw new HttpException(result.left.message, 500); } @@ -60,13 +57,28 @@ export class AssetEditController { } } -const validatePatch = (patch: PatchAsset, policy: AssetEditPolicy) => { +const validatePatch = (user: User, patch: PatchAsset, record?: AssetEditDetail) => { + const policy = new AssetEditPolicy(user); + // Specialization of the policy where we disallow assets to be moved to another workgroup // if the current user is not an editor for that workgroup. if (!policy.canDoEverything() && !policy.hasRole(Role.Editor, patch.workgroupId)) { throw new HttpException( "Can't move asset to a workgroup for which the user is not an editor", - HttpStatus.UNPROCESSABLE_ENTITY + HttpStatus.FORBIDDEN ); } + + // Specialization of the policy where we disallow the internal status to be changed to anything else than `tobechecked` + // if the current user is not a master-editor for the asset's current or future workgroup. + const hasInternalUseChanged = + record == null || record.internalUse.statusAssetUseItemCode !== patch.internalUse.statusAssetUseItemCode; + if ( + hasInternalUseChanged && + patch.internalUse.statusAssetUseItemCode !== 'tobechecked' && + ((record != null && !policy.hasRole(Role.MasterEditor, record.workgroupId)) || + !policy.hasRole(Role.MasterEditor, patch.workgroupId)) + ) { + throw new HttpException("Changing the asset's status is not allowed", HttpStatus.FORBIDDEN); + } }; diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts index af7da0e9..dfe1a8ad 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts @@ -1,4 +1,6 @@ -import { AssetUsage, Contact, PatchAsset, User, UserRoleEnum, dateIdFromDate } from '@asset-sg/shared'; +import { AssetUsage, Contact, PatchAsset, dateIdFromDate } from '@asset-sg/shared'; +import { User } from '@asset-sg/shared/v2'; +import { Role } from '@asset-sg/shared/v2'; import { fakerDE_CH as faker } from '@faker-js/faker'; import * as O from 'fp-ts/Option'; @@ -27,7 +29,8 @@ export const fakeUser = () => email: faker.internet.email(), id: faker.string.uuid(), lang: faker.helpers.fromRegExp(/[a-z]{2}/), - role: faker.helpers.arrayElement(Object.values(UserRoleEnum)), + isAdmin: false, + workgroups: [{ id: 1, role: Role.Viewer }], }); export const fakeContact = () => diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts index 4c8ed216..0189c1d3 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts @@ -1,5 +1,6 @@ import { decodeError, isNotNull } from '@asset-sg/core'; import { AssetUsage, dateFromDateId, DateIdFromDate, PatchAsset } from '@asset-sg/shared'; +import { User } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import * as E from 'fp-ts/Either'; @@ -9,7 +10,6 @@ import { AssetEditDetail } from './asset-edit.service'; import { PrismaService } from '@/core/prisma.service'; import { Repo, RepoListOptions } from '@/core/repo'; -import { User } from '@/features/users/user.model'; import { AssetEditDetailFromPostgres } from '@/models/asset-edit-detail'; import { createStudies, diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts index 5ab4064f..c7d90c36 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.service.ts @@ -1,5 +1,6 @@ import { isNotNull, unknownToUnknownError } from '@asset-sg/core'; import { BaseAssetEditDetail, PatchAsset } from '@asset-sg/shared'; +import { User } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; @@ -9,7 +10,6 @@ import { AssetEditRepo } from './asset-edit.repo'; import { PrismaService } from '@/core/prisma.service'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; -import { User } from '@/features/users/user.model'; import { notFoundError } from '@/utils/errors'; import { deleteFile } from '@/utils/file/delete-file'; import { putFile } from '@/utils/file/put-file'; diff --git a/apps/server-asset-sg/src/features/assets/asset-info.repo.ts b/apps/server-asset-sg/src/features/assets/asset-info.repo.ts index 7613860d..f174d8bb 100644 --- a/apps/server-asset-sg/src/features/assets/asset-info.repo.ts +++ b/apps/server-asset-sg/src/features/assets/asset-info.repo.ts @@ -1,6 +1,6 @@ +import { AssetId, AssetInfo } from '@asset-sg/shared/v2'; import { PrismaService } from '@/core/prisma.service'; import { ReadRepo, RepoListOptions } from '@/core/repo'; -import { AssetId, AssetInfo } from '@/features/assets/asset.model'; import { assetInfoSelection, parseAssetInfoFromPrisma } from '@/features/assets/prisma-asset'; export class AssetInfoRepo implements ReadRepo { diff --git a/apps/server-asset-sg/src/features/assets/asset.model.ts b/apps/server-asset-sg/src/features/assets/asset.model.ts deleted file mode 100644 index c83ab492..00000000 --- a/apps/server-asset-sg/src/features/assets/asset.model.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { Transform, Type } from 'class-transformer'; -import { - IsArray, - IsBoolean, - IsDate, - IsEnum, - IsInt, - IsNumber, - IsObject, - IsString, - ValidateNested, -} from 'class-validator'; - -import { IsNullable, messageNullableInt, messageNullableString } from '@/core/decorators/is-nullable.decorator'; -import { StudyType } from '@/features/studies/study.model'; -import { LocalDate } from '@/utils/data/local-date'; -import { Data, Model } from '@/utils/data/model'; - -// `usageCode` will need to be determined in the frontend - it is no longer included here. -// See `makeUsageCode`. - -// `assetFormatCompositions` seems to be fully unused. -// The table on INT is empty, and there's no way to edit it. -// The field would theoretically be displayed in the search, but since it is empty, -// it's always skipped. - -export interface AssetInfo extends Model { - title: string; - originalTitle: string | null; - - kindCode: string; - formatCode: string; - identifiers: AssetIdentifier[]; - languageCodes: string[]; - contactAssignments: ContactAssignment[]; - manCatLabelCodes: string[]; - natRelCodes: string[]; - links: AssetLinks; - files: FileReference[]; - - createdAt: LocalDate; - receivedAt: LocalDate; - lastProcessedAt: Date; -} - -export interface AssetLinks { - parent: LinkedAsset | null; - children: LinkedAsset[]; - siblings: LinkedAsset[]; -} - -export interface AssetLinksData { - parent: AssetId | null; - siblings: AssetId[]; -} - -// Detailed data about an asset. -// These are the parts of `Asset` that were previously only part of `AssetEdit`. -// They are only visible on the asset edit page. -export interface AssetDetails { - sgsId: number | null; - municipality: string | null; - processor: string | null; - isNatRel: boolean; - infoGeol: InfoGeol; - usage: AssetUsages; - statuses: WorkStatus[]; - studies: AssetStudy[]; - workgroupId: number; -} - -export interface AssetUsages { - public: AssetUsage; - internal: AssetUsage; -} - -export type Asset = AssetInfo & AssetDetails; - -type NonDataKeys = 'processor' | 'identifiers' | 'studies' | 'statuses' | 'links' | 'lastProcessedAt' | 'files'; - -export interface AssetData extends Omit, NonDataKeys> { - links: AssetLinksData; - identifiers: (AssetIdentifier | AssetIdentifierData)[]; - statuses: (WorkStatus | WorkStatusData)[]; - studies: (AssetStudy | StudyData)[]; -} - -interface InfoGeol { - main: string | null; - contact: string | null; - auxiliary: string | null; -} - -export interface AssetIdentifier extends Model { - name: string; - description: string; -} - -export type AssetIdentifierId = number; -export type AssetIdentifierData = Data; - -export type AssetId = number; - -export interface AssetUsage { - isAvailable: boolean; - statusCode: UsageStatusCode; - availableAt: LocalDate | null; -} - -export enum UsageStatusCode { - ToBeChecked = 'tobechecked', - UnderClarification = 'underclarification', - Approved = 'approved', -} - -export interface ContactAssignment { - contactId: number; - role: ContactAssignmentRole; -} - -export enum ContactAssignmentRole { - Author = 'author', - Initiator = 'initiator', - Supplier = 'supplier', -} - -export interface LinkedAsset { - id: AssetId; - title: string; -} - -export interface WorkStatus extends Model { - itemCode: WorkStatusCode; - createdAt: Date; -} - -export type WorkStatusCode = string; -export type WorkStatusData = Data; - -export interface FileReference { - id: number; - name: string; - size: number; -} - -export enum UsageCode { - Public = 'public', - Internal = 'internal', - UseOnRequest = 'useOnRequest', -} - -export interface AssetStudy extends Model { - geom: string; - type: StudyType; -} - -export type StudyData = Data; - -export type AssetStudyId = number; - -export class AssetUsageBoundary implements AssetUsage { - @IsBoolean() - isAvailable!: boolean; - - @IsEnum(UsageStatusCode) - statusCode!: UsageStatusCode; - - @IsNullable() - @ValidateNested() - @Type(() => String) - @Transform(({ value }) => LocalDate.parse(value)) - availableAt!: LocalDate | null; -} - -export class AssetUsagesBoundary implements AssetUsages { - @IsObject() - @ValidateNested() - @Type(() => AssetUsageBoundary) - public!: AssetUsageBoundary; - - @IsObject() - @ValidateNested() - @Type(() => AssetUsageBoundary) - internal!: AssetUsageBoundary; -} - -export class InfoGeolBoundary implements InfoGeol { - @IsString({ message: messageNullableString }) - @IsNullable() - main!: string | null; - - @IsString({ message: messageNullableString }) - @IsNullable() - contact!: string | null; - - @IsString({ message: messageNullableString }) - @IsNullable() - auxiliary!: string | null; -} - -export class ContactAssignmentBoundary implements ContactAssignment { - @IsInt() - contactId!: number; - - @IsEnum(ContactAssignmentRole) - role!: ContactAssignmentRole; -} - -export class StudyDataBoundary implements StudyData { - @IsInt({ message: messageNullableInt }) - @IsNullable() - id?: number | undefined; - - @IsString() - geom!: string; - - @IsEnum(StudyType) - type!: StudyType; -} - -export class WorkStatusBoundary implements WorkStatusData { - @IsInt({ message: messageNullableInt }) - @IsNullable() - id?: number | undefined; - - @IsDate() - @Type(() => Date) - createdAt!: Date; - - @IsString() - itemCode!: string; -} - -export class AssetIdentifierBoundary implements AssetIdentifierData { - @IsInt({ message: messageNullableInt }) - @IsNullable() - id?: number | undefined; - - @IsString() - name!: string; - - @IsString() - description!: string; -} - -export class AssetLinksDataBoundary implements AssetLinksData { - @IsInt({ message: messageNullableInt }) - @IsNullable() - parent!: number | null; - - @IsInt({ each: true }) - siblings!: number[]; -} - -export class AssetDataBoundary implements AssetData { - @IsObject() - @ValidateNested() - @Type(() => AssetLinksDataBoundary) - links!: AssetLinksDataBoundary; - - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetIdentifierBoundary) - identifiers!: AssetIdentifierBoundary[]; - - @IsArray() - @ValidateNested({ each: true }) - @Type(() => WorkStatusBoundary) - statuses!: WorkStatusBoundary[]; - - @IsArray() - @ValidateNested({ each: true }) - @Type(() => StudyDataBoundary) - studies!: StudyDataBoundary[]; - - @IsString() - title!: string; - - @IsString({ message: messageNullableString }) - @IsNullable() - originalTitle!: string | null; - - @IsString() - kindCode!: string; - - @IsString() - formatCode!: string; - - @IsString({ each: true }) - languageCodes!: string[]; - - @IsArray() - @ValidateNested({ each: true }) - @Type(() => ContactAssignmentBoundary) - contactAssignments!: ContactAssignmentBoundary[]; - - @IsString({ each: true }) - manCatLabelCodes!: string[]; - - @IsString({ each: true }) - natRelCodes!: string[]; - - @ValidateNested() - @Type(() => String) - @Transform(({ value }) => LocalDate.tryParse(value)) - createdAt!: LocalDate; - - @ValidateNested() - @Type(() => String) - @Transform(({ value }) => LocalDate.tryParse(value)) - receivedAt!: LocalDate; - - @IsInt({ message: messageNullableInt }) - @IsNullable() - sgsId!: number | null; - - @IsString({ message: messageNullableString }) - @IsNullable() - municipality!: string | null; - - @IsBoolean() - isNatRel!: boolean; - - @IsObject() - @ValidateNested() - @Type(() => InfoGeolBoundary) - infoGeol!: InfoGeolBoundary; - - @IsObject() - @ValidateNested() - @Type(() => AssetUsagesBoundary) - usage!: AssetUsagesBoundary; - - @IsNumber() - workgroupId!: number; -} diff --git a/apps/server-asset-sg/src/features/assets/asset.repo.ts b/apps/server-asset-sg/src/features/assets/asset.repo.ts index a3e48abc..c616982d 100644 --- a/apps/server-asset-sg/src/features/assets/asset.repo.ts +++ b/apps/server-asset-sg/src/features/assets/asset.repo.ts @@ -1,26 +1,17 @@ +import { Asset, AssetData, AssetId, AssetStudy, AssetStudyId, AssetUsage, StudyData } from '@asset-sg/shared/v2'; +import { isNotPersisted, isPersisted } from '@asset-sg/shared/v2'; +import { StudyType } from '@asset-sg/shared/v2'; +import { User } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; - import { PrismaService } from '@/core/prisma.service'; -import { Repo, RepoListOptions } from '@/core/repo'; -import { - Asset, - AssetData, - AssetId, - AssetUsage, - AssetStudy, - StudyData, - AssetStudyId, -} from '@/features/assets/asset.model'; +import { FindRepo, MutateRepo } from '@/core/repo'; import { assetSelection, parseAssetFromPrisma } from '@/features/assets/prisma-asset'; -import { StudyType } from '@/features/studies/study.model'; -import { User } from '@/features/users/user.model'; -import { isNotPersisted, isPersisted } from '@/utils/data/model'; import { satisfy } from '@/utils/define'; import { handlePrismaMutationError } from '@/utils/prisma'; @Injectable() -export class AssetRepo implements Repo { +export class AssetRepo implements FindRepo, MutateRepo { constructor(private readonly prisma: PrismaService) {} async find(id: AssetId): Promise { @@ -31,21 +22,6 @@ export class AssetRepo implements Repo { return entry == null ? null : parseAssetFromPrisma(entry); } - async list({ limit, offset, ids }: RepoListOptions = {}): Promise { - const entries = await this.prisma.asset.findMany({ - where: - ids == null - ? undefined - : { - assetId: { in: ids }, - }, - select: assetSelection, - take: limit, - skip: offset, - }); - return entries.map(parseAssetFromPrisma); - } - async create(data: FullAssetData): Promise { const id = await this.prisma.$transaction(async () => { const { assetId } = await this.prisma.asset.create({ diff --git a/apps/server-asset-sg/src/features/assets/assets.controller.ts b/apps/server-asset-sg/src/features/assets/assets.controller.ts index 931009c1..814fc848 100644 --- a/apps/server-asset-sg/src/features/assets/assets.controller.ts +++ b/apps/server-asset-sg/src/features/assets/assets.controller.ts @@ -1,3 +1,9 @@ +import { Asset, AssetData, AssetId, UsageStatusCode } from '@asset-sg/shared/v2'; + +import { User } from '@asset-sg/shared/v2'; +import { Role } from '@asset-sg/shared/v2'; +import { AssetPolicy } from '@asset-sg/shared/v2'; +import { AssetDataSchema } from '@asset-sg/shared/v2'; import { Controller, Delete, @@ -10,57 +16,46 @@ import { Post, Put, } from '@nestjs/common'; - -import { Authorize } from '@/core/decorators/authorize.decorator'; -import { Authorized } from '@/core/decorators/authorized.decorator'; -import { Boundary } from '@/core/decorators/boundary.decorator'; +import { authorize } from '@/core/authorize'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; -import { UsePolicy } from '@/core/decorators/use-policy.decorator'; -import { UseRepo } from '@/core/decorators/use-repo.decorator'; -import { AssetEditPolicy } from '@/features/asset-edit/asset-edit.policy'; -import { AssetInfoRepo } from '@/features/assets/asset-info.repo'; -import { Asset, AssetData, AssetDataBoundary, AssetId } from '@/features/assets/asset.model'; -import { AssetPolicy } from '@/features/assets/asset.policy'; +import { ParseBody } from '@/core/decorators/parse.decorator'; import { AssetRepo } from '@/features/assets/asset.repo'; -import { User } from '@/features/users/user.model'; -import { Role } from '@/features/workgroups/workgroup.model'; @Controller('/assets') -@UseRepo(AssetRepo) -@UsePolicy(AssetPolicy) export class AssetsController { - constructor(private readonly assetRepo: AssetRepo, private readonly assetInfoRepo: AssetInfoRepo) {} + constructor(private readonly assetRepo: AssetRepo) {} @Get('/:id') - @Authorize.Show({ id: Number }) - async show(@Param('id', ParseIntPipe) id: AssetId): Promise { - const asset = await this.assetRepo.find(id); - if (asset === null) { + async show(@Param('id', ParseIntPipe) id: AssetId, @CurrentUser() user: User): Promise { + const record = await this.assetRepo.find(id); + if (record === null) { throw new HttpException('not found', 404); } - return asset; + authorize(AssetPolicy, user).canShow(record); + return record; } @Post('/') - @Authorize.Create() - async create( - @Boundary(AssetDataBoundary) data: AssetData, - @CurrentUser() user: User, - @Authorized.Policy() policy: AssetEditPolicy - ): Promise { - validateData(data, policy); + async create(@ParseBody(AssetDataSchema) data: AssetData, @CurrentUser() user: User): Promise { + authorize(AssetPolicy, user).canCreate(); + validateData(user, data); return await this.assetRepo.create({ ...data, processor: user }); } @Put('/:id') - @Authorize.Update({ id: Number }) async update( - @Boundary(AssetDataBoundary) data: AssetData, - @CurrentUser() user: User, - @Authorized.Record() record: Asset, - @Authorized.Policy() policy: AssetEditPolicy + @Param('id', ParseIntPipe) id: number, + @ParseBody(AssetDataSchema) data: AssetData, + @CurrentUser() user: User ): Promise { - validateData(data, policy); + const record = await this.assetRepo.find(id); + if (record == null) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + + authorize(AssetPolicy, user).canUpdate(record); + validateData(user, data, record); + const asset = await this.assetRepo.update(record.id, { ...data, processor: user }); if (asset === null) { throw new HttpException('not found', 404); @@ -69,20 +64,38 @@ export class AssetsController { } @Delete('/:id') - @Authorize.Delete({ id: Number }) @HttpCode(HttpStatus.NO_CONTENT) - async delete(@Authorized.Record() record: Asset): Promise { + async delete(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User): Promise { + const record = await this.assetRepo.find(id); + if (record == null) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + authorize(AssetPolicy, user).canDelete(record); await this.assetRepo.delete(record.id); } } -const validateData = (data: AssetData, policy: AssetEditPolicy) => { +const validateData = (user: User, data: AssetData, record?: Asset) => { + const policy = new AssetPolicy(user); + // Specialization of the policy where we disallow assets to be moved to another workgroup // if the current user is not an editor for that workgroup. if (!policy.canDoEverything() && !policy.hasRole(Role.Editor, data.workgroupId)) { throw new HttpException( "Can't move asset to a workgroup for which the user is not an editor", - HttpStatus.UNPROCESSABLE_ENTITY + HttpStatus.FORBIDDEN ); } + + // Specialization of the policy where we disallow the internal status to be changed to anything else than `tobechecked` + // if the current user is not a master-editor for the asset's current or future workgroup. + const hasInternalUseChanged = record == null || record.usage.internal.statusCode !== data.usage.internal.statusCode; + if ( + hasInternalUseChanged && + data.usage.internal.statusCode !== UsageStatusCode.ToBeChecked && + ((record != null && !policy.hasRole(Role.MasterEditor, record.workgroupId)) || + !policy.hasRole(Role.MasterEditor, data.workgroupId)) + ) { + throw new HttpException("Changing the asset's status is not allowed", HttpStatus.FORBIDDEN); + } }; diff --git a/apps/server-asset-sg/src/features/assets/prisma-asset.ts b/apps/server-asset-sg/src/features/assets/prisma-asset.ts index 1ac73140..65518142 100644 --- a/apps/server-asset-sg/src/features/assets/prisma-asset.ts +++ b/apps/server-asset-sg/src/features/assets/prisma-asset.ts @@ -1,5 +1,3 @@ -import { Prisma } from '@prisma/client'; - import { Asset, AssetInfo, @@ -9,9 +7,11 @@ import { AssetStudy, AssetStudyId, UsageStatusCode, -} from '@/features/assets/asset.model'; -import { StudyType } from '@/features/studies/study.model'; -import { LocalDate } from '@/utils/data/local-date'; +} from '@asset-sg/shared/v2'; +import { LocalDate } from '@asset-sg/shared/v2'; + +import { StudyType } from '@asset-sg/shared/v2'; +import { Prisma } from '@prisma/client'; import { satisfy } from '@/utils/define'; type SelectedAssetInfo = Prisma.AssetGetPayload<{ select: typeof assetInfoSelection }>; diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts index 51f45806..7234aff9 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts @@ -5,12 +5,12 @@ import { AssetSearchStats, AssetSearchStatsDTO, } from '@asset-sg/shared'; +import { User } from '@asset-sg/shared/v2'; import { Body, Controller, HttpCode, HttpStatus, Post, Query, ValidationPipe } from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; import { Authorize } from '@/core/decorators/authorize.decorator'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; -import { User } from '@/features/users/user.model'; @Controller('/assets/search') export class AssetSearchController { diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts index 5d59a06d..e5a1e29b 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts @@ -118,12 +118,13 @@ describe(AssetSearchService, () => { (text: T, setup: (asset: PatchAsset, text: T) => PatchAsset | Promise) => async () => { // Given + const user = fakeUser(); const patch = await setup(fakeAssetPatch(), text); - const asset = await create({ patch, user: fakeUser() }); - await create({ patch: fakeAssetPatch(), user: fakeUser() }); + const asset = await create({ patch, user }); + await create({ patch: fakeAssetPatch(), user }); // When - const result = await service.search({ text: `${text}` }); + const result = await service.search({ text: `${text}` }, user); // Then assertSingleResult(result, asset); @@ -161,21 +162,25 @@ describe(AssetSearchService, () => { it('finds assets by minimum createDate', async () => { // Given - const asset = await create({ patch: fakeAssetPatch(), user: fakeUser() }); + const user = fakeUser(); + const asset = await create({ patch: fakeAssetPatch(), user }); await create({ patch: { ...fakeAssetPatch(), createDate: dateIdFromDate(new Date(dateFromDateId(asset.createDate).getTime() - millisPerDay * 2)), }, - user: fakeUser(), + user, }); // When - const result = await service.search({ - createDate: { - min: new Date(dateFromDateId(asset.createDate).getTime() - millisPerDay), + const result = await service.search( + { + createDate: { + min: new Date(dateFromDateId(asset.createDate).getTime() - millisPerDay), + }, }, - }); + user + ); // Then assertSingleResult(result, asset); @@ -183,21 +188,25 @@ describe(AssetSearchService, () => { it('finds assets by maximum createDate', async () => { // Given - const asset = await create({ patch: fakeAssetPatch(), user: fakeUser() }); + const user = fakeUser(); + const asset = await create({ patch: fakeAssetPatch(), user }); await create({ patch: { ...fakeAssetPatch(), createDate: dateIdFromDate(new Date(dateFromDateId(asset.createDate).getTime() + millisPerDay * 2)), }, - user: fakeUser(), + user, }); // When - const result = await service.search({ - createDate: { - max: new Date(dateFromDateId(asset.createDate).getTime() + millisPerDay), + const result = await service.search( + { + createDate: { + max: new Date(dateFromDateId(asset.createDate).getTime() + millisPerDay), + }, }, - }); + user + ); // Then assertSingleResult(result, asset); @@ -205,6 +214,7 @@ describe(AssetSearchService, () => { it('finds assets by createDate range', async () => { // Given + const user = fakeUser(); const asset = await create({ patch: fakeAssetPatch(), user: fakeUser() }); await create({ patch: { @@ -222,12 +232,15 @@ describe(AssetSearchService, () => { }); // When - const result = await service.search({ - createDate: { - min: new Date(dateFromDateId(asset.createDate).getTime() - millisPerDay), - max: new Date(dateFromDateId(asset.createDate).getTime() + millisPerDay), + const result = await service.search( + { + createDate: { + min: new Date(dateFromDateId(asset.createDate).getTime() - millisPerDay), + max: new Date(dateFromDateId(asset.createDate).getTime() + millisPerDay), + }, }, - }); + user + ); // Then assertSingleResult(result, asset); @@ -238,6 +251,7 @@ describe(AssetSearchService, () => { const code1 = languageItems[0].languageItemCode; const code2 = languageItems[1].languageItemCode; const code3 = languageItems[2].languageItemCode; + const user = fakeUser(); const asset = await create({ patch: { ...fakeAssetPatch(), assetLanguages: [{ languageItemCode: code1 }] }, user: fakeUser(), @@ -252,7 +266,7 @@ describe(AssetSearchService, () => { }); // When - const result = await service.search({ languageItemCodes: [code1] }); + const result = await service.search({ languageItemCodes: [code1] }, user); // Then assertSingleResult(result, asset); @@ -263,12 +277,13 @@ describe(AssetSearchService, () => { const code1 = assetKindItems[0].assetKindItemCode; const code2 = assetKindItems[1].assetKindItemCode; const code3 = assetKindItems[2].assetKindItemCode; - const asset = await create({ patch: { ...fakeAssetPatch(), assetKindItemCode: code1 }, user: fakeUser() }); - await create({ patch: { ...fakeAssetPatch(), assetKindItemCode: code2 }, user: fakeUser() }); - await create({ patch: { ...fakeAssetPatch(), assetKindItemCode: code3 }, user: fakeUser() }); + const user = fakeUser(); + const asset = await create({ patch: { ...fakeAssetPatch(), assetKindItemCode: code1 }, user }); + await create({ patch: { ...fakeAssetPatch(), assetKindItemCode: code2 }, user }); + await create({ patch: { ...fakeAssetPatch(), assetKindItemCode: code3 }, user }); // When - const result = await service.search({ assetKindItemCodes: [code1] }); + const result = await service.search({ assetKindItemCodes: [code1] }, user); // Then assertSingleResult(result, asset); @@ -279,12 +294,13 @@ describe(AssetSearchService, () => { const code1 = manCatLabelItems[0].manCatLabelItemCode; const code2 = manCatLabelItems[1].manCatLabelItemCode; const code3 = manCatLabelItems[2].manCatLabelItemCode; - const asset = await create({ patch: { ...fakeAssetPatch(), manCatLabelRefs: [code1] }, user: fakeUser() }); - await create({ patch: { ...fakeAssetPatch(), manCatLabelRefs: [code2] }, user: fakeUser() }); - await create({ patch: { ...fakeAssetPatch(), manCatLabelRefs: [code3] }, user: fakeUser() }); + const user = fakeUser(); + const asset = await create({ patch: { ...fakeAssetPatch(), manCatLabelRefs: [code1] }, user }); + await create({ patch: { ...fakeAssetPatch(), manCatLabelRefs: [code2] }, user }); + await create({ patch: { ...fakeAssetPatch(), manCatLabelRefs: [code3] }, user }); // When - const result = await service.search({ manCatLabelItemCodes: [code1] }); + const result = await service.search({ manCatLabelItemCodes: [code1] }, user); // Then assertSingleResult(result, asset); @@ -293,13 +309,14 @@ describe(AssetSearchService, () => { it('finds assets by usageCode', async () => { // Given const usageCode: UsageCode = 'public'; + const user = fakeUser(); const asset = await create({ patch: { ...fakeAssetPatch(), publicUse: { ...fakeAssetUsage(), isAvailable: true }, internalUse: { ...fakeAssetUsage(), isAvailable: true }, }, - user: fakeUser(), + user, }); await create({ patch: { @@ -307,7 +324,7 @@ describe(AssetSearchService, () => { publicUse: { ...fakeAssetUsage(), isAvailable: false }, internalUse: { ...fakeAssetUsage(), isAvailable: true }, }, - user: fakeUser(), + user, }); await create({ patch: { @@ -315,11 +332,11 @@ describe(AssetSearchService, () => { publicUse: { ...fakeAssetUsage(), isAvailable: false }, internalUse: { ...fakeAssetUsage(), isAvailable: false }, }, - user: fakeUser(), + user, }); // When - const result = await service.search({ usageCodes: [usageCode] }); + const result = await service.search({ usageCodes: [usageCode] }, user); // Then assertSingleResult(result, asset); @@ -329,21 +346,22 @@ describe(AssetSearchService, () => { // Given const contact1 = await prisma.contact.create({ data: fakeContact() }); const contact2 = await prisma.contact.create({ data: fakeContact() }); + const user = fakeUser(); const asset = await create({ patch: { ...fakeAssetPatch(), assetContacts: [{ contactId: contact1.contactId, role: 'author' }] }, - user: fakeUser(), + user, }); await create({ patch: { ...fakeAssetPatch(), assetContacts: [{ contactId: contact1.contactId, role: 'supplier' }] }, - user: fakeUser(), + user, }); await create({ patch: { ...fakeAssetPatch(), assetContacts: [{ contactId: contact2.contactId, role: 'author' }] }, - user: fakeUser(), + user, }); // When - const result = await service.search({ authorId: contact1.contactId }); + const result = await service.search({ authorId: contact1.contactId }, user); // Then assertSingleResult(result, asset); @@ -390,7 +408,8 @@ describe(AssetSearchService, () => { it('returns empty stats when no assets are present', async () => { // When - const result = await service.aggregate({}); + const user = fakeUser(); + const result = await service.aggregate({}, user); // Then expect(result.total).toEqual(0); @@ -404,10 +423,11 @@ describe(AssetSearchService, () => { it('aggregates stats for a single asset', async () => { // Given - const asset = await create({ patch: fakeAssetPatch(), user: fakeUser() }); + const user = fakeUser(); + const asset = await create({ patch: fakeAssetPatch(), user }); // When - const result = await service.aggregate({}); + const result = await service.aggregate({}, user); // Then assertSingleStats(result, asset); diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts index a890ec18..07b61538 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts @@ -19,6 +19,9 @@ import { UsageCode, ValueCount, } from '@asset-sg/shared'; +import { AssetId } from '@asset-sg/shared/v2'; +import { StudyId } from '@asset-sg/shared/v2'; +import { User } from '@asset-sg/shared/v2'; import { Client as ElasticsearchClient } from '@elastic/elasticsearch'; import { BulkOperationContainer, @@ -33,12 +36,9 @@ import proj4 from 'proj4'; // eslint-disable-next-line @nx/enforce-module-boundaries import indexMapping from '../../../../../../development/init/elasticsearch/mappings/swissgeol_asset_asset.json'; -import { AssetId } from '../asset.model'; import { PrismaService } from '@/core/prisma.service'; import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; -import { StudyId } from '@/features/studies/study.model'; import { StudyRepo } from '@/features/studies/study.repo'; -import { User } from '@/features/users/user.model'; const INDEX = 'swissgeol_asset_asset'; export { INDEX as ASSET_ELASTIC_INDEX }; diff --git a/apps/server-asset-sg/src/features/contacts/contact.repo.ts b/apps/server-asset-sg/src/features/contacts/contact.repo.ts index e704fc06..65e4bd47 100644 --- a/apps/server-asset-sg/src/features/contacts/contact.repo.ts +++ b/apps/server-asset-sg/src/features/contacts/contact.repo.ts @@ -1,9 +1,9 @@ +import { Contact, ContactData, ContactId } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '@/core/prisma.service'; import { Repo, RepoListOptions } from '@/core/repo'; -import { Contact, ContactData, ContactId } from '@/features/contacts/contact.model'; import { satisfy } from '@/utils/define'; import { handlePrismaMutationError } from '@/utils/prisma'; diff --git a/apps/server-asset-sg/src/features/contacts/contacts.controller.ts b/apps/server-asset-sg/src/features/contacts/contacts.controller.ts index e26c44fe..cef8c5dd 100644 --- a/apps/server-asset-sg/src/features/contacts/contacts.controller.ts +++ b/apps/server-asset-sg/src/features/contacts/contacts.controller.ts @@ -1,32 +1,35 @@ -import { Controller, HttpCode, HttpException, HttpStatus, Post, Put } from '@nestjs/common'; -import { Authorize } from '@/core/decorators/authorize.decorator'; -import { Authorized } from '@/core/decorators/authorized.decorator'; -import { Boundary } from '@/core/decorators/boundary.decorator'; -import { UsePolicy } from '@/core/decorators/use-policy.decorator'; -import { UseRepo } from '@/core/decorators/use-repo.decorator'; -import { Contact, ContactData, ContactDataBoundary } from '@/features/contacts/contact.model'; -import { ContactPolicy } from '@/features/contacts/contact.policy'; +import { Contact, ContactData } from '@asset-sg/shared/v2'; +import { User } from '@asset-sg/shared/v2'; +import { ContactPolicy } from '@asset-sg/shared/v2'; +import { ContactDataSchema } from '@asset-sg/shared/v2'; +import { Controller, HttpCode, HttpException, HttpStatus, Param, ParseIntPipe, Post, Put } from '@nestjs/common'; +import { authorize } from '@/core/authorize'; +import { CurrentUser } from '@/core/decorators/current-user.decorator'; +import { ParseBody } from '@/core/decorators/parse.decorator'; import { ContactRepo } from '@/features/contacts/contact.repo'; @Controller('/contacts') -@UseRepo(ContactRepo) -@UsePolicy(ContactPolicy) export class ContactsController { constructor(private readonly contactRepo: ContactRepo) {} @Post('/') - @Authorize.Create() @HttpCode(HttpStatus.CREATED) - create(@Boundary(ContactDataBoundary) data: ContactData): Promise { + create(@ParseBody(ContactDataSchema) data: ContactData, @CurrentUser() user: User): Promise { + authorize(ContactPolicy, user).canCreate(); return this.contactRepo.create(data); } @Put('/:id') - @Authorize.Update({ id: Number }) async update( - @Authorized.Record() record: Contact, - @Boundary(ContactDataBoundary) data: ContactData + @Param('id', ParseIntPipe) id: number, + @ParseBody(ContactDataSchema) data: ContactData, + @CurrentUser() user: User ): Promise { + const record = await this.contactRepo.find(id); + if (record == null) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + authorize(ContactPolicy, user).canUpdate(record); const contact = await this.contactRepo.update(record.id, data); if (contact == null) { throw new HttpException('not found', 404); diff --git a/apps/server-asset-sg/src/features/favorites/favorite.repo.ts b/apps/server-asset-sg/src/features/favorites/favorite.repo.ts index 92449a2c..6b3220ba 100644 --- a/apps/server-asset-sg/src/features/favorites/favorite.repo.ts +++ b/apps/server-asset-sg/src/features/favorites/favorite.repo.ts @@ -1,10 +1,10 @@ +import { Favorite } from '@asset-sg/shared/v2'; +import { UserId } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '@/core/prisma.service'; import { CreateRepo, DeleteRepo, ReadRepo, RepoListOptions } from '@/core/repo'; -import { Favorite } from '@/features/favorites/favorite.model'; -import { UserId } from '@/features/users/user.model'; import { satisfy } from '@/utils/define'; import { handlePrismaMutationError } from '@/utils/prisma'; diff --git a/apps/server-asset-sg/src/features/favorites/favorites.controller.ts b/apps/server-asset-sg/src/features/favorites/favorites.controller.ts index 668011f1..f1356277 100644 --- a/apps/server-asset-sg/src/features/favorites/favorites.controller.ts +++ b/apps/server-asset-sg/src/features/favorites/favorites.controller.ts @@ -1,11 +1,11 @@ import { SearchAssetResult, SearchAssetResultEmpty } from '@asset-sg/shared'; +import { User } from '@asset-sg/shared/v2'; import { Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Put } from '@nestjs/common'; import { Authorize } from '@/core/decorators/authorize.decorator'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; import { FavoriteRepo } from '@/features/favorites/favorite.repo'; -import { User } from '@/features/users/user.model'; import { define } from '@/utils/define'; @Controller('/users/current/favorites') diff --git a/apps/server-asset-sg/src/features/files/files.controller.ts b/apps/server-asset-sg/src/features/files/files.controller.ts index 27124134..53adf9df 100644 --- a/apps/server-asset-sg/src/features/files/files.controller.ts +++ b/apps/server-asset-sg/src/features/files/files.controller.ts @@ -1,12 +1,15 @@ -import { AssetEditDetail } from '@asset-sg/shared'; +import { User } from '@asset-sg/shared/v2'; +import { AssetEditPolicy } from '@asset-sg/shared/v2'; import { Controller, Delete, Get, HttpException, + HttpStatus, Param, ParseIntPipe, Post, + Req, Res, UploadedFile, UseInterceptors, @@ -14,27 +17,29 @@ import { import { FileInterceptor } from '@nestjs/platform-express'; import { Response } from 'express'; import * as E from 'fp-ts/Either'; -import { Authorize } from '@/core/decorators/authorize.decorator'; -import { Authorized } from '@/core/decorators/authorized.decorator'; +import { authorize } from '@/core/authorize'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; -import { UsePolicy } from '@/core/decorators/use-policy.decorator'; -import { UseRepo } from '@/core/decorators/use-repo.decorator'; import { PrismaService } from '@/core/prisma.service'; -import { AssetEditPolicy } from '@/features/asset-edit/asset-edit.policy'; import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; import { AssetEditService } from '@/features/asset-edit/asset-edit.service'; -import { User } from '@/features/users/user.model'; import { getFile } from '@/utils/file/get-file'; @Controller('/files') -@UseRepo(AssetEditRepo) -@UsePolicy(AssetEditPolicy) export class FilesController { - constructor(private readonly assetEditService: AssetEditService, private readonly prismaService: PrismaService) {} + constructor( + private readonly assetEditService: AssetEditService, + private readonly assetEditRepo: AssetEditRepo, + private readonly prismaService: PrismaService + ) {} @Get('/:id') - @Authorize.Show({ id: Number }, (repo: AssetEditRepo) => repo.findByFile) - async download(@Res() res: Response, @Param('id', ParseIntPipe) id: number) { + async download(@Res() res: Response, @Param('id', ParseIntPipe) id: number, @CurrentUser() user: User) { + const asset = await this.assetEditRepo.findByFile(id); + if (asset == null) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + authorize(AssetEditPolicy, user).canShow(asset); + const result = await getFile(this.prismaService, id)(); if (E.isLeft(result)) { throw new HttpException(result.left.message, 500); @@ -51,13 +56,18 @@ export class FilesController { } @Post('/') - @Authorize.Update((req) => parseInt(req.body.assetId)) @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 250 * 1024 * 1024 } })) - async upload( - @UploadedFile() file: Express.Multer.File, - @Authorized.Record() asset: AssetEditDetail, - @CurrentUser() user: User - ) { + async upload(@Req() req: Request, @UploadedFile() file: Express.Multer.File, @CurrentUser() user: User) { + const assetId = parseInt((req.body as { assetId?: string }).assetId ?? ''); + if (isNaN(assetId)) { + throw new HttpException('missing assetId', HttpStatus.BAD_REQUEST); + } + const asset = await this.assetEditRepo.find(assetId); + if (asset == null) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + authorize(AssetEditPolicy, user).canUpdate(asset); + const result = await this.assetEditService.uploadFile(user, asset.assetId, { name: file.originalname, buffer: file.buffer, @@ -71,12 +81,13 @@ export class FilesController { } @Delete('/:id') - @Authorize.Delete({ id: Number }, (repo: AssetEditRepo) => repo.findByFile) - async delete( - @Param('id', ParseIntPipe) id: number, - @Authorized.Record() asset: AssetEditDetail, - @CurrentUser() user: User - ) { + async delete(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User) { + const asset = await this.assetEditRepo.findByFile(id); + if (asset == null) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + authorize(AssetEditPolicy, user).canDelete(asset); + const e = await this.assetEditService.deleteFile(user, asset.assetId, id)(); if (E.isLeft(e)) { throw new HttpException(e.left.message, 500); diff --git a/apps/server-asset-sg/src/features/studies/studies.controller.ts b/apps/server-asset-sg/src/features/studies/studies.controller.ts index 6d95c20a..fbccb92d 100644 --- a/apps/server-asset-sg/src/features/studies/studies.controller.ts +++ b/apps/server-asset-sg/src/features/studies/studies.controller.ts @@ -1,11 +1,11 @@ import { Readable } from 'stream'; +import { serializeStudyAsCsv, Study } from '@asset-sg/shared/v2'; +import { User } from '@asset-sg/shared/v2'; import { Controller, Get, Res } from '@nestjs/common'; import { Response } from 'express'; import { Authorize } from '@/core/decorators/authorize.decorator'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; -import { serializeStudyAsCsv, Study } from '@/features/studies/study.model'; import { StudyRepo } from '@/features/studies/study.repo'; -import { User } from '@/features/users/user.model'; @Controller('/studies') export class StudiesController { diff --git a/apps/server-asset-sg/src/features/studies/study.repo.ts b/apps/server-asset-sg/src/features/studies/study.repo.ts index d73ebb81..e9b6c14f 100644 --- a/apps/server-asset-sg/src/features/studies/study.repo.ts +++ b/apps/server-asset-sg/src/features/studies/study.repo.ts @@ -1,10 +1,10 @@ import { parseLV95 } from '@asset-sg/shared'; +import { Study, StudyId } from '@asset-sg/shared/v2'; +import { WorkgroupId } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '@/core/prisma.service'; import { ReadRepo, RepoListOptions } from '@/core/repo'; -import { Study, StudyId } from '@/features/studies/study.model'; -import { WorkgroupId } from '@/features/workgroups/workgroup.model'; @Injectable() export class StudyRepo implements ReadRepo { @@ -38,7 +38,7 @@ export class StudyRepo implements ReadRepo { s.study_id IN (${Prisma.join(ids, ',')}) `); } - if (conditions.length != null) { + if (conditions.length != 0) { parts.push(Prisma.sql`WHERE ${Prisma.join(conditions, ' AND ')}`); } parts.push(Prisma.sql` diff --git a/apps/server-asset-sg/src/features/users/user.model.ts b/apps/server-asset-sg/src/features/users/user.model.ts deleted file mode 100644 index b4e6acd0..00000000 --- a/apps/server-asset-sg/src/features/users/user.model.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IsArray, IsBoolean, IsString } from 'class-validator'; -import { Role } from '@/features/workgroups/workgroup.model'; -import { Data, Model } from '@/utils/data/model'; - -export interface User extends Model { - email: string; - lang: string; - workgroups: WorkgroupOnUser[]; - isAdmin: boolean; -} - -export interface WorkgroupOnUser { - id: number; - role: Role; -} - -export type UserId = string; -export type UserData = Omit, 'email'>; - -export class UserDataBoundary implements UserData { - @IsString() - lang!: string; - - @IsArray() - workgroups!: WorkgroupOnUser[]; - - @IsBoolean() - isAdmin!: boolean; -} diff --git a/apps/server-asset-sg/src/features/users/user.repo.ts b/apps/server-asset-sg/src/features/users/user.repo.ts index ef7ce951..cdbdc28b 100644 --- a/apps/server-asset-sg/src/features/users/user.repo.ts +++ b/apps/server-asset-sg/src/features/users/user.repo.ts @@ -1,9 +1,9 @@ +import { User, UserData, UserId } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '@/core/prisma.service'; import { Repo, RepoListOptions } from '@/core/repo'; -import { User, UserData, UserId } from '@/features/users/user.model'; import { satisfy } from '@/utils/define'; import { handlePrismaMutationError } from '@/utils/prisma'; diff --git a/apps/server-asset-sg/src/features/users/users.controller.ts b/apps/server-asset-sg/src/features/users/users.controller.ts index 792d8abe..d951cfbf 100644 --- a/apps/server-asset-sg/src/features/users/users.controller.ts +++ b/apps/server-asset-sg/src/features/users/users.controller.ts @@ -1,19 +1,9 @@ -import { - Body, - Controller, - Delete, - Get, - HttpCode, - HttpException, - HttpStatus, - Param, - Put, - ValidationPipe, -} from '@nestjs/common'; - +import { User, UserData, UserId } from '@asset-sg/shared/v2'; +import { UserDataSchema } from '@asset-sg/shared/v2'; +import { Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Param, Put } from '@nestjs/common'; import { Authorize } from '@/core/decorators/authorize.decorator'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; -import { User, UserDataBoundary, UserId } from '@/features/users/user.model'; +import { ParseBody } from '@/core/decorators/parse.decorator'; import { UserRepo } from '@/features/users/user.repo'; @Controller('/users') @@ -35,8 +25,8 @@ export class UsersController { @Authorize.Admin() async update( @Param('id') id: UserId, - @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) - data: UserDataBoundary + @ParseBody(UserDataSchema) + data: UserData ): Promise { const user = await this.userRepo.update(id, data); if (user === null) { diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup-simple.repo.ts b/apps/server-asset-sg/src/features/workgroups/workgroup-simple.repo.ts new file mode 100644 index 00000000..77cc56a8 --- /dev/null +++ b/apps/server-asset-sg/src/features/workgroups/workgroup-simple.repo.ts @@ -0,0 +1,54 @@ +import { User } from '@asset-sg/shared'; +import { Prisma } from '@prisma/client'; +import { UserId } from '@shared/models/user'; +import { Role, SimpleWorkgroup, WorkgroupId } from '@shared/models/workgroup'; +import { PrismaService } from '@/core/prisma.service'; +import { ReadRepo, RepoListOptions } from '@/core/repo'; +import { satisfy } from '@/utils/define'; + +export class SimpleWorkgroupRepo implements ReadRepo { + constructor(private readonly prisma: PrismaService, private readonly user: User) {} + + async find(id: WorkgroupId): Promise { + const entry = await this.prisma.workgroup.findFirst({ + where: { id, users: this.user.isAdmin ? undefined : { some: { userId: this.user.id } } }, + select: simpleWorkgroupSelection(this.user.id), + }); + return entry == null ? null : parse(entry, this.user.isAdmin); + } + + async list({ limit, offset, ids }: RepoListOptions = {}): Promise { + const entries = await this.prisma.workgroup.findMany({ + where: { + id: ids == null ? undefined : { in: ids }, + users: this.user.isAdmin ? undefined : { some: { userId: this.user.id } }, + }, + take: limit, + skip: offset, + select: simpleWorkgroupSelection(this.user.id), + }); + return entries.map((it) => parse(it, this.user.isAdmin)); + } +} + +export const simpleWorkgroupSelection = (userId: UserId) => + satisfy()({ + id: true, + name: true, + users: { + where: { + userId, + }, + select: { + role: true, + }, + }, + }); + +type SelectedWorkgroup = Prisma.WorkgroupGetPayload<{ select: ReturnType }>; + +const parse = (data: SelectedWorkgroup, isAdmin: boolean): SimpleWorkgroup => ({ + id: data.id, + name: data.name, + role: isAdmin ? Role.MasterEditor : data.users[0].role, +}); diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.controller.ts b/apps/server-asset-sg/src/features/workgroups/workgroup.controller.ts deleted file mode 100644 index 30e6d9db..00000000 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.controller.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Post, Put } from '@nestjs/common'; -import { Authorize } from '@/core/decorators/authorize.decorator'; -import { Authorized } from '@/core/decorators/authorized.decorator'; -import { Boundary } from '@/core/decorators/boundary.decorator'; -import { CurrentUser } from '@/core/decorators/current-user.decorator'; -import { UsePolicy } from '@/core/decorators/use-policy.decorator'; -import { UseRepo } from '@/core/decorators/use-repo.decorator'; -import { User } from '@/features/users/user.model'; -import { Workgroup, WorkgroupData, WorkgroupDataBoundary } from '@/features/workgroups/workgroup.model'; -import { WorkgroupPolicy } from '@/features/workgroups/workgroup.policy'; -import { WorkgroupRepo } from '@/features/workgroups/workgroup.repo'; - -@Controller('/workgroups') -@UsePolicy(WorkgroupPolicy) -@UseRepo(WorkgroupRepo) -export class WorkgroupController { - constructor(private readonly workgroupRepo: WorkgroupRepo) {} - - @Get('/') - @Authorize.User() - async list(@CurrentUser() user: User): Promise { - return this.workgroupRepo.list({ ids: user.isAdmin ? undefined : user.workgroups.map((it) => it.id) }); - } - - @Get('/:id') - @Authorize.Show({ id: Number }) - async show(@Authorized.Record() workgroup: Workgroup): Promise { - return workgroup; - } - - @Post('/') - @Authorize.Create() - @HttpCode(HttpStatus.CREATED) - async create( - @Boundary(WorkgroupDataBoundary) - data: WorkgroupDataBoundary - ): Promise { - return this.workgroupRepo.create(data); - } - - @Put('/:id') - @Authorize.Update({ id: Number }) - async update( - @Authorized.Record() record: Workgroup, - @Boundary(WorkgroupDataBoundary) - data: WorkgroupData - ): Promise { - const workgroup = await this.workgroupRepo.update(record.id, data); - if (workgroup === null) { - throw new HttpException('not found', 404); - } - return workgroup; - } - - @Delete('/:id') - @Authorize.Delete({ id: Number }) - @HttpCode(HttpStatus.NO_CONTENT) - async delete(@Authorized.Record() record: Workgroup): Promise { - await this.workgroupRepo.delete(record.id); - } -} diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts index dfc844a5..7b0a6165 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts +++ b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts @@ -1,12 +1,11 @@ +import { WorkgroupData, Role } from '@asset-sg/shared/v2'; import { faker } from '@faker-js/faker'; // eslint-disable-next-line @nx/enforce-module-boundaries import { clearPrismaAssets, setupDB, setupDefaultWorkgroup } from '../../../../../test/setup-db'; import { PrismaService } from '@/core/prisma.service'; import { fakeAssetPatch, fakeUser } from '@/features/asset-edit/asset-edit.fake'; import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; -import { Role as UserRole } from '@/features/users/user.model'; import { UserRepo } from '@/features/users/user.repo'; -import { WorkgroupData, Role } from '@/features/workgroups/workgroup.model'; import { WorkgroupRepo } from '@/features/workgroups/workgroup.repo'; describe('WorkgroupRepo', () => { @@ -112,14 +111,13 @@ describe('WorkgroupRepo', () => { lang: 'de', oidcId: faker.string.uuid(), isAdmin: false, - role: UserRole.Viewer, workgroups: [], }); const data: WorkgroupData = { name: 'test', disabled_at: null, assets: [asset], - users: [{ userId: user.id, role: 'MasterEditor' }], + users: [{ userId: user.id, role: Role.MasterEditor }], }; // When @@ -153,7 +151,6 @@ describe('WorkgroupRepo', () => { lang: 'de', oidcId: faker.string.uuid(), isAdmin: false, - role: UserRole.Viewer, workgroups: [], }); const initialWorkgroup: WorkgroupData = { name: 'test', disabled_at: null, assets: [], users: [] }; @@ -192,14 +189,13 @@ describe('WorkgroupRepo', () => { lang: 'de', oidcId: faker.string.uuid(), isAdmin: false, - role: UserRole.Viewer, workgroups: [], }); const data: WorkgroupData = { name: 'test', disabled_at: null, assets: [{ assetId: asset.assetId }], - users: [{ userId: user.id, role: 'MasterEditor' }], + users: [{ userId: user.id, role: Role.MasterEditor }], }; const workgroup = await repo.create(data); diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts index 5e51ef27..e62229a5 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts +++ b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts @@ -1,8 +1,10 @@ +import { User } from '@asset-sg/shared'; +import { Workgroup, WorkgroupData, WorkgroupId } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '@/core/prisma.service'; import { Repo, RepoListOptions } from '@/core/repo'; -import { Workgroup, WorkgroupData, WorkgroupId } from '@/features/workgroups/workgroup.model'; +import { SimpleWorkgroupRepo } from '@/features/workgroups/workgroup-simple.repo'; import { satisfy } from '@/utils/define'; import { handlePrismaMutationError } from '@/utils/prisma'; @@ -10,6 +12,10 @@ import { handlePrismaMutationError } from '@/utils/prisma'; export class WorkgroupRepo implements Repo { constructor(private readonly prisma: PrismaService) {} + simple(user: User): SimpleWorkgroupRepo { + return new SimpleWorkgroupRepo(this.prisma, user); + } + find(id: WorkgroupId): Promise { return this.prisma.workgroup.findUnique({ where: { id }, diff --git a/apps/server-asset-sg/src/features/workgroups/workgroups.controller.ts b/apps/server-asset-sg/src/features/workgroups/workgroups.controller.ts new file mode 100644 index 00000000..d090fbbd --- /dev/null +++ b/apps/server-asset-sg/src/features/workgroups/workgroups.controller.ts @@ -0,0 +1,103 @@ +import { + AssetId, + SimpleWorkgroup, + User, + Workgroup, + WorkgroupData, + WorkgroupDataBoundary, + WorkgroupId, + WorkgroupPolicy, +} from '@asset-sg/shared/v2'; +import { + Controller, + Delete, + Get, + HttpCode, + HttpException, + HttpStatus, + Param, + ParseIntPipe, + Post, + Put, + Query, +} from '@nestjs/common'; +import { Expose, Transform as TransformValue } from 'class-transformer'; +import { IsBoolean } from 'class-validator'; +import { authorize } from '@/core/authorize'; +import { Authorize } from '@/core/decorators/authorize.decorator'; +import { CurrentUser } from '@/core/decorators/current-user.decorator'; +import { ParseBody } from '@/core/decorators/parse.decorator'; +import { RepoListOptions } from '@/core/repo'; +import { WorkgroupRepo } from '@/features/workgroups/workgroup.repo'; + +class ListQuery { + @Expose({ name: 'simple' }) + @TransformValue(({ value }) => value != null && value !== 'false', { toClassOnly: true }) + @IsBoolean() + isSimple!: boolean; +} + +@Controller('/workgroups') +export class WorkgroupsController { + constructor(private readonly workgroupRepo: WorkgroupRepo) {} + + @Get('/') + @Authorize.User() + async list(@CurrentUser() user: User, @Query() query: ListQuery): Promise { + const options: RepoListOptions = { + ids: user.isAdmin ? undefined : user.workgroups.map((it) => it.id), + }; + return query.isSimple ? this.workgroupRepo.simple(user).list(options) : this.workgroupRepo.list(options); + } + + @Get('/:id') + async show(@Param('id', ParseIntPipe) id: AssetId, @CurrentUser() user: User): Promise { + const record = await this.workgroupRepo.find(id); + if (record === null) { + throw new HttpException('not found', 404); + } + authorize(WorkgroupPolicy, user).canShow(record); + return record; + } + + @Post('/') + @HttpCode(HttpStatus.CREATED) + async create( + @ParseBody(WorkgroupDataBoundary) + data: WorkgroupData, + @CurrentUser() user: User + ): Promise { + authorize(WorkgroupPolicy, user).canCreate(); + return this.workgroupRepo.create(data); + } + + @Put('/:id') + async update( + @Param('id', ParseIntPipe) id: number, + @ParseBody(WorkgroupDataBoundary) + data: WorkgroupData, + @CurrentUser() user: User + ): Promise { + const record = await this.workgroupRepo.find(id); + if (record === null) { + throw new HttpException('not found', 404); + } + authorize(WorkgroupPolicy, user).canUpdate(record); + const workgroup = await this.workgroupRepo.update(record.id, data); + if (workgroup === null) { + throw new HttpException('not found', 404); + } + return workgroup; + } + + @Delete('/:id') + @HttpCode(HttpStatus.NO_CONTENT) + async delete(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User): Promise { + const record = await this.workgroupRepo.find(id); + if (record == null) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + authorize(WorkgroupPolicy, user).canDelete(record); + await this.workgroupRepo.delete(record.id); + } +} diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.http b/apps/server-asset-sg/src/features/workgroups/workgroups.http similarity index 91% rename from apps/server-asset-sg/src/features/workgroups/workgroup.http rename to apps/server-asset-sg/src/features/workgroups/workgroups.http index 38a07e84..2afc77af 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.http +++ b/apps/server-asset-sg/src/features/workgroups/workgroups.http @@ -8,6 +8,11 @@ Authorization: Impersonate {{user}} GET {{host}}/api/workgroups Authorization: Impersonate {{user}} +### List simple workgroups +GET {{host}}/api/workgroups?simple +Authorization: Impersonate {{user}} + + ### Create workgroup POST {{host}}/api/workgroups Authorization: Impersonate {{user}} diff --git a/apps/server-asset-sg/src/main.ts b/apps/server-asset-sg/src/main.ts index 364783ef..db70eb6c 100644 --- a/apps/server-asset-sg/src/main.ts +++ b/apps/server-asset-sg/src/main.ts @@ -1,4 +1,4 @@ -import { Logger } from '@nestjs/common'; +import { Logger, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; @@ -12,6 +12,7 @@ const API_PORT = process.env.PORT || 3333; async function bootstrap(): Promise { const app = await NestFactory.create(AppModule); app.setGlobalPrefix(API_PREFIX); + app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })); await app.listen(API_PORT); Logger.log(`🚀 application is running on: http://localhost:${API_PORT}/${API_PREFIX}`); } diff --git a/apps/server-asset-sg/src/models/jwt-request.ts b/apps/server-asset-sg/src/models/jwt-request.ts index 143ffa53..64e81876 100644 --- a/apps/server-asset-sg/src/models/jwt-request.ts +++ b/apps/server-asset-sg/src/models/jwt-request.ts @@ -1,9 +1,8 @@ +import { User } from '@asset-sg/shared/v2'; +import { Policy } from '@asset-sg/shared/v2'; import { Request } from 'express'; import * as jwt from 'jsonwebtoken'; -import { Policy } from '@/core/policy'; -import { User } from '@/features/users/user.model'; - export interface JwtRequest extends Request { user: User; accessToken: string; diff --git a/apps/server-asset-sg/tsconfig.json b/apps/server-asset-sg/tsconfig.json index 9ee0f6b7..093e49da 100644 --- a/apps/server-asset-sg/tsconfig.json +++ b/apps/server-asset-sg/tsconfig.json @@ -24,6 +24,7 @@ "@asset-sg/favourite": ["../../libs/favourite/src/index.ts"], "@asset-sg/profile": ["../../libs/profile/src/index.ts"], "@asset-sg/shared": ["../../libs/shared/src/index.ts"], + "@asset-sg/shared/v2": ["../../libs/shared/v2/src/index.ts"], "ngx-kobalte": ["../../libs/ngx-kobalte/src/index.ts"] } } diff --git a/jest.config.ts b/jest.config.ts index d0dbd1b8..304f68c0 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,5 +1,7 @@ import { getJestProjects } from '@nx/jest'; +import { Config } from 'jest'; -export default { - projects: getJestProjects(), +const config: Config = { + projects: [...getJestProjects(), 'lib/shared/v2'], }; +export default config; diff --git a/libs/admin/src/lib/components/user-expanded/user-expanded.component.html b/libs/admin/src/lib/components/user-expanded/user-expanded.component.html index 7bbe4d03..f01b3502 100644 --- a/libs/admin/src/lib/components/user-expanded/user-expanded.component.html +++ b/libs/admin/src/lib/components/user-expanded/user-expanded.component.html @@ -20,13 +20,6 @@ - - - Admin - Viewer - Editor - Master Editor -
    Sprache
    Sprache diff --git a/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts b/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts index 52ea8f6e..e5102385 100644 --- a/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts +++ b/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts @@ -70,7 +70,6 @@ export class UserExpandedComponent { if (value) { this.editForm.patchValue({ email: value.email, - role: value.role, lang: value.lang, }); } @@ -90,11 +89,11 @@ export class UserExpandedComponent { submit() { this.disableEverything(); if (this.user) { - const { role, lang } = this.editForm.getRawValue(); + const { lang } = this.editForm.getRawValue(); - if (!role || !lang) return; + if (!lang) return; - this.userExpandedOutput.emit(UserExpandedOutput.of.userEdited({ ...this.user, role, lang })); + this.userExpandedOutput.emit(UserExpandedOutput.of.userEdited({ ...this.user, lang })); } } diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts index c041cb07..77368ec1 100644 --- a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts @@ -24,7 +24,6 @@ export class WorkgroupEditComponent implements OnInit { public ngOnInit() { this.initializeForm(); - console.log(this.workgroup); } public initializeForm() { diff --git a/libs/admin/src/lib/components/workgroups/workgroups.component.ts b/libs/admin/src/lib/components/workgroups/workgroups.component.ts index c756b4eb..3546df7d 100644 --- a/libs/admin/src/lib/components/workgroups/workgroups.component.ts +++ b/libs/admin/src/lib/components/workgroups/workgroups.component.ts @@ -26,7 +26,6 @@ export class WorkgroupsComponent implements OnInit, OnDestroy { } public selectWorkgroup(workgroup: Workgroup): void { - console.log('here'); this.selectedWorkgroup = workgroup; } diff --git a/libs/asset-editor/src/lib/asset-editor.module.ts b/libs/asset-editor/src/lib/asset-editor.module.ts index 6657b89c..6b0c3043 100644 --- a/libs/asset-editor/src/lib/asset-editor.module.ts +++ b/libs/asset-editor/src/lib/asset-editor.module.ts @@ -1,7 +1,7 @@ import { A11yModule } from '@angular/cdk/a11y'; import { DialogModule } from '@angular/cdk/dialog'; import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; +import { inject, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -12,7 +12,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatSelectModule } from '@angular/material/select'; -import { CanDeactivateFn, RouterModule } from '@angular/router'; +import { CanActivateFn, CanDeactivateFn, RouterModule } from '@angular/router'; import { AnchorComponent, ButtonComponent, @@ -21,23 +21,30 @@ import { DatepickerToggleIconComponent, DrawerComponent, DrawerPanelComponent, - IsNotMasterEditorPipe, MatDateIdModule, ValueItemDescriptionPipe, ValueItemNamePipe, ViewChildMarker, + fromAppShared, + AdminOnlyDirective, } from '@asset-sg/client-shared'; +import { isNotNull, ORD } from '@asset-sg/core'; +import { AssetEditPolicy } from '@asset-sg/shared/v2'; +import * as RD from '@devexperts/remote-data-ts'; import { SvgIconComponent } from '@ngneat/svg-icon'; import { EffectsModule } from '@ngrx/effects'; -import { StoreModule } from '@ngrx/store'; +import { Store, StoreModule } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; import { ForModule } from '@rx-angular/template/for'; import { LetModule } from '@rx-angular/template/let'; import { PushModule } from '@rx-angular/template/push'; import { de } from 'date-fns/locale/de'; +import * as O from 'fp-ts/Option'; +import { combineLatest, filter, map, tap, withLatestFrom } from 'rxjs'; import { AssetEditorLaunchComponent } from './components/asset-editor-launch'; import { AssetEditorPageComponent } from './components/asset-editor-page'; +import { AssetEditorSyncComponent } from './components/asset-editor-sync/asset-editor-sync.component'; import { AssetEditorTabAdministrationComponent, ReplaceBrPipe } from './components/asset-editor-tab-administration'; import { AssetEditorTabContactsComponent } from './components/asset-editor-tab-contacts'; import { AssetEditorTabGeneralComponent } from './components/asset-editor-tab-general'; @@ -49,12 +56,14 @@ import { AssetMultiselectComponent } from './components/asset-multiselect'; import { Lv95xWithoutPrefixPipe, Lv95yWithoutPrefixPipe } from './components/lv95-without-prefix'; import { AssetEditorEffects } from './state/asset-editor.effects'; import { assetEditorReducer } from './state/asset-editor.reducer'; +import * as fromAssetEditor from './state/asset-editor.selectors'; export const canLeaveEdit: CanDeactivateFn = (c) => c.canLeave(); @NgModule({ declarations: [ AssetEditorLaunchComponent, + AssetEditorSyncComponent, AssetEditorPageComponent, AssetEditorTabAdministrationComponent, AssetEditorTabContactsComponent, @@ -74,6 +83,20 @@ export const canLeaveEdit: CanDeactivateFn = (c) => c. path: '', pathMatch: 'full', component: AssetEditorLaunchComponent, + + // Only users that can create assets are permitted to see the asset admin page. + canActivate: [ + (() => { + const store = inject(Store); + return store.select(fromAppShared.selectUser).pipe( + filter(isNotNull), + map((user) => { + const policy = new AssetEditPolicy(user); + return policy.canDoEverything() || policy.canCreate(); + }) + ); + }) as CanActivateFn, + ], }, { path: ':assetId', @@ -89,6 +112,28 @@ export const canLeaveEdit: CanDeactivateFn = (c) => c. canDeactivate: [canLeaveEdit], }, ], + + // Only users that can create new assets are permitted to access the new asset page. + // Only users that can edit the selected asset are permitted to access the edit asset page. + canActivate: [ + (() => { + const store = inject(Store); + return combineLatest([ + store + .select(fromAssetEditor.selectRDAssetEditDetail) + .pipe(map(RD.toNullable), filter(isNotNull), map(O.toNullable)), + store.select(fromAppShared.selectUser).pipe(filter(isNotNull)), + ]).pipe( + map(([assetEditDetail, user]) => { + const policy = new AssetEditPolicy(user); + return ( + policy.canDoEverything() || + (assetEditDetail == null ? policy.canCreate() : policy.canUpdate(assetEditDetail)) + ); + }) + ); + }) as CanActivateFn, + ], }, ]), TranslateModule.forChild(), @@ -117,7 +162,6 @@ export const canLeaveEdit: CanDeactivateFn = (c) => c. DrawerComponent, DrawerPanelComponent, DatepickerToggleIconComponent, - IsNotMasterEditorPipe, MatAutocompleteModule, MatCheckboxModule, @@ -128,6 +172,7 @@ export const canLeaveEdit: CanDeactivateFn = (c) => c. MatMenuModule, MatProgressBarModule, MatSelectModule, + AdminOnlyDirective, ], providers: [ { provide: MAT_DATE_LOCALE, useValue: de }, diff --git a/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.html b/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.html index e95e0d7b..6f890eb3 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.html +++ b/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.html @@ -7,14 +7,8 @@

    edit.adminInstructionsEditHeading

    edit.adminInstructionsEdit

    edit.adminInstructionsCreateHeading

    edit.adminInstructionsCreate -

    edit.adminInstructionsSyncElasticAssetsHeading

    -
    - -
    {{ syncProgress }}%
    -
    -

    edit.adminInstructionsSyncElasticAssets

    + + diff --git a/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.ts b/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.ts index 58304884..1bab23e9 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.ts @@ -1,24 +1,22 @@ import { TemplatePortal } from '@angular/cdk/portal'; -import { HttpClient } from '@angular/common/http'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - OnInit, + inject, TemplateRef, ViewChild, ViewContainerRef, - inject, } from '@angular/core'; import { AppPortalService, + appSharedStateActions, AppState, LifecycleHooks, LifecycleHooksDirective, - appSharedStateActions, } from '@asset-sg/client-shared'; import { Store } from '@ngrx/store'; -import { Observable, asyncScheduler, observeOn, take } from 'rxjs'; +import { asyncScheduler, observeOn } from 'rxjs'; @Component({ selector: 'asset-sg-editor-launch', @@ -27,7 +25,7 @@ import { Observable, asyncScheduler, observeOn, take } from 'rxjs'; hostDirectives: [LifecycleHooksDirective], styleUrls: ['./asset-editor-launch.component.scss'], }) -export class AssetEditorLaunchComponent implements OnInit { +export class AssetEditorLaunchComponent { @ViewChild('templateDrawerPortalContent') _templateDrawerPortalContent!: TemplateRef; private _lc = inject(LifecycleHooks); @@ -35,9 +33,6 @@ export class AssetEditorLaunchComponent implements OnInit { private _viewContainerRef = inject(ViewContainerRef); private _cd = inject(ChangeDetectorRef); private _store = inject>(Store); - private _httpClient = inject(HttpClient); - - syncProgress: number | null = null; constructor() { this._lc.afterViewInit$.pipe(observeOn(asyncScheduler)).subscribe(() => { @@ -49,42 +44,4 @@ export class AssetEditorLaunchComponent implements OnInit { this._store.dispatch(appSharedStateActions.openPanel()); }); } - - ngOnInit() { - void this.refreshAssetSyncProgress().then(() => { - if (this.syncProgress != null) { - void this.loopAssetSyncProgress(); - } - }); - } - - async synchronizeElastic() { - if (this.syncProgress !== null) { - return; - } - this.syncProgress = 0; - await resolveFirst(this._httpClient.post('/api/assets/sync', null)); - await this.loopAssetSyncProgress(); - } - - private async loopAssetSyncProgress() { - while (this.syncProgress !== null) { - await this.refreshAssetSyncProgress(); - await new Promise((resolve) => setTimeout(resolve, 500)); - } - } - - private async refreshAssetSyncProgress() { - type Progress = { progress: number } | null | undefined; - const progress = await resolveFirst(this._httpClient.get('/api/assets/sync')); - this.syncProgress = progress == null ? null : Math.round(progress.progress * 100); - } } - -const resolveFirst = (value$: Observable): Promise => - new Promise((resolve, reject) => { - value$.pipe(take(1)).subscribe({ - next: resolve, - error: reject, - }); - }); diff --git a/libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.html b/libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.html new file mode 100644 index 00000000..f6369e0e --- /dev/null +++ b/libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.html @@ -0,0 +1,8 @@ +

    edit.adminInstructionsSyncElasticAssetsHeading

    +
    + +
    {{ syncProgress }}%
    +
    +

    edit.adminInstructionsSyncElasticAssets

    diff --git a/libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.scss b/libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.scss new file mode 100644 index 00000000..6d284e43 --- /dev/null +++ b/libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.scss @@ -0,0 +1,31 @@ +@use "../../styles/variables" as variables; + +// asset-sync +.asset-sync { + display: flex; +} + +.asset-sync.active > button { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.asset-sync > .progress { + display: flex; + align-items: center; + padding-inline: 0.25rem; + + border: 2px solid variables.$cyan-09; + border-left: none; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + + transition: 250ms ease-in; + transition-property: opacity, transform; + transform-origin: left; +} + +.asset-sync:not(.active) > .progress { + opacity: 0; + transform: scaleX(0); +} diff --git a/libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.ts b/libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.ts new file mode 100644 index 00000000..096218b2 --- /dev/null +++ b/libs/asset-editor/src/lib/components/asset-editor-sync/asset-editor-sync.component.ts @@ -0,0 +1,52 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, inject, OnInit } from '@angular/core'; +import { Observable, take } from 'rxjs'; + +@Component({ + selector: 'asset-sg-editor-sync', + templateUrl: './asset-editor-sync.component.html', + styleUrls: ['./asset-editor-sync.component.scss'], +}) +export class AssetEditorSyncComponent implements OnInit { + private _httpClient = inject(HttpClient); + + syncProgress: number | null = null; + + ngOnInit() { + void this.refreshAssetSyncProgress().then(() => { + if (this.syncProgress != null) { + void this.loopAssetSyncProgress(); + } + }); + } + + async synchronizeElastic() { + if (this.syncProgress !== null) { + return; + } + this.syncProgress = 0; + await resolveFirst(this._httpClient.post('/api/assets/sync', null)); + await this.loopAssetSyncProgress(); + } + + private async loopAssetSyncProgress() { + while (this.syncProgress !== null) { + await this.refreshAssetSyncProgress(); + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } + + private async refreshAssetSyncProgress() { + type Progress = { progress: number } | null | undefined; + const progress = await resolveFirst(this._httpClient.get('/api/assets/sync')); + this.syncProgress = progress == null ? null : Math.round(progress.progress * 100); + } +} + +const resolveFirst = (value$: Observable): Promise => + new Promise((resolve, reject) => { + value$.pipe(take(1)).subscribe({ + next: resolve, + error: reject, + }); + }); diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.html b/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.html index 578fba55..36ab673a 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.html +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.html @@ -64,7 +64,7 @@ {{ statusWorkItem | valueItemName }} diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.ts index 45c3d44e..613c1c2e 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-administration/asset-editor-tab-administration.component.ts @@ -1,10 +1,14 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core'; -import { FormBuilder, FormGroupDirective } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, EventEmitter, inject, Input, OnInit, Output } from '@angular/core'; +import { FormGroupDirective } from '@angular/forms'; import { fromAppShared } from '@asset-sg/client-shared'; +import { isNotNull } from '@asset-sg/core'; import { DateId } from '@asset-sg/shared'; +import { isMasterEditor } from '@asset-sg/shared/v2'; +import * as RD from '@devexperts/remote-data-ts'; +import { Store } from '@ngrx/store'; import { RxState } from '@rx-angular/state'; import * as O from 'fp-ts/Option'; -import { Observable } from 'rxjs'; +import { filter, map, Observable, withLatestFrom } from 'rxjs'; import { AssetEditDetailVM } from '../../models'; import { AssetEditorAdministrationFormGroup, AssetEditorFormGroup } from '../asset-editor-form-group'; @@ -36,7 +40,6 @@ const initialTabAdministrationState: TabAdministrationState = { export class AssetEditorTabAdministrationComponent implements OnInit { public _rootFormGroupDirective = inject(FormGroupDirective); public _rootFormGroup = this._rootFormGroupDirective.control as AssetEditorFormGroup; - private _formBuilder = inject(FormBuilder); private _state = inject>(RxState); public _form!: AssetEditorAdministrationFormGroup; @@ -44,6 +47,18 @@ export class AssetEditorTabAdministrationComponent implements OnInit { public _referenceDataVM$ = this._state.select('referenceDataVM'); public _assetEditDetail$ = this._state.select('assetEditDetail'); + private readonly filteredAssetEditDetail$ = this._state + .select('assetEditDetail') + .pipe(map(O.toNullable), filter(isNotNull)); + + private readonly store = inject(Store); + public readonly isMasterEditor$ = this.store.select(fromAppShared.selectRDUserProfile).pipe( + map(RD.toNullable), + filter(isNotNull), + withLatestFrom(this.filteredAssetEditDetail$), + map(([user, assetEditDetail]) => isMasterEditor(user, assetEditDetail.workgroupId)) + ); + // eslint-disable-next-line @angular-eslint/no-output-rename @Output('save') public save$ = new EventEmitter(); diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.html b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.html index 1dcd3b81..2775c308 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.html +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.html @@ -6,7 +6,7 @@ - + edit.tabs.usage.status - + {{ vm.statusAssetUseItems["tobechecked"] | valueItemName }} - + {{ vm.statusAssetUseItems["underclarification"] | valueItemName }} - + {{ vm.statusAssetUseItems["approved"] | valueItemName }} @@ -49,18 +44,13 @@ Status - + {{ vm.statusAssetUseItems["tobechecked"] | valueItemName }} - + {{ vm.statusAssetUseItems["underclarification"] | valueItemName }} - + {{ vm.statusAssetUseItems["approved"] | valueItemName }} diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-usage/asset-editor-tab-usage.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-usage/asset-editor-tab-usage.component.ts index 8e0c49af..0c7da25a 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-usage/asset-editor-tab-usage.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-usage/asset-editor-tab-usage.component.ts @@ -3,19 +3,27 @@ import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef, ViewChi import { FormGroupDirective } from '@angular/forms'; import { MatCheckboxChange } from '@angular/material/checkbox'; import { LifecycleHooks, LifecycleHooksDirective, fromAppShared } from '@asset-sg/client-shared'; +import { isNotNull } from '@asset-sg/core'; +import { isMasterEditor } from '@asset-sg/shared/v2'; +import * as RD from '@devexperts/remote-data-ts'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { RxState } from '@rx-angular/state'; -import { Observable, map, of, startWith, switchMap } from 'rxjs'; +import * as O from 'fp-ts/Option'; +import { Observable, map, of, startWith, switchMap, withLatestFrom, filter } from 'rxjs'; +import { AssetEditDetailVM } from '../../models'; import { AssetEditorFormGroup, AssetEditorUsageFormGroup } from '../asset-editor-form-group'; interface AssetEditorTabUsageState { referenceDataVM: fromAppShared.ReferenceDataVM; + assetEditDetail: O.Option; } const initialAssetEditorTabUsageState: AssetEditorTabUsageState = { referenceDataVM: fromAppShared.emptyReferenceDataVM, + assetEditDetail: O.none, }; @UntilDestroy() @@ -43,6 +51,18 @@ export class AssetEditorTabUsageComponent implements OnInit { private _dialogRefRemoveNationalInterestDialog?: DialogRef; + private readonly filteredAssetEditDetail$ = this._state + .select('assetEditDetail') + .pipe(map(O.toNullable), filter(isNotNull)); + + private readonly store = inject(Store); + public readonly isMasterEditor$ = this.store.select(fromAppShared.selectRDUserProfile).pipe( + map(RD.toNullable), + filter(isNotNull), + withLatestFrom(this.filteredAssetEditDetail$), + map(([user, assetEditDetail]) => isMasterEditor(user, assetEditDetail.workgroupId)) + ); + @ViewChild('tmplRemoveNationalInterestDialog') private _tmplRemoveNationalInterestDialog!: TemplateRef; @Input() @@ -50,6 +70,11 @@ export class AssetEditorTabUsageComponent implements OnInit { this._state.connect('referenceDataVM', value); } + @Input() + public set assetEditDetail$(value: Observable>) { + this._state.connect('assetEditDetail', value); + } + private _form$ = this._lc.onInit$.pipe(map(() => this.rootFormGroup.controls['usage'])); public _internalStartAvailabilityDateErrorText$ = this._form$.pipe( @@ -137,12 +162,6 @@ export class AssetEditorTabUsageComponent implements OnInit { } } - public _removeNatRelTypeItemCode(value: string) { - this._form.controls['natRelTypeItemCodes'].setValue( - this._form.controls['natRelTypeItemCodes'].value.filter((v: string) => v !== value) - ); - } - public getUsageErrorText(errors: null | { internalPublicUsageDateError?: true }) { return errors && errors.internalPublicUsageDateError ? this._translateService.get('edit.tabs.usage.validationErrors.internalPublicUsageDateError') diff --git a/libs/asset-viewer/src/lib/asset-viewer.module.ts b/libs/asset-viewer/src/lib/asset-viewer.module.ts index c2bb03ee..2e41a609 100644 --- a/libs/asset-viewer/src/lib/asset-viewer.module.ts +++ b/libs/asset-viewer/src/lib/asset-viewer.module.ts @@ -28,12 +28,13 @@ import { AnchorComponent, AnimateNumberComponent, ButtonComponent, + CanCreateDirective, + CanUpdateDirective, DatepickerToggleIconComponent, DatePipe, DragHandleComponent, DrawerComponent, DrawerPanelComponent, - IsEditorPipe, SmartTranslatePipe, ValueItemDescriptionPipe, ValueItemNamePipe, @@ -88,7 +89,6 @@ import { assetSearchReducer } from './state/asset-search/asset-search.reducer'; ValueItemDescriptionPipe, DatePipe, ZoomControlsComponent, - IsEditorPipe, ValueItemNamePipe, ForModule, @@ -123,6 +123,8 @@ import { assetSearchReducer } from './state/asset-search/asset-search.reducer'; SmartTranslatePipe, CdkMonitorFocus, MatTooltip, + CanCreateDirective, + CanUpdateDirective, ], providers: [ TranslatePipe, diff --git a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html index 2d0f6cad..4553224a 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html +++ b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html @@ -18,182 +18,180 @@
    - - -
    -
    - + + -
    - + +
    +
    + + + + + - - - - - - - + + -
    - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    -
      - +
    +
    {{ assetDetail.assetKindItem | valueItemName }}
    {{ assetDetail.createDate | assetSgDate }}
    + + {{ id.id + " [" + id.description + "]" }} +
    + +
      +
    • + {{ "contactRoles." + contact.role | translate }}:
      + {{ contact.name }}
      + {{ contact.locality }}
      + {{ contact.contactKindItem | valueItemName }} +
    • +
    +
    + + + {{ (manCatLabel | valueItemName) + (last ? "" : ", ") }} + +
    +
      +
    • {{ language | valueItemName }}
    • +
    +
    {{ assetDetail.assetFormatItem | valueItemName }}
    + + + {{ (assetFormatComposition | valueItemName) + (last ? "" : ", ") }} + +
    + + {{ + typeNatRel | valueItemName + }} +
    + + +
    {{ assetDetail.lastProcessedDate | assetSgDate }}
    + + + + + + + +
    {{ statusWork.statusWorkDate | assetSgDate }}{{ statusWork.statusWork | valueItemName }}
    +
    diff --git a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts index ed5731dc..1e56cec0 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts +++ b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; import { AppState } from '@asset-sg/client-shared'; import { AssetFile } from '@asset-sg/shared'; +import { AssetEditPolicy } from '@asset-sg/shared/v2'; import { Store } from '@ngrx/store'; import * as actions from '../../state/asset-search/asset-search.actions'; @@ -63,4 +64,6 @@ export class AssetSearchDetailComponent { }, }); } + + protected readonly AssetEditPolicy = AssetEditPolicy; } diff --git a/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts b/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts index 1d1911df..f15f14df 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts +++ b/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, EventEmitter, @@ -53,6 +54,7 @@ export class AssetSearchResultsComponent implements OnInit, OnDestroy { public readonly pageStats$ = this._store.select(selectAssetSearchPageData); public readonly currentAssetDetail$ = this._store.select(selectCurrentAssetDetail); private readonly subscriptions: Subscription = new Subscription(); + private changeDetector = inject(ChangeDetectorRef); public ngOnInit() { this.initSubscriptions(); @@ -92,6 +94,7 @@ export class AssetSearchResultsComponent implements OnInit, OnDestroy { if (this.scrollContainer) { this.scrollContainer.nativeElement.scrollTop = 0; } + this.changeDetector.markForCheck(); }) ); } diff --git a/libs/auth/src/lib/auth.module.ts b/libs/auth/src/lib/auth.module.ts index 91d70381..2681b5a8 100644 --- a/libs/auth/src/lib/auth.module.ts +++ b/libs/auth/src/lib/auth.module.ts @@ -5,7 +5,6 @@ import { NgModule } from '@angular/core'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { RouterModule } from '@angular/router'; import { AnchorComponent, ButtonComponent, icons } from '@asset-sg/client-shared'; import { provideSvgIcons } from '@ngneat/svg-icon'; import { TranslateModule } from '@ngx-translate/core'; diff --git a/libs/auth/src/lib/services/auth.interceptor.ts b/libs/auth/src/lib/services/auth.interceptor.ts index 3cf03058..40416c0e 100644 --- a/libs/auth/src/lib/services/auth.interceptor.ts +++ b/libs/auth/src/lib/services/auth.interceptor.ts @@ -1,17 +1,34 @@ import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import { inject, Injectable, OnDestroy } from '@angular/core'; +import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router'; import { AlertType, showAlert } from '@asset-sg/client-shared'; import { Store } from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; import { OAuthService } from 'angular-oauth2-oidc'; -import { EMPTY, Observable, catchError, from, switchMap } from 'rxjs'; +import { catchError, EMPTY, from, Observable, Subscription, switchMap } from 'rxjs'; import { AuthService, AuthState } from './auth.service'; @Injectable() -export class AuthInterceptor implements HttpInterceptor { +export class AuthInterceptor implements HttpInterceptor, OnDestroy { private _oauthService = inject(OAuthService); private readonly store = inject(Store); private readonly authService = inject(AuthService); + private readonly translateService = inject(TranslateService); + private readonly router = inject(Router); + + private readonly subscription = new Subscription(); + + /** + * Whether the router is currently in the middle of a navigation. + * If this is `false`, then the site is fully loaded and the user is able to interact with it. + * @private + */ + private isNavigating = false; + + constructor() { + this.initializeRouterSubscription(); + } intercept(req: HttpRequest, next: HttpHandler): Observable> { const token = sessionStorage.getItem('access_token'); @@ -41,7 +58,29 @@ export class AuthInterceptor implements HttpInterceptor { private async handleError(error: HttpErrorResponse): Promise { switch (error.status) { case 403: - this.authService.setState(AuthState.Forbidden); + if (error.error == 'not authorized by eIAM') { + // The initial logging via eIAM was successful, + // but the user does not have access to this application. + this.authService.setState(AuthState.AccessForbidden); + } else if (this.isNavigating) { + // The user attempted to navigate to a page to which they have no access. + // A common way this happens is by manually accessing a forbidden URL. + this.authService.setState(AuthState.ForbiddenResource); + } else { + // The user attempted to load a resource to which they have no access. + // This mainly happens when there's a difference between two databases (e.g. Postgres and Elastic), + // causing the user to be able to request resources they should not be able to. + this.store.dispatch( + showAlert({ + alert: { + id: `resource-forbidden`, + text: this.translateService.instant('resourceForbidden'), + type: AlertType.Error, + isPersistent: false, + }, + }) + ); + } break; case 401: this.store.dispatch( @@ -75,4 +114,32 @@ export class AuthInterceptor implements HttpInterceptor { } } } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } + + private initializeRouterSubscription(): void { + this.subscription.add( + this.router.events.subscribe((event) => { + if (event instanceof NavigationStart) { + this.isNavigating = true; + this.resetAuthState(); + } else if ( + event instanceof NavigationEnd || + event instanceof NavigationCancel || + event instanceof NavigationError + ) { + this.isNavigating = false; + this.resetAuthState(); + } + }) + ); + } + + private resetAuthState(): void { + if (this.authService.isLoggedIn() && this.authService.state === AuthState.ForbiddenResource) { + this.authService.setState(AuthState.Success); + } + } } diff --git a/libs/auth/src/lib/services/auth.service.ts b/libs/auth/src/lib/services/auth.service.ts index cea9aa03..5744667e 100644 --- a/libs/auth/src/lib/services/auth.service.ts +++ b/libs/auth/src/lib/services/auth.service.ts @@ -1,11 +1,11 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable, inject } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { ApiError, httpErrorResponseOrUnknownError } from '@asset-sg/client-shared'; -import { OE, ORD, decode } from '@asset-sg/core'; +import { decode, OE, ORD } from '@asset-sg/core'; import { User } from '@asset-sg/shared'; import * as RD from '@devexperts/remote-data-ts'; import { OAuthService } from 'angular-oauth2-oidc'; -import { BehaviorSubject, Observable, map, startWith } from 'rxjs'; +import { BehaviorSubject, map, Observable, startWith } from 'rxjs'; import urlJoin from 'url-join'; @Injectable({ providedIn: 'root' }) @@ -14,7 +14,7 @@ export class AuthService { private oauthService = inject(OAuthService); - private state = new BehaviorSubject(AuthState.Ongoing); + private _state = new BehaviorSubject(AuthState.Ongoing); public configureOAuth( issuer: string, @@ -38,27 +38,35 @@ export class AuthService { async signIn(): Promise { try { - if (this.state.value === AuthState.Ongoing) { + if (this._state.value === AuthState.Ongoing) { const success = await this.oauthService.loadDiscoveryDocumentAndLogin(); if (success) { this.oauthService.setupAutomaticSilentRefresh(); - this.state.next(AuthState.Success); + + // If something else has interrupted the auth process, then we don't want to signal a success. + if (this._state.value === AuthState.Ongoing) { + this._state.next(AuthState.Success); + } } } else { - this.state.next(AuthState.Ongoing); + this._state.next(AuthState.Ongoing); this.oauthService.initLoginFlow(); } } catch (e) { - this.state.next(AuthState.Aborted); + this._state.next(AuthState.Aborted); } } + get state(): AuthState { + return this._state.value; + } + get state$(): Observable { - return this.state.asObservable(); + return this._state.asObservable(); } setState(state: AuthState): void { - this.state.next(state); + this._state.next(state); } getUserProfile(): ORD.ObservableRemoteData { @@ -78,9 +86,11 @@ export class AuthService { } private _getUserProfile() { - return this._httpClient - .get('/api/users/current') - .pipe(map(decode(User)), OE.catchErrorW(httpErrorResponseOrUnknownError)); + return this._httpClient.get('/api/users/current').pipe( + map((it) => ({ ...it, role: (it as { isAdmin: boolean }).isAdmin ? 'admin' : 'master-editor' })), + map(decode(User)), + OE.catchErrorW(httpErrorResponseOrUnknownError) + ); } buildAuthUrl = (path: string) => urlJoin(`/auth`, path); @@ -89,6 +99,7 @@ export class AuthService { export enum AuthState { Ongoing, Aborted, - Forbidden, + AccessForbidden, + ForbiddenResource, Success, } diff --git a/libs/client-shared/src/index.ts b/libs/client-shared/src/index.ts index 91777201..79691443 100644 --- a/libs/client-shared/src/index.ts +++ b/libs/client-shared/src/index.ts @@ -17,3 +17,9 @@ export * from './lib/features/alert/alert.model'; export * from './lib/features/alert/alert.module'; export * from './lib/features/alert/alert.reducer'; export * from './lib/features/alert/alert.selectors'; + +export * from './lib/directives/admin-only.directive'; +export * from './lib/directives/can-create.directive'; +export * from './lib/directives/can-delete.directive'; +export * from './lib/directives/can-show.directive'; +export * from './lib/directives/can-update.directive'; diff --git a/libs/client-shared/src/lib/components/auth-pipes/base-auth-pipe.ts b/libs/client-shared/src/lib/components/auth-pipes/base-auth-pipe.ts deleted file mode 100644 index d5b14bdc..00000000 --- a/libs/client-shared/src/lib/components/auth-pipes/base-auth-pipe.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ChangeDetectorRef, PipeTransform, inject } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; - -import { AppState } from '../../state'; - -export abstract class BaseAuthPipe implements PipeTransform { - private _store = inject(Store); - private _cdRef = inject(ChangeDetectorRef); - - private _value: boolean; - - constructor(initialValue: boolean, selector: (store: Store) => Observable) { - this._value = initialValue; - selector(this._store).subscribe((value) => { - this._value = value; - this._cdRef.markForCheck(); - }); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - transform(_: null): boolean { - return this._value; - } -} diff --git a/libs/client-shared/src/lib/components/auth-pipes/index.ts b/libs/client-shared/src/lib/components/auth-pipes/index.ts deleted file mode 100644 index 98abe63f..00000000 --- a/libs/client-shared/src/lib/components/auth-pipes/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './is-editor.pipe'; -export * from './is-not-master-editor.pipe'; diff --git a/libs/client-shared/src/lib/components/auth-pipes/is-editor.pipe.ts b/libs/client-shared/src/lib/components/auth-pipes/is-editor.pipe.ts deleted file mode 100644 index a6950a05..00000000 --- a/libs/client-shared/src/lib/components/auth-pipes/is-editor.pipe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -import { fromAppShared } from '../../state'; - -import { BaseAuthPipe } from './base-auth-pipe'; - -@Pipe({ - standalone: true, - name: 'isEditor', - pure: false, -}) -export class IsEditorPipe extends BaseAuthPipe implements PipeTransform { - constructor() { - super(false, (store) => store.select(fromAppShared.selectIsEditor)); - } -} diff --git a/libs/client-shared/src/lib/components/auth-pipes/is-not-master-editor.pipe.ts b/libs/client-shared/src/lib/components/auth-pipes/is-not-master-editor.pipe.ts deleted file mode 100644 index 444608c6..00000000 --- a/libs/client-shared/src/lib/components/auth-pipes/is-not-master-editor.pipe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -import { fromAppShared } from '../../state'; - -import { BaseAuthPipe } from './base-auth-pipe'; - -@Pipe({ - standalone: true, - name: 'isNotMasterEditor', - pure: false, -}) -export class IsNotMasterEditorPipe extends BaseAuthPipe implements PipeTransform { - constructor() { - super(true, (store) => store.select(fromAppShared.selectIsNotMasterEditor)); - } -} diff --git a/libs/client-shared/src/lib/components/index.ts b/libs/client-shared/src/lib/components/index.ts index 37664402..9b2bf182 100644 --- a/libs/client-shared/src/lib/components/index.ts +++ b/libs/client-shared/src/lib/components/index.ts @@ -1,5 +1,4 @@ export * from './animate-number'; -export * from './auth-pipes'; export * from './button'; export * from './date'; export * from './datepicker-toggle-icon'; diff --git a/libs/client-shared/src/lib/directives/admin-only.directive.ts b/libs/client-shared/src/lib/directives/admin-only.directive.ts new file mode 100644 index 00000000..00764e84 --- /dev/null +++ b/libs/client-shared/src/lib/directives/admin-only.directive.ts @@ -0,0 +1,39 @@ +import { ChangeDetectorRef, Directive, inject, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; +import * as RD from '@devexperts/remote-data-ts'; +import { Store } from '@ngrx/store'; +import { map, Subscription } from 'rxjs'; +import { AppState, fromAppShared } from '../state'; + +@Directive({ + selector: '[adminOnly]', + standalone: true, +}) +export class AdminOnlyDirective implements OnInit, OnDestroy { + private store = inject(Store); + + private readonly subscription = new Subscription(); + + private ref = inject(ChangeDetectorRef); + + constructor(private readonly templateRef: TemplateRef, private readonly viewContainer: ViewContainerRef) {} + + ngOnInit(): void { + this.subscription.add( + this.store + .select(fromAppShared.selectRDUserProfile) + .pipe(map(RD.toNullable)) + .subscribe((user) => { + if (user != null && user.isAdmin) { + this.viewContainer.createEmbeddedView(this.templateRef); + } else { + this.viewContainer.clear(); + } + this.ref.markForCheck(); + }) + ); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/libs/client-shared/src/lib/directives/base-policy.directive.ts b/libs/client-shared/src/lib/directives/base-policy.directive.ts new file mode 100644 index 00000000..34bd6764 --- /dev/null +++ b/libs/client-shared/src/lib/directives/base-policy.directive.ts @@ -0,0 +1,67 @@ +import { + ChangeDetectorRef, + Directive, + inject, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; +import { User, Policy } from '@asset-sg/shared/v2'; +import * as RD from '@devexperts/remote-data-ts'; +import { Store } from '@ngrx/store'; +import { map, Subscription } from 'rxjs'; +import { Class } from 'type-fest'; +import { AppState } from '../state/app-shared-state'; +import { selectRDUserProfile } from '../state/app-shared-state.selectors'; + +@Directive({ + standalone: true, +}) +export abstract class BasePolicyDirective implements OnInit, OnChanges, OnDestroy { + abstract get policy(): Class>; + + private readonly store = inject(Store); + + private user: User | null = null; + + private ref = inject(ChangeDetectorRef); + + private readonly subscription = new Subscription(); + + constructor(private readonly templateRef: TemplateRef, private readonly viewContainer: ViewContainerRef) {} + + ngOnInit(): void { + this.subscription.add( + this.store + .select(selectRDUserProfile) + .pipe(map((user) => (RD.isSuccess(user) ? user.value : null))) + .subscribe((user) => { + this.user = user; + this.render(); + }) + ); + } + + ngOnChanges(_changes: SimpleChanges): void { + this.render(); + this.ref.markForCheck(); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } + + private render(): void { + const policyInstance = this.user == null ? null : new this.policy(this.user); + if (policyInstance != null && (policyInstance.canDoEverything() || this.check(policyInstance))) { + this.viewContainer.createEmbeddedView(this.templateRef); + } else { + this.viewContainer.clear(); + } + } + + protected abstract check(policy: Policy): boolean; +} diff --git a/libs/client-shared/src/lib/directives/can-create.directive.ts b/libs/client-shared/src/lib/directives/can-create.directive.ts new file mode 100644 index 00000000..86030e88 --- /dev/null +++ b/libs/client-shared/src/lib/directives/can-create.directive.ts @@ -0,0 +1,17 @@ +import { Directive, Input } from '@angular/core'; +import { Policy } from '@asset-sg/shared/v2'; +import { Class } from 'type-fest'; +import { BasePolicyDirective } from './base-policy.directive'; + +@Directive({ + selector: '[canCreate]', + standalone: true, +}) +export class CanCreateDirective extends BasePolicyDirective { + @Input({ alias: 'canCreate', required: true }) + policy!: Class>; + + protected override check(policy: Policy): boolean { + return policy.canCreate(); + } +} diff --git a/libs/client-shared/src/lib/directives/can-delete.directive.ts b/libs/client-shared/src/lib/directives/can-delete.directive.ts new file mode 100644 index 00000000..692baaed --- /dev/null +++ b/libs/client-shared/src/lib/directives/can-delete.directive.ts @@ -0,0 +1,20 @@ +import { Directive, Input } from '@angular/core'; +import { Policy } from '@asset-sg/shared/v2'; +import { Class } from 'type-fest'; +import { BasePolicyDirective } from './base-policy.directive'; + +@Directive({ + selector: '[canDelete]', + standalone: true, +}) +export class CanDeleteDirective extends BasePolicyDirective { + @Input({ alias: 'canDelete', required: true }) + policy!: Class>; + + @Input({ alias: 'canDeleteWith', required: true }) + with!: T; + + protected override check(policy: Policy): boolean { + return policy.canDelete(this.with); + } +} diff --git a/libs/client-shared/src/lib/directives/can-show.directive.ts b/libs/client-shared/src/lib/directives/can-show.directive.ts new file mode 100644 index 00000000..67221970 --- /dev/null +++ b/libs/client-shared/src/lib/directives/can-show.directive.ts @@ -0,0 +1,20 @@ +import { Directive, Input } from '@angular/core'; +import { Policy } from '@asset-sg/shared/v2'; +import { Class } from 'type-fest'; +import { BasePolicyDirective } from './base-policy.directive'; + +@Directive({ + selector: '[canShow]', + standalone: true, +}) +export class CanShowDirective extends BasePolicyDirective { + @Input({ alias: 'canShow', required: true }) + policy!: Class>; + + @Input({ alias: 'canShowWith', required: true }) + with!: T; + + protected override check(policy: Policy): boolean { + return policy.canShow(this.with); + } +} diff --git a/libs/client-shared/src/lib/directives/can-update.directive.ts b/libs/client-shared/src/lib/directives/can-update.directive.ts new file mode 100644 index 00000000..6d52eda0 --- /dev/null +++ b/libs/client-shared/src/lib/directives/can-update.directive.ts @@ -0,0 +1,20 @@ +import { Directive, Input } from '@angular/core'; +import { Policy } from '@asset-sg/shared/v2'; +import { Class } from 'type-fest'; +import { BasePolicyDirective } from './base-policy.directive'; + +@Directive({ + selector: '[canUpdate]', + standalone: true, +}) +export class CanUpdateDirective extends BasePolicyDirective { + @Input({ alias: 'canUpdate', required: true }) + policy!: Class>; + + @Input({ alias: 'canUpdateWith', required: true }) + with!: T; + + protected override check(policy: Policy): boolean { + return policy.canUpdate(this.with); + } +} diff --git a/libs/client-shared/src/lib/state/app-shared-state.selectors.ts b/libs/client-shared/src/lib/state/app-shared-state.selectors.ts index 18143ebc..6ab44b6a 100644 --- a/libs/client-shared/src/lib/state/app-shared-state.selectors.ts +++ b/libs/client-shared/src/lib/state/app-shared-state.selectors.ts @@ -1,11 +1,4 @@ -import { - Contact, - emptyValueItem, - isEditor, - isMasterEditor, - ReferenceData, - valueItemRecordToArray, -} from '@asset-sg/shared'; +import { Contact, emptyValueItem, ReferenceData, valueItemRecordToArray } from '@asset-sg/shared'; import * as RD from '@devexperts/remote-data-ts'; import { createSelector } from '@ngrx/store'; import * as A from 'fp-ts/Array'; @@ -22,7 +15,9 @@ export const selectRDReferenceData = createSelector(appSharedFeature, (state) => export const selectRDUserProfile = createSelector(appSharedFeature, (state) => state.rdUserProfile); -const makeRefenceDataVM = (referenceData: ReferenceData) => ({ +export const selectUser = createSelector(selectRDUserProfile, RD.toNullable); + +const makeReferenceDataVM = (referenceData: ReferenceData) => ({ ...referenceData, assetFormItemArray: valueItemRecordToArray(referenceData.assetFormatItems), assetKindItemArray: valueItemRecordToArray(referenceData.assetKindItems), @@ -39,7 +34,7 @@ const makeRefenceDataVM = (referenceData: ReferenceData) => ({ A.map((a) => a[1]) ), }); -export type ReferenceDataVM = ReturnType; +export type ReferenceDataVM = ReturnType; export const emptyReferenceDataVM: ReferenceDataVM = { assetFormatItems: {}, @@ -95,19 +90,3 @@ export const selectRDReferenceDataVM = createSelector( ); export const selectLocale = createSelector(appSharedFeature, (state) => (state.lang === 'en' ? 'en-GB' : 'de-CH')); - -export const selectIsNotMasterEditor = createSelector( - selectRDUserProfile, - flow( - RD.map((userProfile) => !isMasterEditor(userProfile)), - RD.getOrElse(() => true) - ) -); - -export const selectIsEditor = createSelector( - selectRDUserProfile, - flow( - RD.map((userProfile) => isEditor(userProfile)), - RD.getOrElse(() => false) - ) -); diff --git a/libs/client-shared/src/lib/utils/map.ts b/libs/client-shared/src/lib/utils/map.ts index c4287a80..dfdd22a2 100644 --- a/libs/client-shared/src/lib/utils/map.ts +++ b/libs/client-shared/src/lib/utils/map.ts @@ -12,7 +12,7 @@ import { LineString, Point, Polygon, SimpleGeometry } from 'ol/geom'; import { fromExtent as polygonFromExtent } from 'ol/geom/Polygon'; import Map from 'ol/Map'; import { fromLonLat } from 'ol/proj'; -import { Circle, Fill, Icon, RegularShape, Stroke, Style } from 'ol/style'; +import { Circle, Fill, RegularShape, Stroke, Style } from 'ol/style'; import View from 'ol/View'; import { isoWGSLat, isoWGSLng } from '../models'; diff --git a/libs/favourite/src/lib/services/favourite.service.ts b/libs/favourite/src/lib/services/favourite.service.ts index 2d243874..53c78e5c 100644 --- a/libs/favourite/src/lib/services/favourite.service.ts +++ b/libs/favourite/src/lib/services/favourite.service.ts @@ -10,6 +10,6 @@ export class FavouriteService { constructor(private http: HttpClient) {} getFavourites(): Observable { - return this.http.get(`/api/favourites`); + return this.http.get(`/api/users/current/favorites`); } } diff --git a/libs/shared/src/lib/models/asset-edit.ts b/libs/shared/src/lib/models/asset-edit.ts index b963198f..790fa8e7 100644 --- a/libs/shared/src/lib/models/asset-edit.ts +++ b/libs/shared/src/lib/models/asset-edit.ts @@ -89,6 +89,8 @@ export const BaseAssetEditDetail = { assetFiles: C.array(AssetFile), workgroupId: C.number, }; +const base = C.struct(BaseAssetEditDetail); +export type BaseAssetEditDetail = C.TypeOf; export const AssetEditDetail = C.struct({ ...BaseAssetEditDetail, diff --git a/libs/shared/src/lib/models/user.ts b/libs/shared/src/lib/models/user.ts index 4e6da192..198d7db6 100644 --- a/libs/shared/src/lib/models/user.ts +++ b/libs/shared/src/lib/models/user.ts @@ -1,5 +1,4 @@ -import { contramap } from 'fp-ts/Ord'; -import { Ord as ordString } from 'fp-ts/string'; +import { Role } from '@prisma/client'; import * as C from 'io-ts/Codec'; import * as D from 'io-ts/Decoder'; @@ -10,45 +9,22 @@ export enum UserRoleEnum { viewer = 'viewer', } -const UserRoleDecoder = D.union( - D.literal(UserRoleEnum.admin), - D.literal(UserRoleEnum.editor), - D.literal(UserRoleEnum.masterEditor), - D.literal(UserRoleEnum.viewer) -); -export const UserRole = C.fromDecoder(UserRoleDecoder); -export type UserRole = D.TypeOf; - -export const isAdmin = (u: User) => u.role === UserRoleEnum.admin; -export const isMasterEditor = (u: User) => [UserRoleEnum.admin, UserRoleEnum.masterEditor].includes(u.role); -export const isEditor = (u: User) => - [UserRoleEnum.admin, UserRoleEnum.editor, UserRoleEnum.masterEditor].includes(u.role); +const WorkgroupRoleDecoder = D.union(D.literal(Role.Editor), D.literal(Role.MasterEditor), D.literal(Role.Viewer)); +export const WorkgroupRole = C.fromDecoder(WorkgroupRoleDecoder); export const User = C.struct({ id: C.string, email: C.string, - role: UserRole, lang: C.string, + isAdmin: C.boolean, + workgroups: C.array( + C.struct({ + id: C.number, + role: WorkgroupRole, + }) + ), }); export type User = D.TypeOf; -export const byEmail = contramap((u: User) => u.email)(ordString); - -export type UserWithoutId = Omit; export const Users = C.array(User); export type Users = User[]; - -export const UserPost = D.struct({ - email: D.string, - role: UserRoleDecoder, - lang: D.string, - oidcId: D.string, - id: D.string, -}); -export type UserPost = D.TypeOf; - -export const UserPatch = D.struct({ - role: UserRoleDecoder, - lang: D.string, -}); -export type UserPatch = D.TypeOf; diff --git a/libs/shared/tsconfig.spec.json b/libs/shared/tsconfig.spec.json index 26ef046a..0068d77f 100644 --- a/libs/shared/tsconfig.spec.json +++ b/libs/shared/tsconfig.spec.json @@ -16,5 +16,6 @@ "src/**/*.test.jsx", "src/**/*.spec.jsx", "src/**/*.d.ts" - ] + ], + "exclude": ["v2"] } diff --git a/libs/shared/v2/.babelrc b/libs/shared/v2/.babelrc new file mode 100644 index 00000000..fd4cbcde --- /dev/null +++ b/libs/shared/v2/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@nx/js/babel", + { + "useBuiltIns": "usage" + } + ] + ] +} diff --git a/libs/shared/v2/README.md b/libs/shared/v2/README.md new file mode 100644 index 00000000..831e1f0a --- /dev/null +++ b/libs/shared/v2/README.md @@ -0,0 +1,11 @@ +# shared + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared` to execute the unit tests via [Jest](https://jestjs.io). + +## Running lint + +Run `nx lint shared` to execute the lint via [ESLint](https://eslint.org/). diff --git a/libs/shared/v2/eslint.config.js b/libs/shared/v2/eslint.config.js new file mode 100644 index 00000000..07e518f7 --- /dev/null +++ b/libs/shared/v2/eslint.config.js @@ -0,0 +1,3 @@ +const baseConfig = require('../../../eslint.config.js'); + +module.exports = [...baseConfig]; diff --git a/libs/shared/v2/jest.config.ts b/libs/shared/v2/jest.config.ts new file mode 100644 index 00000000..4c0d72f8 --- /dev/null +++ b/libs/shared/v2/jest.config.ts @@ -0,0 +1,16 @@ +/* eslint-disable */ +export default { + displayName: 'shared/v2', + preset: '../../../jest.preset.js', + globals: {}, + transform: { + '^.+\\.[tj]sx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json', + }, + ], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/libs/shared/v2', +}; diff --git a/libs/shared/v2/package.json b/libs/shared/v2/package.json new file mode 100644 index 00000000..e21f5fa4 --- /dev/null +++ b/libs/shared/v2/package.json @@ -0,0 +1,4 @@ +{ + "name": "@asset-sg/shared/v2", + "version": "0.0.1" +} diff --git a/libs/shared/v2/project.json b/libs/shared/v2/project.json new file mode 100644 index 00000000..6a32a813 --- /dev/null +++ b/libs/shared/v2/project.json @@ -0,0 +1,33 @@ +{ + "name": "shared/v2", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/v2/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/shared/v2", + "main": "libs/shared/v2/src/index.ts", + "tsConfig": "libs/shared/v2/tsconfig.lib.json", + "assets": ["libs/shared/v2/*.md"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "eslintConfig": "libs/shared/v2/eslint.config.js" + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/shared/v2/jest.config.ts" + } + } + } +} diff --git a/libs/shared/v2/src/index.ts b/libs/shared/v2/src/index.ts new file mode 100644 index 00000000..b4fb0c92 --- /dev/null +++ b/libs/shared/v2/src/index.ts @@ -0,0 +1,21 @@ +export * from './lib/models/base/model'; +export * from './lib/models/base/local-date'; + +export * from './lib/models/asset'; +export * from './lib/models/contact'; +export * from './lib/models/favorite'; +export * from './lib/models/study'; +export * from './lib/models/user'; +export * from './lib/models/workgroup'; + +export * from './lib/policies/base/policy'; +export * from './lib/policies/asset.policy'; +export * from './lib/policies/asset-edit.policy'; +export * from './lib/policies/contact.policy'; +export * from './lib/policies/workgroup.policy'; + +export * from './lib/schemas/asset.schema'; +export * from './lib/schemas/contact.schema'; +export * from './lib/schemas/user.schema'; + +export * from './lib/utils/class-validator/is-nullable.decorator'; diff --git a/libs/shared/v2/src/lib/models/asset.ts b/libs/shared/v2/src/lib/models/asset.ts new file mode 100644 index 00000000..ce030a5f --- /dev/null +++ b/libs/shared/v2/src/lib/models/asset.ts @@ -0,0 +1,145 @@ +import { LocalDate } from './base/local-date'; +import { Data, Model } from './base/model'; +import { StudyType } from './study'; + +// `usageCode` will need to be determined in the frontend - it is no longer included here. +// See `makeUsageCode`. + +// `assetFormatCompositions` seems to be fully unused. +// The table on INT is empty, and there's no way to edit it. +// The field would theoretically be displayed in the search, but since it is empty, +// it's always skipped. + +export interface AssetInfo extends Model { + title: string; + originalTitle: string | null; + + kindCode: string; + formatCode: string; + identifiers: AssetIdentifier[]; + languageCodes: string[]; + contactAssignments: ContactAssignment[]; + manCatLabelCodes: string[]; + natRelCodes: string[]; + links: AssetLinks; + files: FileReference[]; + + createdAt: LocalDate; + receivedAt: LocalDate; + lastProcessedAt: Date; +} + +export interface AssetLinks { + parent: LinkedAsset | null; + children: LinkedAsset[]; + siblings: LinkedAsset[]; +} + +export interface AssetLinksData { + parent: AssetId | null; + siblings: AssetId[]; +} + +// Detailed data about an asset. +// These are the parts of `Asset` that were previously only part of `AssetEdit`. +// They are only visible on the asset edit page. +export interface AssetDetails { + sgsId: number | null; + municipality: string | null; + processor: string | null; + isNatRel: boolean; + infoGeol: InfoGeol; + usage: AssetUsages; + statuses: WorkStatus[]; + studies: AssetStudy[]; + workgroupId: number; +} + +export interface AssetUsages { + public: AssetUsage; + internal: AssetUsage; +} + +export type Asset = AssetInfo & AssetDetails; + +type NonDataKeys = 'processor' | 'identifiers' | 'studies' | 'statuses' | 'links' | 'lastProcessedAt' | 'files'; + +export interface AssetData extends Omit, NonDataKeys> { + links: AssetLinksData; + identifiers: (AssetIdentifier | AssetIdentifierData)[]; + statuses: (WorkStatus | WorkStatusData)[]; + studies: (AssetStudy | StudyData)[]; +} + +export interface InfoGeol { + main: string | null; + contact: string | null; + auxiliary: string | null; +} + +export interface AssetIdentifier extends Model { + name: string; + description: string; +} + +export type AssetIdentifierId = number; +export type AssetIdentifierData = Data; + +export type AssetId = number; + +export interface AssetUsage { + isAvailable: boolean; + statusCode: UsageStatusCode; + availableAt: LocalDate | null; +} + +export enum UsageStatusCode { + ToBeChecked = 'tobechecked', + UnderClarification = 'underclarification', + Approved = 'approved', +} + +export interface ContactAssignment { + contactId: number; + role: ContactAssignmentRole; +} + +export enum ContactAssignmentRole { + Author = 'author', + Initiator = 'initiator', + Supplier = 'supplier', +} + +export interface LinkedAsset { + id: AssetId; + title: string; +} + +export interface WorkStatus extends Model { + itemCode: WorkStatusCode; + createdAt: Date; +} + +export type WorkStatusCode = string; +export type WorkStatusData = Data; + +export interface FileReference { + id: number; + name: string; + size: number; +} + +export enum UsageCode { + Public = 'public', + Internal = 'internal', + UseOnRequest = 'useOnRequest', +} + +export interface AssetStudy extends Model { + geom: string; + type: StudyType; +} + +export type StudyData = Data; + +export type AssetStudyId = number; diff --git a/apps/server-asset-sg/src/utils/data/local-date.ts b/libs/shared/v2/src/lib/models/base/local-date.ts similarity index 100% rename from apps/server-asset-sg/src/utils/data/local-date.ts rename to libs/shared/v2/src/lib/models/base/local-date.ts diff --git a/apps/server-asset-sg/src/utils/data/model.ts b/libs/shared/v2/src/lib/models/base/model.ts similarity index 100% rename from apps/server-asset-sg/src/utils/data/model.ts rename to libs/shared/v2/src/lib/models/base/model.ts diff --git a/libs/shared/v2/src/lib/models/contact.ts b/libs/shared/v2/src/lib/models/contact.ts new file mode 100644 index 00000000..4eb5db50 --- /dev/null +++ b/libs/shared/v2/src/lib/models/contact.ts @@ -0,0 +1,16 @@ +import { Data, Model } from './base/model'; +export interface Contact extends Model { + name: string; + street: string | null; + houseNumber: string | null; + plz: string | null; + locality: string | null; + country: string | null; + telephone: string | null; + email: string | null; + website: string | null; + contactKindItemCode: string; +} + +export type ContactId = number; +export type ContactData = Data; diff --git a/apps/server-asset-sg/src/features/favorites/favorite.model.ts b/libs/shared/v2/src/lib/models/favorite.ts similarity index 55% rename from apps/server-asset-sg/src/features/favorites/favorite.model.ts rename to libs/shared/v2/src/lib/models/favorite.ts index e6bb0f63..f44c0484 100644 --- a/apps/server-asset-sg/src/features/favorites/favorite.model.ts +++ b/libs/shared/v2/src/lib/models/favorite.ts @@ -1,4 +1,4 @@ -import { UserId } from '@/features/users/user.model'; +import { UserId } from './user'; export interface Favorite { assetId: number; diff --git a/apps/server-asset-sg/src/features/studies/study.model.spec.ts b/libs/shared/v2/src/lib/models/study.spec.ts similarity index 93% rename from apps/server-asset-sg/src/features/studies/study.model.spec.ts rename to libs/shared/v2/src/lib/models/study.spec.ts index 9baf87ba..b74c0ba8 100644 --- a/apps/server-asset-sg/src/features/studies/study.model.spec.ts +++ b/libs/shared/v2/src/lib/models/study.spec.ts @@ -1,5 +1,5 @@ import { LV95X, LV95Y } from '@asset-sg/shared'; -import { serializeStudyAsCsv, Study } from './study.model'; +import { serializeStudyAsCsv, Study } from './study'; describe('serializeStudyAsCsv', () => { it('serializes a point study in CSV format', () => { diff --git a/apps/server-asset-sg/src/features/studies/study.model.ts b/libs/shared/v2/src/lib/models/study.ts similarity index 88% rename from apps/server-asset-sg/src/features/studies/study.model.ts rename to libs/shared/v2/src/lib/models/study.ts index 7601edc4..c8b8acb4 100644 --- a/apps/server-asset-sg/src/features/studies/study.model.ts +++ b/libs/shared/v2/src/lib/models/study.ts @@ -1,5 +1,5 @@ import { LV95 } from '@asset-sg/shared'; -import { AssetId } from '@/features/assets/asset.model'; +import { AssetId } from './asset'; export interface Study { id: StudyId; diff --git a/libs/shared/v2/src/lib/models/user.ts b/libs/shared/v2/src/lib/models/user.ts new file mode 100644 index 00000000..c84aa196 --- /dev/null +++ b/libs/shared/v2/src/lib/models/user.ts @@ -0,0 +1,40 @@ +import { Data, Model } from './base/model'; +import { getRoleIndex, Role, WorkgroupId } from './workgroup'; + +export interface User extends Model { + email: string; + lang: string; + workgroups: WorkgroupOnUser[]; + isAdmin: boolean; +} + +export interface WorkgroupOnUser { + id: WorkgroupId; + role: Role; +} + +export type UserId = string; +export type UserData = Omit, 'email'>; + +const hasRole = (role: Role) => (user: User | null | undefined, workgroupId?: WorkgroupId) => { + if (user == null) { + return false; + } + if (user.isAdmin) { + return true; + } + const roleIndex = getRoleIndex(role); + if (workgroupId == null) { + return null != user.workgroups.find((it) => getRoleIndex(it.role) >= roleIndex); + } + for (const workgroup of user.workgroups) { + if (workgroup.id != workgroupId) { + continue; + } + return getRoleIndex(workgroup.role) >= roleIndex; + } + return false; +}; + +export const isMasterEditor = hasRole(Role.MasterEditor); +export const isEditor = hasRole(Role.Editor); diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.model.ts b/libs/shared/v2/src/lib/models/workgroup.ts similarity index 65% rename from apps/server-asset-sg/src/features/workgroups/workgroup.model.ts rename to libs/shared/v2/src/lib/models/workgroup.ts index abac57c6..2af1467f 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.model.ts +++ b/libs/shared/v2/src/lib/models/workgroup.ts @@ -1,25 +1,38 @@ import { Role as PrismaRole } from '@prisma/client'; import { Type } from 'class-transformer'; import { IsArray, IsDate, IsString } from 'class-validator'; -import { IsNullable } from '@/core/decorators/is-nullable.decorator'; -import { AssetId } from '@/features/assets/asset.model'; -import { UserId } from '@/features/users/user.model'; -import { Data, Model } from '@/utils/data/model'; +import { IsNullable } from '../utils/class-validator/is-nullable.decorator'; +import { AssetId } from './asset'; +import { Data, Model } from './base/model'; +import { UserId } from './user'; export interface Workgroup extends Model { name: string; + + // TODO change this to `AssetId[]` assets?: { assetId: AssetId }[]; users?: UserOnWorkgroup[]; + + // TODO make this camel case disabled_at: Date | null; } export type WorkgroupId = number; export type WorkgroupData = Data; +export type SimpleWorkgroup = Pick & { + /** + * The role of the current within this workgroup. + * Note that admins are registered as {@link Role.MasterEditor} for every workgroup. + */ + role: Role; +}; export interface UserOnWorkgroup { + // TODO change this to `id` userId: UserId; role: Role; } + export class WorkgroupDataBoundary implements WorkgroupData { @IsString() name!: string; diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.policy.ts b/libs/shared/v2/src/lib/policies/asset-edit.policy.ts similarity index 58% rename from apps/server-asset-sg/src/features/asset-edit/asset-edit.policy.ts rename to libs/shared/v2/src/lib/policies/asset-edit.policy.ts index 95dc6de7..cbb9e712 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.policy.ts +++ b/libs/shared/v2/src/lib/policies/asset-edit.policy.ts @@ -1,24 +1,25 @@ -import { AssetEditDetail } from '@asset-sg/shared'; -import { Policy } from '@/core/policy'; -import { Role } from '@/features/workgroups/workgroup.model'; +import { Role, WorkgroupId } from '../models/workgroup'; +import { Policy } from './base/policy'; -export class AssetEditPolicy extends Policy { - canShow(value: AssetEditDetail): boolean { +type Asset = { workgroupId: WorkgroupId }; + +export class AssetEditPolicy extends Policy { + override canShow(value: Asset): boolean { // A user can see all assets in all workgroups that they are assigned to. return this.hasWorkgroup(value.workgroupId); } - canCreate(): boolean { + override canCreate(): boolean { // A user can create assets for workgroups for which they are an Editor. return this.hasRole(Role.Editor); } - canUpdate(value: AssetEditDetail): boolean { + override canUpdate(value: Asset): boolean { // A user can update assets for all workgroups for which they are an Editor. return this.hasRole(Role.Editor, value.workgroupId); } - canDelete(value: AssetEditDetail): boolean { + override canDelete(value: Asset): boolean { // A user can delete assets for all workgroups for which they are an Editor. return this.hasRole(Role.Editor, value.workgroupId); } diff --git a/apps/server-asset-sg/src/features/assets/asset.policy.ts b/libs/shared/v2/src/lib/policies/asset.policy.ts similarity index 70% rename from apps/server-asset-sg/src/features/assets/asset.policy.ts rename to libs/shared/v2/src/lib/policies/asset.policy.ts index bd2e4e6d..4fe7817b 100644 --- a/apps/server-asset-sg/src/features/assets/asset.policy.ts +++ b/libs/shared/v2/src/lib/policies/asset.policy.ts @@ -1,6 +1,6 @@ -import { Policy } from '@/core/policy'; -import { Asset } from '@/features/assets/asset.model'; -import { Role } from '@/features/workgroups/workgroup.model'; +import { Asset } from '../models/asset'; +import { Role } from '../models/workgroup'; +import { Policy } from './base/policy'; export class AssetPolicy extends Policy { canShow(value: Asset): boolean { @@ -8,17 +8,17 @@ export class AssetPolicy extends Policy { return this.hasWorkgroup(value.workgroupId); } - canCreate(): boolean { + override canCreate(): boolean { // A user can create assets for workgroups for which they are an Editor. return this.hasRole(Role.Editor); } - canUpdate(value: Asset): boolean { + override canUpdate(value: Asset): boolean { // A user can update assets for all workgroups for which they are an Editor. return this.hasRole(Role.Editor, value.workgroupId); } - canDelete(value: Asset): boolean { + override canDelete(value: Asset): boolean { // A user can delete assets for all workgroups for which they are an Editor. return this.hasRole(Role.Editor, value.workgroupId); } diff --git a/apps/server-asset-sg/src/core/policy.ts b/libs/shared/v2/src/lib/policies/base/policy.ts similarity index 88% rename from apps/server-asset-sg/src/core/policy.ts rename to libs/shared/v2/src/lib/policies/base/policy.ts index 154ea950..88dacaa5 100644 --- a/apps/server-asset-sg/src/core/policy.ts +++ b/libs/shared/v2/src/lib/policies/base/policy.ts @@ -1,5 +1,5 @@ -import { User, WorkgroupOnUser } from '@/features/users/user.model'; -import { getRoleIndex, Role, WorkgroupId } from '@/features/workgroups/workgroup.model'; +import { User, WorkgroupOnUser } from '../../models/user'; +import { getRoleIndex, Role, WorkgroupId } from '../../models/workgroup'; export abstract class Policy { private readonly workgroups = new Map(); @@ -54,7 +54,7 @@ export abstract class Policy { return this.canCreate(); } - canDelete(value: T): boolean { + canDelete(_value: T): boolean { return this.canCreate(); } } diff --git a/apps/server-asset-sg/src/features/contacts/contact.policy.ts b/libs/shared/v2/src/lib/policies/contact.policy.ts similarity index 63% rename from apps/server-asset-sg/src/features/contacts/contact.policy.ts rename to libs/shared/v2/src/lib/policies/contact.policy.ts index 9868ea18..bbccb647 100644 --- a/apps/server-asset-sg/src/features/contacts/contact.policy.ts +++ b/libs/shared/v2/src/lib/policies/contact.policy.ts @@ -1,6 +1,6 @@ -import { Policy } from '@/core/policy'; -import { Contact } from '@/features/contacts/contact.model'; -import { Role } from '@/features/workgroups/workgroup.model'; +import { Contact } from '../models/contact'; +import { Role } from '../models/workgroup'; +import { Policy } from './base/policy'; export class ContactPolicy extends Policy { canShow(_value: Contact): boolean { diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.policy.ts b/libs/shared/v2/src/lib/policies/workgroup.policy.ts similarity index 72% rename from apps/server-asset-sg/src/features/workgroups/workgroup.policy.ts rename to libs/shared/v2/src/lib/policies/workgroup.policy.ts index c793c15d..311723a5 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.policy.ts +++ b/libs/shared/v2/src/lib/policies/workgroup.policy.ts @@ -1,5 +1,5 @@ -import { Policy } from '@/core/policy'; -import { Workgroup } from '@/features/workgroups/workgroup.model'; +import { Workgroup } from '../models/workgroup'; +import { Policy } from './base/policy'; export class WorkgroupPolicy extends Policy { canShow(value: Workgroup): boolean { diff --git a/libs/shared/v2/src/lib/schemas/asset.schema.ts b/libs/shared/v2/src/lib/schemas/asset.schema.ts new file mode 100644 index 00000000..ccb69016 --- /dev/null +++ b/libs/shared/v2/src/lib/schemas/asset.schema.ts @@ -0,0 +1,205 @@ +import { Transform, Type } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsDate, + IsEnum, + IsInt, + IsNumber, + IsObject, + IsString, + ValidateNested, +} from 'class-validator'; +import { + AssetData, + AssetIdentifierData, + AssetLinksData, + AssetUsage, + AssetUsages, + ContactAssignment, + ContactAssignmentRole, + InfoGeol, + StudyData, + UsageStatusCode, + WorkStatusData, +} from '../models/asset'; +import { LocalDate } from '../models/base/local-date'; +import { StudyType } from '../models/study'; +import { IsNullable, messageNullableInt, messageNullableString } from '../utils/class-validator/is-nullable.decorator'; + +export class AssetUsageSchema implements AssetUsage { + @IsBoolean() + isAvailable!: boolean; + + @IsEnum(UsageStatusCode) + statusCode!: UsageStatusCode; + + @IsNullable() + @ValidateNested() + @Type(() => String) + @Transform(({ value }) => LocalDate.parse(value)) + availableAt!: LocalDate | null; +} + +export class AssetUsagesSchema implements AssetUsages { + @IsObject() + @ValidateNested() + @Type(() => AssetUsageSchema) + public!: AssetUsageSchema; + + @IsObject() + @ValidateNested() + @Type(() => AssetUsageSchema) + internal!: AssetUsageSchema; +} + +export class InfoGeolSchema implements InfoGeol { + @IsString({ message: messageNullableString }) + @IsNullable() + main!: string | null; + + @IsString({ message: messageNullableString }) + @IsNullable() + contact!: string | null; + + @IsString({ message: messageNullableString }) + @IsNullable() + auxiliary!: string | null; +} + +export class ContactAssignmentSchema implements ContactAssignment { + @IsInt() + contactId!: number; + + @IsEnum(ContactAssignmentRole) + role!: ContactAssignmentRole; +} + +export class StudyDataSchema implements StudyData { + @IsInt({ message: messageNullableInt }) + @IsNullable() + id?: number | undefined; + + @IsString() + geom!: string; + + @IsEnum(StudyType) + type!: StudyType; +} + +export class WorkStatusSchema implements WorkStatusData { + @IsInt({ message: messageNullableInt }) + @IsNullable() + id?: number | undefined; + + @IsDate() + @Type(() => Date) + createdAt!: Date; + + @IsString() + itemCode!: string; +} + +export class AssetIdentifierSchema implements AssetIdentifierData { + @IsInt({ message: messageNullableInt }) + @IsNullable() + id?: number | undefined; + + @IsString() + name!: string; + + @IsString() + description!: string; +} + +export class AssetLinksDataSchema implements AssetLinksData { + @IsInt({ message: messageNullableInt }) + @IsNullable() + parent!: number | null; + + @IsInt({ each: true }) + siblings!: number[]; +} + +export class AssetDataSchema implements AssetData { + @IsObject() + @ValidateNested() + @Type(() => AssetLinksDataSchema) + links!: AssetLinksDataSchema; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AssetIdentifierSchema) + identifiers!: AssetIdentifierSchema[]; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => WorkStatusSchema) + statuses!: WorkStatusSchema[]; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => StudyDataSchema) + studies!: StudyDataSchema[]; + + @IsString() + title!: string; + + @IsString({ message: messageNullableString }) + @IsNullable() + originalTitle!: string | null; + + @IsString() + kindCode!: string; + + @IsString() + formatCode!: string; + + @IsString({ each: true }) + languageCodes!: string[]; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ContactAssignmentSchema) + contactAssignments!: ContactAssignmentSchema[]; + + @IsString({ each: true }) + manCatLabelCodes!: string[]; + + @IsString({ each: true }) + natRelCodes!: string[]; + + @ValidateNested() + @Type(() => String) + @Transform(({ value }) => LocalDate.tryParse(value)) + createdAt!: LocalDate; + + @ValidateNested() + @Type(() => String) + @Transform(({ value }) => LocalDate.tryParse(value)) + receivedAt!: LocalDate; + + @IsInt({ message: messageNullableInt }) + @IsNullable() + sgsId!: number | null; + + @IsString({ message: messageNullableString }) + @IsNullable() + municipality!: string | null; + + @IsBoolean() + isNatRel!: boolean; + + @IsObject() + @ValidateNested() + @Type(() => InfoGeolSchema) + infoGeol!: InfoGeolSchema; + + @IsObject() + @ValidateNested() + @Type(() => AssetUsagesSchema) + usage!: AssetUsagesSchema; + + @IsNumber() + workgroupId!: number; +} diff --git a/apps/server-asset-sg/src/features/contacts/contact.model.ts b/libs/shared/v2/src/lib/schemas/contact.schema.ts similarity index 54% rename from apps/server-asset-sg/src/features/contacts/contact.model.ts rename to libs/shared/v2/src/lib/schemas/contact.schema.ts index e4fbb210..46a2a0d1 100644 --- a/apps/server-asset-sg/src/features/contacts/contact.model.ts +++ b/libs/shared/v2/src/lib/schemas/contact.schema.ts @@ -1,24 +1,7 @@ import { IsOptional, IsString } from 'class-validator'; +import { ContactData } from '../models/contact'; -import { Data, Model } from '@/utils/data/model'; - -export interface Contact extends Model { - name: string; - street: string | null; - houseNumber: string | null; - plz: string | null; - locality: string | null; - country: string | null; - telephone: string | null; - email: string | null; - website: string | null; - contactKindItemCode: string; -} - -export type ContactId = number; -export type ContactData = Data; - -export class ContactDataBoundary implements ContactData { +export class ContactDataSchema implements ContactData { @IsString() name!: string; diff --git a/libs/shared/v2/src/lib/schemas/user.schema.ts b/libs/shared/v2/src/lib/schemas/user.schema.ts new file mode 100644 index 00000000..eefeed7a --- /dev/null +++ b/libs/shared/v2/src/lib/schemas/user.schema.ts @@ -0,0 +1,13 @@ +import { IsArray, IsBoolean, IsString } from 'class-validator'; +import { UserData, WorkgroupOnUser } from '../models/user'; + +export class UserDataSchema implements UserData { + @IsString() + lang!: string; + + @IsArray() + workgroups!: WorkgroupOnUser[]; + + @IsBoolean() + isAdmin!: boolean; +} diff --git a/apps/server-asset-sg/src/core/decorators/is-nullable.decorator.ts b/libs/shared/v2/src/lib/utils/class-validator/is-nullable.decorator.ts similarity index 100% rename from apps/server-asset-sg/src/core/decorators/is-nullable.decorator.ts rename to libs/shared/v2/src/lib/utils/class-validator/is-nullable.decorator.ts diff --git a/libs/shared/v2/tsconfig.json b/libs/shared/v2/tsconfig.json new file mode 100644 index 00000000..b4fc3fa0 --- /dev/null +++ b/libs/shared/v2/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "allowSyntheticDefaultImports": true + } +} diff --git a/libs/shared/v2/tsconfig.lib.json b/libs/shared/v2/tsconfig.lib.json new file mode 100644 index 00000000..4a05ef7f --- /dev/null +++ b/libs/shared/v2/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts"] +} diff --git a/libs/shared/v2/tsconfig.spec.json b/libs/shared/v2/tsconfig.spec.json new file mode 100644 index 00000000..25b7af8f --- /dev/null +++ b/libs/shared/v2/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index c51d28a2..e0297a78 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -24,6 +24,7 @@ "@asset-sg/favourite": ["libs/favourite/src/index.ts"], "@asset-sg/profile": ["libs/profile/src/index.ts"], "@asset-sg/shared": ["libs/shared/src/index.ts"], + "@asset-sg/shared/v2": ["libs/shared/v2/src/index.ts"], "ngx-kobalte": ["libs/ngx-kobalte/src/index.ts"] } }, diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 00000000..23f53042 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "outDir": "dist/out-tsc", + "module": "commonjs", + "types": ["jest"], + "resolveJsonModule": true, + "esModuleInterop": true + }, + "include": ["**/jest.config.ts", "**/jest.setup.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} From f05a87e70b1322576f6447e0dbdeb8cf95876e52 Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Fri, 19 Jul 2024 11:59:02 +0200 Subject: [PATCH 16/21] Add workgroup dropdown to asset form # Conflicts: # apps/server-asset-sg/src/app.module.ts Add title to workgroup section in asset form --- apps/client-asset-sg/src/app/app.component.ts | 1 + apps/client-asset-sg/src/app/i18n/de.ts | 3 +++ apps/client-asset-sg/src/app/i18n/en.ts | 3 +++ apps/client-asset-sg/src/app/i18n/fr.ts | 3 +++ apps/client-asset-sg/src/app/i18n/it.ts | 3 +++ apps/client-asset-sg/src/app/i18n/rm.ts | 3 +++ .../src/app/state/app-shared-state.service.ts | 7 ++++++- .../src/app/state/app-shared.reducer.ts | 2 ++ .../src/app/state/app.effects.ts | 8 ++++++++ .../src/features/asset-edit/asset-edit.repo.ts | 1 + .../workgroups/workgroup-simple.repo.ts | 4 ++-- .../lib/components/asset-editor-form-group.ts | 3 +++ .../asset-editor-tab-general.component.html | 10 ++++++++++ .../asset-editor-tab-general.component.ts | 17 +++++++++++------ .../asset-editor-tab-page.component.ts | 4 +++- .../src/lib/state/app-shared-state.actions.ts | 7 +++++++ .../src/lib/state/app-shared-state.selectors.ts | 2 ++ .../src/lib/state/app-shared-state.ts | 2 ++ 18 files changed, 73 insertions(+), 10 deletions(-) diff --git a/apps/client-asset-sg/src/app/app.component.ts b/apps/client-asset-sg/src/app/app.component.ts index 6d0aebb0..f76cef43 100644 --- a/apps/client-asset-sg/src/app/app.component.ts +++ b/apps/client-asset-sg/src/app/app.component.ts @@ -41,6 +41,7 @@ export class AppComponent { await this.authService.signIn(); this.store.dispatch(appSharedStateActions.loadUserProfile()); this.store.dispatch(appSharedStateActions.loadReferenceData()); + this.store.dispatch(appSharedStateActions.loadWorkgroups()); }); const wndw = this._wndw; diff --git a/apps/client-asset-sg/src/app/i18n/de.ts b/apps/client-asset-sg/src/app/i18n/de.ts index c9edb25f..58526ad8 100644 --- a/apps/client-asset-sg/src/app/i18n/de.ts +++ b/apps/client-asset-sg/src/app/i18n/de.ts @@ -17,6 +17,9 @@ export const deAppTranslations = { delete: 'Löschen', close: 'Schliessen', datePlaceholder: 'JJJJ-MM-TT', + workgroup: { + title: 'Arbeitsgruppe', + }, menuBar: { assets: 'Assets', admin: 'Verwaltung', diff --git a/apps/client-asset-sg/src/app/i18n/en.ts b/apps/client-asset-sg/src/app/i18n/en.ts index 0ec994f9..5013c604 100644 --- a/apps/client-asset-sg/src/app/i18n/en.ts +++ b/apps/client-asset-sg/src/app/i18n/en.ts @@ -19,6 +19,9 @@ export const enAppTranslations: AppTranslations = { delete: 'Delete', close: 'Close', datePlaceholder: 'YYYY-MM-DD', + workgroup: { + title: 'Workgroup', + }, menuBar: { assets: 'Assets', admin: 'Administration', diff --git a/apps/client-asset-sg/src/app/i18n/fr.ts b/apps/client-asset-sg/src/app/i18n/fr.ts index 51deb345..a4ac23c8 100644 --- a/apps/client-asset-sg/src/app/i18n/fr.ts +++ b/apps/client-asset-sg/src/app/i18n/fr.ts @@ -19,6 +19,9 @@ export const frAppTranslations: AppTranslations = { delete: 'Supprimer', close: 'Fermer', datePlaceholder: 'AAAA-MM-JJ', + workgroup: { + title: 'groupe de travail', + }, menuBar: { assets: 'Assets', admin: 'Administration', diff --git a/apps/client-asset-sg/src/app/i18n/it.ts b/apps/client-asset-sg/src/app/i18n/it.ts index 41b01736..65e13596 100644 --- a/apps/client-asset-sg/src/app/i18n/it.ts +++ b/apps/client-asset-sg/src/app/i18n/it.ts @@ -19,6 +19,9 @@ export const itAppTranslations: AppTranslations = { delete: 'IT Löschen', close: 'IT Schliessen', datePlaceholder: 'AAAA-MM-GG', + workgroup: { + title: 'IT Arbeitsgruppe', + }, menuBar: { assets: 'IT Assets', admin: 'IT Verwaltung', diff --git a/apps/client-asset-sg/src/app/i18n/rm.ts b/apps/client-asset-sg/src/app/i18n/rm.ts index c6cf1b40..5e5f1546 100644 --- a/apps/client-asset-sg/src/app/i18n/rm.ts +++ b/apps/client-asset-sg/src/app/i18n/rm.ts @@ -19,6 +19,9 @@ export const rmAppTranslations: AppTranslations = { delete: 'RM Löschen', close: 'RM Schliessen', datePlaceholder: 'AAAA-MM-GG', + workgroup: { + title: 'IT Arbeitsgruppe', + }, menuBar: { assets: 'RM Assets', admin: 'RM Verwaltung', diff --git a/apps/client-asset-sg/src/app/state/app-shared-state.service.ts b/apps/client-asset-sg/src/app/state/app-shared-state.service.ts index 5fe81529..b56d7843 100644 --- a/apps/client-asset-sg/src/app/state/app-shared-state.service.ts +++ b/apps/client-asset-sg/src/app/state/app-shared-state.service.ts @@ -3,10 +3,11 @@ import { Injectable } from '@angular/core'; import { ApiError, httpErrorResponseOrUnknownError } from '@asset-sg/client-shared'; import { OE, ORD, decodeError } from '@asset-sg/core'; import { ReferenceData } from '@asset-sg/shared'; +import { SimpleWorkgroup } from '@asset-sg/shared/v2'; import * as RD from '@devexperts/remote-data-ts'; import * as E from 'fp-ts/Either'; import { flow } from 'fp-ts/function'; -import { map, startWith } from 'rxjs'; +import { map, Observable, startWith } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AppSharedStateService { @@ -22,4 +23,8 @@ export class AppSharedStateService { startWith(RD.pending) ); } + + public loadWorkgroups(): Observable { + return this._httpClient.get('/api/workgroups?simple'); + } } diff --git a/apps/client-asset-sg/src/app/state/app-shared.reducer.ts b/apps/client-asset-sg/src/app/state/app-shared.reducer.ts index ee243c5d..c1433bd9 100644 --- a/apps/client-asset-sg/src/app/state/app-shared.reducer.ts +++ b/apps/client-asset-sg/src/app/state/app-shared.reducer.ts @@ -8,6 +8,7 @@ import * as R from 'fp-ts/Record'; const initialState: AppSharedState = { rdUserProfile: RD.initial, rdReferenceData: RD.initial, + workgroups: [], lang: 'de', }; @@ -21,6 +22,7 @@ export const appSharedStateReducer = createReducer( appSharedStateActions.loadReferenceDataResult, (state, rdReferenceData): AppSharedState => ({ ...state, rdReferenceData }) ), + on(appSharedStateActions.loadWorkgroupsResult, (state, { workgroups }): AppSharedState => ({ ...state, workgroups })), on(appSharedStateActions.logout, (): AppSharedState => initialState), on(appSharedStateActions.setLang, (state, { lang }): AppSharedState => ({ ...state, lang })), on( diff --git a/apps/client-asset-sg/src/app/state/app.effects.ts b/apps/client-asset-sg/src/app/state/app.effects.ts index d101b1ce..0c9c0738 100644 --- a/apps/client-asset-sg/src/app/state/app.effects.ts +++ b/apps/client-asset-sg/src/app/state/app.effects.ts @@ -83,4 +83,12 @@ export class AppSharedStateEffects { map(appSharedStateActions.loadUserProfileResult) ) ); + + loadWorkgroups$ = createEffect(() => + this.actions$.pipe( + ofType(appSharedStateActions.loadWorkgroups), + switchMap(() => this.appSharedStateService.loadWorkgroups()), + map((workgroups) => appSharedStateActions.loadWorkgroupsResult({ workgroups })) + ) + ); } diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts index 0189c1d3..91369073 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.repo.ts @@ -194,6 +194,7 @@ export class AssetEditRepo implements Repo assetFiles: new FormControl<(AssetFile & { willBeDeleted: boolean })[]>([], { nonNullable: true }), filesToDelete: new FormControl([], { nonNullable: true }), newFiles: formBuilder.array>([]), + workgroupId: new FormControl(null, { + validators: Validators.required, + }), }); export type AssetEditorGeneralFormGroup = ReturnType; diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html index 87985584..90a79a54 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.html @@ -2,6 +2,16 @@
    +
    workgroup.title
    + + workgroup.title + + + {{ workgroup.name }} + + + +
    edit.tabs.general.title
    edit.tabs.general.publicTitle diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts index d1f9836b..c5f605af 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-general/asset-editor-tab-general.component.ts @@ -3,7 +3,9 @@ import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChil import { FormBuilder, FormControl, FormGroupDirective } from '@angular/forms'; import { fromAppShared } from '@asset-sg/client-shared'; import { eqAssetLanguageEdit } from '@asset-sg/shared'; +import { Role } from '@asset-sg/shared/v2'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { Store } from '@ngrx/store'; import { RxState } from '@rx-angular/state'; import * as O from 'fp-ts/Option'; import { @@ -106,6 +108,15 @@ export class AssetEditorTabGeneralComponent implements OnInit { ); public _currentlyEditedIdIndex$ = this._state.select('currentlyEditedIdIndex'); + private store = inject(Store); + + /** + * The workgroups to which the user is allowed to assign an asset. + */ + public availableWorkgroups$ = this.store + .select(fromAppShared.selectWorkgroups) + .pipe(map((workgroups) => workgroups.filter((it) => it.role != Role.Viewer))); + @Input() public set referenceDataVM$(value: Observable) { this._state.connect('referenceDataVM', value); @@ -176,12 +187,6 @@ export class AssetEditorTabGeneralComponent implements OnInit { this._ngOnInit$.next(); } - public _removeManCatLabelRef(value: string) { - this._form.controls['manCatLabelRefs'].setValue( - this._form.controls['manCatLabelRefs'].value.filter((v: string) => v !== value) - ); - } - public _insertNewIdClicked() { this._state.set({ userInsertMode: true, currentlyEditedIdIndex: -1 }); this.idForm.reset(); diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts index ec5c3ccc..16f50c2c 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts @@ -105,6 +105,7 @@ export class AssetEditorTabPageComponent { filesToDelete: [], newFiles: [], assetFiles: asset.assetFiles.map((file) => ({ ...file, willBeDeleted: false })), + workgroupId: asset.workgroupId, }, usage: { publicUse: asset.publicUse.isAvailable, @@ -284,7 +285,8 @@ export class AssetEditorTabPageComponent { newStatusWorkItemCode: O.fromNullable(this._form.getRawValue().administration.newStatusWorkItemCode), assetMainId: O.fromNullable(this._form.getRawValue().references.assetMain?.assetId), siblingAssetIds: this._form.getRawValue().references.siblingAssets.map((asset) => asset.assetId), - workgroupId: 1, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + workgroupId: this._form.getRawValue().general.workgroupId!, }; this._showProgressBar$.next(true); if (this._form.getRawValue().general.id === 0) { diff --git a/libs/client-shared/src/lib/state/app-shared-state.actions.ts b/libs/client-shared/src/lib/state/app-shared-state.actions.ts index 4f0f8117..0e3d1fa4 100644 --- a/libs/client-shared/src/lib/state/app-shared-state.actions.ts +++ b/libs/client-shared/src/lib/state/app-shared-state.actions.ts @@ -1,4 +1,5 @@ import { Contact, Lang, ReferenceData, User } from '@asset-sg/shared'; +import { SimpleWorkgroup } from '@asset-sg/shared/v2'; import * as RD from '@devexperts/remote-data-ts'; import { createAction, props } from '@ngrx/store'; @@ -25,6 +26,12 @@ export const loadUserProfileResult = createAction( props>() ); +export const loadWorkgroups = createAction('[App Shared State] Load Workgroups'); +export const loadWorkgroupsResult = createAction( + '[App Shared State] Load Workgroups Result', + props<{ workgroups: SimpleWorkgroup[] }>() +); + export const openPanel = createAction('[App Shared State] Open Panel'); export const logout = createAction('[App Shared State] Logout'); diff --git a/libs/client-shared/src/lib/state/app-shared-state.selectors.ts b/libs/client-shared/src/lib/state/app-shared-state.selectors.ts index 6ab44b6a..a39a1f43 100644 --- a/libs/client-shared/src/lib/state/app-shared-state.selectors.ts +++ b/libs/client-shared/src/lib/state/app-shared-state.selectors.ts @@ -17,6 +17,8 @@ export const selectRDUserProfile = createSelector(appSharedFeature, (state) => s export const selectUser = createSelector(selectRDUserProfile, RD.toNullable); +export const selectWorkgroups = createSelector(appSharedFeature, (state) => state.workgroups); + const makeReferenceDataVM = (referenceData: ReferenceData) => ({ ...referenceData, assetFormItemArray: valueItemRecordToArray(referenceData.assetFormatItems), diff --git a/libs/client-shared/src/lib/state/app-shared-state.ts b/libs/client-shared/src/lib/state/app-shared-state.ts index f1336abc..8dcff17f 100644 --- a/libs/client-shared/src/lib/state/app-shared-state.ts +++ b/libs/client-shared/src/lib/state/app-shared-state.ts @@ -1,4 +1,5 @@ import { Lang, ReferenceData, User } from '@asset-sg/shared'; +import { SimpleWorkgroup } from '@asset-sg/shared/v2'; import * as RD from '@devexperts/remote-data-ts'; import { ApiError } from '../utils'; @@ -6,6 +7,7 @@ import { ApiError } from '../utils'; export interface AppSharedState { rdUserProfile: RD.RemoteData; rdReferenceData: RD.RemoteData; + workgroups: SimpleWorkgroup[]; lang: Lang; } From b27d281557c6bdf8f1ef68d0a0716b260f27656e Mon Sep 17 00:00:00 2001 From: till_schuetze Date: Wed, 26 Jun 2024 09:19:16 +0200 Subject: [PATCH 17/21] Add workgroup admin ui --- .../src/app/app.component.scss | 1 + apps/client-asset-sg/src/app/i18n/de.ts | 33 ++++ apps/client-asset-sg/src/app/i18n/en.ts | 34 ++++ apps/client-asset-sg/src/app/i18n/fr.ts | 33 ++++ apps/client-asset-sg/src/app/i18n/it.ts | 33 ++++ apps/client-asset-sg/src/app/i18n/rm.ts | 33 ++++ .../src/features/users/user.repo.ts | 2 +- .../src/features/users/users.controller.ts | 5 + .../src/features/users/users.http | 8 + .../src/features/workgroups/workgroup.repo.ts | 13 +- .../src/features/workgroups/workgroups.http | 2 +- libs/admin/src/lib/admin-routing.module.ts | 48 +++++ libs/admin/src/lib/admin.module.ts | 37 ++-- .../add-workgroup-user-dialog.component.html | 34 ++++ .../add-workgroup-user-dialog.component.scss | 13 ++ .../add-workgroup-user-dialog.component.ts | 80 ++++++++ .../admin-page/admin-page.component.html | 32 ++-- .../admin-page/admin-page.component.scss | 61 ++++-- .../admin-page/admin-page.component.ts | 53 +++--- .../lib/components/user-collapsed/index.ts | 1 - .../user-collapsed.component.html | 6 - .../user-collapsed.component.scss | 14 -- .../user-collapsed.component.ts | 14 -- .../user-edit/user-edit.component.html | 136 +++++++++---- .../user-edit/user-edit.component.scss | 58 ++++++ .../user-edit/user-edit.component.ts | 173 ++++++++++++++--- .../src/lib/components/user-expanded/index.ts | 1 - .../user-expanded.component.html | 39 ---- .../user-expanded.component.scss | 72 ------- .../user-expanded/user-expanded.component.ts | 123 ------------ .../lib/components/users/users.component.html | 89 +++++---- .../lib/components/users/users.component.scss | 27 +++ .../lib/components/users/users.component.ts | 34 ++-- .../workgroup-edit-component.ts | 61 ------ .../workgroup-edit.component.html | 93 ++++++--- .../workgroup-edit.component.scss | 57 +++++- .../workgroup-edit.component.ts | 180 ++++++++++++++++++ .../workgroups/workgroups.component.html | 65 ++++--- .../workgroups/workgroups.component.scss | 27 +++ .../workgroups/workgroups.component.ts | 39 ++-- libs/admin/src/lib/services/admin.service.ts | 78 ++++---- libs/admin/src/lib/state/admin.actions.ts | 74 +++++++ libs/admin/src/lib/state/admin.effects.ts | 100 ++++++++++ libs/admin/src/lib/state/admin.reducer.ts | 116 +++++++++++ libs/admin/src/lib/state/admin.selector.ts | 11 ++ .../asset-editor-tab-page.component.ts | 8 +- libs/shared/v2/src/lib/models/workgroup.ts | 6 +- 47 files changed, 1606 insertions(+), 651 deletions(-) create mode 100644 libs/admin/src/lib/admin-routing.module.ts create mode 100644 libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.html create mode 100644 libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.scss create mode 100644 libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.ts delete mode 100644 libs/admin/src/lib/components/user-collapsed/index.ts delete mode 100644 libs/admin/src/lib/components/user-collapsed/user-collapsed.component.html delete mode 100644 libs/admin/src/lib/components/user-collapsed/user-collapsed.component.scss delete mode 100644 libs/admin/src/lib/components/user-collapsed/user-collapsed.component.ts delete mode 100644 libs/admin/src/lib/components/user-expanded/index.ts delete mode 100644 libs/admin/src/lib/components/user-expanded/user-expanded.component.html delete mode 100644 libs/admin/src/lib/components/user-expanded/user-expanded.component.scss delete mode 100644 libs/admin/src/lib/components/user-expanded/user-expanded.component.ts delete mode 100644 libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts create mode 100644 libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts create mode 100644 libs/admin/src/lib/state/admin.actions.ts create mode 100644 libs/admin/src/lib/state/admin.effects.ts create mode 100644 libs/admin/src/lib/state/admin.reducer.ts create mode 100644 libs/admin/src/lib/state/admin.selector.ts diff --git a/apps/client-asset-sg/src/app/app.component.scss b/apps/client-asset-sg/src/app/app.component.scss index 96f5edf3..28b5b0d7 100644 --- a/apps/client-asset-sg/src/app/app.component.scss +++ b/apps/client-asset-sg/src/app/app.component.scss @@ -29,6 +29,7 @@ asset-sg-menu-bar { .router-outlet { grid-area: router-outlet; display: grid; + router-outlet { display: none; } diff --git a/apps/client-asset-sg/src/app/i18n/de.ts b/apps/client-asset-sg/src/app/i18n/de.ts index 58526ad8..79089f00 100644 --- a/apps/client-asset-sg/src/app/i18n/de.ts +++ b/apps/client-asset-sg/src/app/i18n/de.ts @@ -236,4 +236,37 @@ export const deAppTranslations = { ' Damit wird sichergestellt, dass die Suche alle vorhandenen Assets miteinbezieht.', adminInstructionsSyncElasticAssetsStart: 'Synchronisation starten', }, + admin: { + users: 'Benutzer', + workgroups: 'Arbeitsgruppen', + name: 'Name', + role: 'Rolle', + actions: 'Aktionen', + email: 'E-Mail', + back: 'Zurück', + languages: { + de: 'Deutsch', + en: 'Englisch', + fr: 'Französisch', + it: 'Italienisch', + rm: 'Rätoromanisch', + }, + userPage: { + admin: 'Admin', + lang: 'Sprache', + addWorkgroups: 'Arbeitsgruppen hinzufügen', + more: 'weitere', + userAddError: 'Füge mindestens einen Benutzer hinzu', + }, + workgroupPage: { + name: 'Name', + isActive: 'Aktiv', + activate: 'Aktivieren', + deactivate: 'Deaktivieren', + create: 'Erstellen', + isDeactivated: 'Deaktiviert', + chooseUsersText: 'Füge Benutzer hinzu, um sie zu verwalten', + addUsers: 'Benutzer hinzufügen', + }, + }, }; diff --git a/apps/client-asset-sg/src/app/i18n/en.ts b/apps/client-asset-sg/src/app/i18n/en.ts index 5013c604..b882d5f0 100644 --- a/apps/client-asset-sg/src/app/i18n/en.ts +++ b/apps/client-asset-sg/src/app/i18n/en.ts @@ -237,4 +237,38 @@ export const enAppTranslations: AppTranslations = { ' This ensures that the search includes all existing assets.', adminInstructionsSyncElasticAssetsStart: 'Start synchronization', }, + admin: { + users: 'Users', + workgroups: 'Workgroups', + name: 'Name', + role: 'Role', + actions: 'Actions', + email: 'Email', + back: 'Back', + languages: { + de: 'German', + en: 'English', + fr: 'French', + it: 'Italian', + rm: 'Romansh', + }, + userPage: { + admin: 'Admin', + lang: 'Language', + addWorkgroups: 'Add workgroups', + more: 'more', + userAddError: 'Add at least one user', + }, + workgroupPage: { + name: 'Name', + isActive: 'Active', + activate: 'Activate', + deactivate: 'Deactivate', + create: 'Create', + isDeactivated: 'Deactivated', + chooseUsersText: 'Add users to manage', + + addUsers: 'Add users', + }, + }, }; diff --git a/apps/client-asset-sg/src/app/i18n/fr.ts b/apps/client-asset-sg/src/app/i18n/fr.ts index a4ac23c8..73e5c5a7 100644 --- a/apps/client-asset-sg/src/app/i18n/fr.ts +++ b/apps/client-asset-sg/src/app/i18n/fr.ts @@ -238,4 +238,37 @@ export const frAppTranslations: AppTranslations = { " Cela permet de s'assurer que la recherche inclut tous les actifs existants.", adminInstructionsSyncElasticAssetsStart: 'Démarrer la synchronisation', }, + admin: { + users: 'Utilisateurs', + workgroups: 'Groupes de travail', + name: 'Nom', + role: 'Rôle', + actions: 'Actions', + email: 'E-mail', + back: 'Retour', + languages: { + de: 'Allemand', + en: 'Anglais', + fr: 'Français', + it: 'Italien', + rm: 'Romanche', + }, + userPage: { + admin: 'Admin', + lang: 'Langue', + addWorkgroups: 'Ajouter des groupes de travail', + more: 'en plus', + userAddError: 'Ajoute au moins un utilisateur', + }, + workgroupPage: { + name: 'Nom', + isActive: 'Actif', + create: 'Créer', + activate: 'Activer', + deactivate: 'Désactiver', + isDeactivated: 'Désactivé', + chooseUsersText: 'Ajoutez des utilisateurs pour les gérer', + addUsers: 'Ajouter des utilisateurs', + }, + }, }; diff --git a/apps/client-asset-sg/src/app/i18n/it.ts b/apps/client-asset-sg/src/app/i18n/it.ts index 65e13596..4faf09e7 100644 --- a/apps/client-asset-sg/src/app/i18n/it.ts +++ b/apps/client-asset-sg/src/app/i18n/it.ts @@ -237,4 +237,37 @@ export const itAppTranslations: AppTranslations = { 'IT Damit wird sichergestellt, dass die Suche alle vorhandenen Assets miteinbezieht.', adminInstructionsSyncElasticAssetsStart: 'IT Synchronisation starten', }, + admin: { + users: 'IT Benutzer', + workgroups: 'IT Arbeitsgruppen', + name: 'IT Name', + role: 'IT Rolle', + actions: 'IT Aktionen', + email: 'IT E-Mail', + back: 'IT Zurück', + languages: { + de: 'IT Deutsch', + en: 'IT Englisch', + fr: 'IT Französisch', + it: 'IT Italienisch', + rm: 'IT Rätoromanisch', + }, + userPage: { + admin: 'IT Admin', + lang: 'IT Sprache', + addWorkgroups: 'IT Arbeitsgruppen hinzufügen', + more: 'IT weitere', + userAddError: 'IT Füge mindestens einen Benutzer hinzu', + }, + workgroupPage: { + name: 'IT Name', + isActive: 'IT Aktiv', + activate: 'IT Aktivieren', + deactivate: 'IT Deaktivieren', + create: 'IT Erstellen', + isDeactivated: 'IT Deaktiviert', + chooseUsersText: 'IT Füge Nutzer hinzu, um sie zu verwalten', + addUsers: 'IT Benutzer hinzufügen', + }, + }, }; diff --git a/apps/client-asset-sg/src/app/i18n/rm.ts b/apps/client-asset-sg/src/app/i18n/rm.ts index 5e5f1546..a80e575f 100644 --- a/apps/client-asset-sg/src/app/i18n/rm.ts +++ b/apps/client-asset-sg/src/app/i18n/rm.ts @@ -237,4 +237,37 @@ export const rmAppTranslations: AppTranslations = { 'RM Damit wird sichergestellt, dass die Suche alle vorhandenen Assets miteinbezieht.', adminInstructionsSyncElasticAssetsStart: 'RM Synchronisation starten', }, + admin: { + users: 'RM Benutzer', + workgroups: 'RM Arbeitsgruppen', + name: 'RM Name', + role: 'RM Rolle', + actions: 'RM Aktionen', + email: 'RM E-Mail', + back: 'RM Zurück', + languages: { + de: 'RM Deutsch', + en: 'RM Englisch', + fr: 'RM Französisch', + it: 'RM Italienisch', + rm: 'RM Rätoromanisch', + }, + userPage: { + admin: 'RM Admin', + lang: 'RM Sprache', + addWorkgroups: 'RM Arbeitsgruppen hinzufügen', + more: 'RM weitere', + userAddError: 'RM Füge mindestens einen Benutzer hinzu', + }, + workgroupPage: { + name: 'RM Name', + isActive: 'RM Aktiv', + create: 'RM Erstellen', + activate: 'RM Aktivieren', + deactivate: 'RM Deaktivieren', + isDeactivated: 'RM Deaktiviert', + chooseUsersText: 'RM Füge Nutzer hinzu, um sie zu verwalten', + addUsers: 'RM Benutzer hinzufügen', + }, + }, }; diff --git a/apps/server-asset-sg/src/features/users/user.repo.ts b/apps/server-asset-sg/src/features/users/user.repo.ts index cdbdc28b..232c85fc 100644 --- a/apps/server-asset-sg/src/features/users/user.repo.ts +++ b/apps/server-asset-sg/src/features/users/user.repo.ts @@ -64,7 +64,7 @@ export class UserRepo implements Repo workgroup.id) }, + workgroupId: {}, }, createMany: { data: data.workgroups?.map((workgroup) => ({ diff --git a/apps/server-asset-sg/src/features/users/users.controller.ts b/apps/server-asset-sg/src/features/users/users.controller.ts index d951cfbf..ab4a7da8 100644 --- a/apps/server-asset-sg/src/features/users/users.controller.ts +++ b/apps/server-asset-sg/src/features/users/users.controller.ts @@ -21,6 +21,11 @@ export class UsersController { return this.userRepo.list(); } + @Get('/:id') + show(@Param('id') id: UserId): Promise { + return this.userRepo.find(id); + } + @Put('/:id') @Authorize.Admin() async update( diff --git a/apps/server-asset-sg/src/features/users/users.http b/apps/server-asset-sg/src/features/users/users.http index 4d6a6a44..9ddb7c62 100644 --- a/apps/server-asset-sg/src/features/users/users.http +++ b/apps/server-asset-sg/src/features/users/users.http @@ -8,6 +8,14 @@ Authorization: Impersonate {{user}} GET {{host}}/api/users Authorization: Impersonate {{user}} +### Find User by ID +GET {{host}}/api/users/{{user-id}} +Authorization: Impersonate {{user}} + +### Find User by ID +GET {{host}}/api/admin/user/{{user-id}} +Authorization: Impersonate {{user}} + ### Update user PUT {{host}}/api/users/{{user-id}} Authorization: Impersonate {{user}} diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts index e62229a5..92ec5a42 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts +++ b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts @@ -45,7 +45,7 @@ export class WorkgroupRepo implements Repo ({ - userId: user.userId, + userId: user.user.id, role: user.role, })), skipDuplicates: true, @@ -70,11 +70,11 @@ export class WorkgroupRepo implements Repo user.userId) }, + userId: {}, }, createMany: { data: data.users.map((user) => ({ - userId: user.userId, + userId: user.user.id, role: user.role, })), skipDuplicates: true, @@ -108,8 +108,13 @@ export const workGroupSelection = satisfy()({ disabled_at: true, users: { select: { - userId: true, role: true, + user: { + select: { + email: true, + id: true, + }, + }, }, }, assets: { diff --git a/apps/server-asset-sg/src/features/workgroups/workgroups.http b/apps/server-asset-sg/src/features/workgroups/workgroups.http index 2afc77af..be97405b 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroups.http +++ b/apps/server-asset-sg/src/features/workgroups/workgroups.http @@ -19,7 +19,7 @@ Authorization: Impersonate {{user}} Content-Type: application/json { - "name": "Testing2", + "name": "WORKGROUP", "disabled_at": null, "assets": [ { diff --git a/libs/admin/src/lib/admin-routing.module.ts b/libs/admin/src/lib/admin-routing.module.ts new file mode 100644 index 00000000..a412d91f --- /dev/null +++ b/libs/admin/src/lib/admin-routing.module.ts @@ -0,0 +1,48 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AdminPageComponent } from './components/admin-page'; +import { UserEditComponent } from './components/user-edit/user-edit.component'; +import { UsersComponent } from './components/users/users.component'; +import { WorkgroupEditComponent } from './components/workgroup-edit/workgroup-edit.component'; +import { WorkgroupsComponent } from './components/workgroups/workgroups.component'; + +const routes: Routes = [ + { + path: '', + component: AdminPageComponent, + children: [ + { + path: 'users', + component: UsersComponent, + }, + { + path: 'workgroups', + component: WorkgroupsComponent, + }, + { + path: 'workgroups/new', + component: WorkgroupEditComponent, + }, + { + path: 'users/:id', + component: UserEditComponent, + }, + { + path: 'workgroups/:id', + component: WorkgroupEditComponent, + }, + { + pathMatch: 'full', + path: '', + redirectTo: 'users', + }, + ], + }, +]; + +@NgModule({ + declarations: [], + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AdminPageRoutingModule {} diff --git a/libs/admin/src/lib/admin.module.ts b/libs/admin/src/lib/admin.module.ts index 2fbf4865..75b8aec8 100644 --- a/libs/admin/src/lib/admin.module.ts +++ b/libs/admin/src/lib/admin.module.ts @@ -3,12 +3,16 @@ import { DialogModule } from '@angular/cdk/dialog'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatCard } from '@angular/material/card'; import { MatCheckbox } from '@angular/material/checkbox'; +import { MatDialogActions, MatDialogContent, MatDialogTitle } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; import { MatCell, MatCellDef, @@ -21,7 +25,8 @@ import { MatRowDef, MatTable, } from '@angular/material/table'; -import { RouterModule } from '@angular/router'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatTooltip } from '@angular/material/tooltip'; import { AnchorComponent, ButtonComponent, @@ -30,36 +35,36 @@ import { ViewChildMarker, } from '@asset-sg/client-shared'; import { SvgIconComponent } from '@ngneat/svg-icon'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreModule } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; import { ForModule } from '@rx-angular/template/for'; import { LetModule } from '@rx-angular/template/let'; import { PushModule } from '@rx-angular/template/push'; +import { AdminPageRoutingModule } from './admin-routing.module'; +import { AddWorkgroupUserDialog } from './components/add-workgroup-user-dialog/add-workgroup-user-dialog.component'; import { AdminPageComponent } from './components/admin-page'; -import { UserCollapsedComponent } from './components/user-collapsed'; import { UserEditComponent } from './components/user-edit/user-edit.component'; -import { UserExpandedComponent } from './components/user-expanded'; import { UsersComponent } from './components/users/users.component'; -import { WorkgroupEditComponent } from './components/workgroup-edit/workgroup-edit-component'; +import { WorkgroupEditComponent } from './components/workgroup-edit/workgroup-edit.component'; import { WorkgroupsComponent } from './components/workgroups/workgroups.component'; +import { AdminEffects } from './state/admin.effects'; +import { adminReducer } from './state/admin.reducer'; @NgModule({ declarations: [ AdminPageComponent, - UserCollapsedComponent, - UserExpandedComponent, WorkgroupsComponent, WorkgroupEditComponent, UsersComponent, UserEditComponent, + AddWorkgroupUserDialog, ], imports: [ CommonModule, - RouterModule.forChild([ - { - path: '', - component: AdminPageComponent, - }, - ]), + AdminPageRoutingModule, + StoreModule.forFeature('admin', adminReducer), + EffectsModule.forFeature(AdminEffects), TranslateModule.forChild(), ReactiveFormsModule, @@ -74,6 +79,7 @@ import { WorkgroupsComponent } from './components/workgroups/workgroups.componen A11yModule, MatProgressBarModule, + MatTooltip, MatCheckbox, MatFormFieldModule, MatInputModule, @@ -87,11 +93,18 @@ import { WorkgroupsComponent } from './components/workgroups/workgroups.componen MatRow, MatHeaderRowDef, MatRowDef, + MatTabsModule, ViewChildMarker, ButtonComponent, AnchorComponent, DrawerComponent, DrawerPanelComponent, + MatDialogActions, + MatDialogContent, + MatDialogTitle, + MatAutocompleteModule, + MatCard, + MatSlideToggle, ], }) export class AdminModule {} diff --git a/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.html b/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.html new file mode 100644 index 00000000..dfdf82f6 --- /dev/null +++ b/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.html @@ -0,0 +1,34 @@ +
    +
    +

    admin.workgroupPage.addUsers

    +
    +
    +
    admin.users
    + + + admin.users + + + {{ user.email }} + + + @if (formGroup.controls['users'].invalid) { + Add at least one user + } + +
    admin.role
    + + admin.role + + + {{ role }} + + + + +
    +
    + + +
    +
    diff --git a/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.scss b/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.scss new file mode 100644 index 00000000..e78ba581 --- /dev/null +++ b/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.scss @@ -0,0 +1,13 @@ +.header { + padding: 20px 24px 0 24px; +} + +.select { + width: 100%; +} + +.actions { + display: flex; + justify-content: flex-end; + gap: 1rem; +} diff --git a/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.ts b/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.ts new file mode 100644 index 00000000..82d23dec --- /dev/null +++ b/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.ts @@ -0,0 +1,80 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { Role } from '@prisma/client'; +import { Observable, Subscription } from 'rxjs'; +import { User, Workgroup } from '../../services/admin.service'; +import * as actions from '../../state/admin.actions'; +import { AppStateWithAdmin } from '../../state/admin.reducer'; +import { selectUsers } from '../../state/admin.selector'; +import { Mode } from '../workgroup-edit/workgroup-edit.component'; + +@Component({ + selector: 'asset-sg-add-workgroup-user-dialog', + templateUrl: './add-workgroup-user-dialog.component.html', + styleUrls: ['./add-workgroup-user-dialog.component.scss'], +}) +export class AddWorkgroupUserDialog implements OnInit { + public formGroup: FormGroup = new FormGroup({ + users: new FormControl([], Validators.required), + role: new FormControl(Role.Viewer, Validators.required), + }); + + public users: User[] = []; + public workgroup: Workgroup; + public mode: Mode = 'edit'; + public readonly roles: Role[] = Object.values(Role); + + private readonly users$: Observable = this.store.select(selectUsers); + private readonly subscriptions: Subscription = new Subscription(); + + constructor( + @Inject(MAT_DIALOG_DATA) public data: { workgroup: Workgroup; mode: Mode }, + private readonly dialogRef: MatDialogRef, + private store: Store + ) { + this.workgroup = this.data.workgroup; + this.mode = this.data.mode; + } + + public ngOnInit() { + this.initSubscriptions(); + } + + public close() { + this.dialogRef.close(); + } + + public addUsers() { + if (!this.formGroup.valid) { + return; + } + const usersToAdd = this.users + .filter((user) => this.formGroup.controls['users'].value.includes(user.id)) + .map((user) => ({ + user: { email: user.email, id: user.id }, + role: this.formGroup.controls['role'].value, + })); + const updatedWorkgroup = { + name: this.workgroup.name, + assets: this.workgroup.assets, + disabled_at: this.workgroup.disabled_at, + users: [...usersToAdd, ...this.workgroup.users], + }; + if (this.mode === 'edit') { + this.store.dispatch(actions.updateWorkgroup({ workgroupId: this.workgroup.id, workgroup: updatedWorkgroup })); + } else { + this.store.dispatch(actions.setWorkgroup({ workgroup: { ...updatedWorkgroup, id: 0 } })); + } + this.dialogRef.close(); + } + + public isUserInWorkgroup(userId: string): boolean { + return this.workgroup.users.some((user) => user.user.id === userId) ?? false; + } + + private initSubscriptions() { + this.subscriptions.add(this.users$.subscribe((users) => (this.users = users))); + } +} diff --git a/libs/admin/src/lib/components/admin-page/admin-page.component.html b/libs/admin/src/lib/components/admin-page/admin-page.component.html index ad49cf88..21c2eb79 100644 --- a/libs/admin/src/lib/components/admin-page/admin-page.component.html +++ b/libs/admin/src/lib/components/admin-page/admin-page.component.html @@ -1,13 +1,19 @@ - - - - -
    -

    Workgroups

    - -

    Users

    - -
    -
    -
    -
    + +
    + @if (!isDetailPage) { + + } @else { + + } +
    + +
    +
    diff --git a/libs/admin/src/lib/components/admin-page/admin-page.component.scss b/libs/admin/src/lib/components/admin-page/admin-page.component.scss index f7b48697..98a61460 100644 --- a/libs/admin/src/lib/components/admin-page/admin-page.component.scss +++ b/libs/admin/src/lib/components/admin-page/admin-page.component.scss @@ -1,32 +1,59 @@ @use "../../styles/variables"; +::ng-deep .navigation .navigation__link { + .mdc-tab__text-label { + color: variables.$cyan-09; + } + + .mdc-tab__text-label:hover { + color: variables.$cyan-09; + } +} + :host { background-color: variables.$grey-03; position: relative; + height: calc(100vh - 88px); + width: 100%; + padding: 1rem; } -[asset-sg-primary].create-new-user-button { - margin-top: 1rem; - margin-bottom: 1rem; +.loading-bar { + position: absolute; + inset: 0; + z-index: 100; + background-color: variables.$grey-03; } -.drawer-panel-content { - padding: 0rem 1rem 1rem 0; - position: relative; +.admin-page { + display: flex; + flex-direction: column; + overflow: hidden; + height: 100%; } -mat-progress-bar { - position: absolute; - top: 1px; - width: calc(100% - 1rem); +.content { + flex-grow: 1; + overflow-y: auto; } -.user-list { - height: calc(100% - 4.5rem); - overflow-y: scroll; -} +.navigation { + display: flex; + gap: 1rem; + padding: 0 1rem; + font-size: 16px; + + .navigation__link { + color: variables.$cyan-09; + text-decoration: underline; + + &--active { + text-decoration: none; + color: black; + } + } -asset-sg-user-collapsed, -asset-sg-user-expanded { - margin-bottom: 1rem; + .pointer { + cursor: pointer; + } } diff --git a/libs/admin/src/lib/components/admin-page/admin-page.component.ts b/libs/admin/src/lib/components/admin-page/admin-page.component.ts index af461462..8e4e2caf 100644 --- a/libs/admin/src/lib/components/admin-page/admin-page.component.ts +++ b/libs/admin/src/lib/components/admin-page/admin-page.component.ts @@ -1,39 +1,40 @@ -import { TemplatePortal } from '@angular/cdk/portal'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - inject, - TemplateRef, - ViewChild, - ViewContainerRef, -} from '@angular/core'; - -import { AppPortalService, LifecycleHooks, LifecycleHooksDirective } from '@asset-sg/client-shared'; -import { asyncScheduler, observeOn } from 'rxjs'; +import { Location } from '@angular/common'; +import { Component, inject, TemplateRef, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; +import { AppState, LifecycleHooksDirective } from '@asset-sg/client-shared'; +import { Store } from '@ngrx/store'; +import * as actions from '../../state/admin.actions'; +import { selectIsLoading } from '../../state/admin.selector'; @Component({ selector: 'asset-sg-admin', templateUrl: './admin-page.component.html', styleUrls: ['./admin-page.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [LifecycleHooksDirective], }) export class AdminPageComponent { @ViewChild('templateDrawerPortalContent') templateDrawerPortalContent!: TemplateRef; - private _lc = inject(LifecycleHooks); - private _appPortalService = inject(AppPortalService); - private _viewContainerRef = inject(ViewContainerRef); - private _cd = inject(ChangeDetectorRef); + private readonly store = inject(Store); + // private readonly location = inject(Location); + + public readonly isLoading$ = this.store.select(selectIsLoading); + + constructor(private location: Location, private router: Router) { + this.store.dispatch(actions.listWorkgroups()); + this.store.dispatch(actions.listUsers()); + } + + public get isDetailPage(): boolean { + // Example condition, adjust based on your routing structure + return ( + this.router.url.includes('/workgroups/') || + this.router.url.includes('/users/') || + this.router.url.includes('/new') + ); + } - constructor() { - this._lc.afterViewInit$.pipe(observeOn(asyncScheduler)).subscribe(() => { - this._appPortalService.setAppBarPortalContent(null); - this._appPortalService.setDrawerPortalContent( - new TemplatePortal(this.templateDrawerPortalContent, this._viewContainerRef) - ); - this._cd.detectChanges(); - }); + goBack(): void { + this.location.back(); } } diff --git a/libs/admin/src/lib/components/user-collapsed/index.ts b/libs/admin/src/lib/components/user-collapsed/index.ts deleted file mode 100644 index b5c9a75b..00000000 --- a/libs/admin/src/lib/components/user-collapsed/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './user-collapsed.component'; diff --git a/libs/admin/src/lib/components/user-collapsed/user-collapsed.component.html b/libs/admin/src/lib/components/user-collapsed/user-collapsed.component.html deleted file mode 100644 index d1074a28..00000000 --- a/libs/admin/src/lib/components/user-collapsed/user-collapsed.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/libs/admin/src/lib/components/user-collapsed/user-collapsed.component.scss b/libs/admin/src/lib/components/user-collapsed/user-collapsed.component.scss deleted file mode 100644 index 5613b5d8..00000000 --- a/libs/admin/src/lib/components/user-collapsed/user-collapsed.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -@use "../../styles/variables"; - -:host { - display: flex; - align-items: center; - justify-content: space-between; - background-color: variables.$white; - padding: 0.5rem 1rem; -} - -.email { - color: variables.$cyan-09; - font-weight: variables.$font-bold; -} diff --git a/libs/admin/src/lib/components/user-collapsed/user-collapsed.component.ts b/libs/admin/src/lib/components/user-collapsed/user-collapsed.component.ts deleted file mode 100644 index 3005a87a..00000000 --- a/libs/admin/src/lib/components/user-collapsed/user-collapsed.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -import { User } from '@asset-sg/shared'; - -@Component({ - selector: 'asset-sg-user-collapsed', - templateUrl: './user-collapsed.component.html', - styleUrls: ['./user-collapsed.component.scss'], -}) -export class UserCollapsedComponent { - @Input() public user?: User; - @Input() public disableEdit = false; - @Output() public editClicked = new EventEmitter(); -} diff --git a/libs/admin/src/lib/components/user-edit/user-edit.component.html b/libs/admin/src/lib/components/user-edit/user-edit.component.html index f09eefac..5eb6a4be 100644 --- a/libs/admin/src/lib/components/user-edit/user-edit.component.html +++ b/libs/admin/src/lib/components/user-edit/user-edit.component.html @@ -1,44 +1,102 @@ -
    -
    -
    - - + diff --git a/libs/admin/src/lib/components/user-edit/user-edit.component.scss b/libs/admin/src/lib/components/user-edit/user-edit.component.scss index e69de29b..f9bdaf8f 100644 --- a/libs/admin/src/lib/components/user-edit/user-edit.component.scss +++ b/libs/admin/src/lib/components/user-edit/user-edit.component.scss @@ -0,0 +1,58 @@ +@use "../../styles/variables"; + +.card { + margin: 1rem; +} + +.user-edit { + padding: 1.5rem; + height: 100%; + overflow-y: scroll; +} + +.user-data { + display: flex; + gap: 0.5rem; + + .user-data__group { + display: flex; + gap: 1rem; + align-items: center; + } +} + +.workgroups { + .workgroups__header { + display: flex; + justify-content: space-between; + align-items: center; + } + + .mat-column-name { + width: auto !important; + } + + .mat-column-role { + width: 10% !important; + } + + .mat-column-actions { + width: 5% !important; + } + + .workgroups__table__form-field { + padding: 0.5rem 0; + } + + .workgroups__table__header { + background-color: variables.$grey-03; + } + + .mat-mdc-header-row .mat-mdc-cell { + background-color: variables.$grey-01; + } + + .mat-mdc-row:hover .mat-mdc-cell { + background-color: variables.$grey-01; + } +} diff --git a/libs/admin/src/lib/components/user-edit/user-edit.component.ts b/libs/admin/src/lib/components/user-edit/user-edit.component.ts index f3351249..89cf4216 100644 --- a/libs/admin/src/lib/components/user-edit/user-edit.component.ts +++ b/libs/admin/src/lib/components/user-edit/user-edit.component.ts @@ -1,43 +1,166 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, inject, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { User, Workgroup } from '../../services/admin.service'; +import { MatOptionSelectionChange } from '@angular/material/core'; +import { MatSelectChange } from '@angular/material/select'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { Role } from '@prisma/client'; +import { map, startWith, Subscription } from 'rxjs'; +import { User, Workgroup, WorkgroupOnUser } from '../../services/admin.service'; +import * as actions from '../../state/admin.actions'; +import { AppStateWithAdmin } from '../../state/admin.reducer'; +import { selectSelectedUser, selectWorkgroups } from '../../state/admin.selector'; @Component({ selector: 'asset-sg-user-edit', templateUrl: './user-edit.component.html', styleUrls: ['./user-edit.component.scss'], }) -export class UserEditComponent implements OnInit { - @Input() user?: User; - @Input() workgroups: Workgroup[] = []; - +export class UserEditComponent implements OnInit, OnDestroy { + public roles: Role[] = Object.values(Role); + public user?: User; + public workgroups: Workgroup[] = []; + public filteredWorkgroups: Workgroup[] = []; + public workgroupAutoCompleteControl = new FormControl(''); public formGroup = new FormGroup({ isAdmin: new FormControl(false), lang: new FormControl('de'), - asViewer: new FormControl([]), - asEditor: new FormControl([]), - asMasterEditor: new FormControl([]), }); + protected readonly COLUMNS = ['name', 'role', 'actions']; + + private readonly route = inject(ActivatedRoute); + private readonly store = inject(Store); + private readonly workgroups$ = this.store.select(selectWorkgroups); + private readonly user$ = this.store.select(selectSelectedUser); + private readonly subscriptions: Subscription = new Subscription(); + public ngOnInit() { - this.initializeForm(); + this.getUserFromRoute(); + this.initSubscriptions(); } - private initializeForm() { - this.formGroup.patchValue({ - isAdmin: this.user?.isAdmin ?? false, - asViewer: - this.user?.workgroups - .filter((workgroup) => workgroup.role === 'Viewer') - .map((workgroup) => workgroup.workgroupId) ?? [], - asEditor: - this.user?.workgroups - .filter((workgroup) => workgroup.role === 'Editor') - .map((workgroup) => workgroup.workgroupId) ?? [], - asMasterEditor: - this.user?.workgroups - .filter((workgroup) => workgroup.role === 'MasterEditor') - .map((workgroup) => workgroup.workgroupId) ?? [], + public ngOnDestroy() { + this.subscriptions.unsubscribe(); + } + + public resetWorkgroupSearch() { + this.workgroupAutoCompleteControl.setValue(''); + } + + public isUserPartOfWorkgroup(workgroupId: number): boolean { + return !!this.user?.workgroups.find((workgroup) => workgroup.workgroupId === workgroupId); + } + + public addWorkgroupToUser(event: MatOptionSelectionChange, workgroupId: number) { + if (!this.user || !event.isUserInput) { + return; + } + const selectedWorkgroup = this.workgroups.find((workgroup) => workgroup.id === workgroupId); + + const workgroupToAdd: WorkgroupOnUser = { + workgroupId: workgroupId, + workgroup: { name: selectedWorkgroup!.name }, + role: Role.Viewer, + }; + const updatedWorkgroups = [...this.user.workgroups, workgroupToAdd]; + this.store.dispatch(actions.updateUser({ user: { ...this.user, workgroups: updatedWorkgroups } })); + this.resetWorkgroupSearch(); + } + + public updateRoleForWorkgroup(event: MatSelectChange, workgroup: WorkgroupOnUser) { + if (!this.user) { + return; + } + const updatedWorkgroups = this.user.workgroups.map((userWorkgroup) => { + if (userWorkgroup.workgroupId === workgroup.workgroupId) { + return { + ...userWorkgroup, + role: event.value, + }; + } + return userWorkgroup; }); + this.updateUser({ ...this.user, workgroups: updatedWorkgroups }); + } + + public deleteWorkgroupFromUser(workgroup: WorkgroupOnUser) { + if (!this.user) { + return; + } + const updatedWorkgroups = this.user.workgroups.filter( + (userWorkgroup) => userWorkgroup.workgroupId !== workgroup.workgroupId + ); + this.updateUser({ ...this.user, workgroups: updatedWorkgroups }); + } + + private updateUser(user: User) { + this.store.dispatch(actions.updateUser({ user })); + } + + private getUserFromRoute() { + this.subscriptions.add( + this.route.paramMap + .pipe( + map((params: ParamMap) => { + const userId = params.get('id'); + if (userId) { + return this.store.dispatch(actions.findUser({ userId })); + } + }) + ) + .subscribe() + ); + } + + private initializeForm() { + this.formGroup.patchValue( + { + isAdmin: this.user?.isAdmin ?? false, + lang: this.user?.lang ?? 'de', + }, + { emitEvent: false } + ); + } + + private initSubscriptions() { + this.subscriptions.add( + this.user$.subscribe((user) => { + this.user = user; + this.initializeForm(); + }) + ); + this.subscriptions.add( + this.workgroups$.subscribe((workgroups) => { + if (workgroups) { + this.workgroups = workgroups; + this.filteredWorkgroups = workgroups; + } + }) + ); + + this.subscriptions.add( + this.workgroupAutoCompleteControl.valueChanges + .pipe( + startWith(''), + map((value) => + this.workgroups.filter((workgroup) => workgroup.name.toLowerCase().includes(value!.toLowerCase().trim())) + ) + ) + .subscribe((workgroups) => { + this.filteredWorkgroups = workgroups; + }) + ); + + this.subscriptions.add( + this.formGroup.valueChanges.subscribe((value) => { + const updatedUser: User = { + ...this.user!, + isAdmin: value.isAdmin ?? false, + lang: value.lang ?? 'de', + }; + this.updateUser(updatedUser); + }) + ); } } diff --git a/libs/admin/src/lib/components/user-expanded/index.ts b/libs/admin/src/lib/components/user-expanded/index.ts deleted file mode 100644 index 759678c0..00000000 --- a/libs/admin/src/lib/components/user-expanded/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './user-expanded.component'; diff --git a/libs/admin/src/lib/components/user-expanded/user-expanded.component.html b/libs/admin/src/lib/components/user-expanded/user-expanded.component.html deleted file mode 100644 index f01b3502..00000000 --- a/libs/admin/src/lib/components/user-expanded/user-expanded.component.html +++ /dev/null @@ -1,39 +0,0 @@ -
    -
    -
    - - - -
    -
    userManagement.confirmDelete
    - -
    - - -
    -
    -
    -
    -
    Sprache
    - - Sprache - - DE - EN - FR - IT - RM - - -
    - - -
    -
    -
    diff --git a/libs/admin/src/lib/components/user-expanded/user-expanded.component.scss b/libs/admin/src/lib/components/user-expanded/user-expanded.component.scss deleted file mode 100644 index a53841a4..00000000 --- a/libs/admin/src/lib/components/user-expanded/user-expanded.component.scss +++ /dev/null @@ -1,72 +0,0 @@ -@use "../../styles/variables"; - -:host { - display: block; -} - -form { - display: flex; - flex-direction: column; -} - -.edit-area { - display: flex; - flex-direction: column; - background-color: variables.$white; - padding: 0rem 1rem 1rem 1rem; -} - -.label-email { - padding-top: 1rem; -} - -.header { - display: grid; - align-items: center; - padding: 0.5rem 0; - grid-template-areas: "email delete"; - grid-template-columns: 1fr auto; - margin-bottom: 1rem; -} - -.email { - grid-area: email; - color: variables.$cyan-09; - font-weight: variables.$font-bold; -} - -.email-form-field { - align-self: flex-start; - min-width: 32rem; -} - -.roles-radio-group { - margin-bottom: 1rem; - align-self: flex-start; -} - -label, -.label-heading { - font-size: 0.875rem; - color: variables.$grey-09; - font-weight: variables.$font-bold; -} - -.label-heading--before-form-field { - margin-bottom: 0.75rem; -} - -.lang-form-field { - align-self: flex-start; -} - -.mdc-text-field--filled:not(.mdc-text-field--disabled) { - background-color: variables.$grey-00; -} - -.asset-sg-dialog { - min-height: unset; - .email { - margin: 1rem 1rem 2rem 2rem; - } -} diff --git a/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts b/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts deleted file mode 100644 index e5102385..00000000 --- a/libs/admin/src/lib/components/user-expanded/user-expanded.component.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Dialog, DialogRef } from '@angular/cdk/dialog'; -import { - Component, - EventEmitter, - inject, - Input, - Output, - QueryList, - TemplateRef, - ViewChild, - ViewChildren, -} from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { ButtonComponent, ViewChildMarker } from '@asset-sg/client-shared'; -import { User, UserRoleEnum } from '@asset-sg/shared'; -import { makeADT, ofType } from '@morphic-ts/adt'; -import * as O from 'fp-ts/Option'; - -interface UserEdited extends User { - _tag: 'userEdited'; -} - -interface UserDelete extends User { - _tag: 'userDelete'; -} - -interface UserExpandCanceled { - _tag: 'userExpandCanceled'; -} - -export const UserExpandedOutput = makeADT('_tag')({ - userEdited: ofType(), - userExpandCanceled: ofType(), - userDelete: ofType(), -}); - -export type UserExpandedOutput = UserEdited | UserDelete | UserExpandCanceled; - -@Component({ - selector: 'asset-sg-user-expanded', - templateUrl: './user-expanded.component.html', - styleUrls: ['./user-expanded.component.scss'], -}) -export class UserExpandedComponent { - @ViewChild('deleteDialog') deleteDialog!: TemplateRef; - @ViewChildren(ViewChildMarker) viewChildMarkers!: QueryList; - - @Output() - public userExpandedOutput = new EventEmitter(); - private _dialog = inject(Dialog); - - private _formBulder = inject(FormBuilder); - public editForm = this._formBulder.group({ - email: new FormControl(''), - role: new FormControl(UserRoleEnum.viewer), - isAdmin: new FormControl(false), - lang: new FormControl('de'), - }); - public UserRole = UserRoleEnum; - - private _dialogRef: O.Option = O.none; - - @Input() - public get user(): User | undefined { - return this._user; - } - - public set user(value: User | undefined) { - this._user = value; - if (value) { - this.editForm.patchValue({ - email: value.email, - lang: value.lang, - }); - } - } - - private _user?: User | undefined; - - disableEverything() { - this.editForm.disable(); - this.viewChildMarkers.forEach((marker) => { - if (marker.viewChildMarker instanceof ButtonComponent) { - marker.viewChildMarker.disabled = true; - } - }); - } - - submit() { - this.disableEverything(); - if (this.user) { - const { lang } = this.editForm.getRawValue(); - - if (!lang) return; - - this.userExpandedOutput.emit(UserExpandedOutput.of.userEdited({ ...this.user, lang })); - } - } - - cancel() { - this.userExpandedOutput.emit(UserExpandedOutput.of.userExpandCanceled({})); - } - - delete() { - this._dialogRef = O.some(this._dialog.open(this.deleteDialog)); - } - - deleteCancelled() { - if (O.isSome(this._dialogRef)) { - this._dialogRef.value.close(); - } - } - - deleteConfirmed() { - this.disableEverything(); - if (O.isSome(this._dialogRef)) { - this._dialogRef.value.close(); - } - if (this.user) { - this.user && this.userExpandedOutput.emit(UserExpandedOutput.of.userDelete(this.user)); - } - } -} diff --git a/libs/admin/src/lib/components/users/users.component.html b/libs/admin/src/lib/components/users/users.component.html index 48bb856e..354e13a5 100644 --- a/libs/admin/src/lib/components/users/users.component.html +++ b/libs/admin/src/lib/components/users/users.component.html @@ -1,39 +1,54 @@ -
    - @if (selectedUser) { - - } +
    +
    +

    admin.users

    +
    +
    + + + + + + + -
    admin.email{{ user.email }}admin.userPage.admin
    - - - - - - - - - - - - - - - - - - - - - - - -
    Name{{ user.email }}Workgroups - @for (workgroup of user.workgroups; track workgroup.workgroupId) { -
    {{ workgroup.workgroup.name }}: {{ workgroup.role }}
    - } -
    Admin - - Sprache{{ user.lang }}Aktionen - -
    + + + + + + admin.userPage.lang + {{ user.lang }} + + + admin.workgroups + + @for (workgroup of user.workgroups.slice(0, workgroupCutoffLength); track workgroup.workgroupId; let last = + $last) { + {{ workgroup.workgroup.name }}.{{ workgroup.role }} + , + } @if (user.workgroups.length > workgroupCutoffLength) { + , +{{ user.workgroups.length - workgroupCutoffLength }} admin.userPage.more + + } + + + + admin.actions + + + + + + + +
    diff --git a/libs/admin/src/lib/components/users/users.component.scss b/libs/admin/src/lib/components/users/users.component.scss index e69de29b..4c9c9fc8 100644 --- a/libs/admin/src/lib/components/users/users.component.scss +++ b/libs/admin/src/lib/components/users/users.component.scss @@ -0,0 +1,27 @@ +@use "../../styles/variables"; + +::ng-deep .workgroups-tooltip { + font-size: 12px; +} + +.users { + height: 100%; + display: flex; + flex-direction: column; + padding: 0 1rem; +} + +.header { + display: flex; + justify-content: flex-start; + align-items: center; + margin-bottom: 1rem; +} + +.table__header { + background-color: variables.$grey-03; +} + +.mat-mdc-row:hover .mat-mdc-cell { + background-color: variables.$grey-01; +} diff --git a/libs/admin/src/lib/components/users/users.component.ts b/libs/admin/src/lib/components/users/users.component.ts index 19d044e3..c1942d86 100644 --- a/libs/admin/src/lib/components/users/users.component.ts +++ b/libs/admin/src/lib/components/users/users.component.ts @@ -1,7 +1,13 @@ import { Component, inject, OnDestroy, OnInit } from '@angular/core'; import { MatCheckboxChange } from '@angular/material/checkbox'; +import { Store } from '@ngrx/store'; import { Subscription } from 'rxjs'; -import { AdminService, User, Workgroup } from '../../services/admin.service'; +import { User, Workgroup, WorkgroupOnUser } from '../../services/admin.service'; +import * as actions from '../../state/admin.actions'; +import { AppStateWithAdmin } from '../../state/admin.reducer'; +import { selectUsers, selectWorkgroups } from '../../state/admin.selector'; + +const WORKGROUP_CUTOFF_LENGTH = 3; @Component({ selector: 'asset-sg-users', @@ -11,12 +17,14 @@ import { AdminService, User, Workgroup } from '../../services/admin.service'; export class UsersComponent implements OnInit, OnDestroy { public users: User[] = []; public workgroups: Workgroup[] = []; - public selectedUser?: User; - public mode: 'edit' | 'create' | undefined = undefined; - protected readonly COLUMNS = ['email', 'workgroups', 'isAdmin', 'languages', 'actions']; + public readonly workgroupCutoffLength = WORKGROUP_CUTOFF_LENGTH; + + protected readonly COLUMNS = ['email', 'isAdmin', 'languages', 'workgroups', 'actions']; - private _adminService = inject(AdminService); - private subscriptions: Subscription = new Subscription(); + private readonly store = inject(Store); + private readonly users$ = this.store.select(selectUsers); + private readonly workgroups$ = this.store.select(selectWorkgroups); + private readonly subscriptions: Subscription = new Subscription(); public ngOnInit(): void { this.initSubscriptions(); @@ -26,26 +34,24 @@ export class UsersComponent implements OnInit, OnDestroy { this.subscriptions.unsubscribe(); } - public update(user: User, event: MatCheckboxChange) { - this._adminService.updateUser({ ...user, isAdmin: event.checked }).subscribe(); + public updateIsAdminStatus(user: User, event: MatCheckboxChange) { + this.store.dispatch(actions.updateUser({ user: { ...user, isAdmin: event.checked } })); } - public edit(user: User): void { - this.selectedUser = user; + public formatWorkgroupsTooltip(workgroups: WorkgroupOnUser[]): string { + return workgroups.map((wg) => `${wg.workgroup.name}.${wg.role}`).join(', \n'); } private initSubscriptions(): void { this.subscriptions.add( - this._adminService.getUsersNew().subscribe((users) => { + this.users$.subscribe((users) => { this.users = users; }) ); this.subscriptions.add( - this._adminService.getWorkgroups().subscribe((workgroups) => { + this.workgroups$.subscribe((workgroups) => { this.workgroups = workgroups; }) ); } - - protected readonly console = console; } diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts deleted file mode 100644 index 77368ec1..00000000 --- a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit-component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Component, inject, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { AdminService, User, Workgroup } from '../../services/admin.service'; - -@Component({ - selector: 'asset-sg-workgroup-edit', - templateUrl: './workgroup-edit.component.html', - styleUrls: ['./workgroup-edit.component.scss'], -}) -export class WorkgroupEditComponent implements OnInit { - private _adminService = inject(AdminService); - - @Input() workgroup: Workgroup | null = null; - @Input() users: User[] = []; - @Input() mode: 'edit' | 'create' | undefined = undefined; - - public readonly formGroup: FormGroup = new FormGroup({ - name: new FormControl('', Validators.required), - viewers: new FormControl(), - editors: new FormControl(), - masterEditors: new FormControl(), - status: new FormControl(), - }); - - public ngOnInit() { - this.initializeForm(); - } - - public initializeForm() { - this.formGroup.patchValue({ - name: this.workgroup?.name ?? '', - viewers: this.workgroup?.users.filter((user) => user.role === 'Viewer').map((user) => user.userId) ?? [], - editors: this.workgroup?.users.filter((user) => user.role === 'Editor').map((user) => user.userId) ?? [], - masterEditors: - this.workgroup?.users.filter((user) => user.role === 'MasterEditor').map((user) => user.userId) ?? [], - status: !!this.workgroup?.disabled_at, - }); - } - - public save() { - if (!this.formGroup.valid) { - return; - } - const workgroup: Omit = { - name: this.formGroup.controls['name'].value, - users: [ - ...this.formGroup.controls['viewers'].value.map((userId: number) => ({ userId, role: 'Viewer' })), - ...this.formGroup.controls['editors'].value.map((userId: number) => ({ userId, role: 'Editor' })), - ...this.formGroup.controls['masterEditors'].value.map((userId: number) => ({ userId, role: 'MasterEditor' })), - ], - assets: this.workgroup?.assets ?? [], - disabled_at: this.formGroup.controls['status'].value ? new Date() : null, - }; - - if (this.mode === 'create') { - this._adminService.createWorkgroup(workgroup).subscribe((res) => console.log(res)); - } else { - this._adminService.updateWorkgroups(this.workgroup?.id ?? 0, workgroup).subscribe((res) => console.log(res)); - } - } -} diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html index 47396d16..45588461 100644 --- a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html @@ -1,27 +1,68 @@ -
    -
    -
    - - Name - - - Deaktiviert - - - {{ user.email }} - - - - - {{ user.email }} - - - - - {{ user.email }} - - + +
    + + +
    + + admin.name + + + admin.workgroupPage.isDeactivated + +
    + + +
    +
    +

    admin.users

    + +
    + + @if (workgroup.users.length > 0) { + + + + + + + + + + + + + + + + + + + +
    admin.email{{ user.user.email }}admin.role + + admin.role + + + {{ role }} + + + + admin.actions + +
    + + } @else { +

    admin.workgroupPage.chooseUsersText

    + } +
    + + +
    - - -
    +
    + diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.scss b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.scss index c1f7262a..15d36ac5 100644 --- a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.scss +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.scss @@ -1,4 +1,59 @@ +@use "../../styles/variables"; + +.card { + margin: 1rem; +} + +.workgroup-edit { + padding: 1.5rem; +} + .form { display: flex; - flex-direction: column; + align-items: center; + gap: 1rem; +} + +.users-table { + .users-table__header { + background-color: variables.$grey-03; + } + + .mat-column-id { + width: auto !important; + } + + .mat-column-role { + width: 10% !important; + } + + .mat-column-actions { + width: 5% !important; + } + + .mat-mdc-header-row .mat-mdc-cell { + background-color: variables.$grey-01; + } + + .mat-mdc-row:hover .mat-mdc-cell { + background-color: variables.$grey-01; + } + + .users-table__title { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + } + + .users-table__form-field { + padding: 0.5rem 0; + } + + .users-table__save { + display: flex; + justify-content: flex-end; + align-items: center; + margin-top: 1rem; + } } diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts new file mode 100644 index 00000000..4eb8748e --- /dev/null +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts @@ -0,0 +1,180 @@ +import { Component, inject, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSelectChange } from '@angular/material/select'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { Role } from '@prisma/client'; +import { map, Subscription } from 'rxjs'; +import { UserOnWorkgroup, Workgroup, WorkgroupData } from '../../services/admin.service'; +import * as actions from '../../state/admin.actions'; +import { AppStateWithAdmin } from '../../state/admin.reducer'; +import { selectSelectedWorkgroup } from '../../state/admin.selector'; +import { AddWorkgroupUserDialog } from '../add-workgroup-user-dialog/add-workgroup-user-dialog.component'; + +export type Mode = 'edit' | 'create'; + +@Component({ + selector: 'asset-sg-workgroup-edit', + templateUrl: './workgroup-edit.component.html', + styleUrls: ['./workgroup-edit.component.scss'], +}) +export class WorkgroupEditComponent implements OnInit, OnDestroy { + public workgroup: Workgroup | undefined; + public mode: Mode = 'edit'; + + public readonly roles: Role[] = Object.values(Role); + public readonly formGroup: FormGroup = new FormGroup({ + name: new FormControl('', { validators: Validators.required, updateOn: 'blur' }), + status: new FormControl(), + users: new FormControl([]), + }); + + private readonly route = inject(ActivatedRoute); + private readonly dialogService = inject(MatDialog); + private readonly subscriptions: Subscription = new Subscription(); + private readonly store = inject(Store); + private readonly router = inject(Router); + private readonly workgroup$ = this.store.select(selectSelectedWorkgroup); + + public ngOnInit() { + this.findWorkgroupFromRouteParams(); + this.initSubscriptions(); + } + + public ngOnDestroy() { + this.store.dispatch(actions.resetWorkgroup()); + this.subscriptions.unsubscribe(); + } + + public initializeForm(workgroup: Workgroup) { + this.formGroup.patchValue( + { + name: workgroup.name, + status: !!workgroup.disabled_at, + users: workgroup.users, + }, + { emitEvent: false } + ); + } + + public updateRoleForUser(event: MatSelectChange, user: UserOnWorkgroup) { + if (!this.workgroup) { + return; + } + const updatedUsers: UserOnWorkgroup[] = this.workgroup.users.map((u) => + u.user.id === user.user.id + ? { + ...u, + role: event.value, + } + : u + ); + const updatedWorkgroup = { + name: this.workgroup.name, + assets: this.workgroup.assets, + disabled_at: this.workgroup.disabled_at, + users: updatedUsers, + }; + this.updateWorkgroup(this.workgroup.id, updatedWorkgroup); + } + + public addUsersToWorkgroup() { + this.dialogService.open(AddWorkgroupUserDialog, { + width: '400px', + restoreFocus: false, + data: { + workgroup: this.workgroup, + mode: this.mode, + }, + }); + } + + public deleteUserFromWorkgroup(user: UserOnWorkgroup) { + if (!this.workgroup) { + return; + } + const updatedWorkgroup = { + name: this.workgroup.name, + assets: this.workgroup.assets, + disabled_at: this.workgroup.disabled_at, + users: this.workgroup.users.filter((u) => u.user.id !== user.user.id), + }; + this.updateWorkgroup(this.workgroup.id, updatedWorkgroup); + } + + public cancel() { + void this.router.navigate(['../'], { relativeTo: this.route }); + this.store.dispatch(actions.resetWorkgroup()); + } + + public createWorkGroup() { + if (!this.workgroup || !this.formGroup.valid) { + return; + } + const { id, ...workgroup } = this.workgroup; + this.store.dispatch(actions.createWorkgroup({ workgroup })); + } + + private updateWorkgroup(workgroupId: number, workgroup: WorkgroupData) { + if (this.mode === 'edit') { + this.store.dispatch(actions.updateWorkgroup({ workgroupId, workgroup })); + } else { + this.workgroup = { + id: workgroupId, + ...workgroup, + }; + } + } + + private findWorkgroupFromRouteParams() { + this.subscriptions.add( + this.route.paramMap + .pipe( + map((params: ParamMap) => { + const id = params.get('id'); + if (id) { + this.mode = 'edit'; + return this.store.dispatch(actions.findWorkgroup({ workgroupId: parseInt(id) })); + } else { + this.mode = 'create'; + this.workgroup = { + id: 0, + name: '', + users: [], + assets: [], + disabled_at: null, + }; + } + }) + ) + .subscribe() + ); + } + + private initSubscriptions() { + this.subscriptions.add( + this.workgroup$.subscribe((workgroup) => { + if (workgroup) { + this.workgroup = workgroup; + this.initializeForm(workgroup); + } + }) + ); + + this.subscriptions.add( + this.formGroup.valueChanges.subscribe((value) => { + if (!this.workgroup) { + return; + } + const updatedWorkgroup = { + name: value.name, + disabled_at: value.status ? new Date() : null, + assets: this.workgroup.assets, + users: this.workgroup.users, + }; + this.updateWorkgroup(this.workgroup.id, updatedWorkgroup); + }) + ); + } +} diff --git a/libs/admin/src/lib/components/workgroups/workgroups.component.html b/libs/admin/src/lib/components/workgroups/workgroups.component.html index 38973a4a..581b2e9d 100644 --- a/libs/admin/src/lib/components/workgroups/workgroups.component.html +++ b/libs/admin/src/lib/components/workgroups/workgroups.component.html @@ -1,31 +1,36 @@ -
    - - @if (selectedWorkgroup || mode === 'create') { - - - } - - - - - - - - - - - - - - - - - - - -
    Name{{ workgroup.name }}Deaktiviert{{ workgroup.disabled_at ? workgroup.disabled_at : "-" }}Anzahl Benutzer{{ workgroup.users.length }}Aktionen - - - -
    +
    +
    +

    admin.workgroups

    + +
    +
    + + + + + + + + + + + + + + + + + + + +
    admin.name{{ workgroup.name }}admin.users + @for (user of workgroup.users; track user.user.id) { + {{ user.user.email }}, + } + admin.workgroupPage.isActive{{ workgroup.disabled_at ? "inaktiv" : "aktiv" }}admin.actions + +
    +
    diff --git a/libs/admin/src/lib/components/workgroups/workgroups.component.scss b/libs/admin/src/lib/components/workgroups/workgroups.component.scss index e69de29b..ca38ae9a 100644 --- a/libs/admin/src/lib/components/workgroups/workgroups.component.scss +++ b/libs/admin/src/lib/components/workgroups/workgroups.component.scss @@ -0,0 +1,27 @@ +@use "../../styles/variables"; + +.workgroups { + height: 100%; + display: flex; + flex-direction: column; + padding: 0 1rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.table__header { + background-color: variables.$grey-03; +} + +.mat-mdc-header-row .mat-mdc-cell { + background-color: variables.$grey-01; +} + +.mat-mdc-row:hover .mat-mdc-cell { + background-color: variables.$grey-01; +} diff --git a/libs/admin/src/lib/components/workgroups/workgroups.component.ts b/libs/admin/src/lib/components/workgroups/workgroups.component.ts index 3546df7d..0458885a 100644 --- a/libs/admin/src/lib/components/workgroups/workgroups.component.ts +++ b/libs/admin/src/lib/components/workgroups/workgroups.component.ts @@ -1,6 +1,9 @@ import { Component, inject, OnDestroy, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; import { Subscription } from 'rxjs'; -import { AdminService, User, Workgroup } from '../../services/admin.service'; +import { UserOnWorkgroup, Workgroup } from '../../services/admin.service'; +import { AppStateWithAdmin } from '../../state/admin.reducer'; +import { selectUsers, selectWorkgroups } from '../../state/admin.selector'; @Component({ selector: 'asset-sg-workgroups', @@ -8,14 +11,15 @@ import { AdminService, User, Workgroup } from '../../services/admin.service'; styleUrls: ['./workgroups.component.scss'], }) export class WorkgroupsComponent implements OnInit, OnDestroy { - public users: User[] = []; + public users: UserOnWorkgroup[] = []; public workgroups: Workgroup[] = []; - public mode: 'edit' | 'create' | undefined = undefined; - public selectedWorkgroup: Workgroup | null = null; - protected readonly COLUMNS = ['name', 'status', 'users', 'actions']; - private _adminService = inject(AdminService); - private subscriptions: Subscription = new Subscription(); + protected readonly COLUMNS = ['name', 'users', 'status', 'actions']; + + private readonly store = inject(Store); + private readonly workgroups$ = this.store.select(selectWorkgroups); + private readonly users$ = this.store.select(selectUsers); + private readonly subscriptions: Subscription = new Subscription(); public ngOnInit(): void { this.initSubscriptions(); @@ -25,28 +29,15 @@ export class WorkgroupsComponent implements OnInit, OnDestroy { this.subscriptions.unsubscribe(); } - public selectWorkgroup(workgroup: Workgroup): void { - this.selectedWorkgroup = workgroup; - } - - public createWorkgroup(): void { - this.mode = 'create'; - } - - public deactivateWorkgroup(workgroup: Workgroup): void { - const { id, ...disabledWorkgroup } = { ...workgroup, disabled_at: new Date() }; - this._adminService.updateWorkgroups(id, disabledWorkgroup).subscribe(); - } - private initSubscriptions(): void { this.subscriptions.add( - this._adminService.getUsersNew().subscribe((users) => { - this.users = users; + this.workgroups$.subscribe((workgroups) => { + this.workgroups = workgroups; }) ); this.subscriptions.add( - this._adminService.getWorkgroups().subscribe((workgroups) => { - this.workgroups = workgroups; + this.users$.subscribe((users) => { + this.users = users.map((user) => ({ role: user.role, user: { email: user.email, id: user.id } })); }) ); } diff --git a/libs/admin/src/lib/services/admin.service.ts b/libs/admin/src/lib/services/admin.service.ts index 58e067e6..776b6c1e 100644 --- a/libs/admin/src/lib/services/admin.service.ts +++ b/libs/admin/src/lib/services/admin.service.ts @@ -1,22 +1,19 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { ApiError, httpErrorResponseError } from '@asset-sg/client-shared'; -import { decodeError, OE, ORD } from '@asset-sg/core'; -import { Users } from '@asset-sg/shared'; -import * as RD from '@devexperts/remote-data-ts'; import { Role } from '@prisma/client'; -import * as E from 'fp-ts/Either'; -import { flow } from 'fp-ts/function'; -import { map, Observable, startWith, tap } from 'rxjs'; +import { Observable } from 'rxjs'; +//TODO: Move types to /shared2 lib after merging export interface Workgroup { id: number; name: string; assets: { assetId: number }[]; - users: { userId: number; role: Role }[]; + users: UserOnWorkgroup[]; disabled_at: Date | null; } +export type WorkgroupData = Omit; + export interface WorkgroupOnUser { workgroupId: number; role: Role; @@ -25,12 +22,22 @@ export interface WorkgroupOnUser { }; } +export type UserId = string; + +export interface UserOnWorkgroup { + role: Role; + user: { + email: string; + id: UserId; + }; +} + export interface User { id: string; name: string; email: string; lang: string; - role: 'Viewer' | 'Editor' | 'MasterEditor'; + role: Role; isAdmin: boolean; workgroups: WorkgroupOnUser[]; } @@ -41,53 +48,36 @@ export interface User { export class AdminService { private _httpClient = inject(HttpClient); - public getUsers(): ORD.ObservableRemoteData { - return this._httpClient.get('/api/users').pipe( - map(flow(Users.decode, E.mapLeft(decodeError))), - // TODO need to test instance of HttpErrorResponse here - OE.catchErrorW(httpErrorResponseError), - map(RD.fromEither), - startWith(RD.pending) - ); + public getUsers(): Observable { + return this._httpClient.get('/api/admin/user'); } - public getUsersNew(): Observable { - return this._httpClient.get('/api/users').pipe(map((res) => res as User[])); + public getUser(id: string): Observable { + return this._httpClient.get(`/api/users/${id}`); } public getWorkgroups(): Observable { - return this._httpClient.get('/api/workgroups').pipe(map((res) => res as Workgroup[])); + return this._httpClient.get('/api/workgroups'); } - public createWorkgroup(workgroup: Omit): Observable { - return this._httpClient.post(`/api/workgroups`, workgroup).pipe(map((res) => res as Workgroup)); + public getWorkgroup(id: string): Observable { + return this._httpClient.get(`/api/workgroups/${id}`); } - public updateWorkgroups(id: number, workgroup: Omit): Observable { - return this._httpClient.put(`/api/workgroups/${id}`, workgroup).pipe(map((res) => res as Workgroup)); + public createWorkgroup(workgroup: WorkgroupData): Observable { + return this._httpClient.post(`/api/workgroups`, workgroup); } - public updateUser(user: User): Observable { - return this._httpClient - .put(`/api/users/${user.id}`, { - role: user.role, - lang: user.lang, - workgroups: user.workgroups, - isAdmin: user.isAdmin, - }) - .pipe(map((res) => res as User)); + public updateWorkgroups(id: number, workgroup: WorkgroupData): Observable { + return this._httpClient.put(`/api/workgroups/${id}`, workgroup); } - public deleteUser(id: string): ORD.ObservableRemoteData { - console.log('888', id); - return this._httpClient.delete(`/api/users/${id}`).pipe( - tap((a) => console.log('deleteUser', a)), - map(() => E.right(undefined)), - // TODO need to test instance of HttpErrorResponse here - OE.catchErrorW(httpErrorResponseError), - map(RD.fromEither), - startWith(RD.pending), - tap((a) => console.log('deleteUser', a)) - ); + public updateUser(user: User): Observable { + return this._httpClient.put(`/api/users/${user.id}`, { + role: user.role, + lang: user.lang, + workgroups: user.workgroups, + isAdmin: user.isAdmin, + }); } } diff --git a/libs/admin/src/lib/state/admin.actions.ts b/libs/admin/src/lib/state/admin.actions.ts new file mode 100644 index 00000000..8f40bbb2 --- /dev/null +++ b/libs/admin/src/lib/state/admin.actions.ts @@ -0,0 +1,74 @@ +import { createAction, props } from '@ngrx/store'; +import { User, Workgroup, WorkgroupData } from '../services/admin.service'; + +export const findUser = createAction( + '[Admin] Find user', + props<{ + userId: string; + }>() +); + +export const setUser = createAction( + '[Admin] Set user', + props<{ + user: User; + }>() +); + +export const updateUser = createAction( + '[Admin] Update User', + props<{ + user: User; + }>() +); + +export const listUsers = createAction('[Admin] List users'); + +export const setUsers = createAction( + '[Admin] Set users', + props<{ + users: User[]; + }>() +); + +export const findWorkgroup = createAction( + '[Admin] Find workgroup', + props<{ + workgroupId: number; + }>() +); + +export const setWorkgroup = createAction( + '[Admin] Set workgroup', + props<{ + workgroup: Workgroup; + }>() +); + +export const resetWorkgroup = createAction('[Admin] Reset workgroup'); + +export const setWorkgroups = createAction( + '[Admin] Set workgroups', + props<{ + workgroups: Workgroup[]; + }>() +); + +export const updateWorkgroup = createAction( + '[Admin] Update Workgroup', + props<{ + workgroupId: number; + workgroup: WorkgroupData; + }>() +); + +export const createWorkgroup = createAction( + '[Admin] Create Workgroup', + props<{ + workgroup: WorkgroupData; + }>() +); + +export const createWorkgroupSuccess = createAction('[Admin] Create Workgroup Success'); + +export const listWorkgroups = createAction('[Admin] List workgroups'); diff --git a/libs/admin/src/lib/state/admin.effects.ts b/libs/admin/src/lib/state/admin.effects.ts new file mode 100644 index 00000000..5300835d --- /dev/null +++ b/libs/admin/src/lib/state/admin.effects.ts @@ -0,0 +1,100 @@ +import { inject, Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UntilDestroy } from '@ngneat/until-destroy'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { map, switchMap } from 'rxjs'; + +import { AdminService, User, Workgroup } from '../services/admin.service'; +import * as actions from './admin.actions'; + +@UntilDestroy() +@Injectable() +export class AdminEffects { + private readonly actions$ = inject(Actions); + private readonly adminService = inject(AdminService); + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + + public findUser$ = createEffect(() => { + return this.actions$.pipe( + ofType(actions.findUser), + switchMap(({ userId }) => { + return this.adminService.getUser(userId).pipe(map((user) => actions.setUser({ user }))); + }) + ); + }); + + public updateUser$ = createEffect(() => + this.actions$.pipe( + ofType(actions.updateUser), + switchMap(({ user }) => { + return this.adminService.updateUser(user).pipe( + map((user: User) => + actions.setUser({ + user, + }) + ) + ); + }) + ) + ); + + public findWorkgroup$ = createEffect(() => + this.actions$.pipe( + ofType(actions.findWorkgroup), + switchMap(({ workgroupId }) => { + return this.adminService + .getWorkgroup(workgroupId.toString()) + .pipe(map((workgroup: Workgroup) => actions.setWorkgroup({ workgroup }))); + }) + ) + ); + + public createWorkgroup$ = createEffect( + () => { + return this.actions$.pipe( + ofType(actions.createWorkgroup), + switchMap(({ workgroup }) => this.adminService.createWorkgroup(workgroup)), + switchMap((workgroup) => + this.router.navigate([`/de/admin/workgroups/${workgroup.id}`], { relativeTo: this.route }) + ) + ); + }, + { dispatch: false } + ); + + public updateWorkgroup$ = createEffect(() => + this.actions$.pipe( + ofType(actions.updateWorkgroup), + switchMap(({ workgroupId, workgroup }) => { + return this.adminService + .updateWorkgroups(workgroupId, workgroup) + .pipe(map((workgroup: Workgroup) => actions.setWorkgroup({ workgroup }))); + }) + ) + ); + + public listUsers$ = createEffect(() => + this.actions$.pipe( + ofType(actions.listUsers, actions.setUser, actions.setWorkgroup), + switchMap(() => { + return this.adminService.getUsers().pipe( + map((users: User[]) => { + return actions.setUsers({ users }); + }) + ); + }) + ) + ); + + public listWorkgroups$ = createEffect(() => + this.actions$.pipe( + ofType(actions.listWorkgroups, actions.setWorkgroup, actions.setUser), + switchMap(() => { + return this.adminService + .getWorkgroups() + .pipe(map((workgroups: Workgroup[]) => actions.setWorkgroups({ workgroups }))); + }) + ) + ); +} diff --git a/libs/admin/src/lib/state/admin.reducer.ts b/libs/admin/src/lib/state/admin.reducer.ts new file mode 100644 index 00000000..b14ffd34 --- /dev/null +++ b/libs/admin/src/lib/state/admin.reducer.ts @@ -0,0 +1,116 @@ +import { AppState } from '@asset-sg/client-shared'; +import { createReducer, on } from '@ngrx/store'; +import { User, Workgroup } from '../services/admin.service'; +import * as actions from './admin.actions'; + +export interface AdminState { + selectedWorkgroup: Workgroup | undefined; + workgroups: Workgroup[]; + selectedUser: User | undefined; + users: User[]; + isLoading: boolean; +} + +export interface AppStateWithAdmin extends AppState { + admin: AdminState; +} + +const initialState: AdminState = { + selectedWorkgroup: undefined, + workgroups: [], + selectedUser: undefined, + users: [], + isLoading: false, +}; + +export const adminReducer = createReducer( + initialState, + on( + actions.findUser, + (state): AdminState => ({ + ...state, + isLoading: true, + }) + ), + on( + actions.updateUser, + (state): AdminState => ({ + ...state, + isLoading: true, + }) + ), + on( + actions.setUser, + (state, { user }): AdminState => ({ + ...state, + selectedUser: user, + isLoading: false, + }) + ), + on( + actions.findWorkgroup, + (state): AdminState => ({ + ...state, + isLoading: true, + }) + ), + on( + actions.updateWorkgroup, + (state): AdminState => ({ + ...state, + isLoading: true, + }) + ), + on( + actions.createWorkgroup, + (state): AdminState => ({ + ...state, + isLoading: true, + }) + ), + on( + actions.setWorkgroup, + (state, { workgroup }): AdminState => ({ + ...state, + selectedWorkgroup: workgroup, + isLoading: false, + }) + ), + on( + actions.resetWorkgroup, + (state): AdminState => ({ + ...state, + selectedWorkgroup: undefined, + }) + ), + on( + actions.listUsers, + (state): AdminState => ({ + ...state, + isLoading: true, + }) + ), + on( + actions.setUsers, + (state, { users }): AdminState => ({ + ...state, + users, + isLoading: false, + }) + ), + on( + actions.listWorkgroups, + (state): AdminState => ({ + ...state, + isLoading: true, + }) + ), + on( + actions.setWorkgroups, + (state, { workgroups }): AdminState => ({ + ...state, + workgroups, + isLoading: false, + }) + ) +); diff --git a/libs/admin/src/lib/state/admin.selector.ts b/libs/admin/src/lib/state/admin.selector.ts new file mode 100644 index 00000000..52807b1e --- /dev/null +++ b/libs/admin/src/lib/state/admin.selector.ts @@ -0,0 +1,11 @@ +import { createSelector } from '@ngrx/store'; +import { AppStateWithAdmin } from './admin.reducer'; + +const adminFeature = (state: AppStateWithAdmin) => state.admin; + +export const selectAdminState = createSelector(adminFeature, (state) => state); +export const selectSelectedUser = createSelector(adminFeature, (state) => state.selectedUser); +export const selectSelectedWorkgroup = createSelector(adminFeature, (state) => state.selectedWorkgroup); +export const selectWorkgroups = createSelector(adminFeature, (state) => state.workgroups); +export const selectUsers = createSelector(adminFeature, (state) => state.users); +export const selectIsLoading = createSelector(adminFeature, (state) => state.isLoading); diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts index 16f50c2c..120a2a18 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts @@ -4,15 +4,15 @@ import { ChangeDetectionStrategy, Component, ElementRef, + inject, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, - inject, } from '@angular/core'; -import { LifecycleHooks, LifecycleHooksDirective, fromAppShared } from '@asset-sg/client-shared'; -import { ORD, isNotNil, isTruthy } from '@asset-sg/core'; +import { fromAppShared, LifecycleHooks, LifecycleHooksDirective } from '@asset-sg/client-shared'; +import { isNotNil, isTruthy, ORD } from '@asset-sg/core'; import { ContactEdit, GeomFromGeomText, PatchAsset, PatchContact } from '@asset-sg/shared'; import * as RD from '@devexperts/remote-data-ts'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; @@ -22,7 +22,7 @@ import * as A from 'fp-ts/Array'; import { pipe } from 'fp-ts/function'; import * as O from 'fp-ts/Option'; import { KobalteTabs } from 'ngx-kobalte'; -import { BehaviorSubject, Observable, filter, map, of, startWith, switchMap } from 'rxjs'; +import { BehaviorSubject, filter, map, Observable, of, startWith, switchMap } from 'rxjs'; import { from } from 'solid-js'; import { TabPageBridgeService } from '../../services/tab-page-bridge.service'; diff --git a/libs/shared/v2/src/lib/models/workgroup.ts b/libs/shared/v2/src/lib/models/workgroup.ts index 2af1467f..a9df7c06 100644 --- a/libs/shared/v2/src/lib/models/workgroup.ts +++ b/libs/shared/v2/src/lib/models/workgroup.ts @@ -28,9 +28,11 @@ export type SimpleWorkgroup = Pick & { }; export interface UserOnWorkgroup { - // TODO change this to `id` - userId: UserId; role: Role; + user: { + id: UserId; + email: string; + }; } export class WorkgroupDataBoundary implements WorkgroupData { From d3a064e0049df07bef1469bb067b3544dd1c9863 Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Wed, 24 Jul 2024 14:04:43 +0200 Subject: [PATCH 18/21] Align multiple parts of workgroup changes --- .../src/app/app.component.html | 3 +- apps/client-asset-sg/src/app/i18n/de.ts | 2 +- apps/client-asset-sg/src/app/i18n/en.ts | 3 +- apps/client-asset-sg/src/app/i18n/fr.ts | 2 +- apps/client-asset-sg/src/app/i18n/it.ts | 2 +- apps/client-asset-sg/src/app/i18n/rm.ts | 2 +- apps/server-asset-sg/src/app.controller.ts | 2 +- .../src/core/middleware/jwt.middleware.ts | 2 +- .../features/asset-edit/asset-edit.fake.ts | 11 +- .../assets/search/asset-search.service.ts | 2 +- .../features/studies/studies.controller.ts | 2 +- .../src/features/users/user.repo.ts | 34 ++-- .../src/features/users/users.controller.ts | 23 ++- .../workgroups/workgroup-simple.repo.ts | 3 +- .../workgroups/workgroup.repo.spec.ts | 93 +++++---- .../src/features/workgroups/workgroup.repo.ts | 75 ++++--- .../workgroups/workgroups.controller.ts | 22 ++- libs/admin/src/lib/admin.module.ts | 6 +- .../add-workgroup-user-dialog.component.html | 2 +- .../add-workgroup-user-dialog.component.ts | 47 +++-- .../admin-page/admin-page.component.html | 16 +- .../admin-page/admin-page.component.ts | 24 ++- .../user-edit/user-edit.component.html | 14 +- .../user-edit/user-edit.component.scss | 12 +- .../user-edit/user-edit.component.ts | 132 ++++++++----- .../lib/components/users/users.component.html | 25 +-- .../lib/components/users/users.component.ts | 62 ++++-- .../workgroup-edit.component.html | 28 ++- .../workgroup-edit.component.ts | 183 +++++++++--------- .../workgroups/workgroups.component.html | 6 +- .../workgroups/workgroups.component.ts | 42 ++-- libs/admin/src/lib/services/admin.service.ts | 95 ++++----- libs/admin/src/lib/state/admin.actions.ts | 34 ++-- libs/admin/src/lib/state/admin.effects.ts | 86 ++++---- libs/admin/src/lib/state/admin.reducer.ts | 32 ++- libs/admin/src/lib/state/admin.selector.ts | 1 - libs/auth/src/lib/services/auth.service.ts | 20 +- .../alert-list/alert-list.component.html | 2 +- .../src/lib/features/alert/alert.module.ts | 3 +- .../src/lib/state/app-shared-state.actions.ts | 4 +- .../src/lib/state/app-shared-state.ts | 4 +- .../components/profile/profile.component.ts | 5 +- libs/shared/src/lib/models/index.ts | 1 - libs/shared/src/lib/models/user.ts | 30 --- libs/shared/v2/src/index.ts | 2 + libs/shared/v2/src/lib/models/user.ts | 21 +- libs/shared/v2/src/lib/models/workgroup.ts | 44 +---- .../shared/v2/src/lib/policies/base/policy.ts | 30 ++- .../shared/v2/src/lib/schemas/asset.schema.ts | 19 +- libs/shared/v2/src/lib/schemas/base/schema.ts | 112 +++++++++++ .../v2/src/lib/schemas/contact.schema.ts | 3 +- libs/shared/v2/src/lib/schemas/user.schema.ts | 24 ++- .../v2/src/lib/schemas/workgroup.schema.ts | 35 ++++ 53 files changed, 848 insertions(+), 641 deletions(-) delete mode 100644 libs/shared/src/lib/models/user.ts create mode 100644 libs/shared/v2/src/lib/schemas/base/schema.ts create mode 100644 libs/shared/v2/src/lib/schemas/workgroup.schema.ts diff --git a/apps/client-asset-sg/src/app/app.component.html b/apps/client-asset-sg/src/app/app.component.html index 05007fb3..bb843f58 100644 --- a/apps/client-asset-sg/src/app/app.component.html +++ b/apps/client-asset-sg/src/app/app.component.html @@ -5,9 +5,8 @@
    - - + @if (authState === AuthState.Success || authState === AuthState.ForbiddenResource) { diff --git a/apps/client-asset-sg/src/app/i18n/de.ts b/apps/client-asset-sg/src/app/i18n/de.ts index 79089f00..54f3947f 100644 --- a/apps/client-asset-sg/src/app/i18n/de.ts +++ b/apps/client-asset-sg/src/app/i18n/de.ts @@ -264,7 +264,7 @@ export const deAppTranslations = { activate: 'Aktivieren', deactivate: 'Deaktivieren', create: 'Erstellen', - isDeactivated: 'Deaktiviert', + isDisabled: 'Deaktiviert', chooseUsersText: 'Füge Benutzer hinzu, um sie zu verwalten', addUsers: 'Benutzer hinzufügen', }, diff --git a/apps/client-asset-sg/src/app/i18n/en.ts b/apps/client-asset-sg/src/app/i18n/en.ts index b882d5f0..664f8894 100644 --- a/apps/client-asset-sg/src/app/i18n/en.ts +++ b/apps/client-asset-sg/src/app/i18n/en.ts @@ -265,9 +265,8 @@ export const enAppTranslations: AppTranslations = { activate: 'Activate', deactivate: 'Deactivate', create: 'Create', - isDeactivated: 'Deactivated', + isDisabled: 'Deactivated', chooseUsersText: 'Add users to manage', - addUsers: 'Add users', }, }, diff --git a/apps/client-asset-sg/src/app/i18n/fr.ts b/apps/client-asset-sg/src/app/i18n/fr.ts index 73e5c5a7..15b42b5f 100644 --- a/apps/client-asset-sg/src/app/i18n/fr.ts +++ b/apps/client-asset-sg/src/app/i18n/fr.ts @@ -266,7 +266,7 @@ export const frAppTranslations: AppTranslations = { create: 'Créer', activate: 'Activer', deactivate: 'Désactiver', - isDeactivated: 'Désactivé', + isDisabled: 'Désactivé', chooseUsersText: 'Ajoutez des utilisateurs pour les gérer', addUsers: 'Ajouter des utilisateurs', }, diff --git a/apps/client-asset-sg/src/app/i18n/it.ts b/apps/client-asset-sg/src/app/i18n/it.ts index 4faf09e7..20918b94 100644 --- a/apps/client-asset-sg/src/app/i18n/it.ts +++ b/apps/client-asset-sg/src/app/i18n/it.ts @@ -265,7 +265,7 @@ export const itAppTranslations: AppTranslations = { activate: 'IT Aktivieren', deactivate: 'IT Deaktivieren', create: 'IT Erstellen', - isDeactivated: 'IT Deaktiviert', + isDisabled: 'IT Deaktiviert', chooseUsersText: 'IT Füge Nutzer hinzu, um sie zu verwalten', addUsers: 'IT Benutzer hinzufügen', }, diff --git a/apps/client-asset-sg/src/app/i18n/rm.ts b/apps/client-asset-sg/src/app/i18n/rm.ts index a80e575f..1a0ac5ff 100644 --- a/apps/client-asset-sg/src/app/i18n/rm.ts +++ b/apps/client-asset-sg/src/app/i18n/rm.ts @@ -265,7 +265,7 @@ export const rmAppTranslations: AppTranslations = { create: 'RM Erstellen', activate: 'RM Aktivieren', deactivate: 'RM Deaktivieren', - isDeactivated: 'RM Deaktiviert', + isDisabled: 'RM Deaktiviert', chooseUsersText: 'RM Füge Nutzer hinzu, um sie zu verwalten', addUsers: 'RM Benutzer hinzufügen', }, diff --git a/apps/server-asset-sg/src/app.controller.ts b/apps/server-asset-sg/src/app.controller.ts index 2e50a00d..a02b1366 100644 --- a/apps/server-asset-sg/src/app.controller.ts +++ b/apps/server-asset-sg/src/app.controller.ts @@ -91,7 +91,7 @@ const getReferenceData = (user: User, prismaService: PrismaService) => { : prismaService.contact.findMany({ where: { assetContacts: { - some: { asset: { workgroupId: { in: user.workgroups.map((it) => it.id) } } }, + some: { asset: { workgroupId: { in: [...user.roles.keys()] } } }, }, }, }), diff --git a/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts b/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts index 976a9afd..30d8d4d6 100644 --- a/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts +++ b/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts @@ -198,7 +198,7 @@ export class JwtMiddleware implements NestMiddleware { email, lang: 'de', isAdmin: false, - workgroups: [], + roles: new Map(), }); } } diff --git a/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts b/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts index dfe1a8ad..eb182a55 100644 --- a/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts +++ b/apps/server-asset-sg/src/features/asset-edit/asset-edit.fake.ts @@ -1,5 +1,5 @@ import { AssetUsage, Contact, PatchAsset, dateIdFromDate } from '@asset-sg/shared'; -import { User } from '@asset-sg/shared/v2'; +import { User, WorkgroupId } from '@asset-sg/shared/v2'; import { Role } from '@asset-sg/shared/v2'; import { fakerDE_CH as faker } from '@faker-js/faker'; import * as O from 'fp-ts/Option'; @@ -24,14 +24,17 @@ export const fakeAssetUsage = (): AssetUsage => ({ statusAssetUseItemCode: faker.helpers.arrayElement(['tobechecked', 'underclarification', 'approved']), }); -export const fakeUser = () => - define({ +export const fakeUser = () => { + const roles = new Map(); + roles.set(1, Role.Viewer); + return define({ email: faker.internet.email(), id: faker.string.uuid(), lang: faker.helpers.fromRegExp(/[a-z]{2}/), isAdmin: false, - workgroups: [{ id: 1, role: Role.Viewer }], + roles, }); +}; export const fakeContact = () => define>({ diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts index 07b61538..d589a2fa 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts @@ -686,7 +686,7 @@ const mapQueryToElasticDsl = (query: AssetSearchQuery, user: User): QueryDslQuer if (!user.isAdmin) { filters.push({ terms: { - workgroupId: user.workgroups.map((it) => it.id), + workgroupId: [...user.roles.keys()], }, }); } diff --git a/apps/server-asset-sg/src/features/studies/studies.controller.ts b/apps/server-asset-sg/src/features/studies/studies.controller.ts index fbccb92d..f9d8dda7 100644 --- a/apps/server-asset-sg/src/features/studies/studies.controller.ts +++ b/apps/server-asset-sg/src/features/studies/studies.controller.ts @@ -41,7 +41,7 @@ export class StudiesController { let next: Promise | null = studyRepo.list({ limit: INITIAL_BATCH_SIZE, offset: 0, - workgroupIds: user.isAdmin ? null : user.workgroups.map((it) => it.id), + workgroupIds: user.isAdmin ? null : [...user.roles.keys()], }); // The maximal size of the next batch. diff --git a/apps/server-asset-sg/src/features/users/user.repo.ts b/apps/server-asset-sg/src/features/users/user.repo.ts index 232c85fc..e7bbf11f 100644 --- a/apps/server-asset-sg/src/features/users/user.repo.ts +++ b/apps/server-asset-sg/src/features/users/user.repo.ts @@ -1,4 +1,4 @@ -import { User, UserData, UserId } from '@asset-sg/shared/v2'; +import { Role, User, UserData, UserId, WorkgroupId } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; @@ -49,6 +49,13 @@ export class UserRepo implements Repo ({ workgroupId, role })), + skipDuplicates: true, + }, + }, }, select: userSelection, }); @@ -67,10 +74,7 @@ export class UserRepo implements Repo ({ - workgroupId: workgroup.id, - role: workgroup.role, - })), + data: [...data.roles].map(([workgroupId, role]) => ({ workgroupId, role })), skipDuplicates: true, }, }, @@ -113,10 +117,16 @@ export const userSelection = satisfy()({ type SelectedUser = Prisma.AssetUserGetPayload<{ select: typeof userSelection }>; -const parse = (data: SelectedUser): User => ({ - ...data, - workgroups: data.workgroups.map((it) => ({ - id: it.workgroupId, - role: it.role, - })), -}); +const parse = (data: SelectedUser): User => { + const roles = new Map(); + for (const workgroup of data.workgroups) { + roles.set(workgroup.workgroupId, workgroup.role); + } + return { + id: data.id, + email: data.email, + isAdmin: data.isAdmin, + lang: data.lang, + roles, + }; +}; diff --git a/apps/server-asset-sg/src/features/users/users.controller.ts b/apps/server-asset-sg/src/features/users/users.controller.ts index ab4a7da8..b9e6190b 100644 --- a/apps/server-asset-sg/src/features/users/users.controller.ts +++ b/apps/server-asset-sg/src/features/users/users.controller.ts @@ -1,4 +1,4 @@ -import { User, UserData, UserId } from '@asset-sg/shared/v2'; +import { convert, User, UserData, UserId, UserSchema } from '@asset-sg/shared/v2'; import { UserDataSchema } from '@asset-sg/shared/v2'; import { Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Param, Put } from '@nestjs/common'; import { Authorize } from '@/core/decorators/authorize.decorator'; @@ -12,18 +12,25 @@ export class UsersController { @Get('/current') showCurrent(@CurrentUser() user: User | null): User | null { - return user; + if (user == null) { + return null; + } + return convert(UserSchema, user); } @Get('/') @Authorize.Admin() - list(): Promise { - return this.userRepo.list(); + async list(): Promise { + return convert(UserSchema, await this.userRepo.list()); } @Get('/:id') - show(@Param('id') id: UserId): Promise { - return this.userRepo.find(id); + async show(@Param('id') id: UserId): Promise { + const user = await this.userRepo.find(id); + if (user == null) { + return null; + } + return convert(UserSchema, user); } @Put('/:id') @@ -34,10 +41,10 @@ export class UsersController { data: UserData ): Promise { const user = await this.userRepo.update(id, data); - if (user === null) { + if (user == null) { throw new HttpException('not found', 404); } - return user; + return convert(UserSchema, user); } @Delete('/:id') diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup-simple.repo.ts b/apps/server-asset-sg/src/features/workgroups/workgroup-simple.repo.ts index 8d89c64b..4b1ba2bb 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup-simple.repo.ts +++ b/apps/server-asset-sg/src/features/workgroups/workgroup-simple.repo.ts @@ -1,5 +1,4 @@ -import { User } from '@asset-sg/shared'; -import { UserId } from '@asset-sg/shared/v2'; +import { User, UserId } from '@asset-sg/shared/v2'; import { Role, SimpleWorkgroup, WorkgroupId } from '@asset-sg/shared/v2'; import { Prisma } from '@prisma/client'; import { PrismaService } from '@/core/prisma.service'; diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts index 7b0a6165..b89de4bc 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts +++ b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.spec.ts @@ -3,15 +3,12 @@ import { faker } from '@faker-js/faker'; // eslint-disable-next-line @nx/enforce-module-boundaries import { clearPrismaAssets, setupDB, setupDefaultWorkgroup } from '../../../../../test/setup-db'; import { PrismaService } from '@/core/prisma.service'; -import { fakeAssetPatch, fakeUser } from '@/features/asset-edit/asset-edit.fake'; -import { AssetEditRepo } from '@/features/asset-edit/asset-edit.repo'; import { UserRepo } from '@/features/users/user.repo'; import { WorkgroupRepo } from '@/features/workgroups/workgroup.repo'; describe('WorkgroupRepo', () => { const prisma = new PrismaService(); const repo = new WorkgroupRepo(prisma); - const assetRepo = new AssetEditRepo(prisma); const userRepo = new UserRepo(prisma); beforeAll(async () => { @@ -35,7 +32,7 @@ describe('WorkgroupRepo', () => { }); it('returns the record associated with a specific id', async () => { // Given - const data: WorkgroupData = { name: 'test', disabled_at: null, assets: [], users: [] }; + const data: WorkgroupData = { name: 'test', disabledAt: null, users: new Map() }; const expected = await repo.create(data); // When @@ -57,11 +54,11 @@ describe('WorkgroupRepo', () => { }); it('returns the specified amount of records', async () => { // Given - const record1 = await repo.create({ name: 'Test 1', disabled_at: null, assets: [], users: [] }); - const record2 = await repo.create({ name: 'Test 2', disabled_at: null, assets: [], users: [] }); - const record3 = await repo.create({ name: 'Test 3', disabled_at: null, assets: [], users: [] }); - await repo.create({ name: 'Test 4', disabled_at: null, assets: [], users: [] }); - await repo.create({ name: 'Test 5', disabled_at: null, assets: [], users: [] }); + const record1 = await repo.create({ name: 'Test 1', disabledAt: null, users: new Map() }); + const record2 = await repo.create({ name: 'Test 2', disabledAt: null, users: new Map() }); + const record3 = await repo.create({ name: 'Test 3', disabledAt: null, users: new Map() }); + await repo.create({ name: 'Test 4', disabledAt: null, users: new Map() }); + await repo.create({ name: 'Test 5', disabledAt: null, users: new Map() }); // When const workgroups = await repo.list({ limit: 3 }); @@ -72,11 +69,11 @@ describe('WorkgroupRepo', () => { }); it('returns the records appearing after the specified offset', async () => { //Given - await repo.create({ name: 'Test 1', disabled_at: null, assets: [], users: [] }); - await repo.create({ name: 'Test 2', disabled_at: null, assets: [], users: [] }); - const record1 = await repo.create({ name: 'Test 3', disabled_at: null, assets: [], users: [] }); - const record2 = await repo.create({ name: 'Test 4', disabled_at: null, assets: [], users: [] }); - const record3 = await repo.create({ name: 'Test 5', disabled_at: null, assets: [], users: [] }); + await repo.create({ name: 'Test 1', disabledAt: null, users: new Map() }); + await repo.create({ name: 'Test 2', disabledAt: null, users: new Map() }); + const record1 = await repo.create({ name: 'Test 3', disabledAt: null, users: new Map() }); + const record2 = await repo.create({ name: 'Test 4', disabledAt: null, users: new Map() }); + const record3 = await repo.create({ name: 'Test 5', disabledAt: null, users: new Map() }); // When const workgroups = await repo.list({ limit: 3, offset: 2 }); @@ -87,11 +84,11 @@ describe('WorkgroupRepo', () => { }); it('returns an empty list when offset is greater than the number of records', async () => { //Given - await repo.create({ name: 'Test 1', disabled_at: null, assets: [], users: [] }); - await repo.create({ name: 'Test 2', disabled_at: null, assets: [], users: [] }); - await repo.create({ name: 'Test 3', disabled_at: null, assets: [], users: [] }); - await repo.create({ name: 'Test 4', disabled_at: null, assets: [], users: [] }); - await repo.create({ name: 'Test 5', disabled_at: null, assets: [], users: [] }); + await repo.create({ name: 'Test 1', disabledAt: null, users: new Map() }); + await repo.create({ name: 'Test 2', disabledAt: null, users: new Map() }); + await repo.create({ name: 'Test 3', disabledAt: null, users: new Map() }); + await repo.create({ name: 'Test 4', disabledAt: null, users: new Map() }); + await repo.create({ name: 'Test 5', disabledAt: null, users: new Map() }); // When const workgroups = await repo.list({ limit: 3, offset: 5 }); @@ -105,19 +102,25 @@ describe('WorkgroupRepo', () => { it('creates a new record', async () => { // Given await setupDefaultWorkgroup(prisma); - const asset = await assetRepo.create({ patch: fakeAssetPatch(), user: fakeUser() }); const user = await userRepo.create({ email: faker.internet.email(), lang: 'de', oidcId: faker.string.uuid(), isAdmin: false, - workgroups: [], + roles: new Map(), }); const data: WorkgroupData = { name: 'test', - disabled_at: null, - assets: [asset], - users: [{ userId: user.id, role: Role.MasterEditor }], + disabledAt: null, + users: new Map([ + [ + user.id, + { + role: Role.MasterEditor, + email: user.email, + }, + ], + ]), }; // When @@ -125,8 +128,7 @@ describe('WorkgroupRepo', () => { // Then expect(workgroup.name).toEqual(data.name); - expect(workgroup.disabled_at).toEqual(data.disabled_at); - expect(workgroup.assets).toEqual(data.assets.map(({ assetId }) => ({ assetId }))); + expect(workgroup.disabledAt).toEqual(data.disabledAt); expect(workgroup.users).toEqual(data.users); }); }); @@ -134,7 +136,7 @@ describe('WorkgroupRepo', () => { describe('update', () => { it('returns `null` when updating a non-existent record', async () => { //Given - const data: WorkgroupData = { name: 'test', disabled_at: null, assets: [], users: [] }; + const data: WorkgroupData = { name: 'test', disabledAt: null, users: new Map() }; // When const workgroup = await repo.update(1, data); @@ -145,21 +147,27 @@ describe('WorkgroupRepo', () => { it('updates an existing record', async () => { //Given await setupDefaultWorkgroup(prisma); - const asset = await assetRepo.create({ patch: fakeAssetPatch(), user: fakeUser() }); const user = await userRepo.create({ email: faker.internet.email(), lang: 'de', oidcId: faker.string.uuid(), isAdmin: false, - workgroups: [], + roles: new Map(), }); - const initialWorkgroup: WorkgroupData = { name: 'test', disabled_at: null, assets: [], users: [] }; + const initialWorkgroup: WorkgroupData = { name: 'test', disabledAt: null, users: new Map() }; const workgroup = await repo.create(initialWorkgroup); const data: WorkgroupData = { name: 'new name', - disabled_at: new Date(), - assets: [{ assetId: asset.assetId }], - users: [{ userId: user.id, role: Role.MasterEditor }], + disabledAt: new Date(), + users: new Map([ + [ + user.id, + { + role: Role.MasterEditor, + email: user.email, + }, + ], + ]), }; //When @@ -167,8 +175,7 @@ describe('WorkgroupRepo', () => { //Then expect(updatedWorkgroup.name).toEqual(data.name); - expect(updatedWorkgroup.disabled_at).toEqual(data.disabled_at); - expect(updatedWorkgroup.assets).toEqual(data.assets); + expect(updatedWorkgroup.disabledAt).toEqual(data.disabledAt); expect(updatedWorkgroup.users).toEqual(data.users); }); }); @@ -183,19 +190,25 @@ describe('WorkgroupRepo', () => { it('removes a record and its relations from the database', async () => { //Given await setupDefaultWorkgroup(prisma); - const asset = await assetRepo.create({ patch: fakeAssetPatch(), user: fakeUser() }); const user = await userRepo.create({ email: faker.internet.email(), lang: 'de', oidcId: faker.string.uuid(), isAdmin: false, - workgroups: [], + roles: new Map(), }); const data: WorkgroupData = { name: 'test', - disabled_at: null, - assets: [{ assetId: asset.assetId }], - users: [{ userId: user.id, role: Role.MasterEditor }], + disabledAt: null, + users: new Map([ + [ + user.id, + { + role: Role.MasterEditor, + email: user.email, + }, + ], + ]), }; const workgroup = await repo.create(data); diff --git a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts index 92ec5a42..11974ecc 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts +++ b/apps/server-asset-sg/src/features/workgroups/workgroup.repo.ts @@ -1,5 +1,4 @@ -import { User } from '@asset-sg/shared'; -import { Workgroup, WorkgroupData, WorkgroupId } from '@asset-sg/shared/v2'; +import { User, UserId, UserOnWorkgroup, Workgroup, WorkgroupData, WorkgroupId } from '@asset-sg/shared/v2'; import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '@/core/prisma.service'; @@ -16,36 +15,35 @@ export class WorkgroupRepo implements Repo { - return this.prisma.workgroup.findUnique({ + async find(id: WorkgroupId): Promise { + const entry = await this.prisma.workgroup.findUnique({ where: { id }, - select: workGroupSelection, + select: workgroupSelection, }); + return entry == null ? null : parse(entry); } - list({ limit, offset, ids }: RepoListOptions = {}): Promise { - return this.prisma.workgroup.findMany({ + async list({ limit, offset, ids }: RepoListOptions = {}): Promise { + const entries = await this.prisma.workgroup.findMany({ where: ids == null ? undefined : { id: { in: ids } }, take: limit, skip: offset, - select: workGroupSelection, + select: workgroupSelection, }); + return entries.map(parse); } - create(data: WorkgroupData): Promise { - return this.prisma.workgroup.create({ + async create(data: WorkgroupData): Promise { + const entry = await this.prisma.workgroup.create({ data: { name: data.name, created_at: new Date(), - disabled_at: data.disabled_at, - assets: { - connect: data.assets?.map((asset) => ({ assetId: asset.assetId })), - }, + disabled_at: data.disabledAt, users: data.users ? { createMany: { - data: data.users.map((user) => ({ - userId: user.user.id, + data: [...data.users].map(([userId, user]) => ({ + userId, role: user.role, })), skipDuplicates: true, @@ -53,28 +51,26 @@ export class WorkgroupRepo implements Repo { try { - return await this.prisma.workgroup.update({ + const entry = await this.prisma.workgroup.update({ where: { id }, data: { name: data.name, - disabled_at: data.disabled_at, - assets: { - set: data.assets ? data.assets.map((asset) => ({ assetId: asset.assetId })) : undefined, - }, + disabled_at: data.disabledAt, users: data.users ? { deleteMany: { userId: {}, }, createMany: { - data: data.users.map((user) => ({ - userId: user.user.id, + data: [...data.users].map(([userId, user]) => ({ + userId: userId, role: user.role, })), skipDuplicates: true, @@ -82,8 +78,9 @@ export class WorkgroupRepo implements Repo()({ +export const workgroupSelection = satisfy()({ id: true, name: true, disabled_at: true, users: { + orderBy: { + user: { + email: 'asc', + }, + }, select: { role: true, user: { @@ -118,8 +120,29 @@ export const workGroupSelection = satisfy()({ }, }, assets: { + orderBy: { + assetId: 'asc', + }, select: { assetId: true, }, }, }); + +type SelectedWorkgroup = Prisma.WorkgroupGetPayload<{ select: typeof workgroupSelection }>; + +const parse = (data: SelectedWorkgroup): Workgroup => { + const users = new Map(); + for (const user of data.users) { + users.set(user.user.id, { + email: user.user.email, + role: user.role, + }); + } + return { + id: data.id, + name: data.name, + users, + disabledAt: data.disabled_at, + }; +}; diff --git a/apps/server-asset-sg/src/features/workgroups/workgroups.controller.ts b/apps/server-asset-sg/src/features/workgroups/workgroups.controller.ts index d090fbbd..8811c773 100644 --- a/apps/server-asset-sg/src/features/workgroups/workgroups.controller.ts +++ b/apps/server-asset-sg/src/features/workgroups/workgroups.controller.ts @@ -1,12 +1,14 @@ import { AssetId, + convert, SimpleWorkgroup, User, Workgroup, WorkgroupData, - WorkgroupDataBoundary, + WorkgroupDataSchema, WorkgroupId, WorkgroupPolicy, + WorkgroupSchema, } from '@asset-sg/shared/v2'; import { Controller, @@ -45,9 +47,12 @@ export class WorkgroupsController { @Authorize.User() async list(@CurrentUser() user: User, @Query() query: ListQuery): Promise { const options: RepoListOptions = { - ids: user.isAdmin ? undefined : user.workgroups.map((it) => it.id), + ids: user.isAdmin ? undefined : [...user.roles.keys()], }; - return query.isSimple ? this.workgroupRepo.simple(user).list(options) : this.workgroupRepo.list(options); + if (query.isSimple) { + return this.workgroupRepo.simple(user).list(options); + } + return convert(WorkgroupSchema, await this.workgroupRepo.list(options)); } @Get('/:id') @@ -57,24 +62,25 @@ export class WorkgroupsController { throw new HttpException('not found', 404); } authorize(WorkgroupPolicy, user).canShow(record); - return record; + return convert(WorkgroupSchema, record); } @Post('/') @HttpCode(HttpStatus.CREATED) async create( - @ParseBody(WorkgroupDataBoundary) + @ParseBody(WorkgroupDataSchema) data: WorkgroupData, @CurrentUser() user: User ): Promise { authorize(WorkgroupPolicy, user).canCreate(); - return this.workgroupRepo.create(data); + const record = await this.workgroupRepo.create(data); + return convert(WorkgroupSchema, record); } @Put('/:id') async update( @Param('id', ParseIntPipe) id: number, - @ParseBody(WorkgroupDataBoundary) + @ParseBody(WorkgroupDataSchema) data: WorkgroupData, @CurrentUser() user: User ): Promise { @@ -87,7 +93,7 @@ export class WorkgroupsController { if (workgroup === null) { throw new HttpException('not found', 404); } - return workgroup; + return convert(WorkgroupSchema, workgroup); } @Delete('/:id') diff --git a/libs/admin/src/lib/admin.module.ts b/libs/admin/src/lib/admin.module.ts index 75b8aec8..8b39deb5 100644 --- a/libs/admin/src/lib/admin.module.ts +++ b/libs/admin/src/lib/admin.module.ts @@ -39,10 +39,11 @@ import { EffectsModule } from '@ngrx/effects'; import { StoreModule } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; import { ForModule } from '@rx-angular/template/for'; +import { IfModule } from '@rx-angular/template/if'; import { LetModule } from '@rx-angular/template/let'; import { PushModule } from '@rx-angular/template/push'; import { AdminPageRoutingModule } from './admin-routing.module'; -import { AddWorkgroupUserDialog } from './components/add-workgroup-user-dialog/add-workgroup-user-dialog.component'; +import { AddWorkgroupUserDialogComponent } from './components/add-workgroup-user-dialog/add-workgroup-user-dialog.component'; import { AdminPageComponent } from './components/admin-page'; import { UserEditComponent } from './components/user-edit/user-edit.component'; import { UsersComponent } from './components/users/users.component'; @@ -58,7 +59,7 @@ import { adminReducer } from './state/admin.reducer'; WorkgroupEditComponent, UsersComponent, UserEditComponent, - AddWorkgroupUserDialog, + AddWorkgroupUserDialogComponent, ], imports: [ CommonModule, @@ -105,6 +106,7 @@ import { adminReducer } from './state/admin.reducer'; MatAutocompleteModule, MatCard, MatSlideToggle, + IfModule, ], }) export class AdminModule {} diff --git a/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.html b/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.html index dfdf82f6..34ac32c5 100644 --- a/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.html +++ b/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.html @@ -13,7 +13,7 @@

    admin.workgroupPage.addUsers

    @if (formGroup.controls['users'].invalid) { - Add at least one user + Add at least one user }
    admin.role
    diff --git a/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.ts b/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.ts index 82d23dec..3054d805 100644 --- a/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.ts +++ b/libs/admin/src/lib/components/add-workgroup-user-dialog/add-workgroup-user-dialog.component.ts @@ -1,10 +1,10 @@ import { Component, Inject, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { User, UserId, Workgroup, WorkgroupData } from '@asset-sg/shared/v2'; import { Store } from '@ngrx/store'; import { Role } from '@prisma/client'; import { Observable, Subscription } from 'rxjs'; -import { User, Workgroup } from '../../services/admin.service'; import * as actions from '../../state/admin.actions'; import { AppStateWithAdmin } from '../../state/admin.reducer'; import { selectUsers } from '../../state/admin.selector'; @@ -15,23 +15,24 @@ import { Mode } from '../workgroup-edit/workgroup-edit.component'; templateUrl: './add-workgroup-user-dialog.component.html', styleUrls: ['./add-workgroup-user-dialog.component.scss'], }) -export class AddWorkgroupUserDialog implements OnInit { - public formGroup: FormGroup = new FormGroup({ - users: new FormControl([], Validators.required), - role: new FormControl(Role.Viewer, Validators.required), +export class AddWorkgroupUserDialogComponent implements OnInit { + public formGroup = new FormGroup({ + users: new FormControl([], { validators: [Validators.required], nonNullable: true }), + role: new FormControl(Role.Viewer, { validators: [Validators.required], nonNullable: true }), }); + public readonly roles: Role[] = Object.values(Role); + public users: User[] = []; public workgroup: Workgroup; - public mode: Mode = 'edit'; - public readonly roles: Role[] = Object.values(Role); + public mode: Mode; private readonly users$: Observable = this.store.select(selectUsers); private readonly subscriptions: Subscription = new Subscription(); constructor( @Inject(MAT_DIALOG_DATA) public data: { workgroup: Workgroup; mode: Mode }, - private readonly dialogRef: MatDialogRef, + private readonly dialogRef: MatDialogRef, private store: Store ) { this.workgroup = this.data.workgroup; @@ -47,31 +48,35 @@ export class AddWorkgroupUserDialog implements OnInit { } public addUsers() { - if (!this.formGroup.valid) { + if (this.formGroup.invalid) { return; } - const usersToAdd = this.users - .filter((user) => this.formGroup.controls['users'].value.includes(user.id)) - .map((user) => ({ - user: { email: user.email, id: user.id }, + const users = new Map(this.workgroup.users); + const newUserIds = new Set(this.formGroup.controls.users.value); + for (const user of this.users) { + if (!newUserIds.has(user.id)) { + continue; + } + users.set(user.id, { + email: user.email, role: this.formGroup.controls['role'].value, - })); - const updatedWorkgroup = { + }); + } + const workgroup: WorkgroupData = { name: this.workgroup.name, - assets: this.workgroup.assets, - disabled_at: this.workgroup.disabled_at, - users: [...usersToAdd, ...this.workgroup.users], + disabledAt: this.workgroup.disabledAt, + users, }; if (this.mode === 'edit') { - this.store.dispatch(actions.updateWorkgroup({ workgroupId: this.workgroup.id, workgroup: updatedWorkgroup })); + this.store.dispatch(actions.updateWorkgroup({ workgroupId: this.workgroup.id, workgroup })); } else { - this.store.dispatch(actions.setWorkgroup({ workgroup: { ...updatedWorkgroup, id: 0 } })); + this.store.dispatch(actions.setWorkgroup({ workgroup: { ...workgroup, id: -1 } })); } this.dialogRef.close(); } public isUserInWorkgroup(userId: string): boolean { - return this.workgroup.users.some((user) => user.user.id === userId) ?? false; + return this.workgroup.users.has(userId); } private initSubscriptions() { diff --git a/libs/admin/src/lib/components/admin-page/admin-page.component.html b/libs/admin/src/lib/components/admin-page/admin-page.component.html index 21c2eb79..723e59ea 100644 --- a/libs/admin/src/lib/components/admin-page/admin-page.component.html +++ b/libs/admin/src/lib/components/admin-page/admin-page.component.html @@ -1,16 +1,18 @@ - +
    @if (!isDetailPage) { } @else { - @@ -51,7 +51,7 @@

    admin.workgroups

    {{ workgroup.name }} @@ -61,11 +61,11 @@

    admin.workgroups

    - +
    - + @@ -74,7 +74,7 @@

    admin.workgroups

    diff --git a/libs/admin/src/lib/components/user-edit/user-edit.component.scss b/libs/admin/src/lib/components/user-edit/user-edit.component.scss index f9bdaf8f..b42f07b3 100644 --- a/libs/admin/src/lib/components/user-edit/user-edit.component.scss +++ b/libs/admin/src/lib/components/user-edit/user-edit.component.scss @@ -22,22 +22,26 @@ } .workgroups { - .workgroups__header { + & > .workgroups__header { display: flex; justify-content: space-between; align-items: center; + + & > *:last-child > mat-form-field { + width: 20rem; + } } .mat-column-name { - width: auto !important; + width: auto; } .mat-column-role { - width: 10% !important; + width: 12rem; } .mat-column-actions { - width: 5% !important; + width: 6rem; } .workgroups__table__form-field { diff --git a/libs/admin/src/lib/components/user-edit/user-edit.component.ts b/libs/admin/src/lib/components/user-edit/user-edit.component.ts index 89cf4216..d9d38683 100644 --- a/libs/admin/src/lib/components/user-edit/user-edit.component.ts +++ b/libs/admin/src/lib/components/user-edit/user-edit.component.ts @@ -3,13 +3,16 @@ import { FormControl, FormGroup } from '@angular/forms'; import { MatOptionSelectionChange } from '@angular/material/core'; import { MatSelectChange } from '@angular/material/select'; import { ActivatedRoute, ParamMap } from '@angular/router'; +import { fromAppShared } from '@asset-sg/client-shared'; +import { isNotNull } from '@asset-sg/core'; +import { SimpleWorkgroup, User, WorkgroupId } from '@asset-sg/shared/v2'; +import * as RD from '@devexperts/remote-data-ts'; import { Store } from '@ngrx/store'; import { Role } from '@prisma/client'; -import { map, startWith, Subscription } from 'rxjs'; -import { User, Workgroup, WorkgroupOnUser } from '../../services/admin.service'; +import { asapScheduler, filter, map, Observable, startWith, Subscription, withLatestFrom } from 'rxjs'; import * as actions from '../../state/admin.actions'; import { AppStateWithAdmin } from '../../state/admin.reducer'; -import { selectSelectedUser, selectWorkgroups } from '../../state/admin.selector'; +import { selectSelectedUser } from '../../state/admin.selector'; @Component({ selector: 'asset-sg-user-edit', @@ -17,10 +20,11 @@ import { selectSelectedUser, selectWorkgroups } from '../../state/admin.selector styleUrls: ['./user-edit.component.scss'], }) export class UserEditComponent implements OnInit, OnDestroy { - public roles: Role[] = Object.values(Role); - public user?: User; - public workgroups: Workgroup[] = []; - public filteredWorkgroups: Workgroup[] = []; + public roles = Object.values(Role); + public user: User | null = null; + public workgroups: SimpleWorkgroup[] = []; + public filteredWorkgroups: SimpleWorkgroup[] = []; + public workgroupAutoCompleteControl = new FormControl(''); public formGroup = new FormGroup({ isAdmin: new FormControl(false), @@ -31,10 +35,36 @@ export class UserEditComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); private readonly store = inject(Store); - private readonly workgroups$ = this.store.select(selectWorkgroups); + private readonly workgroups$ = this.store.select(fromAppShared.selectWorkgroups); private readonly user$ = this.store.select(selectSelectedUser); private readonly subscriptions: Subscription = new Subscription(); + public readonly isCurrentUser$: Observable = this.store.select(fromAppShared.selectRDUserProfile).pipe( + map((currentUser) => (RD.isSuccess(currentUser) ? currentUser.value : null)), + filter(isNotNull), + withLatestFrom(this.user$.pipe(filter(isNotNull))), + map(([currentUser, user]) => currentUser.id === user.id), + startWith(true) + ); + + public readonly userWorkgroups$: Observable> = this.user$.pipe( + withLatestFrom(this.workgroups$), + map(([user, workgroups]) => { + if (user == null) { + return []; + } + const result: Array = []; + for (const workgroup of workgroups) { + const role = user.roles.get(workgroup.id); + if (role == null) { + continue; + } + result.push({ ...workgroup, role }); + } + return result; + }) + ); + public ngOnInit() { this.getUserFromRoute(); this.initSubscriptions(); @@ -46,52 +76,43 @@ export class UserEditComponent implements OnInit, OnDestroy { public resetWorkgroupSearch() { this.workgroupAutoCompleteControl.setValue(''); + + // Redo the reset a tick later, as the input seems to fall back to its previous value. + asapScheduler.schedule(() => { + this.workgroupAutoCompleteControl.setValue(''); + }); } public isUserPartOfWorkgroup(workgroupId: number): boolean { - return !!this.user?.workgroups.find((workgroup) => workgroup.workgroupId === workgroupId); + return this.user != null && this.user.roles.has(workgroupId); } - public addWorkgroupToUser(event: MatOptionSelectionChange, workgroupId: number) { - if (!this.user || !event.isUserInput) { + public addWorkgroupRole(event: MatOptionSelectionChange, workgroupId: number) { + if (this.user == null || !event.isUserInput) { return; } - const selectedWorkgroup = this.workgroups.find((workgroup) => workgroup.id === workgroupId); - - const workgroupToAdd: WorkgroupOnUser = { - workgroupId: workgroupId, - workgroup: { name: selectedWorkgroup!.name }, - role: Role.Viewer, - }; - const updatedWorkgroups = [...this.user.workgroups, workgroupToAdd]; - this.store.dispatch(actions.updateUser({ user: { ...this.user, workgroups: updatedWorkgroups } })); + const roles = new Map(this.user.roles); + roles.set(workgroupId, Role.Viewer); + this.store.dispatch(actions.updateUser({ user: { ...this.user, roles } })); this.resetWorkgroupSearch(); } - public updateRoleForWorkgroup(event: MatSelectChange, workgroup: WorkgroupOnUser) { + public updateWorkgroupRole(event: MatSelectChange, workgroupId: WorkgroupId) { if (!this.user) { return; } - const updatedWorkgroups = this.user.workgroups.map((userWorkgroup) => { - if (userWorkgroup.workgroupId === workgroup.workgroupId) { - return { - ...userWorkgroup, - role: event.value, - }; - } - return userWorkgroup; - }); - this.updateUser({ ...this.user, workgroups: updatedWorkgroups }); + const roles = new Map(this.user.roles); + roles.set(workgroupId, event.value as Role); + this.updateUser({ ...this.user, roles }); } - public deleteWorkgroupFromUser(workgroup: WorkgroupOnUser) { + public removeWorkgroupRole(workgroupId: WorkgroupId) { if (!this.user) { return; } - const updatedWorkgroups = this.user.workgroups.filter( - (userWorkgroup) => userWorkgroup.workgroupId !== workgroup.workgroupId - ); - this.updateUser({ ...this.user, workgroups: updatedWorkgroups }); + const roles = new Map(this.user.roles); + roles.delete(workgroupId); + this.updateUser({ ...this.user, roles }); } private updateUser(user: User) { @@ -100,16 +121,12 @@ export class UserEditComponent implements OnInit, OnDestroy { private getUserFromRoute() { this.subscriptions.add( - this.route.paramMap - .pipe( - map((params: ParamMap) => { - const userId = params.get('id'); - if (userId) { - return this.store.dispatch(actions.findUser({ userId })); - } - }) - ) - .subscribe() + this.route.paramMap.subscribe((params: ParamMap) => { + const userId = params.get('id'); + if (userId) { + this.store.dispatch(actions.findUser({ userId })); + } + }) ); } @@ -142,9 +159,10 @@ export class UserEditComponent implements OnInit, OnDestroy { this.subscriptions.add( this.workgroupAutoCompleteControl.valueChanges .pipe( + map((it) => it ?? ''), startWith(''), map((value) => - this.workgroups.filter((workgroup) => workgroup.name.toLowerCase().includes(value!.toLowerCase().trim())) + this.workgroups.filter((workgroup) => workgroup.name.toLowerCase().includes(value.toLowerCase().trim())) ) ) .subscribe((workgroups) => { @@ -154,12 +172,24 @@ export class UserEditComponent implements OnInit, OnDestroy { this.subscriptions.add( this.formGroup.valueChanges.subscribe((value) => { - const updatedUser: User = { - ...this.user!, + if (this.user == null || this.formGroup.pristine) { + return; + } + this.updateUser({ + ...this.user, isAdmin: value.isAdmin ?? false, lang: value.lang ?? 'de', - }; - this.updateUser(updatedUser); + }); + }) + ); + + this.subscriptions.add( + this.isCurrentUser$.subscribe((isCurrentUser) => { + if (isCurrentUser) { + this.formGroup.controls.isAdmin.disable(); + } else { + this.formGroup.controls.isAdmin.enable(); + } }) ); } diff --git a/libs/admin/src/lib/components/users/users.component.html b/libs/admin/src/lib/components/users/users.component.html index 354e13a5..0d7f4629 100644 --- a/libs/admin/src/lib/components/users/users.component.html +++ b/libs/admin/src/lib/components/users/users.component.html @@ -2,8 +2,8 @@

    admin.users

    -
    -
    Name{{ workgroup.workgroup.name }}{{ workgroup.name }} admin.role - + {{ role }} @@ -86,7 +86,7 @@

    admin.workgroups

    admin.actions -
    +
    +
    @@ -12,7 +12,11 @@

    admin.users

    @@ -24,18 +28,15 @@

    admin.users

    diff --git a/libs/admin/src/lib/components/users/users.component.ts b/libs/admin/src/lib/components/users/users.component.ts index c1942d86..9bdbff13 100644 --- a/libs/admin/src/lib/components/users/users.component.ts +++ b/libs/admin/src/lib/components/users/users.component.ts @@ -1,31 +1,36 @@ import { Component, inject, OnDestroy, OnInit } from '@angular/core'; import { MatCheckboxChange } from '@angular/material/checkbox'; +import { fromAppShared } from '@asset-sg/client-shared'; +import { isNotNull } from '@asset-sg/core'; +import { Role, User, UserOnWorkgroup, Workgroup, WorkgroupId } from '@asset-sg/shared/v2'; +import * as RD from '@devexperts/remote-data-ts'; import { Store } from '@ngrx/store'; -import { Subscription } from 'rxjs'; -import { User, Workgroup, WorkgroupOnUser } from '../../services/admin.service'; +import { filter, map, Observable, startWith, Subscription, withLatestFrom } from 'rxjs'; import * as actions from '../../state/admin.actions'; import { AppStateWithAdmin } from '../../state/admin.reducer'; import { selectUsers, selectWorkgroups } from '../../state/admin.selector'; -const WORKGROUP_CUTOFF_LENGTH = 3; - @Component({ selector: 'asset-sg-users', templateUrl: './users.component.html', styleUrls: ['./users.component.scss'], }) export class UsersComponent implements OnInit, OnDestroy { - public users: User[] = []; - public workgroups: Workgroup[] = []; - public readonly workgroupCutoffLength = WORKGROUP_CUTOFF_LENGTH; + public workgroups = new Map(); protected readonly COLUMNS = ['email', 'isAdmin', 'languages', 'workgroups', 'actions']; + protected readonly WORKGROUP_DISPLAY_COUNT = 3; private readonly store = inject(Store); - private readonly users$ = this.store.select(selectUsers); + public readonly users$ = this.store.select(selectUsers); private readonly workgroups$ = this.store.select(selectWorkgroups); private readonly subscriptions: Subscription = new Subscription(); + public readonly currentUser$: Observable = this.store.select(fromAppShared.selectRDUserProfile).pipe( + map((currentUser) => (RD.isSuccess(currentUser) ? currentUser.value : null)), + filter(isNotNull) + ); + public ngOnInit(): void { this.initSubscriptions(); } @@ -34,23 +39,48 @@ export class UsersComponent implements OnInit, OnDestroy { this.subscriptions.unsubscribe(); } + public *getUserWorkgroups(user: User): Iterable { + const iter = user.roles.entries(); + for (let i = 0; i < this.WORKGROUP_DISPLAY_COUNT; i++) { + const { value, done } = iter.next(); + if (done) { + break; + } + const [workgroupId, role] = value; + const workgroup = this.workgroups.get(workgroupId); + if (workgroup == null) { + continue; + } + yield { ...workgroup, role }; + } + } + public updateIsAdminStatus(user: User, event: MatCheckboxChange) { this.store.dispatch(actions.updateUser({ user: { ...user, isAdmin: event.checked } })); } - public formatWorkgroupsTooltip(workgroups: WorkgroupOnUser[]): string { - return workgroups.map((wg) => `${wg.workgroup.name}.${wg.role}`).join(', \n'); + public formatWorkgroupsTooltip(roles: User['roles']): string { + let tooltip = ''; + for (const [workgroupId, workgroupRole] of roles) { + const workgroup = this.workgroups.get(workgroupId); + if (workgroup == null) { + continue; + } + if (tooltip.length !== 0) { + tooltip += ',\n'; + } + tooltip += `${workgroup.name}.${workgroupRole}`; + } + return tooltip; } private initSubscriptions(): void { - this.subscriptions.add( - this.users$.subscribe((users) => { - this.users = users; - }) - ); this.subscriptions.add( this.workgroups$.subscribe((workgroups) => { - this.workgroups = workgroups; + this.workgroups.clear(); + for (const workgroup of workgroups) { + this.workgroups.set(workgroup.id, workgroup); + } }) ); } diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html index 45588461..0c7fc666 100644 --- a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html @@ -1,5 +1,5 @@ -
    +
    @@ -9,8 +9,8 @@
    @@ -21,12 +21,16 @@

    admin.users

    - @if (workgroup.users.length > 0) { + @if (workgroup.users.size === 0) { +

    admin.workgroupPage.chooseUsersText

    + } @else { +
    admin.email {{ user.email }} admin.userPage.admin - + - @for (workgroup of user.workgroups.slice(0, workgroupCutoffLength); track workgroup.workgroupId; let last = - $last) { - {{ workgroup.workgroup.name }}.{{ workgroup.role }} - , - } @if (user.workgroups.length > workgroupCutoffLength) { - , +{{ user.workgroups.length - workgroupCutoffLength }} admin.userPage.more - + @for (workgroup of getUserWorkgroups(user); track workgroup.id; let isLast = $last) { + {{ workgroup.name }}.{{ workgroup.role }} + , + } @if (user.roles.size > WORKGROUP_DISPLAY_COUNT) { + , +{{ user.roles.size - WORKGROUP_DISPLAY_COUNT }} admin.userPage.more }
    + + -
    - + @@ -34,7 +38,7 @@

    admin.users

    - - -
    admin.email{{ user.user.email }}{{ user.email }} admin.role - + {{ role }} @@ -46,22 +50,16 @@

    admin.users

    admin.actions -
    - - } @else { -

    admin.workgroupPage.chooseUsersText

    }
    - +
    diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts index 4eb8748e..46dc876f 100644 --- a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts @@ -3,14 +3,14 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { MatSelectChange } from '@angular/material/select'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { UserId, UserOnWorkgroup, Workgroup, WorkgroupData } from '@asset-sg/shared/v2'; import { Store } from '@ngrx/store'; import { Role } from '@prisma/client'; -import { map, Subscription } from 'rxjs'; -import { UserOnWorkgroup, Workgroup, WorkgroupData } from '../../services/admin.service'; +import { BehaviorSubject, map, share, startWith, Subscription } from 'rxjs'; import * as actions from '../../state/admin.actions'; import { AppStateWithAdmin } from '../../state/admin.reducer'; import { selectSelectedWorkgroup } from '../../state/admin.selector'; -import { AddWorkgroupUserDialog } from '../add-workgroup-user-dialog/add-workgroup-user-dialog.component'; +import { AddWorkgroupUserDialogComponent } from '../add-workgroup-user-dialog/add-workgroup-user-dialog.component'; export type Mode = 'edit' | 'create'; @@ -20,14 +20,14 @@ export type Mode = 'edit' | 'create'; styleUrls: ['./workgroup-edit.component.scss'], }) export class WorkgroupEditComponent implements OnInit, OnDestroy { - public workgroup: Workgroup | undefined; + public workgroup$ = new BehaviorSubject(null); public mode: Mode = 'edit'; public readonly roles: Role[] = Object.values(Role); - public readonly formGroup: FormGroup = new FormGroup({ - name: new FormControl('', { validators: Validators.required, updateOn: 'blur' }), - status: new FormControl(), - users: new FormControl([]), + public readonly formGroup = new FormGroup({ + name: new FormControl('', { validators: Validators.required, updateOn: 'blur', nonNullable: true }), + disabledAt: new FormControl(null), + users: new FormControl>(new Map(), { nonNullable: true }), }); private readonly route = inject(ActivatedRoute); @@ -35,11 +35,26 @@ export class WorkgroupEditComponent implements OnInit, OnDestroy { private readonly subscriptions: Subscription = new Subscription(); private readonly store = inject(Store); private readonly router = inject(Router); - private readonly workgroup$ = this.store.select(selectSelectedWorkgroup); + private readonly selectedWorkgroup$ = this.store.select(selectSelectedWorkgroup); + + public readonly users$ = this.workgroup$.pipe( + map((workgroup) => { + if (workgroup == null) { + return []; + } + const users: Array = []; + for (const [id, user] of workgroup.users) { + users.push({ ...user, id }); + } + return users; + }), + startWith([]), + share() + ); public ngOnInit() { - this.findWorkgroupFromRouteParams(); - this.initSubscriptions(); + this.loadWorkgroupFromRouteParams(); + this.initializeSubscriptions(); } public ngOnDestroy() { @@ -47,40 +62,62 @@ export class WorkgroupEditComponent implements OnInit, OnDestroy { this.subscriptions.unsubscribe(); } + private get workgroup(): Workgroup | null { + return this.workgroup$.value; + } + public initializeForm(workgroup: Workgroup) { this.formGroup.patchValue( { name: workgroup.name, - status: !!workgroup.disabled_at, + disabledAt: workgroup.disabledAt, users: workgroup.users, }, { emitEvent: false } ); } - public updateRoleForUser(event: MatSelectChange, user: UserOnWorkgroup) { - if (!this.workgroup) { + private initializeSubscriptions() { + this.subscriptions.add( + this.selectedWorkgroup$.subscribe((workgroup) => { + if (workgroup) { + this.workgroup$.next(workgroup); + this.initializeForm(workgroup); + } + }) + ); + + this.subscriptions.add( + this.formGroup.valueChanges.subscribe((value) => { + if (this.workgroup == null || this.formGroup.pristine) { + return; + } + this.updateWorkgroup(this.workgroup.id, { + name: value.name ?? '', + disabledAt: value.disabledAt ?? null, + users: this.workgroup.users, + }); + }) + ); + } + + public updateRoleForUser(event: MatSelectChange, userId: UserId, user: UserOnWorkgroup) { + if (this.workgroup == null) { return; } - const updatedUsers: UserOnWorkgroup[] = this.workgroup.users.map((u) => - u.user.id === user.user.id - ? { - ...u, - role: event.value, - } - : u - ); - const updatedWorkgroup = { - name: this.workgroup.name, - assets: this.workgroup.assets, - disabled_at: this.workgroup.disabled_at, - users: updatedUsers, - }; - this.updateWorkgroup(this.workgroup.id, updatedWorkgroup); + const users = new Map(this.workgroup.users); + users.set(userId, { + email: user.email, + role: event.value as Role, + }); + this.updateWorkgroup(this.workgroup.id, { + ...this.workgroup, + users, + }); } public addUsersToWorkgroup() { - this.dialogService.open(AddWorkgroupUserDialog, { + this.dialogService.open(AddWorkgroupUserDialogComponent, { width: '400px', restoreFocus: false, data: { @@ -90,17 +127,16 @@ export class WorkgroupEditComponent implements OnInit, OnDestroy { }); } - public deleteUserFromWorkgroup(user: UserOnWorkgroup) { - if (!this.workgroup) { + public deleteUserFromWorkgroup(userId: UserId) { + if (this.workgroup == null) { return; } - const updatedWorkgroup = { - name: this.workgroup.name, - assets: this.workgroup.assets, - disabled_at: this.workgroup.disabled_at, - users: this.workgroup.users.filter((u) => u.user.id !== user.user.id), - }; - this.updateWorkgroup(this.workgroup.id, updatedWorkgroup); + const users = new Map(this.workgroup.users); + users.delete(userId); + this.updateWorkgroup(this.workgroup.id, { + ...this.workgroup, + users, + }); } public cancel() { @@ -108,11 +144,11 @@ export class WorkgroupEditComponent implements OnInit, OnDestroy { this.store.dispatch(actions.resetWorkgroup()); } - public createWorkGroup() { - if (!this.workgroup || !this.formGroup.valid) { + public createWorkgroup() { + if (this.workgroup == null || !this.formGroup.valid) { return; } - const { id, ...workgroup } = this.workgroup; + const { id: _id, ...workgroup } = this.workgroup; this.store.dispatch(actions.createWorkgroup({ workgroup })); } @@ -120,60 +156,29 @@ export class WorkgroupEditComponent implements OnInit, OnDestroy { if (this.mode === 'edit') { this.store.dispatch(actions.updateWorkgroup({ workgroupId, workgroup })); } else { - this.workgroup = { + this.workgroup$.next({ id: workgroupId, ...workgroup, - }; + }); } } - private findWorkgroupFromRouteParams() { - this.subscriptions.add( - this.route.paramMap - .pipe( - map((params: ParamMap) => { - const id = params.get('id'); - if (id) { - this.mode = 'edit'; - return this.store.dispatch(actions.findWorkgroup({ workgroupId: parseInt(id) })); - } else { - this.mode = 'create'; - this.workgroup = { - id: 0, - name: '', - users: [], - assets: [], - disabled_at: null, - }; - } - }) - ) - .subscribe() - ); - } - - private initSubscriptions() { - this.subscriptions.add( - this.workgroup$.subscribe((workgroup) => { - if (workgroup) { - this.workgroup = workgroup; - this.initializeForm(workgroup); - } - }) - ); - + private loadWorkgroupFromRouteParams() { this.subscriptions.add( - this.formGroup.valueChanges.subscribe((value) => { - if (!this.workgroup) { - return; + this.route.paramMap.subscribe((params: ParamMap) => { + const id = params.get('id'); + if (id) { + this.mode = 'edit'; + return this.store.dispatch(actions.findWorkgroup({ workgroupId: parseInt(id) })); + } else { + this.mode = 'create'; + this.workgroup$.next({ + id: 0, + name: '', + users: new Map(), + disabledAt: null, + }); } - const updatedWorkgroup = { - name: value.name, - disabled_at: value.status ? new Date() : null, - assets: this.workgroup.assets, - users: this.workgroup.users, - }; - this.updateWorkgroup(this.workgroup.id, updatedWorkgroup); }) ); } diff --git a/libs/admin/src/lib/components/workgroups/workgroups.component.html b/libs/admin/src/lib/components/workgroups/workgroups.component.html index 581b2e9d..45761627 100644 --- a/libs/admin/src/lib/components/workgroups/workgroups.component.html +++ b/libs/admin/src/lib/components/workgroups/workgroups.component.html @@ -4,7 +4,7 @@

    admin.workgroups

    - +
    @@ -12,8 +12,8 @@

    admin.workgroups

    diff --git a/libs/admin/src/lib/components/workgroups/workgroups.component.ts b/libs/admin/src/lib/components/workgroups/workgroups.component.ts index 0458885a..2ac59c00 100644 --- a/libs/admin/src/lib/components/workgroups/workgroups.component.ts +++ b/libs/admin/src/lib/components/workgroups/workgroups.component.ts @@ -1,44 +1,26 @@ -import { Component, inject, OnDestroy, OnInit } from '@angular/core'; +import { KeyValue } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { UserId, UserOnWorkgroup, Workgroup } from '@asset-sg/shared/v2'; import { Store } from '@ngrx/store'; -import { Subscription } from 'rxjs'; -import { UserOnWorkgroup, Workgroup } from '../../services/admin.service'; import { AppStateWithAdmin } from '../../state/admin.reducer'; -import { selectUsers, selectWorkgroups } from '../../state/admin.selector'; +import { selectWorkgroups } from '../../state/admin.selector'; @Component({ selector: 'asset-sg-workgroups', templateUrl: './workgroups.component.html', styleUrls: ['./workgroups.component.scss'], }) -export class WorkgroupsComponent implements OnInit, OnDestroy { - public users: UserOnWorkgroup[] = []; - public workgroups: Workgroup[] = []; - +export class WorkgroupsComponent { protected readonly COLUMNS = ['name', 'users', 'status', 'actions']; private readonly store = inject(Store); - private readonly workgroups$ = this.store.select(selectWorkgroups); - private readonly users$ = this.store.select(selectUsers); - private readonly subscriptions: Subscription = new Subscription(); - - public ngOnInit(): void { - this.initSubscriptions(); - } - - public ngOnDestroy(): void { - this.subscriptions.unsubscribe(); - } + readonly workgroups$ = this.store.select(selectWorkgroups); - private initSubscriptions(): void { - this.subscriptions.add( - this.workgroups$.subscribe((workgroups) => { - this.workgroups = workgroups; - }) - ); - this.subscriptions.add( - this.users$.subscribe((users) => { - this.users = users.map((user) => ({ role: user.role, user: { email: user.email, id: user.id } })); - }) - ); + getWorkgroupUsers(workgroup: Workgroup): Array { + const result: Array = []; + for (const [id, user] of workgroup.users) { + result.push({ ...user, id }); + } + return result; } } diff --git a/libs/admin/src/lib/services/admin.service.ts b/libs/admin/src/lib/services/admin.service.ts index 776b6c1e..c5c3838a 100644 --- a/libs/admin/src/lib/services/admin.service.ts +++ b/libs/admin/src/lib/services/admin.service.ts @@ -1,46 +1,18 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { Role } from '@prisma/client'; -import { Observable } from 'rxjs'; - -//TODO: Move types to /shared2 lib after merging -export interface Workgroup { - id: number; - name: string; - assets: { assetId: number }[]; - users: UserOnWorkgroup[]; - disabled_at: Date | null; -} - -export type WorkgroupData = Omit; - -export interface WorkgroupOnUser { - workgroupId: number; - role: Role; - workgroup: { - name: string; - }; -} - -export type UserId = string; - -export interface UserOnWorkgroup { - role: Role; - user: { - email: string; - id: UserId; - }; -} - -export interface User { - id: string; - name: string; - email: string; - lang: string; - role: Role; - isAdmin: boolean; - workgroups: WorkgroupOnUser[]; -} +import { + convert, + User, + UserData, + UserDataSchema, + UserSchema, + Workgroup, + WorkgroupData, + WorkgroupDataSchema, + WorkgroupSchema, +} from '@asset-sg/shared/v2'; +import { plainToInstance } from 'class-transformer'; +import { map, Observable, tap } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -49,35 +21,50 @@ export class AdminService { private _httpClient = inject(HttpClient); public getUsers(): Observable { - return this._httpClient.get('/api/admin/user'); + return this._httpClient.get('/api/users').pipe(map((it) => plainToInstance(UserSchema, it))); } public getUser(id: string): Observable { - return this._httpClient.get(`/api/users/${id}`); + return this._httpClient.get(`/api/users/${id}`).pipe(map((it) => plainToInstance(UserSchema, it))); } public getWorkgroups(): Observable { - return this._httpClient.get('/api/workgroups'); + return this._httpClient.get('/api/workgroups').pipe(map((it) => plainToInstance(WorkgroupSchema, it))); } public getWorkgroup(id: string): Observable { - return this._httpClient.get(`/api/workgroups/${id}`); + return this._httpClient + .get(`/api/workgroups/${id}`) + .pipe(map((it) => plainToInstance(WorkgroupSchema, it))); } public createWorkgroup(workgroup: WorkgroupData): Observable { - return this._httpClient.post(`/api/workgroups`, workgroup); + return this._httpClient.post(`/api/workgroups`, convertWorkgroupData(workgroup)); } - public updateWorkgroups(id: number, workgroup: WorkgroupData): Observable { - return this._httpClient.put(`/api/workgroups/${id}`, workgroup); + public updateWorkgroup(id: number, workgroup: WorkgroupData): Observable { + return this._httpClient + .put(`/api/workgroups/${id}`, convertWorkgroupData(workgroup)) + .pipe(map((it) => plainToInstance(WorkgroupSchema, it))); } public updateUser(user: User): Observable { - return this._httpClient.put(`/api/users/${user.id}`, { - role: user.role, - lang: user.lang, - workgroups: user.workgroups, - isAdmin: user.isAdmin, - }); + return this._httpClient + .put( + `/api/users/${user.id}`, + convert(UserDataSchema, { + lang: user.lang, + roles: user.roles, + isAdmin: user.isAdmin, + } as UserData) + ) + .pipe(map((it) => plainToInstance(UserSchema, it))); } } + +const convertWorkgroupData = (data: WorkgroupData): WorkgroupData => + convert(WorkgroupDataSchema, { + name: data.name, + users: data.users, + disabledAt: data.disabledAt, + } as WorkgroupData); diff --git a/libs/admin/src/lib/state/admin.actions.ts b/libs/admin/src/lib/state/admin.actions.ts index 8f40bbb2..69295154 100644 --- a/libs/admin/src/lib/state/admin.actions.ts +++ b/libs/admin/src/lib/state/admin.actions.ts @@ -1,19 +1,14 @@ +import { User, Workgroup, WorkgroupData } from '@asset-sg/shared/v2'; import { createAction, props } from '@ngrx/store'; -import { User, Workgroup, WorkgroupData } from '../services/admin.service'; export const findUser = createAction( - '[Admin] Find user', + '[Admin] Find User', props<{ userId: string; }>() ); -export const setUser = createAction( - '[Admin] Set user', - props<{ - user: User; - }>() -); +export const setUser = createAction('[Admin] Set User', props<{ user: User }>()); export const updateUser = createAction( '[Admin] Update User', @@ -22,33 +17,40 @@ export const updateUser = createAction( }>() ); -export const listUsers = createAction('[Admin] List users'); +export const listUsers = createAction('[Admin] List Users'); export const setUsers = createAction( - '[Admin] Set users', + '[Admin] Set Users', props<{ users: User[]; }>() ); export const findWorkgroup = createAction( - '[Admin] Find workgroup', + '[Admin] Find Workgroup', props<{ workgroupId: number; }>() ); export const setWorkgroup = createAction( - '[Admin] Set workgroup', + '[Admin] Set Workgroup', + props<{ + workgroup: Workgroup; + }>() +); + +export const addWorkgroup = createAction( + '[Admin] Add Workgroup', props<{ workgroup: Workgroup; }>() ); -export const resetWorkgroup = createAction('[Admin] Reset workgroup'); +export const resetWorkgroup = createAction('[Admin] Reset Workgroup'); export const setWorkgroups = createAction( - '[Admin] Set workgroups', + '[Admin] Set Workgroups', props<{ workgroups: Workgroup[]; }>() @@ -69,6 +71,4 @@ export const createWorkgroup = createAction( }>() ); -export const createWorkgroupSuccess = createAction('[Admin] Create Workgroup Success'); - -export const listWorkgroups = createAction('[Admin] List workgroups'); +export const listWorkgroups = createAction('[Admin] List Workgroups'); diff --git a/libs/admin/src/lib/state/admin.effects.ts b/libs/admin/src/lib/state/admin.effects.ts index 5300835d..10d31fb3 100644 --- a/libs/admin/src/lib/state/admin.effects.ts +++ b/libs/admin/src/lib/state/admin.effects.ts @@ -1,10 +1,12 @@ import { inject, Injectable } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { CURRENT_LANG } from '@asset-sg/client-shared'; +import { User, Workgroup } from '@asset-sg/shared/v2'; import { UntilDestroy } from '@ngneat/until-destroy'; import { Actions, createEffect, ofType } from '@ngrx/effects'; -import { map, switchMap } from 'rxjs'; +import { first, map, switchMap, tap, withLatestFrom } from 'rxjs'; -import { AdminService, User, Workgroup } from '../services/admin.service'; +import { AdminService } from '../services/admin.service'; import * as actions from './admin.actions'; @UntilDestroy() @@ -14,87 +16,71 @@ export class AdminEffects { private readonly adminService = inject(AdminService); private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); + private readonly currentLang$ = inject(CURRENT_LANG); - public findUser$ = createEffect(() => { - return this.actions$.pipe( + public findUser$ = createEffect(() => + this.actions$.pipe( ofType(actions.findUser), - switchMap(({ userId }) => { - return this.adminService.getUser(userId).pipe(map((user) => actions.setUser({ user }))); - }) - ); - }); + switchMap(({ userId }) => this.adminService.getUser(userId).pipe(map((user) => actions.setUser({ user })))) + ) + ); public updateUser$ = createEffect(() => this.actions$.pipe( ofType(actions.updateUser), - switchMap(({ user }) => { - return this.adminService.updateUser(user).pipe( - map((user: User) => - actions.setUser({ - user, - }) - ) - ); - }) + switchMap(({ user }) => this.adminService.updateUser(user).pipe(map((user: User) => actions.setUser({ user })))) ) ); public findWorkgroup$ = createEffect(() => this.actions$.pipe( ofType(actions.findWorkgroup), - switchMap(({ workgroupId }) => { - return this.adminService + switchMap(({ workgroupId }) => + this.adminService .getWorkgroup(workgroupId.toString()) - .pipe(map((workgroup: Workgroup) => actions.setWorkgroup({ workgroup }))); - }) + .pipe(map((workgroup: Workgroup) => actions.setWorkgroup({ workgroup }))) + ) ) ); - public createWorkgroup$ = createEffect( - () => { - return this.actions$.pipe( - ofType(actions.createWorkgroup), - switchMap(({ workgroup }) => this.adminService.createWorkgroup(workgroup)), - switchMap((workgroup) => - this.router.navigate([`/de/admin/workgroups/${workgroup.id}`], { relativeTo: this.route }) - ) - ); - }, - { dispatch: false } + public createWorkgroup$ = createEffect(() => + this.actions$.pipe( + ofType(actions.createWorkgroup), + switchMap(({ workgroup }) => this.adminService.createWorkgroup(workgroup)), + withLatestFrom(this.currentLang$), + map(([workgroup, currentLang]) => { + void this.router.navigate([`/${currentLang}/admin/workgroups/${workgroup.id}`], { relativeTo: this.route }); + return actions.addWorkgroup({ workgroup }); + }) + ) ); public updateWorkgroup$ = createEffect(() => this.actions$.pipe( ofType(actions.updateWorkgroup), - switchMap(({ workgroupId, workgroup }) => { - return this.adminService - .updateWorkgroups(workgroupId, workgroup) - .pipe(map((workgroup: Workgroup) => actions.setWorkgroup({ workgroup }))); - }) + switchMap(({ workgroupId, workgroup }) => + this.adminService + .updateWorkgroup(workgroupId, workgroup) + .pipe(map((workgroup: Workgroup) => actions.setWorkgroup({ workgroup }))) + ) ) ); public listUsers$ = createEffect(() => this.actions$.pipe( ofType(actions.listUsers, actions.setUser, actions.setWorkgroup), - switchMap(() => { - return this.adminService.getUsers().pipe( - map((users: User[]) => { - return actions.setUsers({ users }); - }) - ); - }) + first(), + switchMap(() => this.adminService.getUsers().pipe(map((users: User[]) => actions.setUsers({ users })))) ) ); public listWorkgroups$ = createEffect(() => this.actions$.pipe( ofType(actions.listWorkgroups, actions.setWorkgroup, actions.setUser), - switchMap(() => { - return this.adminService - .getWorkgroups() - .pipe(map((workgroups: Workgroup[]) => actions.setWorkgroups({ workgroups }))); - }) + first(), + switchMap(() => + this.adminService.getWorkgroups().pipe(map((workgroups: Workgroup[]) => actions.setWorkgroups({ workgroups }))) + ) ) ); } diff --git a/libs/admin/src/lib/state/admin.reducer.ts b/libs/admin/src/lib/state/admin.reducer.ts index b14ffd34..a5f45d6f 100644 --- a/libs/admin/src/lib/state/admin.reducer.ts +++ b/libs/admin/src/lib/state/admin.reducer.ts @@ -1,12 +1,12 @@ import { AppState } from '@asset-sg/client-shared'; +import { User, Workgroup } from '@asset-sg/shared/v2'; import { createReducer, on } from '@ngrx/store'; -import { User, Workgroup } from '../services/admin.service'; import * as actions from './admin.actions'; export interface AdminState { - selectedWorkgroup: Workgroup | undefined; + selectedWorkgroup: Workgroup | null; workgroups: Workgroup[]; - selectedUser: User | undefined; + selectedUser: User | null; users: User[]; isLoading: boolean; } @@ -16,9 +16,9 @@ export interface AppStateWithAdmin extends AppState { } const initialState: AdminState = { - selectedWorkgroup: undefined, + selectedWorkgroup: null, workgroups: [], - selectedUser: undefined, + selectedUser: null, users: [], isLoading: false, }; @@ -68,19 +68,33 @@ export const adminReducer = createReducer( isLoading: true, }) ), - on( - actions.setWorkgroup, - (state, { workgroup }): AdminState => ({ + on(actions.setWorkgroup, (state, { workgroup }): AdminState => { + const workgroups = [...state.workgroups]; + const i = workgroups.findIndex((it) => it.id === workgroup.id); + if (i < 0) { + workgroups.push(workgroup); + } else { + workgroups[i] = workgroup; + } + return { ...state, + workgroups, selectedWorkgroup: workgroup, isLoading: false, + }; + }), + on( + actions.addWorkgroup, + (state, { workgroup }): AdminState => ({ + ...state, + workgroups: [...state.workgroups, workgroup], }) ), on( actions.resetWorkgroup, (state): AdminState => ({ ...state, - selectedWorkgroup: undefined, + selectedWorkgroup: null, }) ), on( diff --git a/libs/admin/src/lib/state/admin.selector.ts b/libs/admin/src/lib/state/admin.selector.ts index 52807b1e..94b5131f 100644 --- a/libs/admin/src/lib/state/admin.selector.ts +++ b/libs/admin/src/lib/state/admin.selector.ts @@ -3,7 +3,6 @@ import { AppStateWithAdmin } from './admin.reducer'; const adminFeature = (state: AppStateWithAdmin) => state.admin; -export const selectAdminState = createSelector(adminFeature, (state) => state); export const selectSelectedUser = createSelector(adminFeature, (state) => state.selectedUser); export const selectSelectedWorkgroup = createSelector(adminFeature, (state) => state.selectedWorkgroup); export const selectWorkgroups = createSelector(adminFeature, (state) => state.workgroups); diff --git a/libs/auth/src/lib/services/auth.service.ts b/libs/auth/src/lib/services/auth.service.ts index 5744667e..549b6927 100644 --- a/libs/auth/src/lib/services/auth.service.ts +++ b/libs/auth/src/lib/services/auth.service.ts @@ -1,10 +1,11 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { ApiError, httpErrorResponseOrUnknownError } from '@asset-sg/client-shared'; -import { decode, OE, ORD } from '@asset-sg/core'; -import { User } from '@asset-sg/shared'; +import { ApiError } from '@asset-sg/client-shared'; +import { ORD } from '@asset-sg/core'; +import { User, UserSchema } from '@asset-sg/shared/v2'; import * as RD from '@devexperts/remote-data-ts'; import { OAuthService } from 'angular-oauth2-oidc'; +import { plainToInstance } from 'class-transformer'; import { BehaviorSubject, map, Observable, startWith } from 'rxjs'; import urlJoin from 'url-join'; @@ -70,7 +71,10 @@ export class AuthService { } getUserProfile(): ORD.ObservableRemoteData { - return this._getUserProfile().pipe(map(RD.fromEither), startWith(RD.pending)); + return this._getUserProfile().pipe( + map((it) => RD.success(it)), + startWith(RD.pending) + ); } isLoggedIn(): boolean { @@ -85,12 +89,8 @@ export class AuthService { }); } - private _getUserProfile() { - return this._httpClient.get('/api/users/current').pipe( - map((it) => ({ ...it, role: (it as { isAdmin: boolean }).isAdmin ? 'admin' : 'master-editor' })), - map(decode(User)), - OE.catchErrorW(httpErrorResponseOrUnknownError) - ); + private _getUserProfile(): Observable { + return this._httpClient.get('/api/users/current').pipe(map((it) => plainToInstance(UserSchema, it))); } buildAuthUrl = (path: string) => urlJoin(`/auth`, path); diff --git a/libs/client-shared/src/lib/features/alert/alert-list/alert-list.component.html b/libs/client-shared/src/lib/features/alert/alert-list/alert-list.component.html index da955956..7c8eabd7 100644 --- a/libs/client-shared/src/lib/features/alert/alert-list/alert-list.component.html +++ b/libs/client-shared/src/lib/features/alert/alert-list/alert-list.component.html @@ -1,3 +1,3 @@ - +
  • diff --git a/libs/client-shared/src/lib/features/alert/alert.module.ts b/libs/client-shared/src/lib/features/alert/alert.module.ts index 85a9f216..0171f0b4 100644 --- a/libs/client-shared/src/lib/features/alert/alert.module.ts +++ b/libs/client-shared/src/lib/features/alert/alert.module.ts @@ -3,13 +3,14 @@ import { NgModule } from '@angular/core'; import { SvgIconComponent } from '@ngneat/svg-icon'; import { StoreModule } from '@ngrx/store'; +import { ForModule } from '@rx-angular/template/for'; import { AlertComponent } from './alert/alert.component'; import { AlertListComponent } from './alert-list/alert-list.component'; import { alertFeature, alertReducer } from './alert.reducer'; @NgModule({ declarations: [AlertListComponent, AlertComponent], - imports: [CommonModule, AsyncPipe, StoreModule.forFeature(alertFeature, alertReducer), SvgIconComponent], + imports: [CommonModule, AsyncPipe, StoreModule.forFeature(alertFeature, alertReducer), SvgIconComponent, ForModule], exports: [AlertListComponent], }) export class AlertModule {} diff --git a/libs/client-shared/src/lib/state/app-shared-state.actions.ts b/libs/client-shared/src/lib/state/app-shared-state.actions.ts index 0e3d1fa4..33fd9242 100644 --- a/libs/client-shared/src/lib/state/app-shared-state.actions.ts +++ b/libs/client-shared/src/lib/state/app-shared-state.actions.ts @@ -1,5 +1,5 @@ -import { Contact, Lang, ReferenceData, User } from '@asset-sg/shared'; -import { SimpleWorkgroup } from '@asset-sg/shared/v2'; +import { Contact, Lang, ReferenceData } from '@asset-sg/shared'; +import { SimpleWorkgroup, User } from '@asset-sg/shared/v2'; import * as RD from '@devexperts/remote-data-ts'; import { createAction, props } from '@ngrx/store'; diff --git a/libs/client-shared/src/lib/state/app-shared-state.ts b/libs/client-shared/src/lib/state/app-shared-state.ts index 8dcff17f..fc8afb4c 100644 --- a/libs/client-shared/src/lib/state/app-shared-state.ts +++ b/libs/client-shared/src/lib/state/app-shared-state.ts @@ -1,5 +1,5 @@ -import { Lang, ReferenceData, User } from '@asset-sg/shared'; -import { SimpleWorkgroup } from '@asset-sg/shared/v2'; +import { Lang, ReferenceData } from '@asset-sg/shared'; +import { SimpleWorkgroup, User } from '@asset-sg/shared/v2'; import * as RD from '@devexperts/remote-data-ts'; import { ApiError } from '../utils'; diff --git a/libs/profile/src/lib/components/profile/profile.component.ts b/libs/profile/src/lib/components/profile/profile.component.ts index 196a4f57..a6500187 100644 --- a/libs/profile/src/lib/components/profile/profile.component.ts +++ b/libs/profile/src/lib/components/profile/profile.component.ts @@ -10,8 +10,7 @@ import { LifecycleHooksDirective, appSharedStateActions, } from '@asset-sg/client-shared'; -import { rdIsComplete } from '@asset-sg/core'; -import { User } from '@asset-sg/shared'; +import { User } from '@asset-sg/shared/v2'; import * as RD from '@devexperts/remote-data-ts'; import { Store } from '@ngrx/store'; import { RxState } from '@rx-angular/state'; @@ -56,7 +55,7 @@ export class ProfileComponent extends RxState { this.logoutClicked$.pipe(switchMap(async () => this._authService.logOut())).subscribe((rd) => { this._store.dispatch(appSharedStateActions.logout()); - this._router.navigate(['/']); + void this._router.navigate(['/']); }); } } diff --git a/libs/shared/src/lib/models/index.ts b/libs/shared/src/lib/models/index.ts index bdea0064..994ea5da 100644 --- a/libs/shared/src/lib/models/index.ts +++ b/libs/shared/src/lib/models/index.ts @@ -17,7 +17,6 @@ export * from './study'; export * from './study-dto'; export * from './ReferenceData'; export * from './usage'; -export * from './user'; export * from './asset-search/asset-search-query'; export * from './asset-search/asset-search-query.dto'; diff --git a/libs/shared/src/lib/models/user.ts b/libs/shared/src/lib/models/user.ts deleted file mode 100644 index 198d7db6..00000000 --- a/libs/shared/src/lib/models/user.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Role } from '@prisma/client'; -import * as C from 'io-ts/Codec'; -import * as D from 'io-ts/Decoder'; - -export enum UserRoleEnum { - admin = 'admin', - editor = 'editor', - masterEditor = 'master-editor', - viewer = 'viewer', -} - -const WorkgroupRoleDecoder = D.union(D.literal(Role.Editor), D.literal(Role.MasterEditor), D.literal(Role.Viewer)); -export const WorkgroupRole = C.fromDecoder(WorkgroupRoleDecoder); - -export const User = C.struct({ - id: C.string, - email: C.string, - lang: C.string, - isAdmin: C.boolean, - workgroups: C.array( - C.struct({ - id: C.number, - role: WorkgroupRole, - }) - ), -}); -export type User = D.TypeOf; - -export const Users = C.array(User); -export type Users = User[]; diff --git a/libs/shared/v2/src/index.ts b/libs/shared/v2/src/index.ts index b4fb0c92..c9036f77 100644 --- a/libs/shared/v2/src/index.ts +++ b/libs/shared/v2/src/index.ts @@ -14,8 +14,10 @@ export * from './lib/policies/asset-edit.policy'; export * from './lib/policies/contact.policy'; export * from './lib/policies/workgroup.policy'; +export * from './lib/schemas/base/schema'; export * from './lib/schemas/asset.schema'; export * from './lib/schemas/contact.schema'; export * from './lib/schemas/user.schema'; +export * from './lib/schemas/workgroup.schema'; export * from './lib/utils/class-validator/is-nullable.decorator'; diff --git a/libs/shared/v2/src/lib/models/user.ts b/libs/shared/v2/src/lib/models/user.ts index c84aa196..2213d554 100644 --- a/libs/shared/v2/src/lib/models/user.ts +++ b/libs/shared/v2/src/lib/models/user.ts @@ -4,13 +4,12 @@ import { getRoleIndex, Role, WorkgroupId } from './workgroup'; export interface User extends Model { email: string; lang: string; - workgroups: WorkgroupOnUser[]; isAdmin: boolean; -} -export interface WorkgroupOnUser { - id: WorkgroupId; - role: Role; + /** + * The user's roles, mapped by the id of the workgroup to which they apply. + */ + roles: Map; } export type UserId = string; @@ -24,14 +23,14 @@ const hasRole = (role: Role) => (user: User | null | undefined, workgroupId?: Wo return true; } const roleIndex = getRoleIndex(role); - if (workgroupId == null) { - return null != user.workgroups.find((it) => getRoleIndex(it.role) >= roleIndex); + if (workgroupId != null) { + const role = user.roles.get(workgroupId); + return role != null && getRoleIndex(role) >= roleIndex; } - for (const workgroup of user.workgroups) { - if (workgroup.id != workgroupId) { - continue; + for (const userRole of user.roles.values()) { + if (getRoleIndex(userRole) >= roleIndex) { + return true; } - return getRoleIndex(workgroup.role) >= roleIndex; } return false; }; diff --git a/libs/shared/v2/src/lib/models/workgroup.ts b/libs/shared/v2/src/lib/models/workgroup.ts index a9df7c06..7f74d21d 100644 --- a/libs/shared/v2/src/lib/models/workgroup.ts +++ b/libs/shared/v2/src/lib/models/workgroup.ts @@ -1,56 +1,28 @@ import { Role as PrismaRole } from '@prisma/client'; -import { Type } from 'class-transformer'; -import { IsArray, IsDate, IsString } from 'class-validator'; -import { IsNullable } from '../utils/class-validator/is-nullable.decorator'; -import { AssetId } from './asset'; import { Data, Model } from './base/model'; import { UserId } from './user'; export interface Workgroup extends Model { name: string; + users: Map; + disabledAt: Date | null; +} - // TODO change this to `AssetId[]` - assets?: { assetId: AssetId }[]; - users?: UserOnWorkgroup[]; - - // TODO make this camel case - disabled_at: Date | null; +export interface UserOnWorkgroup { + email: string; + role: Role; } export type WorkgroupId = number; -export type WorkgroupData = Data; +export type WorkgroupData = Omit, 'assets'>; export type SimpleWorkgroup = Pick & { /** - * The role of the current within this workgroup. + * The role of the current user within this workgroup. * Note that admins are registered as {@link Role.MasterEditor} for every workgroup. */ role: Role; }; -export interface UserOnWorkgroup { - role: Role; - user: { - id: UserId; - email: string; - }; -} - -export class WorkgroupDataBoundary implements WorkgroupData { - @IsString() - name!: string; - - @IsArray() - assets?: { assetId: AssetId }[]; - - @IsArray() - users?: UserOnWorkgroup[]; - - @IsDate() - @IsNullable() - @Type(() => Date) - disabled_at!: Date | null; -} - export type Role = PrismaRole; export const Role = PrismaRole; diff --git a/libs/shared/v2/src/lib/policies/base/policy.ts b/libs/shared/v2/src/lib/policies/base/policy.ts index 88dacaa5..09051585 100644 --- a/libs/shared/v2/src/lib/policies/base/policy.ts +++ b/libs/shared/v2/src/lib/policies/base/policy.ts @@ -1,33 +1,24 @@ -import { User, WorkgroupOnUser } from '../../models/user'; +import { User } from '../../models/user'; import { getRoleIndex, Role, WorkgroupId } from '../../models/workgroup'; export abstract class Policy { - private readonly workgroups = new Map(); - - constructor(protected readonly user: User) { - for (const workgroup of this.user.workgroups) { - this.workgroups.set(workgroup.id, workgroup); - } - } + constructor(protected readonly user: User) {} protected hasWorkgroup(ids: WorkgroupId | Iterable): boolean { ids = typeof ids === 'number' ? [ids] : ids; for (const id of ids) { - if (this.workgroups.has(id)) { + if (this.user.roles.has(id)) { return true; } } return false; } - protected withWorkgroup( - ids: WorkgroupId | Iterable, - action: (workgroup: WorkgroupOnUser) => boolean - ): boolean { + protected withWorkgroupRole(ids: WorkgroupId | Iterable, action: (role: Role) => boolean): boolean { ids = typeof ids === 'number' ? [ids] : ids; for (const id of ids) { - const workgroup = this.workgroups.get(id); - if (workgroup != null && action(workgroup)) { + const role = this.user.roles.get(id); + if (role != null && action(role)) { return true; } } @@ -37,9 +28,14 @@ export abstract class Policy { hasRole(role: Role, ids?: WorkgroupId | Iterable): boolean { const roleIndex = getRoleIndex(role); if (ids == null) { - return null != this.user.workgroups.find((group) => getRoleIndex(group.role) >= roleIndex); + for (const role of this.user.roles.values()) { + if (getRoleIndex(role) >= roleIndex) { + return true; + } + } + return false; } - return this.withWorkgroup(ids, (group) => getRoleIndex(group.role) >= roleIndex); + return this.withWorkgroupRole(ids, (role) => getRoleIndex(role) >= roleIndex); } canDoEverything(): boolean { diff --git a/libs/shared/v2/src/lib/schemas/asset.schema.ts b/libs/shared/v2/src/lib/schemas/asset.schema.ts index ccb69016..68b055fb 100644 --- a/libs/shared/v2/src/lib/schemas/asset.schema.ts +++ b/libs/shared/v2/src/lib/schemas/asset.schema.ts @@ -26,8 +26,9 @@ import { import { LocalDate } from '../models/base/local-date'; import { StudyType } from '../models/study'; import { IsNullable, messageNullableInt, messageNullableString } from '../utils/class-validator/is-nullable.decorator'; +import { Schema } from './base/schema'; -export class AssetUsageSchema implements AssetUsage { +export class AssetUsageSchema extends Schema implements AssetUsage { @IsBoolean() isAvailable!: boolean; @@ -41,7 +42,7 @@ export class AssetUsageSchema implements AssetUsage { availableAt!: LocalDate | null; } -export class AssetUsagesSchema implements AssetUsages { +export class AssetUsagesSchema extends Schema implements AssetUsages { @IsObject() @ValidateNested() @Type(() => AssetUsageSchema) @@ -53,7 +54,7 @@ export class AssetUsagesSchema implements AssetUsages { internal!: AssetUsageSchema; } -export class InfoGeolSchema implements InfoGeol { +export class InfoGeolSchema extends Schema implements InfoGeol { @IsString({ message: messageNullableString }) @IsNullable() main!: string | null; @@ -67,7 +68,7 @@ export class InfoGeolSchema implements InfoGeol { auxiliary!: string | null; } -export class ContactAssignmentSchema implements ContactAssignment { +export class ContactAssignmentSchema extends Schema implements ContactAssignment { @IsInt() contactId!: number; @@ -75,7 +76,7 @@ export class ContactAssignmentSchema implements ContactAssignment { role!: ContactAssignmentRole; } -export class StudyDataSchema implements StudyData { +export class StudyDataSchema extends Schema implements StudyData { @IsInt({ message: messageNullableInt }) @IsNullable() id?: number | undefined; @@ -87,7 +88,7 @@ export class StudyDataSchema implements StudyData { type!: StudyType; } -export class WorkStatusSchema implements WorkStatusData { +export class WorkStatusSchema extends Schema implements WorkStatusData { @IsInt({ message: messageNullableInt }) @IsNullable() id?: number | undefined; @@ -100,7 +101,7 @@ export class WorkStatusSchema implements WorkStatusData { itemCode!: string; } -export class AssetIdentifierSchema implements AssetIdentifierData { +export class AssetIdentifierSchema extends Schema implements AssetIdentifierData { @IsInt({ message: messageNullableInt }) @IsNullable() id?: number | undefined; @@ -112,7 +113,7 @@ export class AssetIdentifierSchema implements AssetIdentifierData { description!: string; } -export class AssetLinksDataSchema implements AssetLinksData { +export class AssetLinksDataSchema extends Schema implements AssetLinksData { @IsInt({ message: messageNullableInt }) @IsNullable() parent!: number | null; @@ -121,7 +122,7 @@ export class AssetLinksDataSchema implements AssetLinksData { siblings!: number[]; } -export class AssetDataSchema implements AssetData { +export class AssetDataSchema extends Schema implements AssetData { @IsObject() @ValidateNested() @Type(() => AssetLinksDataSchema) diff --git a/libs/shared/v2/src/lib/schemas/base/schema.ts b/libs/shared/v2/src/lib/schemas/base/schema.ts new file mode 100644 index 00000000..ccf5c4ed --- /dev/null +++ b/libs/shared/v2/src/lib/schemas/base/schema.ts @@ -0,0 +1,112 @@ +import { instanceToPlain, Transform } from 'class-transformer'; +import { validate } from 'class-validator'; +import { Class } from 'type-fest'; + +/** + * A _schema_ is a class that defines how a specific interface is converted from and to JSON. + * This is their base type. + * + * Schemas support validation using the `class-validator` library and transform values using the `class-transform` library. + * + * # Converting plain values + * A _plain value_ is an object as it is represented in JSON. + * In other words, it's what you get when converting a schema instance to JSON and then parsing it using {@link JSON.parse}. + * To convert a plain value into an instance of schema, use {@link plainToInstance}: + * ```ts + * const plainValue = JSON.parse(...); + * const instanceValue = plainToInstance(MySchema, plainValue); + * ``` + * Whenever you're using a schema, this conversion has to be done right after parsing a value from JSON. + * + * > In NestJS, the conversion and validation of values is handled automatically, + * > as long as you define a `ValidationPipe` and use the schemas in the parameters of your API routes. + * + * # Converting instance values + * Conversion of schema instances is handled automatically by {@link JSON.stringify}. + * If you ever need to the plain value as a JSON object, use {@link instanceToPlain}: + * ```ts + * const instanceValue = ...; + * const plainValue = instanceToPlain(instanceValue); + * ``` + * + * # Converting non-instance values to instance values + * Often, you will have values that conform to a schema's interface, but are not instances of the schema itself. + * For these values, conversion to plain values will not happen automatically. + * To convert them to schema instances, use {@link convert}: + * ```ts + * const objectValue = ... + * const instanceValue = convert(MySchema, objectValue); + * ``` + * > A common reason for having to deal with non-instance values is due to loading values from a database + * > or other external service. + * + * # Validating instance values + * After converting a plain value to an instance value, it is often a good idea to validate it, + * ensuring that it fits all criteria of the schema. + * This can be done using the {@link validate} function: + * ```ts + * const instanceValue = ...; + * const errors = await validate(instanceValue); + * ``` + * If you simply want to throw an exception when validation fails, use {@link validateOrReject}: + * ```ts + * await validateOrReject(instanceValue); + * ``` + * > Note that it's perfectly okay to not validate a newly converted instance + * > in case you trust its source to always provide valid data. + * > For example, validating API responses will most likely not make much sense. + */ +export class Schema { + /** + * Converts the schema instance to a plain value that can be converted to JSON using {@link JSON.stringify}. + */ + toJSON(): object { + return instanceToPlain(this); + } +} + +/** + * Converts an object into a {@link Schema} instance. + * + * @param schema The schema class. + * @param value The value to convert. + */ +export function convert(schema: Class, value: T): S; + +/** + * Converts an array of object into an array of {@link Schema} instances. + * + * @param schema The schema class. + * @param value The values to convert. + */ +export function convert(schema: Class, value: T[]): S[]; + +export function convert(schema: Class, value: T | T[]): S | S[] { + return Array.isArray(value) ? value.map((it) => convertSingle(schema, it)) : convertSingle(schema, value); +} + +const convertSingle = (schema: Class, value: T): S => { + const instance: S = Object.create(schema.prototype); + for (const [key, keyValue] of Object.entries(value) as Array<[keyof T, S[keyof T]]>) { + instance[key] = keyValue; + } + return instance; +}; + +export const TransformMap = (): PropertyDecorator => { + const transformToClass = Transform( + ({ value }) => { + const map = new Map(); + for (const [key, keyValue] of value) { + map.set(key, keyValue); + } + return map; + }, + { toClassOnly: true } + ); + const transformToPlain = Transform(({ value }) => [...value.entries()], { toPlainOnly: true }); + return (target, propertyKey) => { + transformToClass(target, propertyKey); + transformToPlain(target, propertyKey); + }; +}; diff --git a/libs/shared/v2/src/lib/schemas/contact.schema.ts b/libs/shared/v2/src/lib/schemas/contact.schema.ts index 46a2a0d1..353ab06e 100644 --- a/libs/shared/v2/src/lib/schemas/contact.schema.ts +++ b/libs/shared/v2/src/lib/schemas/contact.schema.ts @@ -1,7 +1,8 @@ import { IsOptional, IsString } from 'class-validator'; import { ContactData } from '../models/contact'; +import { Schema } from './base/schema'; -export class ContactDataSchema implements ContactData { +export class ContactDataSchema extends Schema implements ContactData { @IsString() name!: string; diff --git a/libs/shared/v2/src/lib/schemas/user.schema.ts b/libs/shared/v2/src/lib/schemas/user.schema.ts index eefeed7a..0838aed5 100644 --- a/libs/shared/v2/src/lib/schemas/user.schema.ts +++ b/libs/shared/v2/src/lib/schemas/user.schema.ts @@ -1,13 +1,25 @@ -import { IsArray, IsBoolean, IsString } from 'class-validator'; -import { UserData, WorkgroupOnUser } from '../models/user'; +import { Role } from '@prisma/client'; +import { IsBoolean, IsEnum, IsString } from 'class-validator'; +import { User, UserData, UserId } from '../models/user'; +import { WorkgroupId } from '../models/workgroup'; +import { Schema, TransformMap } from './base/schema'; -export class UserDataSchema implements UserData { +export class UserDataSchema extends Schema implements UserData { @IsString() lang!: string; - @IsArray() - workgroups!: WorkgroupOnUser[]; - @IsBoolean() isAdmin!: boolean; + + @TransformMap() + @IsEnum(Role, { each: true }) + roles!: Map; +} + +export class UserSchema extends UserDataSchema implements User { + @IsString() + id!: UserId; + + @IsString() + email!: string; } diff --git a/libs/shared/v2/src/lib/schemas/workgroup.schema.ts b/libs/shared/v2/src/lib/schemas/workgroup.schema.ts new file mode 100644 index 00000000..5724318f --- /dev/null +++ b/libs/shared/v2/src/lib/schemas/workgroup.schema.ts @@ -0,0 +1,35 @@ +import { Role } from '@prisma/client'; +import { Type } from 'class-transformer'; +import { IsDate, IsEnum, IsNumber, IsString, ValidateNested } from 'class-validator'; +import { UserId } from '../models/user'; +import { UserOnWorkgroup, Workgroup, WorkgroupData, WorkgroupId } from '../models/workgroup'; +import { IsNullable } from '../utils/class-validator/is-nullable.decorator'; +import { Schema, TransformMap } from './base/schema'; + +export class WorkgroupDataSchema extends Schema implements WorkgroupData { + @IsString() + name!: string; + + @TransformMap() + @ValidateNested({ each: true }) + @Type(() => UserOnWorkgroupSchema) + users!: Map; + + @IsDate() + @IsNullable() + @Type(() => Date) + disabledAt!: Date | null; +} + +export class WorkgroupSchema extends WorkgroupDataSchema implements Workgroup { + @IsNumber() + id!: WorkgroupId; +} + +export class UserOnWorkgroupSchema implements UserOnWorkgroup { + @IsString() + email!: string; + + @IsEnum(Role) + role!: Role; +} From f33438b11c78111465482935b08cc6601c39d662 Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Wed, 24 Jul 2024 17:24:30 +0200 Subject: [PATCH 19/21] Add workgroup filter to asset search Move workgroup filter to top Add empty workgroups to asset search filters Fix collision on user creation Fix user and workgroup forms overwriting existing values on save Fix eIAM forbidden errors not displaying unauthorized login page --- apps/client-asset-sg/src/app/i18n/de.ts | 1 + apps/client-asset-sg/src/app/i18n/en.ts | 1 + apps/client-asset-sg/src/app/i18n/fr.ts | 1 + apps/client-asset-sg/src/app/i18n/it.ts | 1 + apps/client-asset-sg/src/app/i18n/rm.ts | 1 + .../src/core/middleware/jwt.middleware.ts | 28 +++++++--- .../assets/search/asset-search.controller.ts | 24 ++++++--- .../search/asset-search.service.spec.ts | 54 ++++++++----------- .../assets/search/asset-search.service.ts | 46 ++++++++-------- .../user-edit/user-edit.component.ts | 6 +-- .../workgroup-edit.component.ts | 6 +-- .../asset-search-filter-list.component.ts | 2 +- .../asset-search-refine.component.html | 4 ++ .../asset-search-refine.component.ts | 2 + .../asset-search/asset-search.reducer.ts | 3 +- .../asset-search/asset-search.selector.ts | 49 +++++++++++++++-- .../auth/src/lib/services/auth.interceptor.ts | 5 +- .../lib/components/smart-translate.pipe.ts | 7 ++- .../asset-search/asset-search-query.dto.ts | 4 ++ .../models/asset-search/asset-search-query.ts | 1 + .../asset-search/asset-search-result.dto.ts | 3 +- .../asset-search/asset-search-result.ts | 3 +- 22 files changed, 167 insertions(+), 85 deletions(-) diff --git a/apps/client-asset-sg/src/app/i18n/de.ts b/apps/client-asset-sg/src/app/i18n/de.ts index 54f3947f..1c4a9c1a 100644 --- a/apps/client-asset-sg/src/app/i18n/de.ts +++ b/apps/client-asset-sg/src/app/i18n/de.ts @@ -81,6 +81,7 @@ export const deAppTranslations = { languageItem: { None: 'keine', }, + workgroup: 'Arbeitsgruppe', resetSearch: 'Suche zurücksetzen', file: 'Datei', openFileInNewTab: '{{fileName}} in neuem Tab öffnen', diff --git a/apps/client-asset-sg/src/app/i18n/en.ts b/apps/client-asset-sg/src/app/i18n/en.ts index 664f8894..4846c2c2 100644 --- a/apps/client-asset-sg/src/app/i18n/en.ts +++ b/apps/client-asset-sg/src/app/i18n/en.ts @@ -82,6 +82,7 @@ export const enAppTranslations: AppTranslations = { languageItem: { None: 'none', }, + workgroup: 'Workgroup', resetSearch: 'Reset search', file: 'File', openFileInNewTab: 'Open {{fileName}} in new tab', diff --git a/apps/client-asset-sg/src/app/i18n/fr.ts b/apps/client-asset-sg/src/app/i18n/fr.ts index 15b42b5f..a7b2aeab 100644 --- a/apps/client-asset-sg/src/app/i18n/fr.ts +++ b/apps/client-asset-sg/src/app/i18n/fr.ts @@ -82,6 +82,7 @@ export const frAppTranslations: AppTranslations = { languageItem: { None: 'aucune', }, + workgroup: 'groupe de travail', resetSearch: 'Réinitialiser la recherche', file: 'Fichier', openFileInNewTab: 'Ouvrir {{fileName}} dans un nouvel onglet', diff --git a/apps/client-asset-sg/src/app/i18n/it.ts b/apps/client-asset-sg/src/app/i18n/it.ts index 20918b94..c0dc7e83 100644 --- a/apps/client-asset-sg/src/app/i18n/it.ts +++ b/apps/client-asset-sg/src/app/i18n/it.ts @@ -82,6 +82,7 @@ export const itAppTranslations: AppTranslations = { languageItem: { None: 'IT keine', }, + workgroup: 'IT Arbeitsgruppe', resetSearch: 'IT Suche zurücksetzen', file: 'IT Datei', openFileInNewTab: 'IT {{fileName}} in neuem Tab öffnen', diff --git a/apps/client-asset-sg/src/app/i18n/rm.ts b/apps/client-asset-sg/src/app/i18n/rm.ts index 1a0ac5ff..f4f0457b 100644 --- a/apps/client-asset-sg/src/app/i18n/rm.ts +++ b/apps/client-asset-sg/src/app/i18n/rm.ts @@ -82,6 +82,7 @@ export const rmAppTranslations: AppTranslations = { languageItem: { None: 'RM keine', }, + workgroup: 'RM Arbeitsgruppe', resetSearch: 'RM Suche zurücksetzen', file: 'RM Datei', openFileInNewTab: 'RM {{fileName}} in neuem Tab öffnen', diff --git a/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts b/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts index 30d8d4d6..68db865a 100644 --- a/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts +++ b/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts @@ -14,6 +14,7 @@ import jwkToPem from 'jwk-to-pem'; import { UserRepo } from '@/features/users/user.repo'; import { JwtRequest } from '@/models/jwt-request'; +import { Prisma } from '@prisma/client'; @Injectable() export class JwtMiddleware implements NestMiddleware { @@ -193,13 +194,26 @@ export class JwtMiddleware implements NestMiddleware { if (email == null || !/^.+@.+\..+$/.test(email)) { throw new HttpException('invalid JWT payload: username does not contain an email', 401); } - return await this.userRepo.create({ - oidcId, - email, - lang: 'de', - isAdmin: false, - roles: new Map(), - }); + try { + return await this.userRepo.create({ + oidcId, + email, + lang: 'de', + isAdmin: false, + roles: new Map(), + }); + } catch (e) { + // If two requests of the same user overlap, it is possible that the user creation collides. + // If this happens, we load the user that has already been created by someone else from the DB. + if (!(e instanceof Prisma.PrismaClientKnownRequestError) || e.code !== 'P2002') { + throw e; + } + const user = await this.userRepo.find(oidcId); + if (user == null) { + throw e; + } + return user; + } } } diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts index 7234aff9..48e18abc 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.controller.ts @@ -1,4 +1,5 @@ import { + AssetSearchQuery, AssetSearchQueryDTO, AssetSearchResult, AssetSearchResultDTO, @@ -10,6 +11,7 @@ import { Body, Controller, HttpCode, HttpStatus, Post, Query, ValidationPipe } f import { plainToInstance } from 'class-transformer'; import { Authorize } from '@/core/decorators/authorize.decorator'; import { CurrentUser } from '@/core/decorators/current-user.decorator'; +import { ParseBody } from '@/core/decorators/parse.decorator'; import { AssetSearchService } from '@/features/assets/search/asset-search.service'; @Controller('/assets/search') @@ -20,8 +22,8 @@ export class AssetSearchController { @Authorize.User() @HttpCode(HttpStatus.OK) async search( - @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) - query: AssetSearchQueryDTO, + @ParseBody(AssetSearchQueryDTO) + query: AssetSearchQuery, @CurrentUser() user: User, @Query('limit') @@ -32,7 +34,8 @@ export class AssetSearchController { ): Promise { limit = limit == null ? limit : Number(limit); offset = offset == null ? offset : Number(offset); - const result = await this.assetSearchService.search(query, user, { limit, offset, decode: false }); + restrictQueryForUser(query, user); + const result = await this.assetSearchService.search(query, { limit, offset, decode: false }); return plainToInstance(AssetSearchResultDTO, result); } @@ -40,11 +43,20 @@ export class AssetSearchController { @Authorize.User() @HttpCode(HttpStatus.OK) async showStats( - @Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true })) - query: AssetSearchQueryDTO, + @ParseBody(AssetSearchQueryDTO) + query: AssetSearchQuery, @CurrentUser() user: User ): Promise { - const stats = await this.assetSearchService.aggregate(query, user); + restrictQueryForUser(query, user); + const stats = await this.assetSearchService.aggregate(query); return plainToInstance(AssetSearchStatsDTO, stats); } } + +const restrictQueryForUser = (query: AssetSearchQuery, user: User) => { + if (user.isAdmin) { + return; + } + query.workgroupIds = + query.workgroupIds == null ? [...user.roles.keys()] : query.workgroupIds.filter((it) => user.roles.has(it)); +}; diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts index e5a1e29b..f6486124 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.service.spec.ts @@ -124,7 +124,7 @@ describe(AssetSearchService, () => { await create({ patch: fakeAssetPatch(), user }); // When - const result = await service.search({ text: `${text}` }, user); + const result = await service.search({ text: `${text}` }); // Then assertSingleResult(result, asset); @@ -173,14 +173,11 @@ describe(AssetSearchService, () => { }); // When - const result = await service.search( - { - createDate: { - min: new Date(dateFromDateId(asset.createDate).getTime() - millisPerDay), - }, + const result = await service.search({ + createDate: { + min: new Date(dateFromDateId(asset.createDate).getTime() - millisPerDay), }, - user - ); + }); // Then assertSingleResult(result, asset); @@ -199,14 +196,11 @@ describe(AssetSearchService, () => { }); // When - const result = await service.search( - { - createDate: { - max: new Date(dateFromDateId(asset.createDate).getTime() + millisPerDay), - }, + const result = await service.search({ + createDate: { + max: new Date(dateFromDateId(asset.createDate).getTime() + millisPerDay), }, - user - ); + }); // Then assertSingleResult(result, asset); @@ -214,7 +208,6 @@ describe(AssetSearchService, () => { it('finds assets by createDate range', async () => { // Given - const user = fakeUser(); const asset = await create({ patch: fakeAssetPatch(), user: fakeUser() }); await create({ patch: { @@ -232,15 +225,12 @@ describe(AssetSearchService, () => { }); // When - const result = await service.search( - { - createDate: { - min: new Date(dateFromDateId(asset.createDate).getTime() - millisPerDay), - max: new Date(dateFromDateId(asset.createDate).getTime() + millisPerDay), - }, + const result = await service.search({ + createDate: { + min: new Date(dateFromDateId(asset.createDate).getTime() - millisPerDay), + max: new Date(dateFromDateId(asset.createDate).getTime() + millisPerDay), }, - user - ); + }); // Then assertSingleResult(result, asset); @@ -251,7 +241,6 @@ describe(AssetSearchService, () => { const code1 = languageItems[0].languageItemCode; const code2 = languageItems[1].languageItemCode; const code3 = languageItems[2].languageItemCode; - const user = fakeUser(); const asset = await create({ patch: { ...fakeAssetPatch(), assetLanguages: [{ languageItemCode: code1 }] }, user: fakeUser(), @@ -266,7 +255,7 @@ describe(AssetSearchService, () => { }); // When - const result = await service.search({ languageItemCodes: [code1] }, user); + const result = await service.search({ languageItemCodes: [code1] }); // Then assertSingleResult(result, asset); @@ -283,7 +272,7 @@ describe(AssetSearchService, () => { await create({ patch: { ...fakeAssetPatch(), assetKindItemCode: code3 }, user }); // When - const result = await service.search({ assetKindItemCodes: [code1] }, user); + const result = await service.search({ assetKindItemCodes: [code1] }); // Then assertSingleResult(result, asset); @@ -300,7 +289,7 @@ describe(AssetSearchService, () => { await create({ patch: { ...fakeAssetPatch(), manCatLabelRefs: [code3] }, user }); // When - const result = await service.search({ manCatLabelItemCodes: [code1] }, user); + const result = await service.search({ manCatLabelItemCodes: [code1] }); // Then assertSingleResult(result, asset); @@ -336,7 +325,7 @@ describe(AssetSearchService, () => { }); // When - const result = await service.search({ usageCodes: [usageCode] }, user); + const result = await service.search({ usageCodes: [usageCode] }); // Then assertSingleResult(result, asset); @@ -361,7 +350,7 @@ describe(AssetSearchService, () => { }); // When - const result = await service.search({ authorId: contact1.contactId }, user); + const result = await service.search({ authorId: contact1.contactId }); // Then assertSingleResult(result, asset); @@ -408,8 +397,7 @@ describe(AssetSearchService, () => { it('returns empty stats when no assets are present', async () => { // When - const user = fakeUser(); - const result = await service.aggregate({}, user); + const result = await service.aggregate({}); // Then expect(result.total).toEqual(0); @@ -427,7 +415,7 @@ describe(AssetSearchService, () => { const asset = await create({ patch: fakeAssetPatch(), user }); // When - const result = await service.aggregate({}, user); + const result = await service.aggregate({}); // Then assertSingleStats(result, asset); diff --git a/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts b/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts index d589a2fa..bcce17a8 100644 --- a/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts +++ b/apps/server-asset-sg/src/features/assets/search/asset-search.service.ts @@ -19,9 +19,7 @@ import { UsageCode, ValueCount, } from '@asset-sg/shared'; -import { AssetId } from '@asset-sg/shared/v2'; -import { StudyId } from '@asset-sg/shared/v2'; -import { User } from '@asset-sg/shared/v2'; +import { AssetId, StudyId } from '@asset-sg/shared/v2'; import { Client as ElasticsearchClient } from '@elastic/elasticsearch'; import { BulkOperationContainer, @@ -162,7 +160,6 @@ export class AssetSearchService { * Searches for assets using a {@link AssetSearchQuery}. * * @param query The query to match with. - * @param user The user whose assets are to be searched. * @param limit The maximum amount of assets to load. Defaults to `100`. * @param offset The amount of assets being skipped before loading the assets. * @param decode Whether to decode the assets. If this is set to `false`, the assets should be decoded via `AssetEditDetail` before accessing them. @@ -170,11 +167,10 @@ export class AssetSearchService { */ async search( query: AssetSearchQuery, - user: User, { limit = 100, offset = 0, decode = true }: PageOptions & { decode?: boolean } = {} ): Promise { // Apply the query to find all matching ids. - const [serializedAssets, total] = await this.searchAssetsByQuery(query, user, { limit, offset }); + const [serializedAssets, total] = await this.searchAssetsByQuery(query, { limit, offset }); // Load the matched assets from the database. const data: AssetEditDetail[] = []; @@ -198,9 +194,8 @@ export class AssetSearchService { * Aggregates the stats over all assets matching a specific {@link AssetSearchQuery}. * * @param query The query to match with. - * @param user The user whose assets are to be aggregated. */ - async aggregate(query: AssetSearchQuery, user: User): Promise { + async aggregate(query: AssetSearchQuery): Promise { interface Result { minCreateDate: { value: DateId }; maxCreateDate: { value: DateId }; @@ -216,11 +211,14 @@ export class AssetSearchService { geometryCodes: { buckets: AggregationBucket[]; }; + manCatLabelItemCodes: { + buckets: AggregationBucket[]; + }; usageCodes: { buckets: AggregationBucket[]; }; - manCatLabelItemCodes: { - buckets: AggregationBucket[]; + workgroupIds: { + buckets: AggregationBucket[]; }; } @@ -230,7 +228,7 @@ export class AssetSearchService { } const aggregateGroup = async (query: AssetSearchQuery, operator: string, groupName: string, fieldName?: string) => { - const elasticDslQuery = mapQueryToElasticDsl({ ...query, [groupName]: undefined }, user); + const elasticDslQuery = mapQueryToElasticDsl({ ...query, [groupName]: undefined }); return ( await this.elastic.search({ index: INDEX, @@ -244,7 +242,7 @@ export class AssetSearchService { ).aggregations?.agg; }; - const elasticQuery = mapQueryToElasticDsl(query, user); + const elasticQuery = mapQueryToElasticDsl(query); const response = await this.elastic.search({ index: INDEX, size: 0, @@ -262,6 +260,7 @@ export class AssetSearchService { geometryCodes: [], manCatLabelItemCodes: [], usageCodes: [], + workgroupIds: [], }; } @@ -272,6 +271,7 @@ export class AssetSearchService { geometryCodes, manCatLabelItemCodes, usageCodes, + workgroupIds, minCreateDate, maxCreateDate, ] = await Promise.all([ @@ -281,6 +281,7 @@ export class AssetSearchService { aggregateGroup(query, 'terms', 'geometryCodes'), aggregateGroup(query, 'terms', 'manCatLabelItemCodes'), aggregateGroup(query, 'terms', 'usageCodes', 'usageCode'), + aggregateGroup(query, 'terms', 'workgroupIds', 'workgroupId'), aggregateGroup(query, 'min', 'minCreateDate', 'createDate'), aggregateGroup(query, 'max', 'maxCreateDate', 'createDate'), ]); @@ -291,6 +292,7 @@ export class AssetSearchService { geometryCodes, manCatLabelItemCodes, usageCodes, + workgroupIds, minCreateDate, maxCreateDate, } as unknown as Result; @@ -307,6 +309,7 @@ export class AssetSearchService { geometryCodes: aggs.geometryCodes.buckets.map(mapBucket), manCatLabelItemCodes: aggs.manCatLabelItemCodes.buckets.map(mapBucket), usageCodes: aggs.usageCodes.buckets.map(mapBucket), + workgroupIds: aggs.workgroupIds.buckets.map(mapBucket), createDate: { min: dateFromDateId(aggs.minCreateDate.value), max: dateFromDateId(aggs.maxCreateDate.value), @@ -351,12 +354,11 @@ export class AssetSearchService { private async searchAssetsByQuery( query: AssetSearchQuery, - user: User, page: PageOptions = {} ): Promise<[Map, number]> { const BATCH_SIZE = 10_000; - const elasticQuery = mapQueryToElasticDsl(query, user); + const elasticQuery = mapQueryToElasticDsl(query); const matchedAssets = new Map(); let lastAssetId: number | null = null; let totalCount: number | null = null; @@ -679,17 +681,10 @@ const mapLv95ToElastic = (lv95: LV95): ElasticPoint => { return { lat: wgs[1], lon: wgs[0] }; }; -const mapQueryToElasticDsl = (query: AssetSearchQuery, user: User): QueryDslQueryContainer => { +const mapQueryToElasticDsl = (query: AssetSearchQuery): QueryDslQueryContainer => { const scope = ['titlePublic', 'titleOriginal', 'contactNames', 'sgsId']; const queries: QueryDslQueryContainer[] = []; const filters: QueryDslQueryContainer[] = []; - if (!user.isAdmin) { - filters.push({ - terms: { - workgroupId: [...user.roles.keys()], - }, - }); - } if (query.text != null && query.text.length > 0) { queries.push({ bool: { @@ -740,6 +735,13 @@ const mapQueryToElasticDsl = (query: AssetSearchQuery, user: User): QueryDslQuer if (query.geometryCodes != null) { filters.push(makeArrayFilter('geometryCodes', query.geometryCodes)); } + if (query.workgroupIds != null) { + filters.push({ + terms: { + workgroupId: query.workgroupIds, + }, + }); + } if (query.polygon != null) { queries.push({ geo_polygon: { diff --git a/libs/admin/src/lib/components/user-edit/user-edit.component.ts b/libs/admin/src/lib/components/user-edit/user-edit.component.ts index d9d38683..b22ae705 100644 --- a/libs/admin/src/lib/components/user-edit/user-edit.component.ts +++ b/libs/admin/src/lib/components/user-edit/user-edit.component.ts @@ -171,14 +171,14 @@ export class UserEditComponent implements OnInit, OnDestroy { ); this.subscriptions.add( - this.formGroup.valueChanges.subscribe((value) => { + this.formGroup.valueChanges.subscribe(() => { if (this.user == null || this.formGroup.pristine) { return; } this.updateUser({ ...this.user, - isAdmin: value.isAdmin ?? false, - lang: value.lang ?? 'de', + isAdmin: this.formGroup.controls.isAdmin.value ?? false, + lang: this.formGroup.controls.lang.value ?? 'de', }); }) ); diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts index 46dc876f..288344dc 100644 --- a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.ts @@ -88,13 +88,13 @@ export class WorkgroupEditComponent implements OnInit, OnDestroy { ); this.subscriptions.add( - this.formGroup.valueChanges.subscribe((value) => { + this.formGroup.valueChanges.subscribe(() => { if (this.workgroup == null || this.formGroup.pristine) { return; } this.updateWorkgroup(this.workgroup.id, { - name: value.name ?? '', - disabledAt: value.disabledAt ?? null, + name: this.formGroup.controls.name.value ?? '', + disabledAt: this.formGroup.controls.disabledAt.value ?? null, users: this.workgroup.users, }); }) diff --git a/libs/asset-viewer/src/lib/components/asset-search-filter-list/asset-search-filter-list.component.ts b/libs/asset-viewer/src/lib/components/asset-search-filter-list/asset-search-filter-list.component.ts index 5cae871f..e2bd230f 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-filter-list/asset-search-filter-list.component.ts +++ b/libs/asset-viewer/src/lib/components/asset-search-filter-list/asset-search-filter-list.component.ts @@ -9,7 +9,7 @@ import { Filter } from '../../state/asset-search/asset-search.selector'; templateUrl: './asset-search-filter-list.component.html', styleUrl: './asset-search-filter-list.component.scss', }) -export class AssetSearchFilterListComponent { +export class AssetSearchFilterListComponent { @Input({ required: true }) filters!: Array>; diff --git a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html index 370ab92c..a389c04b 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html +++ b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.html @@ -9,6 +9,10 @@

    search.searchControl

    search.refineSearch

    +
    +

    search.workgroup

    + +

    search.usage

    diff --git a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts index a790f674..872938f6 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts +++ b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts @@ -18,6 +18,7 @@ import { selectLanguageFilters, selectManCatLabelFilters, selectUsageCodeFilters, + selectWorkgroupFilters, } from '../../state/asset-search/asset-search.selector'; const MIN_CREATE_DATE = new Date(1800, 0, 1); @@ -51,6 +52,7 @@ export class AssetSearchRefineComponent implements OnInit, OnDestroy, AfterViewI readonly manCatLabelFilters$ = this.store.select(selectManCatLabelFilters); readonly languageFilters$ = this.store.select(selectLanguageFilters); readonly assetKindFilters$ = this.store.select(selectAssetKindFilters); + readonly workgroupFilters$ = this.store.select(selectWorkgroupFilters); private readonly subscriptions: Subscription = new Subscription(); diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts index 147516a2..9976bdd8 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts @@ -53,8 +53,9 @@ const initialState: AssetSearchState = { assetKindItemCodes: [], languageItemCodes: [], geometryCodes: [], - usageCodes: [], manCatLabelItemCodes: [], + usageCodes: [], + workgroupIds: [], createDate: null, }, isMapInitialised: false, diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts index 2e3ba21b..798d4539 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts @@ -21,6 +21,7 @@ import { ValueCount, ValueItem, } from '@asset-sg/shared'; +import { SimpleWorkgroup, WorkgroupId } from '@asset-sg/shared/v2'; import * as RD from '@devexperts/remote-data-ts'; import { createSelector } from '@ngrx/store'; import * as A from 'fp-ts/Array'; @@ -138,7 +139,7 @@ export const selectAvailableAuthors = createSelector( export const selectCreateDate = createSelector(selectAssetsSearchStats, (stats): DateRange | null => stats.createDate); -const makeFilters = ( +const makeFilters = ( configs: Array>, counts: Array>, activeValues: T[] | undefined, @@ -147,7 +148,7 @@ const makeFilters = ( return configs.map((filter) => makeFilter(filter, activeValues, counts, queryKey)); }; -const makeFilter = ( +const makeFilter = ( filter: FilterConfig, activeValues: T[] | undefined, counts: Array>, @@ -195,6 +196,46 @@ export const selectFilters = ( } ); +export const selectWorkgroupFilters = createSelector( + fromAppShared.selectWorkgroups, + selectAssetSearchQuery, + selectAssetsSearchStats, + (workgroups, query, stats) => { + // Create a mapping of workgroups by their id for easier and more performant lookup. + const workgroupsById = new Map(); + for (const workgroup of workgroups) { + workgroupsById.set(workgroup.id, workgroup); + } + + // Map the workgroups with stats to filters. + const configs: FilterConfig[] = []; + for (const stat of stats.workgroupIds) { + const workgroup = workgroupsById.get(stat.value); + if (workgroup == null) { + continue; + } + workgroupsById.delete(stat.value); + configs.push({ + name: workgroup.name, + value: workgroup.id, + }); + } + + // Include workgroups with no assigned assets. + for (const workgroup of workgroupsById.values()) { + configs.push({ + name: workgroup.name, + value: workgroup.id, + }); + } + + // Sort the filters so their orders stays consistent. + configs.sort((a, b) => (a.name as string).localeCompare(b.name as string)); + + return makeFilters(configs, stats.workgroupIds, query.workgroupIds, 'workgroupIds'); + } +); + export const selectUsageCodeFilters = selectFilters('usageCodes', () => usageCodes.map((code) => ({ name: { key: `search.usageCode.${code}` }, @@ -248,7 +289,7 @@ export interface FullContact extends Contact { role?: AssetContactRole; } -export interface Filter { +export interface Filter { name: Translation; value: T; @@ -270,7 +311,7 @@ export interface Filter { queryKey: keyof AssetSearchQuery; } -type FilterConfig = Pick, 'name' | 'value'>; +type FilterConfig = Pick, 'name' | 'value'>; export const makeTranslatedValueFromItemName = (item: ValueItem): TranslatedValue => ({ de: item.nameDe, diff --git a/libs/auth/src/lib/services/auth.interceptor.ts b/libs/auth/src/lib/services/auth.interceptor.ts index 40416c0e..fc8c6c3d 100644 --- a/libs/auth/src/lib/services/auth.interceptor.ts +++ b/libs/auth/src/lib/services/auth.interceptor.ts @@ -58,7 +58,10 @@ export class AuthInterceptor implements HttpInterceptor, OnDestroy { private async handleError(error: HttpErrorResponse): Promise { switch (error.status) { case 403: - if (error.error == 'not authorized by eIAM') { + if ( + error.error == 'not authorized by eIAM' || + (typeof error.error === 'object' && error.error != null && error.error.error === 'not authorized by eIAM') + ) { // The initial logging via eIAM was successful, // but the user does not have access to this application. this.authService.setState(AuthState.AccessForbidden); diff --git a/libs/client-shared/src/lib/components/smart-translate.pipe.ts b/libs/client-shared/src/lib/components/smart-translate.pipe.ts index fa305e0e..4d447abb 100644 --- a/libs/client-shared/src/lib/components/smart-translate.pipe.ts +++ b/libs/client-shared/src/lib/components/smart-translate.pipe.ts @@ -32,7 +32,10 @@ export class SmartTranslatePipe implements PipeTransform, OnDestroy { this.subscription.unsubscribe(); } - transform(value: TranslationKey | TranslatedValue): string { + transform(value: Translation): string { + if (typeof value === 'string') { + return value; + } if (isTranslationKey(value)) { return this.translationPipe.transform(value.key); } @@ -40,7 +43,7 @@ export class SmartTranslatePipe implements PipeTransform, OnDestroy { } } -export type Translation = TranslationKey | TranslatedValue; +export type Translation = TranslationKey | TranslatedValue | string; export interface TranslationKey { key: string; diff --git a/libs/shared/src/lib/models/asset-search/asset-search-query.dto.ts b/libs/shared/src/lib/models/asset-search/asset-search-query.dto.ts index 228d860f..9f5d360a 100644 --- a/libs/shared/src/lib/models/asset-search/asset-search-query.dto.ts +++ b/libs/shared/src/lib/models/asset-search/asset-search-query.dto.ts @@ -38,6 +38,10 @@ export class AssetSearchQueryDTO implements AssetSearchQuery { @IsOptional() languageItemCodes?: string[]; + @IsNumber({}, { each: true }) + @IsOptional() + workgroupIds?: number[]; + @IsOptional() @ValidateNested() @Type(() => PartialDateRangeDTO) diff --git a/libs/shared/src/lib/models/asset-search/asset-search-query.ts b/libs/shared/src/lib/models/asset-search/asset-search-query.ts index 2e05e927..38e59060 100644 --- a/libs/shared/src/lib/models/asset-search/asset-search-query.ts +++ b/libs/shared/src/lib/models/asset-search/asset-search-query.ts @@ -12,6 +12,7 @@ export interface AssetSearchQuery { usageCodes?: UsageCode[]; geometryCodes?: Array; languageItemCodes?: string[]; + workgroupIds?: number[]; } export enum GeometryCode { diff --git a/libs/shared/src/lib/models/asset-search/asset-search-result.dto.ts b/libs/shared/src/lib/models/asset-search/asset-search-result.dto.ts index 06b0f835..cd1696a2 100644 --- a/libs/shared/src/lib/models/asset-search/asset-search-result.dto.ts +++ b/libs/shared/src/lib/models/asset-search/asset-search-result.dto.ts @@ -18,8 +18,9 @@ export class AssetSearchStatsDTO implements AssetSearchStats { assetKindItemCodes!: ValueCount[]; languageItemCodes!: ValueCount[]; geometryCodes!: ValueCount[]; - usageCodes!: ValueCount[]; manCatLabelItemCodes!: ValueCount[]; + usageCodes!: ValueCount[]; + workgroupIds!: ValueCount[]; @Type(() => DateRangeDTO) createDate!: DateRangeDTO | null; diff --git a/libs/shared/src/lib/models/asset-search/asset-search-result.ts b/libs/shared/src/lib/models/asset-search/asset-search-result.ts index 2f6981be..0501ceeb 100644 --- a/libs/shared/src/lib/models/asset-search/asset-search-result.ts +++ b/libs/shared/src/lib/models/asset-search/asset-search-result.ts @@ -21,8 +21,9 @@ export interface AssetSearchStats { assetKindItemCodes: ValueCount[]; languageItemCodes: ValueCount[]; geometryCodes: ValueCount[]; - usageCodes: ValueCount[]; manCatLabelItemCodes: ValueCount[]; + usageCodes: ValueCount[]; + workgroupIds: ValueCount[]; createDate: DateRange | null; } From 84ebfeeebc838c3ca106ee037634986ce3833415 Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Thu, 25 Jul 2024 13:10:57 +0200 Subject: [PATCH 20/21] Update CHANGELOG.md Fix lint error --- CHANGELOG.md | 2 ++ apps/server-asset-sg/src/core/middleware/jwt.middleware.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b42a8c1e..01b8b669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,14 @@ - Direktauswahl von Assets ohne Suche - Testing - Regeneriere Elasticsearch Index via Admin Panel +- Einteilung von Assets in Arbeitsgruppen ### Changed - UI Refactoring: Neuanordnung der Container - UI Refactoring: Suchergebnisse als Tabelle - Update Dependencies +- Bearbeitungsrechte werden auf Basis der Arbeitsgruppen vergeben anstatt global ### Fixed diff --git a/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts b/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts index 68db865a..89582cc3 100644 --- a/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts +++ b/apps/server-asset-sg/src/core/middleware/jwt.middleware.ts @@ -2,6 +2,7 @@ import { User } from '@asset-sg/shared/v2'; import { environment } from '@environment'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { HttpException, Inject, Injectable, NestMiddleware } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; import axios from 'axios'; import { Cache } from 'cache-manager'; import { NextFunction, Request, Response } from 'express'; @@ -14,7 +15,6 @@ import jwkToPem from 'jwk-to-pem'; import { UserRepo } from '@/features/users/user.repo'; import { JwtRequest } from '@/models/jwt-request'; -import { Prisma } from '@prisma/client'; @Injectable() export class JwtMiddleware implements NestMiddleware { From 910327574a47480beb3d1043cbba7776edccf2f3 Mon Sep 17 00:00:00 2001 From: Daniel von Atzigen Date: Thu, 25 Jul 2024 13:43:29 +0200 Subject: [PATCH 21/21] Fix workgroup activation not doing anything --- .../components/workgroup-edit/workgroup-edit.component.html | 2 +- .../components/workgroup-edit/workgroup-edit.component.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html index 0c7fc666..cdd3035e 100644 --- a/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html +++ b/libs/admin/src/lib/components/workgroup-edit/workgroup-edit.component.html @@ -9,7 +9,7 @@
    admin.name {{ workgroup.name }} admin.users - @for (user of workgroup.users; track user.user.id) { - {{ user.user.email }}, + @for (user of getWorkgroupUsers(workgroup); track user.id) { + {{ user.email }}, }